Blog

The flowers package

#engineering#generative

The flower on this site is never saved anywhere. There's no image file, no database row, no S3 bucket. When you load a page, the flower is grown from scratch, from a string, in a few milliseconds. Give it the same string tomorrow and it grows the exact same flower. On any machine, forever.

That's the premise of @nbot/flowers, a small package I built and published on npm. It takes a seed, returns an SVG.

npm install @nbot/flowers
import { cultivar, plumeria } from "@nbot/flowers";
 
const svg = plumeria({ seed: "2026-06-15" });
const name = cultivar({ seed: "2026-06-15" }); // e.g. "celadine"

No dependencies. Runs the same on the server and in the browser.

The seed becomes a genome

The first thing the package does with a seed string is turn it into a stream of numbers. Not Math.random — that would give different results every time. Instead, it runs the seed through a small deterministic hash (xmur3) to get four 32-bit integers, then feeds those into a generator called sfc32 that produces a reproducible sequence of floats between 0 and 1.

Think of it as a very small slot machine that always lands on the same symbols in the same order, for a given starting position.

Those numbers then build a genome. A plumeria genome has everything: the color of the petals at the base and the tip, the color of the throat, the intensity of the blush on the shoulders, whether there's a flame in the center, how long and curved the petals are, how full or concave the shape is.

Every flower comes from one of eight cultivars, each modelled on a real frangipani variety: Celadine (white with a cadmium yellow center), Rainbow (cream body, orange throat, red rays), Pink Pearl, Sunset, Fuchsia, Gold, Candy Stripe, Carmine. They have different weights, so some appear more often than others. Celadine and Rainbow are three times as common as Candy Stripe.

About one in five flowers hybridises two cultivars. When that happens, all the color properties blend in OKLCH space at a random ratio between 30 and 70 percent, and the name becomes something like "sunset × fuchsia" or "gold × rainbow". The dominant parent comes first.

OKLCH, because RGB lies

All the color work happens in OKLCH, not RGB. OKLCH stands for lightness, chroma, hue, in a color space where equal steps look like equal steps to a human eye. In RGB, moving the same distance toward red and toward green looks very different. In OKLCH, it doesn't.

This matters a lot for hybridisation. When you blend gold (hue around 85) and pink (hue around 355) in HSL, you pass through green. In OKLCH, you pass through orange, because the code finds the short arc between the two hue values. The blends stay harmonious.

It also matters for the final output. Every color in OKLCH might not fit inside the sRGB gamut that screens use. Instead of clipping it (which would make it harsh), the package walks the chroma down gently until the color fits. The result is always printable and never blows out.

The pinwheel problem

A plumeria has five petals arranged in a pinwheel: each one covers its right neighbour and slides under its left neighbour, all the way around the circle. This is a problem.

If you try to draw the petals in any fixed order, the last petal you draw sits on top of everything, breaking the cycle. The first petal should cover the last, but the last was drawn after the first, so it wins. There is no draw order that closes the loop.

The fix is to not fight it. Draw all five petals, accept that the last one wrongly covers the first, and then draw the first petal a second time, clipped to the silhouette of the last. That thin sliver is the only piece of the first petal that was incorrectly hidden. Paste it back on top and the loop closes.

Every petal also casts a contact shadow on the one beneath it: a tight crisp edge that spreads into a soft penumbra, the same way a real petal pressed against another one looks in sunlight.

The geometry

Each petal starts as a straight shape in its own coordinate system, then gets rotated into place. The center line is a parabola, and because parabolas are a special case of Bezier curves, they survive rotation without any distortion.

The sides of the petal follow a Beta distribution kernel, which is a curve that can be wide and symmetric or narrow and offset, depending on two parameters. The two sides of a petal are asymmetric by design: the side that overlaps its neighbour is broader, the side that slides underneath is slimmer. Fullness (concave vs convex) changes the parameters continuously, not in discrete steps.

One subtle thing: every flower that comes out of the math is slightly tilted. Rather than correcting for this at render time, the code measures the lean of the shape using a Fourier integral over the silhouette, then bakes a counter-rotation directly into the petal geometry. The flower emerges already upright.

A garden and a chain

The garden on this site grows a new plumeria every day. Each day's flower is not just seeded by the date. It's seeded by the date, the domain, and the digest of the day before.

genesis (2026-06-15)
    sha256(genesis | 2026-06-15 | cultivar name that bloomed)
    → a 12-character digest
    
2026-06-16 seed: "2026-06-16|nicolabottari.com|<yesterday's digest>"
    sha256(yesterday's digest | 2026-06-16 | cultivar name that bloomed)
    → next digest
 
2026-06-17 seed: "2026-06-17|nicolabottari.com|<yesterday's digest>"
    ...

Each day folds the previous day's result into itself. This means the seed for June 30th contains all of June inside it, compressed into 12 hex characters.

It's not a blockchain. Nothing is distributed, nothing is verified by a network. But it has one property blockchains care about: a flower lifted out of the sequence is an orphan. If someone wants to claim that a given bloom belonged to a specific day, they can replay the chain from genesis and check. The SHA256 of every day's cultivar name is public in the sense that anyone with the package can compute it.

A flower that fits the chain is legitimate. One that doesn't is a forgery or a pre-genesis bloom, which are marked differently: their seed is just the date and the domain, no digest.

What this looks like in practice

The garden shows every flower since genesis. Each one links to its own page, which shows the full bloom with its name and chain position. The flower on a blog post is the bloom for the day the post was written.

The package itself is a few hundred lines, MIT licensed, and has no opinion about how you use it. If you want to grow flowers for your own project, install it and pass any string as a seed. The same string will always grow the same flower.

plumeria({ seed: "your-string-here", theme: "light" }); // SVG string
plumeria({ seed: "your-string-here", theme: "dark" });  // same flower, dark palette
plumeria({ seed: "your-string-here", bloom: true });    // with opening animation

The garden is at /garden. Every flower that has ever bloomed here is there.