A 3D rose plant that grows and blooms in response to your hands, and whose petals are placed by phyllotaxis.
Try here: rose
Spread your left hand and the plant grows: a tapered stem, pedicels fanning off the top, a rose head on each one. Spread your right hand and the flowers bloom, scrubbing a staged timeline from tight bud to petals lifting and separating to arching outward and curling back to full bloom, forward or backward.
Underneath, every petal in every head is placed by Vogel's model, and the divergence angle between one petal and the next is a dial on the arrow keys. Only at the golden angle of 137.507 degrees do the petals pack into a flower. Everywhere else the arrangement collapses into spokes and gaps, and the colour goes flat until you find it again.
The page is a single self contained index.html with no build step. It needs
to be served over http rather than opened as a file, because ES module import
maps and the camera both refuse to work from file://.
cd rose
python3 -m http.server 8000
Then open http://localhost:8000.
Camera access requires a secure context. localhost counts as one, and so does
GitHub Pages, so it works in both places unchanged.
.github/workflows/pages.yml publishes the repo root to GitHub Pages on every
push to main, and can also be run by hand from the Actions tab. There is no
build step, it uploads the root as it stands.
One setting has to be changed once, in the repository on github.com:
Settings, then Pages, then Source, set it to GitHub Actions rather than "Deploy from a branch".
After that, pushing to main deploys. The site URL appears on the workflow run
and under Settings, Pages. .nojekyll is there to stop Pages running the files
through Jekyll, which is a no op here but would silently drop any file or
folder beginning with an underscore if one were ever added.
If the camera is denied or unavailable, the piece falls back to demo mode and sweeps the angle by itself rather than showing you a blank page.
| Input | Effect |
|---|---|
| Left hand position | Move the plant around the frame |
| Left hand, spread thumb and index | Grow the plant |
| Right hand, spread thumb and index | Bloom the flowers |
| Left and right arrows | Divergence angle, coarse, 0.6 degrees per second |
| Up and down arrows | Divergence angle, fine, 0.11 degrees per second |
| D | Demo mode, grows and blooms on a cycle with no hands needed |
| 1, 2, 3 | Pin the bloom at bud, arching, or full |
| 0 | Release the pin |
| H | Hide the readout and the webcam preview |
| Drag, scroll | Orbit and zoom. The camera also slowly auto rotates |
Reaching 78 percent of a full spread already counts as maximum
(CONFIG.hands.fullAt), so you do not have to strain your hand open to finish
the gesture. Hands are identified by MediaPipe handedness, so the mapping holds
even if you cross your hands. The webcam window shows a live tracking overlay:
white dots on your thumb and index tips joined by a line, with the value each
hand is driving labelled beside it. It runs in demo mode too, so you can always
check that MediaPipe is seeing you.
CONFIGat the top ofindex.html, includingbloomPoses, the key poses of the bloom choreography. Exposed aswindow.CONFIGso you can retune it live from the browser console.- Bloom timeline. The poses are stops on a scrub track and the hand seeks along it rather than triggering it, so the stages always play in order in either direction. Interpolation between stops is smootherstep, so nothing arrives or leaves with a jolt.
- Plant skeleton. A main stem with a lean that grows with height, and pedicels fanning off the top, both rendered as solid tapered tubes swept along a polyline with parallel transport frames. Frenet frames would flip at every inflection and put a visible twist in the stem.
- Rose heads, one per pedicel, oriented along the direction the pedicel is pointing when it arrives.
- Petal geometry, rebuilt every frame from the current pose: a cupped blade with recurve and edge ripple, painted with layered vertex colour. Petals are sorted into three rings by radius, each with its own geometry, so the inner ones stay curled inward while the outer ones recurve back. That difference is most of what separates a rose from a daisy.
- Leaves, on the same blade machinery as the petals but with their own silhouette, placed up the stem at the same divergence angle. Leaf arrangement and floret arrangement are the same phyllotaxis problem, so tuning the angle respirals the leaves as well as the flowers. Each leaf waits its turn as the plant grows, which makes growing read as unfurling rather than as inflating.
- Screen anchoring. The plant is offset along the camera's own right and up axes rather than along world X and Y, so it tracks your hand across the frame no matter where the camera has been orbited to, and leans into the direction it is travelling.
- Idle sway and pollen motes. The stem sways on two frequencies that do not divide into each other so it never visibly repeats, each head nods on its own phase, and the motes orbit their head and drift upward until they run out of life and recycle. Motes only appear where there is a bloom to shed them, and brighten once the angle locks.
- Hand tracking. MediaPipe HandLandmarker in video mode, wrist position to place the plant and pinch normalised to grow and bloom.
- Main loop, with time based smoothing on every input, the demo cycle, and bloom and vignette post processing.
The petal colour is layered multiplicatively: instance colour supplies the plum to blush base across the head, and vertex colour pushes it around within a single petal, with a pale warm throat, a highlight down the midrib, edges and tips falling toward crimson, and faint veins.
Petal n goes at
theta = n * d
r = c * sqrt(n)
where d is the divergence angle and c is a spacing constant.
The square root is the part that does the work. Area grows as the square of
radius, so if you want every petal to occupy the same amount of area, the
radius of the nth petal has to grow as the square root of n. Use r = c * n
instead and the centre becomes a dense knot while the rim thins out to nothing.
With the square root, the density is constant from core to edge, which is
exactly what a real seed head does.
That formula only decides how far out each petal sits. The divergence angle decides where it sits around the circle, and that is the whole piece.
On top of the placement, three ramps run from centre to rim and turn a spiral
of points into a flower: petals pitch from near vertical at the core to splayed
at the rim, scale up as they go out, and shade from plum to blush. The head
itself sits on a shallow paraboloid, y = -a * r^2, so the rim falls away.
Write the divergence angle as a fraction of a full turn. If that fraction is
rational, say p/q, then every qth petal lands on exactly the same ray, and
the whole head collapses onto q straight spokes with empty wedges between
them. That is the failure you can see happening in this piece.
So you want an irrational fraction. But irrational is not enough, because some irrationals are very well approximated by simple fractions, and a fraction that is nearly right produces spokes that are nearly straight, which looks just as broken.
The golden ratio is the number that is hardest of all to approximate with a fraction. Its continued fraction expansion is nothing but ones, which is the slowest possible convergence. The golden angle is the full turn divided according to that ratio:
360 * (1 - 1/phi) = 360 / phi^2 = 137.50776 degrees
Because no simple fraction comes close, no set of petals ever lines up, and the packing has nowhere to fail. It is the only angle with that property.
The range in this piece is 135 to 140 degrees, and it is deliberately narrow, because it is crowded with near misses. Every one of these is a rational fraction of a turn hiding inside five degrees:
| Angle | Fraction of a turn | What you see |
|---|---|---|
| 135.000 | 3/8 | 8 hard spokes |
| 137.143 | 8/21 | 21 spokes |
| 137.455 | 21/55 | 55 spokes |
| 137.500 | 55/144 | 144 spokes, too fine to see at this petal count |
| 137.508 | golden | no spokes at any count |
| 137.647 | 13/34 | 34 spokes |
| 138.462 | 5/13 | 13 spokes |
Worth noting that the commonly quoted round value of 137.5 degrees is not the golden angle. It is 55/144 of a turn, it does produce spokes, and it is only saved here by being inside the snap radius.
Within 0.15 degrees of golden, the angle is pulled in by a magnet whose
strength grows toward the centre of the well, and a locked value climbs from
0 to 1. Locked drives colour saturation, core brightness, and bloom, so a tuned
rose looks lit from inside and an untuned one looks like grey silk.
The magnet is deliberately weaker than the coarse key rate, so holding an arrow key can always drag the angle back out. The colour response starts much further out than the magnet does, at 0.75 degrees, which is the tell that lets you know to ease off before you overshoot.
pinch = dist(thumb tip, index tip) / dist(wrist, middle knuckle)
The numerator is the gesture. The denominator is a fixed bone length across the palm, which shrinks and grows exactly as your hand moves toward and away from the lens. Dividing one by the other cancels distance from the camera, leaving a scale free number that runs from roughly 0.1 at a hard pinch to above 1.0 with the hand open. That is clamped, remapped onto 0 to 1, and exponentially smoothed, because raw landmarks shiver by a pixel or two every frame and that reads as the whole flower vibrating.
Once the rose is locked, the arms you can see in it are Fibonacci numbers, and this is a direct consequence of the golden angle rather than a coincidence.
The best rational approximations to 1/phi^2 are ratios of consecutive
Fibonacci numbers: 3/8, 5/13, 8/21, 13/34, 21/55. Take any Fibonacci number F.
Because F * 137.507 degrees lands very nearly on a whole number of turns,
petal n and petal n + F sit at nearly the same angle, just a little further
out. Follow that relationship and you trace a spiral arm. There are exactly F
of them.
To count them:
- Lock the angle, then press H to clear the readout.
- Orbit until you are looking straight down at the head.
- Pick one obvious spiral arm at the rim and follow it inward with your eye.
- Now count how many parallel arms of that same family you cross going once around the head. That count is your Fibonacci number.
- Do it again for the family curving the other way.
The two numbers you get will be adjacent in the Fibonacci sequence, something like 21 and 34. Which pair dominates depends on where you look: nearer the core the smaller pairs are clearest, nearer the rim the larger ones take over, and in between you can often see both at once, which is why sunflower heads seem to shimmer between two patterns.
Then nudge the angle to 137.143 and count again. You will get 21, and they will be straight lines rather than spirals.
Everything adjustable lives in the CONFIG object at the top of index.html,
grouped and commented: palette, camera, controls, petal silhouette and cupping,
the rose ramps, the tuning magnet, hand tracking, demo mode, and post
processing. Nothing below that block holds a number you would reasonably want
to change by hand.
CONFIG is also exposed as window.CONFIG, so you can tweak live from the
browser console:
CONFIG.post.bloomStrength = 0.6 // applies immediately
CONFIG.bloomPoses[3].recurve = 0.5 // applies on the next frame that moves
CONFIG.rose.petalsPerHead = 140 // structural, needs a reloadPer frame values (colours, glow, pose fields, ramps) apply immediately. Counts and segment resolutions are allocated once and need a reload.
A few worth knowing about:
rose.spacingis the single most important number for whether it reads as a rose. Petals are far larger than the spacing between their bases, and that heavy overlap is what makes a bloom. Raise it and the head opens out into a sunflower disc of separate straps.rose.petalsPerHeadchanges how fine the spoke patterns get, so more of the near miss angles in the table above become legible as you raise it.rose.ringRecurveis the curl of each of the three petal rings. Making the inner value more negative gives a tighter, more wrapped core.bloomPosesis the choreography itself. Each entry is a full set of head parameters at one stop on the timeline.hands.rawMinandhands.rawMaxare the pinch calibration. If your pinch never quite reaches either end of the range, widen them.hands.swapHandednessfixes the controls being the wrong way round. The reason it exists is documented in a comment next to it.tuning.snapRadiusandtuning.snapStrengthare the feel of the magnet.
Three.js and MediaPipe Tasks Vision, both from CDN via an import map. One HTML file, no build step, no dependencies to install.
