SourceLittle ducklings were walking
then they fell
and they died.— Rosie, 3
One of the unexpected discoveries from playing with Fig was how much fun I had designing, messing with (and breaking!) brushes. I thought I'd share how they work, how they don't work and why I will keep many of the bugs collected during my escapade in pixel land.
First, we have three types of brushes in Fig:
- normal → draw a (slightly noisy) rectangle
- drawable → draw the shape of the brush on a 9x9 matrix
- code → control your brush tixy.land style!
Regular
Regular brushes are just rectangles with occasional holes, Edam cheese style (or Gouda, depending on how fast you draw):
Drawn brushes
Draw the shape of your brush on a tiny 9x9 grid based editor. That's all!
No, I didn't mix x and y coords, you're sideways! (and Jonathan is a school.)
The original reason I added that little editor was to have a simple debugging/prototyping tool. What I particularly like about it:
- it's surprisingly quick and easy to use
- doodling a quick brush then immediately using it feels a bit like mixing colours in a physical palette
I suspect this might not be as interesting to others as it has been to me. I still remember the feeling of excitement when I tried the brush editor in Paint Shop Pro.
The UX was fun to handle as well, partially because it's more complex than it seems on the surface. Some things to consider:
- the editor will be used with: fingers, mouse and a stylus
- how to reduce the number of clicks/taps required to edit the brush?
- how to make the interaction obvious or make it easy to learn?
(This is also one of the reasons I enjoy typography: when it works, it becomes almost invisible.)
Some of the interactions I tested:
- Every pixel is a button! Click to toggle the pixel (too slow)
- Have an erase and draw mode (add/remove pixels from the brush), control via the
touchmove
event- too slow and error prone
- also, weird
- draw with
pointerdown
/pointermove
, but choose edit/erase mode depending on where you started- if I start drawing on a black pixel → remove brush pixels
- if I start drawing on an empty pixel → draw brush pixels
Code brushes
Feels like this entire project has been a 4-week long stream of consciousness. The code brush is an extension of the regular drawable brush but it's controlled via a tixy.land inspired JS snippet:
(i: int, x: int, y: int) → boolean
Let's break this down:
i
is the number of the current iteration, increased each framex
andy
are the grid coords
We run ixy
for each pixel of the brush grid, for each iteration. Every time the function returns true
we draw a pixel, otherwise we leave it empty.
Since i
can be used as a poor man's replacement for time, we can use ixy
to create animated brushes:
We can create looped/cyclical animations by using trigonometry, e.g. Math.sin
.
I'm often impressed by the work of people who are capable of writing complex shader code. I like to think of this brush editor as a simple, me-sized shader toy. Having said that, let's see if we can hack or break it!
Prototyping new brushes using aye aye AI
tixy.land caps the number of code characters at 32. We don't have this limitation, but to create more complex behaviours we'll still need:
- a place to store variables, e.g. computed temporary state
- someone with a better knowledge of maths than yours truly
- someone who will bother to write the code
1. can be solved with IIFEs (immediately-invoked function expressions):
(() => { ... })()
// can be used as:
const ixy = (i, x, y) => (
() => {
// use i, x, y here
let someComputedPropertyUsingXandY = ...
return someOtherComputedProperty
}
)()
2. can be offloaded to Claude!
I'm using some basic prompt vibing™ tricks here: chain of thought prompting, providing examples.
None of this song and dance is really needed, as providing two examples seemed to be enough to give me useful results: thread on Xitter.
Here's the prompt:
(see My default Coding Assistant System prompt for context)
implement the function animate (time, x, y) => void
the function:
- operates on a grid of 9x9 pixels
- returns either true or false for each pixel
- is implemented in Javascript
Example implementations:
- random spray/noise: `return Math.random < 1`
- diagonal line: `return x === y`
Now: implement the body of this function that will render a circle (9x9):
- wrap the result in an IIFE and pass these arguments to it: (i, x, y)
Result:
The woods are lovely dark and deep, so these are the bugs I'd like to keep:
Procreate is a wonderful piece of software. The faster, messier my brush strokes are, the better they look (thanks for their stabilisation and motion filtering algos). Every line is smooth, dynamic, drawn by a (hand-held) hand. This is useful and valuable as it allows more people to express themselves in ways that were previously impossible or very difficult. I used Procreate and MS Paint as a reference when looking for UX patterns.
(Yes, nothing can replace patience and discipline when it comes to drawing. I know that because I have low supplies of either.)
Fig doesn't even draw lines, which results in a bunch of small glitches. Every frame (however often it happens, usually between 30 and 60 fps) we sample the brush and drop it on the screen. This means that in order to draw strokes you'll need to draw more slowly. This means that if you draw really fast you can simulate brush spacing. And, finally, this also means that the brush dynamics will vary slightly between devices, just like it does when I switch from my pastel paper to vellum (or a crappy notebook.)
I tried to fix it and it looked too pretty. I'm keeping it.
That's all for today, see you tomorrow!