Skip to main content

The Power of Pride Bundle 2026 — $10 PWYC Edition
On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines
(4 edits) (+6)

I would like to have some swoopy lines decorating my deck, but I do not have a graphics tablet to draw them with, and drawing large curves pixel-at-a-time can be tedious. Some graphics apps have a “curve” or “Bézier curve” tool, which is like a regular line-drawing tool except you get some extra “handles” you can move around to affect how the line bends. Decker doesn’t have a “curve” tool either, but it does have contraptions, so I made a Bézier contraption.

Edit: Version 2 changes how you set the pattern used to draw the curve, and adds the “step size” property to speed up rendering. Version 3 significantly speeds up rendering, so the “step size” is less necessary. Version 4 adds a little “pin” button in the top left to quickly hide the handles so you can see what the curve looks like without having to switch modes twice, lock and unlock.

Normally, the contraption draws the curve, plus a little circle at each handle, and a dotted line connecting them so you can see which one is which. In interact mode, you can drag the handles around and see how they affect the shape of the curve.

Bezier curve with handles visible, and a little push-pin at the top left

If you want to quickly see what the curve looks like without the handles, you can click the “pin” button in the top-left, then click it again to unpin:

Bezier curve with no handles visible, and the push-pin pushed in

If you switch to Widget mode, lock the contraption, then switch back to Interact, the curve is drawn without the handles or the pin, and clicking won’t change the shape. You can just use this as a decoration directly, or go back into Widget mode and do Edit → Copy as Image to get something you can paste into your own artwork, or touch up with Decker’s tools.

Bezier curve unadorned

In Widget mode, you can use Widget → Pattern… to set the pattern used to draw the curve.

The properties are pretty simple: you can set the brush and set the handle coordinates specifically. You can have as many handles as you like, if you want a lot of control over the shape of the curve.

Contraption properties, listing “Brush”, “Coords” (coordinate pairs, one per line) and Step Size

If you do have a lot of handles (or make the contraption very large) it may take a while to calculate the exact position of the curve, which can make editing laggy. Here is a curve with 10 handles, and on my computer dragging one gets very laggy.

A very complicated Bézier curve

If I set the “Step size” property up to 50, the line gets very chunky, but dragging handles around becomes buttery smooth:

If I set the “Step size” back down to 10, it’s nearly as smooth as the original but still a lot less laggy:

The exact number you use depends on how fast your computer is, how big you make the contraption, how many handles you want, and what brush you use (smaller brushes make the chunkiness more visible). There’s also nothing stopping you from bumping the number up for editing, and then back down for display (or to use Copy as Image).

