PageSourceSearch

https://aymuos15.github.io/js/research-sphere-orb.js

js aymuos15.github.io collected 2026-10-03 08:41:09 UTC 3,744 bytes, 130 lines download raw bytes

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.