Skip to content

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rose

The rose, tuned to the golden angle

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.

Running it

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.

Deploying

.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.

Controls

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.

How it is put together

  1. CONFIG at the top of index.html, including bloomPoses, the key poses of the bloom choreography. Exposed as window.CONFIG so you can retune it live from the browser console.
  2. 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.
  3. 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.
  4. Rose heads, one per pedicel, oriented along the direction the pedicel is pointing when it arrives.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. Hand tracking. MediaPipe HandLandmarker in video mode, wrist position to place the plant and pinch normalised to grow and bloom.
  10. 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.

The maths

Vogel placement

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.

Why 137.507 and nothing else

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.

The snap

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.

Reading the pinch

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.

Counting the spirals

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:

  1. Lock the angle, then press H to clear the readout.
  2. Orbit until you are looking straight down at the head.
  3. Pick one obvious spiral arm at the rim and follow it inward with your eye.
  4. Now count how many parallel arms of that same family you cross going once around the head. That count is your Fibonacci number.
  5. 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.

Tweaking it

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 reload

Per 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.spacing is 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.petalsPerHead changes how fine the spoke patterns get, so more of the near miss angles in the table above become legible as you raise it.
  • rose.ringRecurve is the curl of each of the three petal rings. Making the inner value more negative gives a tighter, more wrapped core.
  • bloomPoses is the choreography itself. Each entry is a full set of head parameters at one stop on the timeline.
  • hands.rawMin and hands.rawMax are the pinch calibration. If your pinch never quite reaches either end of the range, widen them.
  • hands.swapHandedness fixes the controls being the wrong way round. The reason it exists is documented in a comment next to it.
  • tuning.snapRadius and tuning.snapStrength are the feel of the magnet.

Stack

Three.js and MediaPipe Tasks Vision, both from CDN via an import map. One HTML file, no build step, no dependencies to install.

About

A 3D rose plant that grows and blooms in response to your hands, and whose petals are placed by phyllotaxis.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages