1// Phyllotaxis dotted-sphere orb for the Instance Metrics research card. 2// Inspired by the "Threejs Challenge #0" dotted sphere: a Fibonacci lattice of 3// small instanced spheres tiling a larger sphere, with a time-driven radial 4// wave rippling out from the poles so the surface breathes as it rotates. 5// Grayscale + softly lit to sit in the site's MRI-scan language. WebGL (r169), 6// gated to the Research section, static under reduced motion. 7import * as THREE from "https://esm.sh/[email protected]"; 8 9const canvas = document.querySelector(".research-card-orb-sphere"); 10 11const DOTS = 620; 12const BASE_R = 1.0; 13const DOT_R = 0.052; 14const WAVE_AMP = 0.11; 15const GOLDEN = Math.PI * (3 - Math.sqrt(5)); 16 17const reduceMotion = window.matchMedia( 18 "(prefers-reduced-motion: reduce)" 19).matches; 20 21let renderer, 22 scene, 23 camera, 24 group, 25 mesh, 26 clock, 27 running = false, 28 ready = false; 29const dummy = new THREE.Object3D(); 30const dirs = []; // unit direction per dot 31const phis = []; // polar angle per dot (for the wave) 32 33function inResearch() { 34 return ( 35 document.documentElement.getAttribute("data-section") === "research" || 36 document.querySelector("#research.section.active") !== null 37 ); 38} 39 40function build() { 41 renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas }); 42 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 43 renderer.setSize(canvas.width, canvas.height, false); 44 renderer.setClearColor(0x00_00_00, 0); 45 46 scene = new THREE.Scene(); 47 camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100); 48 camera.position.set(0, 0, 3.5); 49 50 scene.add(new THREE.AmbientLight(0xff_ff_ff, 1.5)); 51 const key = new THREE.PointLight(0xff_ff_ff, 40, 0, 2); 52 key.position.set(2.5, 3, 4); 53 scene.add(key); 54 const rim = new THREE.PointLight(0xff_ff_ff, 12, 0, 2); 55 rim.position.set(-3, -1, -2); 56 scene.add(rim); 57 58 group = new THREE.Group(); 59 scene.add(group); 60 61 const geo = new THREE.SphereGeometry(DOT_R, 10, 10); 62 const mat = new THREE.MeshStandardMaterial({ 63 color: 0xde_de_de, 64 metalness: 0.0, 65 roughness: 0.75, 66 }); 67 mesh = new THREE.InstancedMesh(geo, mat, DOTS); 68 group.add(mesh); 69 70 for (let i = 0; i < DOTS; i += 1) { 71 const y = 1 - (i / (DOTS - 1)) * 2; // 1 â -1 72 const r = Math.sqrt(Math.max(0, 1 - y * y)); 73 const theta = i * GOLDEN; 74 dirs.push(new THREE.Vector3(Math.cos(theta) * r, y, Math.sin(theta) * r)); 75 phis.push(Math.acos(y)); // 0 at top pole â Ï at bottom 76 } 77 78 ready = true; 79} 80 81function updateDots(time) { 82 for (let i = 0; i < DOTS; i += 1) { 83 const d = BASE_R + WAVE_AMP * Math.sin(time * 1.6 - phis[i] * 6.5); 84 const p = dirs[i]; 85 dummy.position.set(p.x * d, p.y * d, p.z * d); 86 const s = 0.8 + 0.5 * (0.5 + 0.5 * Math.sin(time * 1.6 - phis[i] * 6.5)); 87 dummy.scale.setScalar(s); 88 dummy.updateMatrix(); 89 mesh.setMatrixAt(i, dummy.matrix); 90 } 91 mesh.instanceMatrix.needsUpdate = true; 92} 93 94function frame() { 95 if (!ready || reduceMotion || !inResearch()) { 96 running = false; 97 return; 98 } 99 const t = clock.getElapsedTime(); 100 updateDots(t); 101 group.rotation.y = t * 0.35; 102 group.rotation.x = Math.sin(t * 0.2) * 0.25; 103 renderer.render(scene, camera); 104 requestAnimationFrame(frame); 105} 106 107function ensureRunning() { 108 if (running || !ready || reduceMotion || !inResearch()) { 109 return; 110 } 111 running = true; 112 requestAnimationFrame(frame); 113} 114 115if (canvas) { 116 build(); 117 clock = new THREE.Clock(); 118 updateDots(0); 119 group.rotation.x = 0.2; 120 renderer.render(scene, camera); // first paint 121 if (!reduceMotion) { 122 ensureRunning(); 123 } 124 125 const observer = new MutationObserver(ensureRunning); 126 observer.observe(document.documentElement, { 127 attributeFilter: ["data-section"], 128 attributes: true, 129 }); 130}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.