Where it came from
In 2022 I found some math flowers built in Mathematica: János Karsai's virtual flowers. His Virtual Flowers with Crispate Petals demonstration and his Flowers in Mathematica notebook draw flowers as polar plots, a sine term folding a circle into petals. I wanted to know how far the idea could go. Every flower, generated in the browser from equations and a seed, then ray-traced right there on your GPU. It shipped on fxhash in 2023 as Study of Parametric Flowers, a grid of flowers generated the moment you load them. The fxhash listing is gone now; the raster.art page survives, and the piece runs right here; every load rolls a fresh hash and generates a new set of flowers; the same hash always rebuilds the same ones.

This one is live. It's a Lilium martagon from the generator; the equations below build the geometry, the spots and the stamens. It's rendered here in WebGL and you can see it render.
Before the equations
The flowers didn't start as flowers, and the first one wasn't even 3D. In July 2022, right after I found those polar plots, I built a p5.js sketch I called Polar flower: the same pLen · |sin(pNum/2 · φ)|^pSharp folded a circle into petals, with a height profile bending the flat disc into a cup and a little sine term for bumpiness. It is the exact equation this tool still runs on. You can watch it become this project inside one Glitch folder: Polar flower turns into Spherical flower, then the p5 lines get commented out and the whole thing is reborn in three.js.
Around it was a sketchbook. For a few months in the middle of 2022 I worked through the generative canon one p5 sketch at a time: Perlin flow fields, worley and value noise, square packing, quadtrees, metaballs, gradient color maps, learning out loud from Daniel Shiffman's Nature of Code and Roni Kaufman's palettes. The flow fields mattered most. You paint a field of brightness, read the gradient at each point, and grow a stroke along it; a thousand of those is grass, or hair, or stems. By July I was already seeding sketches with fxrand(), fxhash's PRNG, so one hash would always rebuild the same image. The destination was set before I knew what I was shipping.
Further back, in May 2022, there was bio. I took Laurens Lapre's lparser, the old C program from the L-system literature (Prusinkiewicz and Lindenmayer's The Algorithmic Beauty of Plants) that reads a grammar and grows a 3D plant, compiled it to JavaScript with Emscripten, and wired it to three.js with a live code editor and a day-night sky. An L-system is just an axiom and a few rewrite rules applied over and over, then read by a "turtle" that walks the resulting string, drawing a segment on F, turning on + and -, branching on [ and ]. The plant I shipped it with:
rule A = ~(7)$t(.1)F[+(40)C][-(40)C]!(.95)~(7)t(.1)FcA
rule C = ~(10)$tF[+(60)L][-(60)L]C
A main axis grows forward dropping paired side-shoots at forty degrees, each segment thinner than the last (!(.95)), pulled down by gravity (t) and jittered a few random degrees (~) so no two plants are identical, carrying leaves that are just filled polygons. bio was the first time I grew a plant from a rule and a seed instead of drawing one, which is the whole premise of the piece this became.
The flowers were even supposed to have stems. I built two kinds and shipped neither. One was a random walk: a Simplex Flower Generator where each stem is a Walker that steps forward while Simplex noise nudges its heading, so the line wanders upward the way a real stalk reaches for light. The other was a parametric generator I called with a single comment, //MAKE STEMS!, that grew a stem up to each bloom from a height and a growth count rolled off the hash. Both got cut before launch. What survived is the spire, the flower's central axis built as a convex hull through the throat: the stem stopped being a thing that grows and became the thing the flower hangs from.
And the color used to be a shader. The early build painted each petal with a GLSL fragment shader, a vertical fade crossed with a radial one and a scatter of Worley dots, the color coming straight from position with almost no lighting. I loved how simple it was. It had to go, because a path tracer cannot run a fragment shader; to get real light through the petals I baked the color into a texture instead. I kept the old shader version and brought it back to run on its own, because the look is still the one I reach for.
Two generators
The first generator extends the Wolfram idea to 3D. One function of angle sweeps the whole corolla:
r(φ) = (pLen · |sin(pNum/2 · φ)|^pSharp + fD) · t
pNum sets the lobe count and pSharp decides how deep the cuts go: near zero the petals fuse into a morning glory, high values cut them apart into a daisy. A height profile A · e^(−b·rᶜ) · rᵃ then bends the flat disc into cups, bells and trumpets.



