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.