import * as THREE from 'three'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { createStage } from './lib/stage.js'; // Phyllotaxis: nature places seeds, leaves and florets one golden angle // (137.5077...) apart. Nudge the angle a tenth of a degree and the pattern // collapses into spokes - the irrationality of phi is what packs it perfectly. const { scene, colors, onUpdate } = createStage(); const GOLDEN_ANGLE = 180 * ( 3 - Math.sqrt( 5 ) ); // 137.5077... const MAX_COUNT = 3000; const params = { count: 800, angle: GOLDEN_ANGLE, form: 'sunflower', grow: false, speed: 1, spin: true, resetAngle: () => { params.angle = GOLDEN_ANGLE; gui.controllersRecursive().forEach( ( c ) => c.updateDisplay() ); renderPattern(); }, }; const group = new THREE.Group(); // stand the disc up facing the light and camera, like the circle pages group.rotation.y = Math.PI / 2; scene.add( group ); const material = new THREE.MeshStandardMaterial( { metalness: 0.7, roughness: 0.3 } ); const mesh = new THREE.InstancedMesh( new THREE.SphereGeometry( 1, 16, 16 ), material, MAX_COUNT ); mesh.castShadow = true; group.add( mesh ); const dummy = new THREE.Object3D(); const colour = new THREE.Color(); const inner = new THREE.Color( colors.fills.aether ); const outer = new THREE.Color( colors.fills.fire ); function renderPattern() { const n = params.count; const angle = params.angle * ( Math.PI / 180 ); for ( let i = 0; i < n; i++ ) { const theta = i * angle; const t = i / ( n - 1 ); if ( params.form === 'sunflower' ) { // r ~ sqrt(i) keeps the seed density constant across the disc const r = 6.5 * Math.sqrt( t ); dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 0 ); dummy.scale.setScalar( 0.15 ); } else if ( params.form === 'pinecone' ) { const r = 4.5 * Math.sqrt( t ); dummy.position.set( r * Math.cos( theta ), r * Math.sin( theta ), 6 * ( 1 - t ) - 3 ); dummy.scale.setScalar( 0.17 ); } else { // fibonacci sphere: even latitude steps + golden angle in longitude // spreads points almost perfectly uniformly over a sphere const y = 1 - 2 * t; const r = Math.sqrt( Math.max( 0, 1 - y * y ) ); dummy.position.set( 5.5 * r * Math.cos( theta ), 5.5 * r * Math.sin( theta ), 5.5 * y ); dummy.scale.setScalar( 0.16 ); } dummy.updateMatrix(); mesh.setMatrixAt( i, dummy.matrix ); mesh.setColorAt( i, colour.lerpColors( inner, outer, t ) ); } mesh.count = n; mesh.instanceMatrix.needsUpdate = true; mesh.instanceColor.needsUpdate = true; } renderPattern(); // growth loop: seeds appear one by one from the centre the way the plant grows // them, rest at the full shape, then start over; speed scrubs the growth rate const GROW_S = 6; const PAUSE_S = 4; onUpdate( ( time ) => { if ( params.spin ) group.rotation.x = time * 0.1; if ( params.grow ) { const growDuration = GROW_S / params.speed; const position = time % ( growDuration + PAUSE_S ); const progress = Math.min( 1, position / growDuration ); mesh.count = Math.max( 1, Math.floor( progress * params.count ) ); } } ); const gui = new GUI(); gui.add( params, 'count', 10, MAX_COUNT ).step( 10 ).onChange( renderPattern ); gui.add( params, 'angle', 130, 145 ).step( 0.001 ).onChange( renderPattern ); gui.add( params, 'resetAngle' ).name( 'reset to golden angle' ); gui.add( params, 'form', [ 'sunflower', 'pinecone', 'sphere' ] ).onChange( renderPattern ); const animation = gui.addFolder( 'Animation' ); animation.add( params, 'grow' ).onChange( () => { mesh.count = params.count; } ); animation.add( params, 'speed', 0.25, 4 ).step( 0.05 ); animation.add( params, 'spin' );