Sitemap
Boris Bodin

Boris Bodin

I'm Software Architect. I mainly design software architectures in Angular and Spring Boot. I rely on clean code and tdd. https://bbodin.dev/

The Painting Was a Number All Along

10 min readJul 14, 2026

--

I built an art engine that can’t paint. It rolls dice instead. Here’s the con it pulls, and the some equations hiding behind its best work.

Press enter or click to view image in full size

It started with a black rectangle

The first thousand images my engine produced were black. Not “moody gallery black.” Void black. Empty-file black. I’d feed it a number, wait for something beautiful, and get a rectangle that looked exactly like the inside of my eyelids.

That’s the confession up front: this thing doesn’t paint. It can’t hold a brush. What it does is roll a single loaded die — an integer called a seed — and let a pile of equations argue about what that number means. Sometimes they agree on a flower. Sometimes they agree on nothing, and you get the void.

The bargain is strange and I’ve grown to love it. You don’t make an image. You make a number, and the number makes the image. Same number, same image, forever. Send me the integer 1954974687 and I can hand you back the exact same four-thousand-pixel painting, pixel for pixel, on any machine, next Tuesday or next decade.

That last part isn’t poetry, it’s a test that fails the build if I break it:

  • Same seed → identical pixels.
  • Save the artwork to JSON, reload it → identical pixels.

To keep that promise, every coin flip in the whole system runs through one deterministic random generator, seeded once. The day I let a rogue numpy.random sneak in "just for the initial positions," reproducibility quietly died and took me two hours to find. Rule number one now has scar tissue.

The trick: everyone is a point cloud

Here’s the sleight of hand that makes the whole thing work.

A parametric curve, a chaotic attractor, a Julia fractal, a field of swirling particles — these have roughly as much in common as a jellyfish and a tax form. Getting them to share a rendering pipeline should be a nightmare of special cases.

So I banned special cases. Every equation family, no matter how exotic, has to answer exactly one question:

class Equation:
def sample(self, n: int) -> tuple[points[N, 2], values[N] in [0, 1]]:
...

“Give me n points, and for each one, a number between 0 and 1 to color it with." That's the whole job interview. A Clifford attractor and a Buddhabrot orbit walk in as wildly different creatures and walk out as the same thing: a dusting of dots with a secret color baked into each.

One renderer eats all of them. And because they all pass through the same mouth, they come out sharing a family resemblance, like cousins who’ve never met but have the same nose.

The part that actually makes it pretty

The equation just throws confetti. All the style happens downstream, in a chain every layer marches through:

1. sample        → raw points + a color value each
2. cleanup → toss the NaN/infinity (equations love to divide by zero)
3. centering → P ← P − median(P)
4. symmetry → clone the cloud around by rotations and mirrors
5. framing → squash it onto pixels (aspect ratio survives)
6. color → turn each value into RGB through the palette
7. accumulation → pile the points into an HDR buffer, with thickness
8. tone mapping → log compression + normalization
9. glow → a halo, via Gaussian blur
10. compositing → blend onto the background

Three stages do the heavy aesthetic lifting.

Symmetry. Take the centered cloud, spin copies of it around the origin at angle θ_k = 2πk/order. Rotation alone gives you a mandala. Add a mirror and you get a kaleidoscope — the same move a six-year-old makes with a hinged mirror and a fistful of beads, minus the beads.

radial       : ⋃ₖ  R(θₖ) · P
kaleidoscope : ⋃ₖ R(θₖ) · P ∪ ⋃ₖ R(θₖ) · (mirror · P)

Palette. No swatches. No hex codes copied from a design blog. Color is a function. An HSL palette just slides the hue along each point’s value:

h(t) = hue₀ + t · hue₁          # hue drifts as t runs 0 → 1
(R, G, B) = HSL→RGB(h, S, L) # saturation and lightness stay put

Light accumulation. Every point deposits a little glow into a high-dynamic-range buffer, then I crush the whole thing down with a logarithm. This is the step that makes a fractal and a vector field look like they were painted by the same nocturnal hand.

