PageSourceSearch

https://biodynai.com/cycle.js

js biodynai.com collected 2026-10-08 10:07:43 UTC 13,446 bytes, 379 lines download raw bytes

1/* ========================================
2   BIODYN , The loop
3   Five stages on a ring that genuinely closes:
4   automation frees the capacity that starts the
5   next round of discovery. Selecting a stage
6   turns the ring to face it; a pulse runs the
7   loop continuously so the direction is legible.
8   ======================================== */
9
10(() => {
11  'use strict';
12
13  const A = window.BiodynAbout;
14  const scope = document.getElementById('methodology');
15  const canvas = document.getElementById('cycleCanvas');
16  const rail = document.getElementById('cycleRail');
17  const panelHost = document.getElementById('cyclePanel');
18  if (!A || !scope || !canvas || !rail) return;
19
20  const STAGES = [
21    {
22      name: 'Discover',
23      title: 'Find something worth testing',
24      body: 'Scan models, datasets and literature for structure that looks real and is not yet '
25        + 'accounted for. Most of what turns up at this stage is a correlation with a compute bill '
26        + 'attached; the job is to state it as a claim precise enough that the next stage can kill it.'
27    },
28    {
29      name: 'Stress test',
30      title: 'Try hard to break it',
31      body: 'Covariate-matched permutation nulls, expression-matched controls, depth-only baselines, '
32        + 'and the obvious alternative explanations run before the interesting one. A finding that has '
33        + 'not survived a deliberate attempt to destroy it is not a finding yet.'
34    },
35    {
36      name: 'Replicate',
37      title: 'See how far it generalises',
38      body: 'Another dataset, another model, another donor cohort, sometimes another species. '
39        + 'Donor-disjoint splits by default. This is where most surviving claims narrow to something '
40        + 'smaller and truer than what was originally proposed.'
41    },
42    {
43      name: 'Scale',
44      title: 'Run the exhaustive version',
45      body: 'Once a method holds, stop sampling. Ablate every head, sweep every layer, screen hundreds '
46        + 'of hypotheses rather than arguing about which three to try. On models this size the '
47        + 'exhaustive experiment is affordable, so it is the one worth running.'
48    },
49    {
50      name: 'Automate',
51      title: 'Make it cost nothing next time',
52      body: 'Whatever survived becomes a pinned pipeline, a reusable command, an agent skill. The point '
53        + 'is not tidiness: it is that automating a settled step frees the capacity that starts the '
54        + 'next round of discovery, which is what closes this loop rather than ending it.'
55    }
56  ];
57
58  const ctx = canvas.getContext('2d');
59  const chamber = document.getElementById('cycleChamber');
60  const COUNT = STAGES.length;
61  const TAU = Math.PI * 2;
62  const CAMERA = 3.3;
63  const TURN_MS = 900;
64  const RING_POINTS = 340;
65  const STATION_POINTS = 34;
66  const N = RING_POINTS + COUNT * STATION_POINTS;
67
68  /* Ring path plus a cluster at each station. */
69  const pos = new Float32Array(N * 3);
70  const base = new Float32Array(N);
71  const phaseOf = new Float32Array(N);
72  const stationOf = new Int8Array(N);
73  {
74    const rand = A.makeRandom(2468);
75    for (let i = 0; i < RING_POINTS; i += 1) {
76      const t = i / RING_POINTS;
77      const a = t * TAU;
78      pos[i * 3] = Math.cos(a) * 0.95 + A.gaussian(rand) * 0.012;
79      pos[i * 3 + 1] = A.gaussian(rand) * 0.012;
80      pos[i * 3 + 2] = Math.sin(a) * 0.95 + A.gaussian(rand) * 0.012;
81      base[i] = 0.24;
82      phaseOf[i] = t;
83      stationOf[i] = -1;
84    }
85    for (let s = 0; s < COUNT; s += 1) {
86      const t = s / COUNT;
87      const a = t * TAU;
88      for (let k = 0; k < STATION_POINTS; k += 1) {
89        const i = RING_POINTS + s * STATION_POINTS + k;
90        pos[i * 3] = Math.cos(a) * 0.95 + A.gaussian(rand) * 0.085;
91        pos[i * 3 + 1] = A.gaussian(rand) * 0.085;
92        pos[i * 3 + 2] = Math.sin(a) * 0.95 + A.gaussian(rand) * 0.085;
93        base[i] = 0.75;
94        phaseOf[i] = t;
95        stationOf[i] = s;
96      }
97    }
98  }
99
100  let active = 0;
101  let yawFrom = 0;
102  let yawTo = 0;
103  let progress = 1;
104  let progressStart = 0;
105  let pulse = 0;
106  let dragYaw = 0;
107  let dragPitch = 0;
108  let velocityYaw = 0;
109  let velocityPitch = 0;
110  let dragging = false;
111  let lastPointer = null;
112
113  let width = 0;
114  let height = 0;
115  let dpr = 1;
116  let running = false;
117  let inView = false;
118  let frameHandle = 0;
119  let lastFrame = 0;
120  let manual = false;
121  let virtualNow = 0;
122  const clock = () => (manual ? virtualNow : performance.now());
123
124  const order = new Array(N);
125  const depth = new Float32Array(N);
126  const sx = new Float32Array(N);
127  const sy = new Float32Array(N);
128  const ss = new Float32Array(N);
129  const sa = new Float32Array(N);
130  const vec = [0, 0, 0];
131
132  let rgb = [87, 216, 234];
133  const refreshColour = () => { rgb = A.hexToRgb(A.readVar(scope, '--ab-ch', '#57d8ea')); };
134
135  const resize = () => {
136    const rect = canvas.getBoundingClientRect();
137    if (rect.width === 0 || rect.height === 0) return;
138    dpr = Math.min(window.devicePixelRatio || 1, 2);
139    width = rect.width;
140    height = rect.height;
141    canvas.width = Math.round(width * dpr);
142    canvas.height = Math.round(height * dpr);
143  };
144
145  const renderFrame = (now) => {
146    if (width === 0 || height === 0) return false;
147    const reduced = A.prefersReducedMotion();
148    const dt = lastFrame ? Math.min((now - lastFrame) / 1000, 0.05) : 0;
149    lastFrame = now;
150
151    if (progress < 1) progress = reduced ? 1 : A.clamp01((now - progressStart) / TURN_MS);
152    if (!reduced && !dragging) pulse = (pulse + dt * 0.16) % 1;
153
154    if (!dragging) {
155      dragYaw += velocityYaw;
156      dragPitch += velocityPitch;
157      velocityYaw *= 0.94;
158      velocityPitch *= 0.94;
159      if (Math.abs(velocityYaw) < 0.00002) velocityYaw = 0;
160      if (Math.abs(velocityPitch) < 0.00002) velocityPitch = 0;
161    }
162    dragPitch = Math.max(-1.0, Math.min(1.0, dragPitch));
163
164    const yaw = yawFrom + (yawTo - yawFrom) * A.easeInOut(progress) + dragYaw;
165    const pitch = 0.62 + dragPitch;
166
167    const brightfield = A.isBrightfield();
168    const sprite = A.getSprite(rgb, brightfield);
169    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
170    ctx.clearRect(0, 0, width, height);
171    ctx.globalCompositeOperation = brightfield ? 'source-over' : 'lighter';
172
173    const cxPix = width / 2;
174    const cyPix = height / 2;
175    const span = Math.min(width, height);
176    const focal = span * 1.5;
177    const unit = span * 0.0052;
178
179    for (let i = 0; i < N; i += 1) {
180      let inten = base[i];
181      // A pulse running the loop, so the direction of travel is visible.
182      let d = Math.abs(phaseOf[i] - pulse);
183      if (d > 0.5) d = 1 - d;
184      inten += Math.max(0, 1 - d / 0.085) * 0.75;
185      if (stationOf[i] >= 0) inten *= stationOf[i] === active ? 1.35 : 0.6;
186
187      A.rotate(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2], yaw, pitch, 0, vec);
188      const persp = focal / (CAMERA + vec[2]);
189      sx[i] = cxPix + vec[0] * persp;
190      sy[i] = cyPix - vec[1] * persp;
191      depth[i] = vec[2];
192      const near = 1 - (vec[2] + 1.2) / 2.6;
193      ss[i] = unit * (0.5 + inten * 0.8) * (persp / (focal / CAMERA));
194      sa[i] = Math.max(0, Math.min(1, inten * (0.2 + near * 0.45)));
195      order[i] = i;
196    }
197
198    order.sort((p, q) => depth[q] - depth[p]);
199    for (let k = 0; k < N; k += 1) {
200      const i = order[k];
201      if (ss[i] <= 0.05) continue;
202      ctx.globalAlpha = brightfield ? sa[i] * 1.15 : sa[i];
203      const d = ss[i] * 3.4;
204      ctx.drawImage(sprite, sx[i] - d / 2, sy[i] - d / 2, d, d);
205    }
206    ctx.globalAlpha = 1;
207    ctx.globalCompositeOperation = 'source-over';
208    return !reduced || progress < 1 || velocityYaw !== 0 || velocityPitch !== 0;
209  };
210
211  const loop = (now) => {
212    frameHandle = 0;
213    if (!running) return;
214    if (renderFrame(now)) frameHandle = requestAnimationFrame(loop);
215    else running = false;
216  };
217
218  const start = () => {
219    if (!inView) return;
220    if (width === 0 || height === 0) resize();
221    if (manual) { renderFrame(virtualNow); return; }
222    if (document.hidden) { progress = 1; lastFrame = 0; renderFrame(performance.now()); return; }
223    if (running) return;
224    running = true;
225    lastFrame = 0;
226    frameHandle = requestAnimationFrame(loop);
227  };
228
229  const stop = () => {
230    running = false;
231    if (frameHandle) cancelAnimationFrame(frameHandle);
232    frameHandle = 0;
233  };
234
235  /* --- Stages --- */
236
237  const tabs = [];
238
239  const renderPanel = (index) => {
240    const stage = STAGES[index];
241    const panel = document.createElement('div');
242    panel.className = 'cy-panel is-entering';
243    panel.id = 'cyclePanelBody';
244    panel.setAttribute('role', 'tabpanel');
245    panel.setAttribute('aria-labelledby', `cycleTab${index}`);
246    const h = document.createElement('h3');
247    h.className = 'cy-panel-title';
248    h.textContent = stage.title;
249    const p = document.createElement('p');
250    p.className = 'cy-panel-body';
251    p.textContent = stage.body;
252    panel.append(h, p);
253    return panel;
254  };
255
256  const select = (index, force) => {
257    if (index === active && !force) return;
258    // Turn the ring the short way to bring the chosen stage to the front.
259    const settled = yawFrom + (yawTo - yawFrom) * A.easeInOut(progress);
260    let delta = ((index - active) % COUNT + COUNT) % COUNT;
261    if (delta > COUNT / 2) delta -= COUNT;
262    yawFrom = settled;
263    yawTo = settled - delta * (TAU / COUNT);
264    progress = 0;
265    progressStart = clock();
266    active = index;
267
268    tabs.forEach((tab, i) => {
269      tab.classList.toggle('is-active', i === index);
270      tab.setAttribute('aria-selected', String(i === index));
271      tab.tabIndex = i === index ? 0 : -1;
272    });
273
274    const existing = document.getElementById('cyclePanelBody');
275    const panel = renderPanel(index);
276    if (existing) existing.replaceWith(panel); else panelHost.appendChild(panel);
277    start();
278  };
279
280  const canHover = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
281  let hoverTimer = 0;
282
283  STAGES.forEach((stage, index) => {
284    const tab = document.createElement('button');
285    tab.type = 'button';
286    tab.className = 'cy-stage' + (index === 0 ? ' is-active' : '');
287    tab.id = `cycleTab${index}`;
288    tab.setAttribute('role', 'tab');
289    tab.setAttribute('aria-controls', 'cyclePanelBody');
290    tab.setAttribute('aria-selected', String(index === 0));
291    tab.tabIndex = index === 0 ? 0 : -1;
292    tab.innerHTML = '<span class="cy-stage-dot" aria-hidden="true"></span><span class="cy-stage-name"></span>';
293    tab.querySelector('.cy-stage-name').textContent = stage.name;
294    tab.addEventListener('click', () => select(index));
295    tab.addEventListener('focus', () => select(index));
296    if (canHover) {
297      tab.addEventListener('pointerenter', () => {
298        window.clearTimeout(hoverTimer);
299        hoverTimer = window.setTimeout(() => select(index), 90);
300      });
301      tab.addEventListener('pointerleave', () => window.clearTimeout(hoverTimer));
302    }
303    rail.appendChild(tab);
304    tabs.push(tab);
305  });
306
307  rail.addEventListener('keydown', (event) => {
308    const i = tabs.indexOf(document.activeElement);
309    if (i === -1) return;
310    let next = null;
311    if (event.key === 'ArrowRight' || event.key === 'ArrowDown') next = (i + 1) % COUNT;
312    if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') next = (i - 1 + COUNT) % COUNT;
313    if (next !== null) { event.preventDefault(); select(next); tabs[next].focus(); }
314  });
315
316  /* --- Drag --- */
317
318  canvas.addEventListener('pointerdown', (e) => {
319    dragging = true;
320    lastPointer = { x: e.clientX, y: e.clientY };
321    velocityYaw = 0; velocityPitch = 0;
322    canvas.classList.add('is-dragging');
323    canvas.setPointerCapture(e.pointerId);
324    start();
325  });
326  canvas.addEventListener('pointermove', (e) => {
327    if (!dragging) return;
328    const sy_ = (e.clientX - lastPointer.x) * 0.008;
329    const sp = (e.clientY - lastPointer.y) * 0.006;
330    lastPointer = { x: e.clientX, y: e.clientY };
331    dragYaw += sy_; dragPitch += sp;
332    velocityYaw = sy_ * 0.55; velocityPitch = sp * 0.55;
333    start();
334  });
335  const release = (e) => {
336    if (!dragging) return;
337    dragging = false; lastPointer = null;
338    canvas.classList.remove('is-dragging');
339    if (e && e.pointerId !== undefined && canvas.hasPointerCapture(e.pointerId)) {
340      canvas.releasePointerCapture(e.pointerId);
341    }
342    start();
343  };
344  canvas.addEventListener('pointerup', release);
345  canvas.addEventListener('pointercancel', release);
346
347  /* --- Lifecycle --- */
348
349  refreshColour();
350  select(0, true);
351  progress = 1;
352  resize();
353
354  if ('ResizeObserver' in window) new ResizeObserver(() => { resize(); start(); }).observe(chamber);
355  window.addEventListener('resize', () => { resize(); start(); }, { passive: true });
356  if ('IntersectionObserver' in window) {
357    new IntersectionObserver((e) => {
358      inView = e[0].isIntersecting;
359      if (inView) start(); else stop();
360    }, { threshold: 0.02 }).observe(canvas);
361  } else inView = true;
362
363  document.addEventListener('visibilitychange', () => {
364    if (document.hidden) { stop(); return; }
365    resize(); start();
366  });
367
368  new MutationObserver(() => { refreshColour(); start(); })
369    .observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
370
371  A.exposeStepper((deltaMs) => {
372    if (!manual) { manual = true; stop(); inView = true; virtualNow = performance.now(); lastFrame = 0; }
373    virtualNow += (deltaMs || 16);
374    renderFrame(virtualNow);
375    return { cycle: active, progress: +progress.toFixed(2) };
376  });
377
378  start();
379})();

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.