%%WGT0{"w":[{"name":"bezier1","type":"contraption","size":[189,137],"pos":[16,32],"show":"transparent","def":"bezier","widgets":{"canvas":{"size":[189,137]},"brush":{"size":[64,27],"pos":[205,0],"value":"2"},"coords":{"size":[96,137],"pos":[285,0],"value":{"x":[21,150,45,143],"y":[93,116,33,56]},"row":3},"sprites":{"size":[64,27],"pos":[205,55]},"stepsize":{"size":[64,28],"pos":[205,27]},"pinned":{"size":[64,28],"pos":[205,82]}}}],"d":{"bezier":{"name":"bezier","size":[80,80],"resizable":1,"margin":[0,0,0,0],"description":"Draws a Bezier curve. Use Widgets -> Pattern to set the colour, click the pin to hide the handles, lock the contraption to prevent editing.","version":4,"script":"on lerp t a b do a+(b-a)*0|1&t end\n\non bezier t ps do\n # Use lerping to combine adjacent pairs of positions\n # until there's only one left\n while 1 < count ps ps:lerp[t 1 drop ps -1 drop ps] end\n \n # Return the one that's left\n first ps\nend\n\non get_brush      do brush.data        end\non set_brush b    do brush.data:b      end\non get_stepsize   do stepsize.data     end\non set_stepsize s do stepsize.data:1|s end\n\non get_coords do\n (\"\\n\", \"%i,%i\") format coords.value.x join coords.value.y\nend\n\non set_coords t do\n coords.value:table \"xy\" dict flip \"%i,%i\" parse \"\\n\" split t\nend\n\non view do\n if !brush.data set_brush[0] end\n \n canvas.clear[]\n handles:coords.value.x join coords.value.y\n steps:ceil (max canvas.size)/stepsize.data\n canvas.pattern:card.pattern\n canvas.brush:brush.data\n canvas.line[0.5+each t in (range steps+1)/steps bezier[t handles] end]\n \n if ! card.locked\n  if pinned.value\n   # Draw the pinned sprite\n   canvas.paste[sprites.images[2] 0,0]\n  else\n   # Draw a dotted line connecting the handles in order\n   canvas.pattern:13\n   canvas.brush:0\n   canvas.line[handles]\n   \n   # Draw the handle sprite on top of each handle\n   sprite:sprites.images[0]\n   # Paste the handle sprite on top of each handle\n   each pos in handles canvas.paste[sprite pos-(floor sprite.size/2) 1] end\n   \n   # Draw the unpinned sprite\n   canvas.paste[sprites.images[1] 0,0]\n  end\n end\nend","attributes":{"name":["brush","coords","stepsize"],"label":["Brush","Coords","Step size"],"type":["number","code","number"]},"widgets":{"canvas":{"type":"canvas","size":[80,80],"pos":[0,0],"volatile":1,"script":"on click pos do\n if ! (pinned.value|card.locked)\n  best_row:-1\n  best_mag:me.size[0]*me.size[1] # an impossibly large number\n  \n  each handle i in coords.value.x join coords.value.y\n   handle_mag:mag (handle - pos)\n   if best_mag > handle_mag\n    best_row:i\n    best_mag:handle_mag\n   end\n  end\n  \n  coords.row:best_row\n end\nend\n\non drag pos do\n if ! (pinned.value|card.locked)\n  coords.rowvalue:\"xy\" dict pos\n  view[]\n end\nend\n\non release pos do\n if ! card.locked\n  # If the pointer was released in the top left 8x8 pixels...\n  if min pos < 8,8\n   pinned.value:!pinned.value\n   view[]\n  end\n end\nend","pattern":13,"show":"transparent","border":0,"scale":1},"brush":{"type":"field","size":[64,16],"pos":[96,0],"locked":1,"style":"code","value":"0"},"coords":{"type":"grid","size":[96,80],"pos":[176,0],"locked":1,"scrollbar":0,"lines":0,"format":"ii","value":{"x":[9,27,69],"y":[15,55,35]},"row":0},"sprites":{"type":"field","size":[64,16],"pos":[96,32],"locked":1,"value":{"text":["","i","i","i"],"font":["","","",""],"arg":["","%%IMG3AAUABQZAQGAoBBmHRhDyKCQGAQ==","%%IMG3AAgACAZAgBAACoCGwqKRCFIGjM9okygdOo/U5hKwxQKC","%%IMG3AAgACAZAgBAQKA6JoGSAWEyCmk8n9GkMOJ9Dq/JYXAqD"]}},"stepsize":{"type":"field","size":[64,16],"pos":[96,16],"locked":1,"style":"code","value":"2"},"pinned":{"type":"button","size":[64,16],"pos":[96,48],"locked":1,"text":"Pinned","style":"check","value":0}}}}}

Fun fact: Bézier curves are named for Pierre Bézier, the second person to discover them and use them to design more attractive vehicles at a French car manufacturer.

(+2)

This looks handy!

If you referenced card.pattern from within the prototype, you could take advantage of the normal Widgets -> Pattern menu item/modal for changing line color.

(+2)

Ah, that’s really handy! Thanks for pointing it out!

(2 edits) (+4)

I love this!! Even though I do have an art tablet, sometimes I would rather do the work with my mouse instead. Have a Bézier kitty for your hard work:


For anyone else doing this, you can do a sweeping-select of every widget and lock them all or assign show:none/show:transparent all at once. From the listener, you can run the script 'deck.card.image:app.render[deck.card]' to flatten all visible widgets to the card background.

I immediately saw a future where this can be used to 'tether' other widgets or create wiring:

# this is a script attached to the black sphere, which is an animated, draggable canvas widget.
on view do
 p:"\n" split bezier.coords
 newpos:me.pos-bezier.pos-(floor me.size/2)
 p[2]:"," fuse newpos
 bezier.coords:"\n" fuse p
end

 (As a note: When using a larger contraption size / more handles on the Bézier curve,  the script profiler shoots up anywhere between 20%-70%, I didn't have any performance issues while using this on Decker native, but after adding 3 handles to a single curve, usage went up to 200%, and I wasn't able to edit it anymore. )

(+3)

Have a Bézier kitty for your hard work

Wow, thank you!

When using a larger contraption size / more handles on the Bézier curve, the script profiler shoots up anywhere between 20%-70%

Yeah, I was working on Native Decker, with only three or four handles total. Computing the curve position can be expensive (up to the square of the number of handles), and the original contraption I posted tried to do it for every pixel along the curve for maximum smoothness, so I’m not surprised it might be expensive.

(+3)

I’ve updated the contraption to version 3, which leans harder on Lil’s “conforming” to calculate the curve rendering. As a result, the ten-handle curve that was “very laggy” on my computer before (with the Script Profiler capping out at 100% CPU usage) is now smooth (capping out at 64% CPU) even at the default step-size of 2. The new version still has a step-size property, though, because somebody might want to draw an even bigger curve. :)

(+2)

I got sick of having to switch to Widget mode, lock the contraption, and switch back to see what the curve “really” looked like, then do the dance again to resume editing. Now there’s a little “pin” button in the top left to hide and disable the handles in Interact mode. Locking the contraption now hides the handles and the pin.

I also updated the rendering code to try rounding coordinates to the nearest pixel, which might make lines a bit smoother.