Place each new seed at a fixed angle from the last one and push it slightly further out. Almost every angle wastes space. One angle does not, and it is the one sunflowers use.
EXP-006
Move the cursor over the canvas — it is part of the simulation.
Drag your finger across the canvas — it is part of the simulation.
What to look for
Spirals appear that nobody placed. Count them and you get Fibonacci numbers, and the count changes depending on whether you follow the clockwise or the counterclockwise family.
The maths
φ = 137.507…°
The golden angle: 360° divided by the golden ratio squared. It is the most irrational number available, meaning it is the hardest to approximate with a fraction.
r = c√n, θ = n·φ
Vogel's model. The square root is what keeps the density constant: area grows as r², so r must grow as √n for each seed to get the same room.
How it is built
420 points, each drawn directly from the formula with no simulation and no state. The slow rotation is just a phase added to θ. Point size and opacity grow with the index so the outer ring reads as the newest growth.
The code, step by step
Cut straight from lab.js. These are the real lines that run above, not a simplified version.
01
The divergence angle
const GA = P.angle * Math.PI / 180;
const n = P.count | 0, sc = Math.min(w, h) * P.spread, cx = w / 2, cy = h / 2;
const spin = M.in ? (M.x / w - 0.5) * 6 : t * 0.12;
The slider defaults to 137.507°, the golden angle. Move it a tenth of a degree in either direction and the spirals collapse into radial spokes: that is how sharp the optimum is.
02
Angle linear, radius as a square root
const a = i * GA + spin, r = sc * Math.sqrt(i);
const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a);
The √i is the whole trick. Area grows as r², so for every seed to occupy the same area the radius has to grow as the square root of the index.
03
Newer points read as newer
const f = i / n;
ctx.fillStyle = `rgba(${acc},${(0.15 + f * 0.7).toFixed(3)})`;
ctx.beginPath(); ctx.arc(x, y, 0.8 + f * 2.1, 0, TAU); ctx.fill();
Size and opacity scale with the index, so the outer ring looks like fresh growth. No simulation: it is drawn straight from the formula every frame.
Why it matters
The plant is not doing number theory. It grows each primordium in the largest gap available, and that local greedy rule converges on the golden angle by itself. It is one of the cleanest cases of mathematics being discovered rather than invented.
The whole thing
Everything above, in one piece. This is the entire experiment as it lives in lab.js.
FULL
lab.js · phyllotaxis
{ id: 'phyllotaxis',
name: L('Phyllotaxis', 'Filotaxis'),
note: L('The golden angle. The same packing a sunflower solved first.',
'El ángulo áureo. El mismo empaquetado que un girasol resolvió antes.'),
params: [
{ key: 'angle', label: L('Divergence angle', 'Ángulo de divergencia'), min: 130, max: 145, step: 0.01, def: 137.507, unit: '°' },
{ key: 'count', label: L('Seeds', 'Semillas'), min: 60, max: 900, step: 10, def: 420 },
{ key: 'spread', label: L('Spread', 'Dispersión'), min: 0.01, max: 0.06, step: 0.002, def: 0.028 }
],
make() {
return {
step(ctx, w, h, t, acc, P, M) {
const GA = P.angle * Math.PI / 180;
const n = P.count | 0, sc = Math.min(w, h) * P.spread, cx = w / 2, cy = h / 2;
const spin = M.in ? (M.x / w - 0.5) * 6 : t * 0.12;
for (let i = 0; i < n; i++) {
const a = i * GA + spin, r = sc * Math.sqrt(i);
const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a);
const f = i / n;
ctx.fillStyle = `rgba(${acc},${(0.15 + f * 0.7).toFixed(3)})`;
ctx.beginPath(); ctx.arc(x, y, 0.8 + f * 2.1, 0, TAU); ctx.fill();
}
}
};
}
},