b = log(1 + density · exposure)
b = clip(b / P₉₈(b), 0, 1) # normalize by the 98th percentile
b = b^0.65 # rescue the shadows
b = b + glow · gaussian_blur(b) # halo

Hold onto one thought before the reveal: two paintings can run on the exact same equation and look like total strangers. Symmetry, palette, and tone mapping are a witness protection program for math. Watch.

Why so many black rectangles

Back to the void. It turns out most random seeds are duds. An attractor collapses to a single dramatic dot. A curve flatlines. A field is too limp to fill the frame. Left alone, maybe one seed in ten is worth looking at.

Get Boris Bodin’s stories in your inbox

Join Medium for free to get updates from this writer.

So before committing to a shape, the engine sketches a rough draft onto a grid and asks, “Did you actually fill any space, or are you wasting my afternoon?” Too empty, and the shape gets rejected — deterministically, driven by the same seed, so a rejected seed stays rejected on every machine. That grumpy little bouncer is the difference between an art engine and a black-rectangle factory.

So what kind of artist makes nothing?

I’ve made peace with a job title that would horrify my old art teacher. I don’t compose these images. I curate them. Somewhere out there sits the space of every artwork this engine could ever produce, billions of them, the overwhelming majority black rectangles, and my entire job is to wander through it and point at the good ones.

That quietly rewrites what “making art” means here. A painter starts with an intention and chases it across a canvas. I start with a system and get ambushed by it. I’m not the author of any single flower. I’m the author of the garden it grew in, and the first tourist to walk through and gasp. The engine surprises me almost every session; I’ve found images I could never have pictured on my own, and I can’t honestly claim I imagined them. I only kept them.

So where does taste sneak in, if not through a brush? Two doors.

The first is the filter. That grumpy bouncer from a few paragraphs ago is an aesthetic judgment wearing a hard hat. Every seed it rejects is me saying “no, not that one,” ten thousand times a second. The curation is the art. Duchamp pointed at a urinal and called it sculpture; I point at a differential equation and call it a flower.

The second door is construction. I refuse to leave color to the dice. The palettes are built on old color-wheel harmony, the analogous and complementary and triadic schemes your art teacher drew on the whiteboard, so the hues come out related by design instead of by luck. I can’t promise a given seed is beautiful. I can rig the game so that when it lands, it never lands garish.

And when I find something almost right, I don’t start over. I nudge. One button takes a tiny step to a neighboring genome and keeps the shape intact; another keeps the staging but rolls fresh equations underneath it. It feels less like painting and more like tuning an old radio across a city of stations, waiting for the one song worth stopping on.

There’s a deeper reason I let a machine ambush me, and it’s the whole thesis of the project: I think the beauty was never in the equation. A raw Julia set is a math-textbook diagram. A raw vector field is a weather map. What turns a diagram into an image is everything I wrap around it afterward: the symmetry that folds it into a mandala, the palette that gives it a mood, the light that makes it glow like it was lit from behind. The equation supplies the bones. The art is entirely in the staging. Which is a slightly humbling thing to learn after naming a piece “Fractal Flower” when it contains neither.

Exhibit A — Vibrant Neon Fractal Flower

Press enter or click to view image in full size

Let’s get the awkward part out of the way: there is no fractal in the Fractal Flower. I named it before I checked. It’s a vector field wearing a costume.

The recipe: define a velocity field, dump thousands of particles into it, and nudge them forward one tiny Euler step at a time. Each particle drags a streamline behind it like a comet; a couple thousand comets later, you can see the shape of the wind.

dx/dt = sin(a·y) + c·cos(b·x)
dy/dt = sin(d·x) − cos(a·y)

xₙ₊₁ = xₙ + dt · [ sin(a·yₙ) + c·cos(b·xₙ) ]
yₙ₊₁ = yₙ + dt · [ sin(d·xₙ) − cos(a·yₙ) ]

a = 3.5 b = 1.7 c = 1.5 d = 3.0 dt = 0.012
(x₀, y₀) scattered uniformly across [−1.5, 1.5]²
color value = how old the particle is along its path

Layer 1 — the body of the flower.

