Flowing Brush Strokes

Oct 2025 Β· repainted Jul 2026

JavaScript vector fields painting Van Gogh-style swirls. Built for no reason except that it's beautiful.

Tap the painting for a new one.

brush
palette
More controls

The math, plainly

Every painting starts as a field of arrows: at each point of the canvas, a direction. The arrows come from two sources added together. Vortices β€” a few points the sky turns around, spinning hardest at a core radius and fading smoothly beyond it. And turbulence β€” a lattice of random values smoothed between grid corners, stacked at several zoom levels so small eddies ride on big ones. That sum is the whole physics β€” each brush just shapes the vortex falloff its own way. There is no simulation, just geometry.

v(p) β‰ˆ Ξ£α΅’ sα΅’ Β· (βˆ’Ξ”yα΅’, Ξ”xα΅’) Β· rβ‚€α΅’ / (rΒ² + rβ‚€α΅’Β²)  +  c Β· curl N(p)        Night,  r = |p βˆ’ cα΅’|
N(p) = Ξ£β‚’ aα΅’ Β· n(Ξ»α΅’ p),  a β‰ˆ Β½,  Ξ» β‰ˆ 2        n = lattice noise, corners blended by tΒ²(3 βˆ’ 2t)

Strokes are not scattered at random; pure scatter reads as static, not brushwork. A new stroke may only begin a set distance from the ones already painted β€” one brush lays its candidates on an evenly-spread sequence, another spawns each stroke beside the last β€” and a coarse grid of already-painted cells enforces the spacing. Each start point is then traced along the arrows, so neighbors run parallel and comb around the eddies the way brush hair lies.

pβ‚™ = ( {Β½ + nΒ·0.754877…},  {Β½ + nΒ·0.569840…} )        R2 low-discrepancy sequence
grow while dist(other strokes) > 0.52Β·d_sep        spawn neighbors at Β±1.05Β·d_sep

A stroke is not a line but one filled shape: a belly that tapers to a point at both ends, laid at 55–96% opacity. Many carry a thin lighter ridge along their lit side β€” paint catching light. That, not blur, is what makes flat pixels read as raised paint.

w(t) = w_max Β· (0.30 + 0.70Β·sin(Ο€t)^0.70),  Ξ± = 0.60 + 0.32Β·ΞΎ        Night
w(t) = w_max Β· (0.22 + 0.78Β·sin(Ο€t)^0.62),  ridge + underside        Impasto

The three brushes are three answers to where the strokes should go. Night currents composes a picture first: a moon, sometimes a second star, halo rings, a combed dark ground. Impasto cares about the paint itself: fatter ribbons with bristle streaks, an underside shadow, seams of canvas left showing. Cypress lets the line loose: long licks that overshoot around the vortices, and dark flame-shapes climbing the sky.

And none of it is random at the moment you look. A seed β€” one number, 0 to 9,999 β€” feeds a small generator (mulberry32) that deals out every "random" decision in a fixed order, like a shuffled deck dealt the same way twice. Same seed, same screen, same brush, same sliders: the same painting, stroke for stroke. Ten thousand seeds, three brushes, three palettes β€” ninety thousand paintings in this one file, before you touch a slider.

mulberry32:  s ← s + 0x6D2B79F5 (mod 2Β³Β²)  β†’  xor-shift mix  β†’  uniform [0, 1)
painting = F(seed, brush, palette, sliders, W, H)        bit-for-bit reproducible

The constants above are lifted straight from the source. The rest is one View Source away.

Notes

This started in October 2025 as a small piece of math built for the joy of it: a flow field, a few swirl centers, strokes traced through. Its job was to make a flow field read as paint instead of a plot β€” one HTML file, no JS libraries, no build step.

In July 2026 I tore out my original brushwork and rebuilt it using Claude Fable 5 and it came back as three brushes. Night currents composes a sky. Impasto sculpts the paint. Cypress lets the line run. Depending on the brush and your screen, a painting is several hundred to five thousand strokes β€” the exact count is under it, with the seconds it took.

If you want to talk about this, or about the more serious kind of building I do, start at eshahabi.com. The rest of the lab is at Successfully Failed.