That surface has a hard limit: r is single-valued per angle, so petals can never overlap. A poppy is impossible. The second generator, the spherical branch, winds one continuous spiral toward the pole of a sphere, and the petals are a modulation on the winding, an asin(sin(petalCutPow·θ)) cut that narrows the surface as it spins. Overlap follows automatically, because the surface passes over itself on the way in. The dahlias, camellias, zinnias and roses all come from this branch.
Petal count is just arithmetic, petals = cols · petalCutPow / 360.
The dahlia runs 2400 columns at power 10, which makes 67 petals, every time.


The crinkle in those petals comes from value noise over three octaves, displacing along the axis only, so the folds vary and the petal outline stays put.
Dots or no dots
Lily spotting is rejection sampling: pick a random point on a random triangle (barycentric; dots snapped to mesh vertices line up in visible rows), then reject anything closer to a placed dot than 1.5× its own long axis. Each spot is an ellipsoid stretched along the petal's uv gradient, so the markings run down the blade the way real papillae do, and they shrink toward the tip.
Whether a flower is spotted at all belongs to its palette. A tiger lily without spots is a contradiction, so tiger orange spots at 100%; cream trumpets usually go clean, so cream spots at 45%. The dot color is chosen together with the ramp: a maroon spot on a yellow tepal and a maroon spot on a red one are two different maroons.



The stamens
Stamens ruined more renders than anything else. A filament is a curve swept with a varying radius (a tube geometry holds one radius, so I swept my own). The problem: filament length and splay look like two independent parameters and turn out to be coupled. The curve's outward reach at the tip is (stamenRadius/20) · stamenScale, so lengthening the filaments also moved them sideways. The fix that worked was to stop sampling and solve: pick how far the anthers should project past the tepals, derive the scale from that, then derive the radius back from a target spread. The pistil is measured against the stamens, because what your eye reads is whether the stigma clears the anthers and by how much.

One number decides every color
The hash draws a single start hue, and everything else derives from it. Each flower in the grid steps around the color wheel from that hue by a scheme the hash also picks: complementary (+180°), triadic (+120°), quadratic (+90°), or a slow analogous drift of a few degrees per flower. The background comes from the same number, the start hue rotated by one of those harmony angles. The page's CSS variables and the loading screen derive from it too, so the UI always matches the flowers.

One hard rule: no green. The picker re-rolls until every flower avoids the band between 70° and 190° on the wheel, because green petals look like foliage. Within a flower, saturation and lightness vary with the radius, throat to margin. The piece shipped with four named lightness schemes (lightCenter, lightEdge, darkEdge, darkCenter); when I diffed them during a later cleanup, all four were byte-identical code with exactly four numbers changed. They're one function now, and the four numbers are a table.
The controls
The minted piece looks like a still image, but it carries a full set of controls. It starts with the loading screen: a 2000×2000 canvas handed to a second thread as an OffscreenCanvas, because the main thread is too busy building geometry to animate anything. It spins, tinted to the palette the hash already chose, and reports real progress (Building flowers: 64%, Placing dots: 91%), then ends with "Press F to debug, K for keybinds."
Those keybinds are real. K lists them, F opens the debug readout, and Z opens a panel of options: high-res dots, high-res stamens, transparent background, depth of field, auto-save PNG, overrides for the spherical flowers. The camera is orbit controls plus WASD/QE flying, with a slider that sets the step size, and more sliders that rotate the environment light around the scene. A pool of bin-packing workers places the flowers on the canvas by solving no-fit polygons, so flowers pack closely without overlapping.
The renderer
The realtime view is ordinary WebGL. A checkbox enables the path tracer, labeled "Raytrace high resolution PNG (warning: processor intense)": a full GPU path tracer (three-gpu-pathtracer) with a physical camera model. Real depth of field focused on the flowers, environment lighting from a blurred env map, and progressive sampling, so the frame starts noisy and refines the longer it runs. People assume Blender; the frames come from rays traced in a browser tab.