1720 particles, ≈ 239,000 points
symmetry : radial, order 8 → ⋃ₖ₌₀..₇ R(2πk/8) · P
palette : HSL, h(t) = 0.7107 + t·0.6966, S = 0.942, L = 0.484
blend : add, opacity 0.80, exposure 1.43, glow 0.80, thickness 3.0

Layer 2 — the troublemaker that carves out the edges.

500 particles, 200,000 points
symmetry : radial, order 9 → ⋃ₖ₌₀..₈ R(2πk/9) · P
palette : linear gradient across 3 stops (position, R, G, B):
(0.0, 0.191, 0.759, 0.940)
(0.5, 0.928, 0.068, 0.290)
(1.0, 0.938, 0.737, 0.160)
blend : difference, opacity 0.66, exposure 0.91, glow 0.51

Layer 2 uses difference blending, which is a fancy way of saying it picks a fight with Layer 1 and subtracts its colors. Pair that with the offbeat clash between 8-fold and 9-fold symmetry — two rhythms that refuse to line up — and you get those electric neon veins. The best part of this image is an argument.

Exhibit B — Abstract Purple Particle Mandala

Press enter or click to view image in full size

This one is a duet between two equations that really shouldn’t get along, both forced under an order-3 kaleidoscope.

Layer 1 — a Julia set, rendered Buddhabrot-style. Instead of coloring pixels the normal way, I follow the orbits of points as they spiral out to infinity and accumulate where they’ve been. It’s the difference between photographing a firework and photographing the smoke it leaves. You get ghosts.

zₙ₊₁ = zₙ² + c        with c = 0.0522 + 0.1811·i   (fixed)
z₀ drawn in the complex plane, escapes when |zₙ| > 2, up to 169 iterations
color value = how many iterations it survived / max_iter

symmetry : kaleidoscope, order 3
⋃ₖ₌₀..₂ R(2πk/3)·P ∪ ⋃ₖ₌₀..₂ R(2πk/3)·(mirror·P)
palette : HSL, h(t) = 0.8233 + t·0.2375, S = 0.895, L = 0.274
blend : add, opacity 1.0, exposure 1.80, glow 0.21

Layer 2 — the same vector field from Exhibit A. Go ahead, squint at it:

dx/dt = sin(a·y) + c·cos(b·x)
dy/dt = sin(d·x) − cos(a·y)

a = 2.232 b = 2.980 c = 0.517 d = 2.939 dt = 0.0181
1639 particles, ≈ 263,000 points

symmetry : kaleidoscope, order 3 (same as layer 1)
palette : HSL, h(t) = 0.5549 + t·0.1687, S = 0.805, L = 0.318
blend : add, opacity 0.41, exposure 1.45, glow 0.34, fBm noise

There’s the reveal I promised. Layer 2 of this brooding purple mandala and the whole neon flower from Exhibit A are running the same differential equation. Identical math. I only changed four coefficients, the symmetry, the palette, and the blend, and the two images wouldn’t recognize each other in a police lineup. The equation is a skeleton. Everything you’d call “the art” is the outfit I put on it afterward.

What a thousand black rectangles taught me

Keep the inventor and the stylist in separate rooms. The equation invents a shape and has no taste whatsoever; symmetry, palette, and light have all the taste and can’t invent anything. Because the two never touch, I can bolt on a brand-new equation family with one class and one line in a registry, and the renderer never even notices a stranger walked in.

One seed, one genome, one image. The rest is just arithmetic keeping its promises — and, occasionally, a black rectangle reminding me who’s actually in charge.

🎨 Try it yourself — the engine runs right in your browser, no install: boris-bodin.github.io/art-generator.

🖼️ Want one on your wall? A few of these seeds escaped the black-rectangle graveyard and made it onto metal prints — grab one here: displate.com/claudebodin.

I’m a technical leader and software architect. I’ve been designing and building software architectures in Angular and Java for more than 7 years, following the principles of software craft, clean code, and TDD as much as I can. This engine is my side project for when the day job gets too tidy.

Thank you for reading! If you enjoyed this article, please feel free to 👏 and help others find it. And don’t hesitate to share your thoughts in the comments below.

--

--

Boris Bodin
Boris Bodin