PageSourceSearch

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

js aymuos15.github.io collected 2026-10-03 08:41:08 UTC 4,152 bytes, 138 lines download raw bytes

1// Live scanned-head volume for the Segmentation research card.
2// Reuses the three.js webgl_texture2darray example (256x256x109 CT volume,
3// WebGL2 sampler2DArray) — the same volume as the corner head-scan ornament —
4// but here it floats on the card's tinted disc and slowly scrubs through the
5// slice stack so it reads as an active scan. Renders only while the Research
6// section is showing; a single static slice under reduced-motion.
7import * as THREE from "https://esm.sh/[email protected]";
8import { unzipSync } from "https://esm.sh/[email protected]/examples/jsm/libs/fflate.module.js";
9
10const canvas = document.querySelector(".research-card-orb-scan");
11
12const VOLUME_URL =
13  "https://cdn.jsdelivr.net/gh/mrdoob/three.js@r169/examples/textures/3d/head256x256x109.zip";
14
15// Scrub through the anatomically rich middle of the 109-slice stack.
16const DEPTH_MIN = 28;
17const DEPTH_MAX = 88;
18const DEPTH_MID = (DEPTH_MIN + DEPTH_MAX) / 2;
19const DEPTH_AMP = (DEPTH_MAX - DEPTH_MIN) / 2;
20const SCRUB_SPEED = 0.006; // radians/frame — a slow, calm sweep
21
22const PLANE = 50;
23
24const vertexShader = /* glsl */ `
25    uniform vec2 size;
26    out vec2 vUv;
27    void main() {
28        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
29        vUv.xy = position.xy / size + 0.5;
30        vUv.y = 1.0 - vUv.y; // source data is upside down
31    }
32`;
33
34const fragmentShader = /* glsl */ `
35    precision highp float;
36    precision highp int;
37    precision highp sampler2DArray;
38
39    uniform sampler2DArray diffuse;
40    uniform float depth;
41    in vec2 vUv;
42    out vec4 outColor;
43
44    void main() {
45        float v = texture( diffuse, vec3( vUv, depth ) ).r;
46        // Head opaque and near-white, air transparent, so it floats on the disc.
47        outColor = vec4( vec3( v * 1.75 ), smoothstep( 0.04, 0.24, v ) );
48    }
49`;
50
51const reduceMotion = window.matchMedia(
52  "(prefers-reduced-motion: reduce)"
53).matches;
54
55let renderer, scene, camera, mesh;
56let ready = false;
57let running = false;
58let phase = 0;
59
60function inResearch() {
61  return (
62    document.documentElement.getAttribute("data-section") === "research" ||
63    document.querySelector("#research.section.active") !== null
64  );
65}
66
67function build() {
68  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas });
69  renderer.setPixelRatio(1);
70  renderer.setSize(canvas.width, canvas.height, false);
71  renderer.setClearColor(0x00_00_00, 0);
72
73  scene = new THREE.Scene();
74
75  const h = PLANE / 2;
76  camera = new THREE.OrthographicCamera(-h, h, h, -h, 0.1, 1000);
77  camera.position.z = 100;
78
79  new THREE.FileLoader()
80    .setResponseType("arraybuffer")
81    .load(VOLUME_URL, (data) => {
82      const zip = unzipSync(new Uint8Array(data));
83      const array = new Uint8Array(zip.head256x256x109.buffer);
84
85      const texture = new THREE.DataArrayTexture(array, 256, 256, 109);
86      texture.format = THREE.RedFormat;
87      texture.needsUpdate = true;
88
89      const material = new THREE.ShaderMaterial({
90        fragmentShader,
91        glslVersion: THREE.GLSL3,
92        transparent: true,
93        uniforms: {
94          depth: { value: DEPTH_MID },
95          diffuse: { value: texture },
96          size: { value: new THREE.Vector2(PLANE, PLANE) },
97        },
98        vertexShader,
99      });
100
101      mesh = new THREE.Mesh(new THREE.PlaneGeometry(PLANE, PLANE), material);
102      scene.add(mesh);
103      ready = true;
104
105      renderer.render(scene, camera); // first paint at the mid slice
106      if (!reduceMotion) {
107        ensureRunning();
108      }
109    });
110}
111
112function tick() {
113  if (!ready || reduceMotion || !inResearch()) {
114    running = false;
115    return;
116  }
117  phase += SCRUB_SPEED;
118  mesh.material.uniforms.depth.value = DEPTH_MID + DEPTH_AMP * Math.sin(phase);
119  renderer.render(scene, camera);
120  requestAnimationFrame(tick);
121}
122
123function ensureRunning() {
124  if (running || !ready || reduceMotion || !inResearch()) {
125    return;
126  }
127  running = true;
128  requestAnimationFrame(tick);
129}
130
131if (canvas) {
132  build();
133  const observer = new MutationObserver(ensureRunning);
134  observer.observe(document.documentElement, {
135    attributeFilter: ["data-section"],
136    attributes: true,
137  });
138}

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.