A print-quality frame exceeds the browser's maximum canvas size, so the export renders the scene in tiles (shifting the camera's view offset square by square) and dekapng stitches them into a PNG past any canvas limit. A generative token in a browser tab produces files large enough to print.
The stack
| Layer | Choice |
|---|---|
| Page | It's an html page |
| Modules | Import maps resolving bare names to vendored, pinned files |
| 3D | three.js r164 on the page, r148 for the convex-hull helpers |
| Ray tracing | three-gpu-pathtracer, on acceleration structures from three-mesh-bvh |
| Lighting | An HDR environment probe through RGBELoader |
| Geometry | A worker pool sized to your CPU cores, with an edge-collapse simplifier before geometry crosses a thread |
| Dots | Two workers per unique flower: rejection sampling, then a CSG intersect into the petal surface. Optionally a texture map and no CSG. |
| Layout | A genetic algorithm over no-fit polygons, clipper.js for the polygon math, in a worker that spawns its own |
| Loading screen | A 2000×2000 OffscreenCanvas on its own thread |
| Randomness | The fxhash snippet seeding an sfc32 PRNG; every draw in the piece pulls from it, so a flower is reproducible from its hash alone |
| Export | dekapng, writing PNGs past the browser's canvas size limit |
| Type | Authentic Sans, on CSS custom properties fed by the same hue as the flowers |
| The martagon up top | A GLB out of the generator on three r128, with GLTFLoader and OrbitControls, fetched lazily |
Everything is vendored and pinned, so the piece theoretically renders the same way years from now. The main thread only draws; geometry, dots and layout all run on workers.
The impossible bouquet
Dutch painters did this four hundred years ago. A flower still life by Jan van Huysum looks like one vase cut from one morning, but he worked a single canvas over a year or two, adding each flower as it came into season, so the finished bouquet holds spring tulips beside late-summer blooms that never actually shared a day. Painters called it, approvingly, surpassing nature: a perfection no real garden could assemble. My grid is the same move by other means. Every load rolls a hash and lays out a vase of flowers that never shared a seed, a dahlia beside a martagon beside a poppy, all generated in the same instant. Their composite came from studies drawn outdoors as each flower bloomed; mine comes from equations.
A flower piece was also always about death. Beauty at its peak is already dying, and the painters staged the whole arc in one frame: one bloom open, one in bud, one already browning, a fallen petal, a fly. Vanitas, from the line in Ecclesiastes, and memento mori, remember you must die. The hyper-real finish is the point; they froze an hour that a real flower only gives you once. Mine never wilt, which is its own version of the same idea, a synthetic bloom that cannot die because it was never alive, rebuilt identically from its hash forever.
There is exactly one aesthetic law in my color picker: re-roll until no petal lands in the green band between 70 and 190 degrees, because green reads as foliage and a green petal stops looking like a flower. That rule turns out to rhyme with how the whole tradition worked. After the Reformation, overt religious imagery fell out of favor in Dutch painting, so flowers carried the meaning instead, quietly: the white lily for the Virgin and for death at the same time, the tulip for fleeting wealth, the sunflower for devotion because it turns to follow the sun. The prized "broken" tulips, the flame-striped ones people paid the price of a canal house for, owed their markings to a virus that was slowly killing the bulb. The most valuable flower in Europe was literally diseased. The Victorians later turned all of this into a dictionary, floriography, a parlor game traced back to the Ottoman court where you could write a whole letter in a bouquet. My flowers carry no message I coded in; the palette is only hue math. But the instinct behind the no-green rule, keep a bloom legible as a bloom, is the same one that kept these paintings readable for four centuries.
The closest living relative to this piece isn't a painter. Anna Ridler's Mosaic Virus (2018) is a screen of flowers generated by a neural network, blooming and mutating, their striping driven by the live price of Bitcoin: the broken-tulip virus restaged as a speculative bubble, 1637 and the crypto mania folded into one image. To make it she hand-photographed and hand-labeled ten thousand tulips into a dataset, then called the flowers it produced impossible. That is the honest name for all of it. A generative model is an impossible bouquet, a composite assembled from many studies into flowers that never existed. Mine just assembles from equations instead of a dataset.
Where it's going
The current build treats each petal as its own surface: each tepal is a parametric patch (u along the blade, v across it) with a width profile in two halves that meet at the widest point with zero slope. Six blades share one axis, so each is confined to a sector of 2π/N, and the constraint halfWidth ≤ r · tan(π/N) is a hard boundary. The concave claw at a tepal's base follows directly from that clamp.

The current target is Lilium: 98 species tuned against reference photos, each rolled from triangular distributions so no single flower draws extreme values for every parameter at once.


Every image on this page came out of the generator this week. Same equations, new flowers.