daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

particle-field.ts (38146B)


      1 /**
      2  * The dot field, framework-free. Generated from daemon-sec's
      3  * client/src/components/daemon/particle-field.tsx (same shaders, same
      4  * timing) so the sibling sites share one look without React.
      5  *
      6  * Two layers. The cloud: blue-noise dashes that drift and lean toward a
      7  * ring lagging the pointer, thinning under any `[data-index-zone]` box.
      8  * The icon layer (opt-in): dots that gather into the lit shelf's icon,
      9  * placed on a `[data-field-icon]` box inside the canvas's
     10  * `[data-field-host]` ancestor. On a light ground dots get a minimum size
     11  * and more ink. Colours are read off the canvas's computed style and
     12  * re-read whenever <html> changes its theme attributes. Pauses off-screen
     13  * and in hidden tabs; one still frame under reduced motion (OS setting or
     14  * `<html data-motion="reduce">`); `onFail` if WebGL is unavailable.
     15  */
     16 import { minDistFor, poisson, rng } from "./poisson";
     17 import { nearestInked, rasterise, targetsFor } from "./icon-targets";
     18 
     19 export type Accent = string;
     20 export interface LitShelf {
     21   index: number | null;
     22   accent: Accent | null;
     23   /** True while a pointer holds the row (vs. a tour lighting it). */
     24   held: boolean;
     25 }
     26 export interface ShelfSource {
     27   get(): LitShelf;
     28   subscribe(f: (s: LitShelf) => void): () => void;
     29 }
     30 
     31 export interface FieldOptions {
     32   /** Custom-property names for colours, resolved on the canvas. */
     33   vars?: { ink: string; base: string; iris: string; love: string };
     34   /** Accent colour variable for a shelf accent name. */
     35   accentVar?: (accent: string) => string;
     36   pageFamily?: string;
     37   plateFamily?: string;
     38   tone?: "page" | "plate";
     39   /** Draw the drifting cloud (default true). */
     40   cloud?: boolean;
     41   /** SVG markup per shelf, in shelf order; enables the icon layer. */
     42   icons?: string[];
     43   /** Which shelf is lit; required with `icons`. */
     44   shelf?: ShelfSource;
     45   /** Icon dots rest spread across the canvas and stay visible. */
     46   restFill?: boolean;
     47   /** Also gather for a shelf a tour lights, not only a held one. */
     48   tour?: boolean;
     49   /** Override motion; defaults to off under reduced motion. */
     50   live?: boolean;
     51   onFail?: () => void;
     52 }
     53 
     54 const NO_SHELF: ShelfSource = {
     55   get: () => ({ index: null, accent: null, held: false }),
     56   subscribe: () => () => {},
     57 };
     58 
     59 type Rgb = [number, number, number];
     60 
     61 function hexToRgb(hex: string, fallback: Rgb): Rgb {
     62   const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
     63   if (!m) return fallback;
     64   const n = parseInt(m[1], 16);
     65   return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
     66 }
     67 
     68 function readVar(el: HTMLElement, name: string, fallback: Rgb): Rgb {
     69   return hexToRgb(getComputedStyle(el).getPropertyValue(name), fallback);
     70 }
     71 
     72 const NOISE = `
     73 vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
     74 vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
     75 vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
     76 vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
     77 float snoise(vec3 v) {
     78   const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
     79   const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
     80   vec3 i = floor(v + dot(v, C.yyy));
     81   vec3 x0 = v - i + dot(i, C.xxx);
     82   vec3 g = step(x0.yzx, x0.xyz);
     83   vec3 l = 1.0 - g;
     84   vec3 i1 = min(g.xyz, l.zxy);
     85   vec3 i2 = max(g.xyz, l.zxy);
     86   vec3 x1 = x0 - i1 + C.xxx;
     87   vec3 x2 = x0 - i2 + C.yyy;
     88   vec3 x3 = x0 - D.yyy;
     89   i = mod289(i);
     90   vec4 p = permute(permute(permute(
     91     i.z + vec4(0.0, i1.z, i2.z, 1.0))
     92     + i.y + vec4(0.0, i1.y, i2.y, 1.0))
     93     + i.x + vec4(0.0, i1.x, i2.x, 1.0));
     94   float n_ = 0.142857142857;
     95   vec3 ns = n_ * D.wyz - D.xzx;
     96   vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
     97   vec4 x_ = floor(j * ns.z);
     98   vec4 y_ = floor(j - 7.0 * x_);
     99   vec4 x = x_ * ns.x + ns.yyyy;
    100   vec4 y = y_ * ns.x + ns.yyyy;
    101   vec4 h = 1.0 - abs(x) - abs(y);
    102   vec4 b0 = vec4(x.xy, y.xy);
    103   vec4 b1 = vec4(x.zw, y.zw);
    104   vec4 s0 = floor(b0) * 2.0 + 1.0;
    105   vec4 s1 = floor(b1) * 2.0 + 1.0;
    106   vec4 sh = -step(h, vec4(0.0));
    107   vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
    108   vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
    109   vec3 p0 = vec3(a0.xy, h.x);
    110   vec3 p1 = vec3(a0.zw, h.y);
    111   vec3 p2 = vec3(a1.xy, h.z);
    112   vec3 p3 = vec3(a1.zw, h.w);
    113   vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
    114   p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
    115   vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
    116   m = m * m;
    117   return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
    118 }
    119 `;
    120 
    121 /* Field units: y runs -1..1 over the canvas height, x is ±aspect. */
    122 const VERT = `
    123 precision highp float;
    124 attribute vec2 a_rest;
    125 attribute vec4 a_seed;
    126 attribute vec2 a_from;
    127 attribute vec2 a_to;
    128 
    129 uniform float u_time;
    130 uniform vec2 u_extent;
    131 uniform float u_pr;
    132 uniform float u_pscale;
    133 uniform float u_maxPoint;
    134 uniform float u_layer;
    135 uniform float u_hover;
    136 uniform float u_morph;
    137 uniform float u_pulse;
    138 uniform float u_iconReady;
    139 uniform float u_fill;
    140 uniform float u_light;
    141 uniform float u_ringR;
    142 uniform float u_ringW;
    143 uniform float u_ringW2;
    144 uniform float u_ringDisp;
    145 uniform vec2 u_ring;
    146 uniform vec2 u_ringLag;
    147 uniform vec4 u_zone;
    148 uniform vec4 u_icon;
    149 
    150 varying float v_scale;
    151 varying float v_near;
    152 varying float v_col;
    153 varying vec2 v_dir;
    154 
    155 ${NOISE}
    156 
    157 /* The cloud thins under the shelf list so the rows stay readable, but
    158    only partly and on a wide, rounded falloff: a hard cut-out read as a
    159    box drawn round the list. Measured as a rounded rectangle with a
    160    radius as big as its short side, so there is no corner to see.
    161    No list, no thinning. */
    162 float zoneFade(vec2 p, float margin) {
    163   if (u_zone.z <= u_zone.x) return 1.0;
    164   vec2 c = 0.5 * (u_zone.xy + u_zone.zw);
    165   vec2 h = 0.5 * (u_zone.zw - u_zone.xy);
    166   float r = min(h.x, h.y) * 0.9;
    167   vec2 q = abs(p - c) - h + r;
    168   float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
    169   return mix(0.45, 1.0, smoothstep(-0.25, margin, sd));
    170 }
    171 
    172 void main() {
    173   float t = u_time * 0.5;
    174   vec2 p;
    175   float scale;
    176   float near = 0.0;
    177 
    178   if (u_layer < 0.5) {
    179     vec2 rest = a_rest;
    180     /* each dot sees the ring a little behind where it is, by its own
    181        amount, which spreads the settle across the field */
    182     vec2 ring = mix(u_ringLag, u_ring, a_seed.w);
    183     float dist = distance(rest, ring);
    184     float noise0 = snoise(vec3(rest * 0.2 + vec2(18.4924, 72.9744), t * 0.5));
    185     float dist1 = distance(rest + noise0 * 0.005, ring);
    186     float R = u_ringR;
    187     float t1 = smoothstep(R - u_ringW * 2.0, R, dist) - smoothstep(R, R + u_ringW, dist1);
    188     float t2 = smoothstep(R - u_ringW2 * 2.0, R, dist) - smoothstep(R, R + u_ringW2, dist1);
    189     float t3 = smoothstep(R + u_ringW2, R, dist);
    190     t1 = t1 * t1;
    191     t2 = t2 * t2 * t2;
    192     float s = t1 + t2 * 3.0 + t3 * 0.4;
    193     s += snoise(vec3(rest * 30.0 + vec2(11.4924, 12.9744), t * 0.5)) * t3 * 0.5;
    194     float nS = snoise(vec3(rest * 2.0 + vec2(18.4924, 72.9744), t * 0.5));
    195     s += pow((nS + 1.5) * 0.5, 2.0) * 0.6;
    196 
    197     float n1 = snoise(vec3(rest * 4.0 + vec2(88.494, 32.4397), t * 0.35));
    198     float n2 = snoise(vec3(rest * 4.0 + vec2(50.904, 120.947), t * 0.35));
    199     float n3 = snoise(vec3(rest * 20.0 + vec2(18.4924, 72.9744), t * 0.5));
    200     float n4 = snoise(vec3(rest * 20.0 + vec2(50.904, 120.947), t * 0.5));
    201     vec2 disp = vec2(n1, n2) * 0.03 + vec2(n3, n4) * 0.005;
    202     float reach = clamp(dist, 0.0, 1.0);
    203     disp.x += sin(rest.x * 20.0 + t * 4.0) * 0.02 * reach;
    204     disp.y += cos(rest.y * 20.0 + t * 3.0) * 0.02 * reach;
    205 
    206     vec2 cur = rest + disp;
    207     /* the rim is pushed off the ring */
    208     p = cur - (ring - cur) * pow(max(t2, 0.0), 0.75) * u_ringDisp * 0.3;
    209     scale = (s + 0.2) * zoneFade(p, 0.25);
    210 
    211     float nA = snoise(vec3(rest * 10.0 + vec2(18.4924, 72.9744), u_time * 0.85));
    212     float ang = atan(p.y - ring.y, p.x - ring.x) + nA * 0.5;
    213     v_dir = vec2(cos(ang), sin(ang));
    214     v_col = (snoise(vec3(rest * 2.0 + vec2(74.664, 91.556), u_time * 0.5)) + 1.0) * 0.5;
    215   } else {
    216     vec2 iconLocal = mix(a_from, a_to, smoothstep(0.0, 1.0, u_morph));
    217 
    218     /* The icon as a volume, not a stencil. Each dot has its own depth
    219        through the figure (a stable hash of its seed), and the figure
    220        turns a little: a slow idle sway, plus a lean toward the pointer
    221        so it follows the hand the way the cloud does. With depth, the
    222        turn makes the front of the figure travel further than the back
    223        and the dots nearest the eye draw larger and brighter (below), so
    224        a gathered shelf icon reads as a small object made of dots
    225        rather than a flat outline — the same outline, with a thickness.
    226        The rotation is small enough that the glyph never stops reading
    227        as itself. */
    228     float depth = fract(a_seed.y * 13.37 + a_seed.z * 0.71) - 0.5;
    229     vec2 lean = clamp((u_ringLag - u_icon.xy) / max(u_icon.z * 3.0, 0.001), -1.0, 1.0);
    230     float ay = sin(u_time * 0.55) * 0.28 + lean.x * 0.3;
    231     float ax = cos(u_time * 0.41) * 0.18 - lean.y * 0.22;
    232     vec3 q = vec3(iconLocal, depth * 0.34);
    233     q = vec3(q.x * cos(ay) + q.z * sin(ay), q.y, -q.x * sin(ay) + q.z * cos(ay));
    234     q = vec3(q.x, q.y * cos(ax) - q.z * sin(ax), q.y * sin(ax) + q.z * cos(ax));
    235     /* a touch of perspective: what is nearer is a little bigger */
    236     float nearness = q.z / 0.34;
    237     vec2 iconPos = u_icon.xy + q.xy * (1.0 + nearness * 0.09) * u_icon.zw;
    238     /* Where a dot sets out from: a soft disc round the icon (the square
    239        sample, folded to polar), so nothing square is ever drawn. */
    240     float ang0 = atan(a_rest.y, a_rest.x);
    241     float rad0 = max(abs(a_rest.x), abs(a_rest.y)) * 2.0;
    242     vec2 restDisc = u_icon.xy + vec2(cos(ang0), sin(ang0)) * sqrt(rad0) * u_icon.z * 0.95;
    243     /* or, on the plate, spread over the whole surface: the plate's own
    244        dots are the ones that gather */
    245     vec2 restFill = a_rest * 2.0 * u_extent * 0.94;
    246     vec2 restW = mix(restDisc, restFill, u_fill);
    247     restW += 0.02 * vec2(
    248       snoise(vec3(a_rest * 3.0, t * 0.2)),
    249       snoise(vec3(a_rest * 3.0 + 31.7, t * 0.2)));
    250 
    251     /* arrival, staggered per dot and eased like power3.out */
    252     float h = clamp((u_hover - a_seed.x * 0.35) / 0.65, 0.0, 1.0);
    253     h = 1.0 - pow(1.0 - h, 3.0);
    254     h *= u_iconReady;
    255     /* On the plate a share of the dots stays behind, so the card keeps
    256        its texture round the icon instead of emptying out. */
    257     h *= mix(1.0, step(0.3, a_seed.w), u_fill);
    258 
    259     /* twinkle: a life cycle from the seed, invisible at its wrap */
    260     float life = fract(u_time / a_seed.z + a_seed.y);
    261     float env = smoothstep(0.0, 0.2, life) * (1.0 - smoothstep(0.65, 1.0, life));
    262 
    263     p = mix(restW, iconPos, h);
    264     float dist = distance(p, iconPos) / max(u_icon.z, 0.001);
    265     near = h * smoothstep(0.08, 0.0, dist);
    266 
    267     /* a ring expanding from the icon's centre after a change, nudging
    268        the dots outward as it passes */
    269     if (u_pulse >= 0.0) {
    270       vec2 fromC = p - u_icon.xy;
    271       float ringD = abs(length(fromC) - u_pulse * u_icon.z * 0.9);
    272       float k = ringD / max(u_icon.z, 0.001);
    273       p += normalize(fromC + 0.0001) * 0.025 * u_icon.z * exp(-k * k * 60.0) * (1.0 - u_pulse);
    274     }
    275 
    276     /* Unseen at rest: the icon's dots only exist while a shelf is
    277        hovered, appearing as they set out and fading as they return. */
    278     float shown = mix(smoothstep(0.0, 0.3, h), 1.0, u_fill);
    279     scale = (env * (0.45 + 0.55 * a_seed.w) * 0.7 * (1.0 - h) + 1.6 * near + 0.3 * env * h) * shown;
    280     /* the front of the figure draws a size up, the back a size down;
    281        only once the dot has arrived, so the journey is unchanged */
    282     scale *= 1.0 + nearness * 0.22 * near;
    283     v_col = near;
    284     /* the depth rides out on v_dir, which the icon layer never reads
    285        for direction: x is nearness in -1..1 */
    286     v_dir = vec2(nearness, 0.0);
    287   }
    288 
    289   v_near = near;
    290   v_scale = scale;
    291   if (scale < 0.1) {
    292     gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
    293     gl_PointSize = 1.0;
    294     return;
    295   }
    296   /* The light-ground minimum is for the page cloud; the plate's dots
    297      are already drawn a size up, and doubling it made them chunky. */
    298   float px = scale * 7.0 * u_pr * 0.5 * u_pscale + u_light * 1.1 * u_pr * (1.0 - u_layer);
    299   /* The plate's dots are its whole surface, not a wash behind a
    300      page, so they are drawn a size up. */
    301   if (u_layer > 0.5) px = (px * 1.25 + 0.6 * u_pr) * (1.0 + 0.7 * u_fill);
    302   gl_PointSize = min(u_maxPoint, px);
    303   gl_Position = vec4(p.x / u_extent.x, p.y / u_extent.y, 0.0, 1.0);
    304 }
    305 `;
    306 
    307 const FRAG = `
    308 precision highp float;
    309 uniform float u_layer;
    310 uniform float u_inkAlpha;
    311 uniform float u_iconAlpha;
    312 uniform vec3 u_c1;
    313 uniform vec3 u_c2;
    314 uniform vec3 u_c3;
    315 uniform vec3 u_ink;
    316 uniform vec3 u_accent;
    317 uniform float u_light;
    318 
    319 varying float v_scale;
    320 varying float v_near;
    321 varying float v_col;
    322 varying vec2 v_dir;
    323 
    324 float sdRoundBox(vec2 p, vec2 b, float r) {
    325   vec2 q = abs(p) - b + r;
    326   return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
    327 }
    328 
    329 void main() {
    330   vec2 uv = gl_PointCoord - 0.5;
    331   uv.y = -uv.y;
    332   float a;
    333   vec3 c;
    334   if (u_layer < 0.5) {
    335     /* a short dash, turned to face the ring */
    336     vec2 q = vec2(uv.x * v_dir.x + uv.y * v_dir.y, -uv.x * v_dir.y + uv.y * v_dir.x);
    337     float sd = sdRoundBox(q, vec2(0.5, mix(0.2, 0.3, u_light)), 0.25);
    338     float shape = smoothstep(0.1, 0.0, sd);
    339     a = shape * smoothstep(0.1, 0.2, v_scale) * u_inkAlpha;
    340     float h = 0.8;
    341     c = v_col < h ? mix(u_c1, u_c2, v_col / h) : mix(u_c2, u_c3, (v_col - h) / (1.0 - h));
    342   } else {
    343     float disc = smoothstep(0.5, 0.4, length(uv));
    344     a = disc * smoothstep(0.1, 0.2, v_scale) * mix(u_inkAlpha, u_iconAlpha, v_near);
    345     /* the back of the figure sits a little further into the ground
    346        than the front: the depth cue the size change alone lacks */
    347     a *= 1.0 - 0.28 * v_near * clamp(-v_dir.x, 0.0, 1.0);
    348     c = mix(u_ink, u_accent, smoothstep(0.05, 0.6, v_near));
    349   }
    350   if (a < 0.01) discard;
    351   gl_FragColor = vec4(c * a, a);
    352 }
    353 `;
    354 
    355 function compile(gl: WebGLRenderingContext, type: number, src: string) {
    356   const sh = gl.createShader(type);
    357   if (!sh) return null;
    358   gl.shaderSource(sh, src);
    359   gl.compileShader(sh);
    360   if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
    361     if (import.meta.env.DEV) console.error("particle-field shader:", gl.getShaderInfoLog(sh));
    362     gl.deleteShader(sh);
    363     return null;
    364   }
    365   return sh;
    366 }
    367 
    368 /* ── Tweens, the GSAP way: retarget from wherever the value is now ── */
    369 const easeOutCubic = (x: number) => 1 - Math.pow(1 - x, 3);
    370 
    371 class Tween {
    372   value: number;
    373   private from: number;
    374   private to: number;
    375   private t0 = 0;
    376   private dur = 1;
    377   constructor(v: number) {
    378     this.value = v;
    379     this.from = v;
    380     this.to = v;
    381   }
    382   go(to: number, dur: number, now: number) {
    383     if (to === this.to && dur === this.dur) return;
    384     this.from = this.value;
    385     this.to = to;
    386     this.dur = Math.max(1, dur);
    387     this.t0 = now;
    388   }
    389   set(v: number) {
    390     this.value = this.from = this.to = v;
    391   }
    392   tick(now: number) {
    393     const x = Math.min(1, (now - this.t0) / this.dur);
    394     this.value = this.from + (this.to - this.from) * easeOutCubic(x);
    395     return this.value;
    396   }
    397 }
    398 
    399 /**
    400  * Pixels per field unit. The hero is a unit per half its height; a
    401  * shorter band keeps roughly the hero's scale instead of squeezing the
    402  * same cloud into less height, so dot spacing, the ring and the drift
    403  * read the same on every page.
    404  */
    405 function unitPx(w: number, h: number) {
    406   return Math.max(h / 2, w * 0.26, 1);
    407 }
    408 
    409 /** The phase the still frame is drawn at. */
    410 const HELD_TIME = 31.0;
    411 /** How long the pointer may rest before the ring starts to wander. */
    412 const IDLE_MS = 2800;
    413 
    414 
    415 export function mountParticleField(cv: HTMLCanvasElement, options: FieldOptions = {}): () => void {
    416   const fam0 = options.tone === "plate" ? options.plateFamily ?? "night" : options.pageFamily ?? "dawn";
    417   const opts = {
    418     pageFamily: "dawn",
    419     plateFamily: "night",
    420     accentVar: (a: string) => `--color-${fam0}-${a}`,
    421     ...options,
    422   };
    423   const tone = opts.tone ?? "page";
    424   const withIcons = Boolean(opts.icons && opts.icons.length);
    425   const withCloud = opts.cloud ?? true;
    426   const restFill = opts.restFill ?? false;
    427   const tour = opts.tour ?? false;
    428   const heroShelf = opts.shelf ?? NO_SHELF;
    429   const live =
    430     opts.live ??
    431     (!window.matchMedia("(prefers-reduced-motion: reduce)").matches &&
    432       document.documentElement.dataset.motion !== "reduce");
    433   const coarse = window.matchMedia("(pointer: coarse)").matches;
    434   const setFailed = (_f: boolean) => opts.onFail?.();
    435 
    436     const attrs: WebGLContextAttributes = {
    437       alpha: true,
    438       premultipliedAlpha: true,
    439       antialias: false,
    440       depth: false,
    441       stencil: false,
    442       powerPreference: "low-power",
    443     };
    444     const gl =
    445       cv.getContext("webgl", attrs) ||
    446       (cv.getContext("experimental-webgl", attrs) as WebGLRenderingContext | null);
    447     if (!gl) {
    448       if (import.meta.env.DEV) console.error("particle-field: no WebGL context");
    449       setFailed(true);
    450       return () => {};
    451     }
    452     const vs = compile(gl, gl.VERTEX_SHADER, VERT);
    453     const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);
    454     const prog = gl.createProgram();
    455     if (!vs || !fs || !prog) {
    456       setFailed(true);
    457       return () => {};
    458     }
    459     gl.attachShader(prog, vs);
    460     gl.attachShader(prog, fs);
    461     gl.linkProgram(prog);
    462     if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    463       if (import.meta.env.DEV) console.error("particle-field link:", gl.getProgramInfoLog(prog));
    464       setFailed(true);
    465       return () => {};
    466     }
    467     gl.useProgram(prog);
    468     gl.enable(gl.BLEND);
    469     gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    470     gl.clearColor(0, 0, 0, 0);
    471 
    472     /* ── Geometry ── */
    473     /* Dots per square field unit: the hero's 8,000 over its 8.8. */
    474     const perUnit = coarse ? 400 : 900;
    475     const morphCount = coarse ? 1600 : 2400;
    476     /* Half-extents of the cloud in field units, regrown when the canvas
    477        outgrows them. */
    478     const domain = { w: 0, h: 0 };
    479     const hash = rng(7);
    480 
    481     const seeds = (n: number) => {
    482       const s = new Float32Array(n * 4);
    483       for (let i = 0; i < n; i++) {
    484         s[i * 4] = hash(); // arrival delay
    485         s[i * 4 + 1] = hash(); // life phase
    486         s[i * 4 + 2] = 2 + hash() * 2; // life length, seconds
    487         s[i * 4 + 3] = hash(); // general purpose
    488       }
    489       return s;
    490     };
    491 
    492     const buf = {
    493       cloudRest: gl.createBuffer(),
    494       cloudSeed: gl.createBuffer(),
    495       morphRest: gl.createBuffer(),
    496       morphSeed: gl.createBuffer(),
    497       morphFrom: gl.createBuffer(),
    498       morphTo: gl.createBuffer(),
    499     };
    500     let cloudN = 0;
    501     const buildCloud = () => {
    502       const area = domain.w * 2 * domain.h * 2;
    503       const count = Math.min(16000, Math.round(perUnit * area));
    504       const pts = poisson({
    505         halfW: domain.w,
    506         halfH: domain.h,
    507         r: minDistFor(count, domain.w * 2, domain.h * 2),
    508         seed: 11,
    509       });
    510       cloudN = pts.length / 2;
    511       gl.bindBuffer(gl.ARRAY_BUFFER, buf.cloudRest);
    512       gl.bufferData(gl.ARRAY_BUFFER, pts, gl.STATIC_DRAW);
    513       gl.bindBuffer(gl.ARRAY_BUFFER, buf.cloudSeed);
    514       gl.bufferData(gl.ARRAY_BUFFER, seeds(cloudN), gl.STATIC_DRAW);
    515     };
    516 
    517     const morphRest = poisson({ halfW: 0.5, halfH: 0.5, r: minDistFor(morphCount, 1, 1), seed: 23 });
    518     const morphN = morphRest.length / 2;
    519     gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphRest);
    520     gl.bufferData(gl.ARRAY_BUFFER, morphRest, gl.STATIC_DRAW);
    521     gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphSeed);
    522     gl.bufferData(gl.ARRAY_BUFFER, seeds(morphN), gl.STATIC_DRAW);
    523     const fromArr = new Float32Array(morphRest);
    524     const toArr = new Float32Array(morphRest);
    525     gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphFrom);
    526     gl.bufferData(gl.ARRAY_BUFFER, fromArr, gl.DYNAMIC_DRAW);
    527     gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphTo);
    528     gl.bufferData(gl.ARRAY_BUFFER, toArr, gl.DYNAMIC_DRAW);
    529 
    530     const loc = {
    531       rest: gl.getAttribLocation(prog, "a_rest"),
    532       seed: gl.getAttribLocation(prog, "a_seed"),
    533       from: gl.getAttribLocation(prog, "a_from"),
    534       to: gl.getAttribLocation(prog, "a_to"),
    535     };
    536     gl.enableVertexAttribArray(loc.rest);
    537     gl.enableVertexAttribArray(loc.seed);
    538     gl.enableVertexAttribArray(loc.from);
    539     gl.enableVertexAttribArray(loc.to);
    540 
    541     const bindLayer = (layer: 0 | 1) => {
    542       gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphRest : buf.cloudRest);
    543       gl.vertexAttribPointer(loc.rest, 2, gl.FLOAT, false, 0, 0);
    544       gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphSeed : buf.cloudSeed);
    545       gl.vertexAttribPointer(loc.seed, 4, gl.FLOAT, false, 0, 0);
    546       /* the cloud never reads these; point them at something valid */
    547       gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphFrom : buf.cloudRest);
    548       gl.vertexAttribPointer(loc.from, 2, gl.FLOAT, false, 0, 0);
    549       gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphTo : buf.cloudRest);
    550       gl.vertexAttribPointer(loc.to, 2, gl.FLOAT, false, 0, 0);
    551     };
    552 
    553     /* ── Uniforms ── */
    554     const u = (name: string) => gl.getUniformLocation(prog, name);
    555     const U = {
    556       time: u("u_time"),
    557       extent: u("u_extent"),
    558       pr: u("u_pr"),
    559       pscale: u("u_pscale"),
    560       maxPoint: u("u_maxPoint"),
    561       layer: u("u_layer"),
    562       hover: u("u_hover"),
    563       morph: u("u_morph"),
    564       pulse: u("u_pulse"),
    565       iconReady: u("u_iconReady"),
    566       fill: u("u_fill"),
    567       light: u("u_light"),
    568       ringR: u("u_ringR"),
    569       ringW: u("u_ringW"),
    570       ringW2: u("u_ringW2"),
    571       ringDisp: u("u_ringDisp"),
    572       ring: u("u_ring"),
    573       ringLag: u("u_ringLag"),
    574       zone: u("u_zone"),
    575       icon: u("u_icon"),
    576       inkAlpha: u("u_inkAlpha"),
    577       iconAlpha: u("u_iconAlpha"),
    578       c1: u("u_c1"),
    579       c2: u("u_c2"),
    580       c3: u("u_c3"),
    581       ink: u("u_ink"),
    582       accent: u("u_accent"),
    583     };
    584     const range = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) as Float32Array;
    585     gl.uniform1f(U.maxPoint, range ? range[1] : 64);
    586     gl.uniform1f(U.ringW, 0.03);
    587     gl.uniform1f(U.ringW2, 0.1);
    588     gl.uniform1f(U.ringDisp, 0.5);
    589     gl.uniform1f(U.inkAlpha, 0.7);
    590     gl.uniform1f(U.iconAlpha, 0.6);
    591     gl.uniform4f(U.zone, 0, 0, -1, -1);
    592     gl.uniform4f(U.icon, 0, 0, 0.5, 0.5);
    593     gl.uniform1f(U.iconReady, 0);
    594     gl.uniform1f(U.fill, restFill ? 1 : 0);
    595 
    596     /* ── Colour ── */
    597     const INK: Rgb = [0.34, 0.32, 0.47];
    598     let accentName: Accent | null = null;
    599     const fam = tone === "plate" ? opts.plateFamily : opts.pageFamily;
    600     /* The reading weights, not the -soft washes: the pastels sank into
    601        the cream ground and read as white specks. These tokens invert
    602        with the mode, so the dots stay dark on light and light on dark. */
    603     const pushPalette = () => {
    604       /* Is the ground behind this field light? The page's dawn base, or
    605          the plate's night base, after the mode has inverted them. */
    606       const V = opts.vars ?? {
    607         ink: tone === "plate" ? `--color-${fam}-text` : `--color-${fam}-ink`,
    608         base: `--color-${fam}-base`,
    609         iris: `--color-${fam}-iris`,
    610         love: `--color-${fam}-love`,
    611       };
    612       const [br, bg, bb] = readVar(cv, V.base, [0.98, 0.96, 0.93]);
    613       const light = 0.2126 * br + 0.7152 * bg + 0.0722 * bb > 0.5;
    614       gl.uniform1f(U.light, light ? 1 : 0);
    615       gl.uniform1f(U.inkAlpha, light ? 0.9 : 0.7);
    616       const ink = readVar(cv, V.ink, INK);
    617       gl.uniform3fv(U.ink, ink);
    618       gl.uniform3fv(U.c1, ink);
    619       gl.uniform3fv(
    620         U.c2,
    621         readVar(cv, V.iris, [0.47, 0.35, 0.64]),
    622       );
    623       gl.uniform3fv(
    624         U.c3,
    625         readVar(cv, V.love, [0.69, 0.25, 0.42]),
    626       );
    627       gl.uniform3fv(
    628         U.accent,
    629         accentName ? readVar(cv, opts.accentVar(accentName), ink) : ink,
    630       );
    631     };
    632     pushPalette();
    633 
    634     /* ── State the frame reads ── */
    635     const box = { left: 0, pageTop: 0, width: 1, height: 1 };
    636     let unit = unitPx(cv.clientWidth, cv.clientHeight);
    637     const pointer = { x: 0, y: 0, inside: false };
    638     const ring = { x: 0.3, y: 0.1 };
    639     const ringLag = { x: 0.3, y: 0.1 };
    640     const hover = new Tween(0);
    641     const morph = new Tween(1);
    642     let pulseT0 = -Infinity;
    643     let lastMove = -Infinity;
    644     let raf = 0;
    645     let running = false;
    646     let visible = !document.hidden;
    647     let inView = true;
    648     let lost = false;
    649     let lastNow = 0;
    650     const start = performance.now();
    651 
    652     const measureBox = () => {
    653       const r = cv.getBoundingClientRect();
    654       box.left = r.left;
    655       box.pageTop = r.top + window.scrollY;
    656       box.width = r.width || 1;
    657       box.height = r.height || 1;
    658     };
    659 
    660     const size = () => {
    661       const pr = Math.min(2, window.devicePixelRatio || 1);
    662       const w = Math.max(1, Math.round(cv.clientWidth * pr));
    663       const h = Math.max(1, Math.round(cv.clientHeight * pr));
    664       if (cv.width !== w || cv.height !== h) {
    665         cv.width = w;
    666         cv.height = h;
    667       }
    668       gl.viewport(0, 0, w, h);
    669       unit = unitPx(cv.clientWidth, cv.clientHeight);
    670       const ex = cv.clientWidth / 2 / unit;
    671       const ey = cv.clientHeight / 2 / unit;
    672       gl.uniform2f(U.extent, ex, ey);
    673       gl.uniform1f(U.pr, pr);
    674       gl.uniform1f(U.pscale, (cv.clientWidth / 2000) * 0.9);
    675       if (ex + 0.1 > domain.w || ey + 0.1 > domain.h) {
    676         domain.w = Math.max(domain.w, ex + 0.2);
    677         domain.h = Math.max(domain.h, ey + 0.15);
    678         buildCloud();
    679       }
    680     };
    681 
    682     /* ── The shelf list (keep-out for the cloud) and the icon's home ──
    683        Rects are in page pixels; the canvas may sit inside a scaled
    684        composition (the hero scales its 1920×1080 artboard), so a page
    685        pixel is `scale` canvas pixels. */
    686     let zoneEl: HTMLElement | null = null;
    687     let zoneRo: ResizeObserver | null = null;
    688     let iconEl: HTMLElement | null = null;
    689     const toField = (px: number, py: number, c: DOMRect) => {
    690       const k = unit * (c.width / Math.max(1, cv.clientWidth));
    691       return [(px - c.left - c.width / 2) / k, (c.height / 2 - (py - c.top)) / k];
    692     };
    693     const pageUnit = (c: DOMRect) => unit * (c.width / Math.max(1, cv.clientWidth));
    694     /* The hero swaps between two layouts at 1280px and rebuilds the list
    695        when it does, so a held reference goes stale: re-find it whenever
    696        the one we have has left the document. */
    697     const adoptZone = (el: HTMLElement) => {
    698       if (zoneEl === el) return;
    699       zoneRo?.disconnect();
    700       zoneEl = el;
    701       zoneRo = new ResizeObserver(() => measureZone());
    702       zoneRo.observe(el);
    703     };
    704     /* Where the icon forms: a `[data-field-icon]` box inside the field's
    705        own host if there is one (the plate marks its open upper half),
    706        otherwise centred on the shelf list. */
    707     const findIcon = () => {
    708       const host = cv.closest("[data-field-host]");
    709       iconEl = host?.querySelector<HTMLElement>("[data-field-icon]") ?? null;
    710     };
    711     const measureZone = () => {
    712       if (!zoneEl || !zoneEl.isConnected) {
    713         const el = document.querySelector<HTMLElement>("[data-index-zone]");
    714         if (el) adoptZone(el);
    715       }
    716       if (withIcons && (!iconEl || !iconEl.isConnected)) findIcon();
    717       const c = cv.getBoundingClientRect();
    718       if (withCloud && zoneEl && zoneEl.isConnected) {
    719         const z = zoneEl.getBoundingClientRect();
    720         const [x0, y0] = toField(z.left, z.bottom, c);
    721         const [x1, y1] = toField(z.right, z.top, c);
    722         gl.uniform4f(U.zone, x0, y0, x1, y1);
    723       } else {
    724         gl.uniform4f(U.zone, 0, 0, -1, -1);
    725       }
    726       if (!withIcons) return;
    727       const home = iconEl && iconEl.isConnected ? iconEl : zoneEl && zoneEl.isConnected ? zoneEl : null;
    728       if (!home) {
    729         gl.uniform4f(U.icon, 0, 0, 0.5, 0.5);
    730         return;
    731       }
    732       const z = home.getBoundingClientRect();
    733       const k = c.width / Math.max(1, cv.clientWidth);
    734       const sizePx = Math.min(440 * k, Math.max(96 * k, 0.9 * Math.min(z.width, z.height)));
    735       const [cx, cy] = toField(z.left + z.width / 2, z.top + z.height / 2, c);
    736       const s = sizePx / pageUnit(c);
    737       gl.uniform4f(U.icon, cx, cy, s, s);
    738     };
    739     let zoneTimer = 0;
    740     const findZone = (attempt = 0) => {
    741       const el = document.querySelector<HTMLElement>("[data-index-zone]");
    742       if (el) {
    743         adoptZone(el);
    744         measureZone();
    745         return;
    746       }
    747       if (attempt < 8) zoneTimer = window.setTimeout(() => findZone(attempt + 1), 250);
    748     };
    749 
    750     /* ── Icons ── */
    751     let targets: Float32Array[] | null = null;
    752     let curIndex: number | null = null;
    753     let pending: LitShelf = heroShelf.get();
    754     let cancelled = false;
    755     const scratch = document.createElement("canvas");
    756     const jitter = rng(99);
    757     const jitters = new Float32Array(morphN * 2);
    758     for (let i = 0; i < jitters.length; i++) jitters[i] = jitter();
    759 
    760     /* The icon answers the hand only: a shelf the loop lights on its own
    761        tour leaves the cloud as it is, so the gather always means "you
    762        are pointing at this". */
    763     const applyShelf = (s: LitShelf, now: number, snap = false) => {
    764       pending = s;
    765       /* One layout read per hover change: cheap, and it means the icon
    766          always forms on the list as it is laid out right now. */
    767       const wanted = s.index !== null && (s.held || tour);
    768       if (wanted) measureZone();
    769       if (!wanted || s.index === null) {
    770         hover.go(0, 800, now);
    771         return;
    772       }
    773       hover.go(1, 500, now);
    774       if (!targets || s.index === curIndex) return;
    775       /* continue from wherever the last gather got to */
    776       const m = easeOutCubic(Math.min(1, morph.value));
    777       for (let i = 0; i < fromArr.length; i++) fromArr[i] = fromArr[i] + (toArr[i] - fromArr[i]) * m;
    778       toArr.set(targets[s.index]);
    779       gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphFrom);
    780       gl.bufferSubData(gl.ARRAY_BUFFER, 0, fromArr);
    781       gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphTo);
    782       gl.bufferSubData(gl.ARRAY_BUFFER, 0, toArr);
    783       curIndex = s.index;
    784       if (snap) {
    785         morph.set(1);
    786       } else {
    787         morph.set(0);
    788         morph.go(1, 600, now);
    789         pulseT0 = now;
    790       }
    791       if (s.accent !== accentName) {
    792         accentName = s.accent;
    793         pushPalette();
    794       }
    795     };
    796 
    797     /* ── Frames ── */
    798     const drawFrame = (t: number, now: number) => {
    799       if (lost) return;
    800       gl.uniform1f(U.time, t);
    801       gl.uniform1f(U.ringR, 0.175 + Math.sin(t) * 0.03 + Math.cos(t * 3) * 0.02);
    802       gl.uniform2f(U.ring, ring.x, ring.y);
    803       gl.uniform2f(U.ringLag, ringLag.x, ringLag.y);
    804       gl.uniform1f(U.hover, hover.value);
    805       gl.uniform1f(U.morph, morph.value);
    806       const pulse = (now - pulseT0) / 2000;
    807       gl.uniform1f(U.pulse, pulse >= 0 && pulse < 1 ? pulse : -1);
    808       gl.clear(gl.COLOR_BUFFER_BIT);
    809       gl.uniform1f(U.layer, 0);
    810       if (withCloud) {
    811         bindLayer(0);
    812         gl.drawArrays(gl.POINTS, 0, cloudN);
    813       }
    814       if (withIcons) {
    815         gl.uniform1f(U.layer, 1);
    816         bindLayer(1);
    817         gl.drawArrays(gl.POINTS, 0, morphN);
    818       }
    819     };
    820 
    821     const still = () => {
    822       size();
    823       ring.x = ringLag.x = 0.3;
    824       ring.y = ringLag.y = 0.1;
    825       /* A held row still gets its icon with motion off; otherwise the
    826          frame is the plain cloud. */
    827       const s = withIcons ? heroShelf.get() : { index: null, accent: null, held: false };
    828       if (targets) applyShelf(s, performance.now(), true);
    829       hover.set((s.held || tour) && s.index !== null ? 1 : 0);
    830       morph.set(1);
    831       drawFrame(HELD_TIME, -Infinity);
    832       cv.classList.add("is-live");
    833     };
    834 
    835     const tick = (now: number) => {
    836       raf = requestAnimationFrame(tick);
    837       const t = (now - start) / 1000;
    838       const dt = Math.min(0.05, (now - lastNow) / 1000 || 0.016);
    839       lastNow = now;
    840       /* The ring drifts on its own, and only partly toward the hand:
    841          a 2%/frame lerp toward the pointer is what reads as weight. */
    842       const wx = 0.35 * Math.sin(t * 0.19) + 0.12 * Math.sin(t * 0.47);
    843       const wy = 0.3 * Math.cos(t * 0.14) + 0.1 * Math.cos(t * 0.37);
    844       const follow = pointer.inside && now - lastMove < IDLE_MS;
    845       const tx = follow ? pointer.x + wx * 0.1 : wx * 0.6;
    846       const ty = follow ? pointer.y + wy * 0.1 : wy * 0.6;
    847       const k = 1 - Math.pow(1 - (follow ? 0.02 : 0.01), dt * 60);
    848       ring.x += (tx - ring.x) * k;
    849       ring.y += (ty - ring.y) * k;
    850       const k2 = 1 - Math.pow(1 - 0.06, dt * 60);
    851       ringLag.x += (ring.x - ringLag.x) * k2;
    852       ringLag.y += (ring.y - ringLag.y) * k2;
    853       hover.tick(now);
    854       morph.tick(now);
    855       drawFrame(t, now);
    856       cv.classList.add("is-live");
    857     };
    858 
    859     const sync = () => {
    860       const want = live && visible && inView;
    861       if (want && !running) {
    862         running = true;
    863         size();
    864         lastNow = performance.now();
    865         raf = requestAnimationFrame(tick);
    866       } else if (!want && running) {
    867         running = false;
    868         cancelAnimationFrame(raf);
    869       }
    870       if (!live) still();
    871     };
    872 
    873     const onMove = (e: PointerEvent) => {
    874       if (e.pointerType === "touch") return;
    875       const top = box.pageTop - window.scrollY;
    876       const nx = ((e.clientX - box.left) / box.width) * 2 - 1;
    877       const ny = -(((e.clientY - top) / box.height) * 2 - 1);
    878       if (nx < -1.05 || nx > 1.05 || ny < -1.05 || ny > 1.05) {
    879         pointer.inside = false;
    880         return;
    881       }
    882       pointer.inside = true;
    883       pointer.x = (nx * box.width) / 2 / unit;
    884       pointer.y = (ny * box.height) / 2 / unit;
    885       lastMove = e.timeStamp;
    886     };
    887     const onVis = () => {
    888       visible = !document.hidden;
    889       sync();
    890     };
    891     const onMode = () => {
    892       pushPalette();
    893       if (!running) still();
    894     };
    895     const onLost = (e: Event) => {
    896       e.preventDefault();
    897       lost = true;
    898       cancelAnimationFrame(raf);
    899       setFailed(true);
    900     };
    901     const unsubscribe = withIcons
    902       ? heroShelf.subscribe((s) => {
    903           if (live) applyShelf(s, performance.now());
    904           else still();
    905         })
    906       : () => {};
    907 
    908     let settleRaf = 0;
    909     let settleTimer = 0;
    910     const measureSettled = () => {
    911       cancelAnimationFrame(settleRaf);
    912       clearTimeout(settleTimer);
    913       settleRaf = requestAnimationFrame(() => {
    914         settleRaf = requestAnimationFrame(() => {
    915           measureBox();
    916           measureZone();
    917           if (!running) still();
    918         });
    919       });
    920       /* and once more after the hero's own observer has re-rendered */
    921       settleTimer = window.setTimeout(() => {
    922         measureBox();
    923         measureZone();
    924         if (!running) still();
    925       }, 160);
    926     };
    927     const ro = new ResizeObserver(() => {
    928       measureBox();
    929       size();
    930       measureZone();
    931       measureSettled();
    932       if (!running) still();
    933     });
    934     ro.observe(cv);
    935     const io =
    936       typeof IntersectionObserver !== "undefined"
    937         ? new IntersectionObserver(([e]) => {
    938             inView = e.isIntersecting;
    939             sync();
    940           })
    941         : null;
    942     io?.observe(cv);
    943 
    944     if (live) window.addEventListener("pointermove", onMove, { passive: true });
    945     window.addEventListener("resize", measureSettled);
    946     document.addEventListener("visibilitychange", onVis);
    947     window.addEventListener("daemonmodechange", onMode);
    948     const modeMo = new MutationObserver(onMode);
    949     modeMo.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-mode", "data-theme", "style"] });
    950     cv.addEventListener("webglcontextlost", onLost);
    951 
    952     /* Prepare the icons; the morph layer twinkles at rest until they land. */
    953     (async () => {
    954       if (!withIcons) return;
    955       const svgs = opts.icons ?? [];
    956       const out: Float32Array[] = [];
    957       for (const svg of svgs) {
    958         const ink = await rasterise(svg, scratch);
    959         if (cancelled) return;
    960         out.push(targetsFor(morphRest, nearestInked(ink), (i) => jitters[i]));
    961       }
    962       if (cancelled) return;
    963       targets = out;
    964       gl.uniform1f(U.iconReady, 1);
    965       applyShelf(pending, performance.now(), true);
    966       if (!running) still();
    967     })().catch(() => {
    968       /* No icon: the morph layer just twinkles. */
    969     });
    970 
    971     measureBox();
    972     findZone();
    973     if (withIcons) {
    974       if (live) applyShelf(heroShelf.get(), performance.now(), true);
    975     }
    976     sync();
    977 
    978     if (import.meta.env.DEV) {
    979       (window as unknown as Record<string, unknown>)[withIcons ? "__dmnField" : "__dmnCloud"] = {
    980         get hover() {
    981           return hover.value;
    982         },
    983         get morph() {
    984           return morph.value;
    985         },
    986         get index() {
    987           return curIndex;
    988         },
    989         get ready() {
    990           return targets !== null;
    991         },
    992         get counts() {
    993           return { cloud: cloudN, morph: morphN };
    994         },
    995         get ring() {
    996           return { ...ring };
    997         },
    998         get iconPx() {
    999           /* the icon's centre and size in CSS px, page-relative to the canvas */
   1000           const v = gl.getUniform(prog, U.icon as WebGLUniformLocation) as Float32Array;
   1001           const c = cv.getBoundingClientRect();
   1002           const k = unit * (c.width / Math.max(1, cv.clientWidth));
   1003           return { x: c.left + c.width / 2 + v[0] * k, y: c.top + c.height / 2 - v[1] * k, size: v[2] * k };
   1004         },
   1005         get pointer() {
   1006           return { ...pointer, box: { ...box }, lastMove };
   1007         },
   1008         set(index: number | null, accent: Accent | null) {
   1009           applyShelf({ index, accent, held: true }, performance.now());
   1010         },
   1011       };
   1012     }
   1013 
   1014     return () => {
   1015       cancelled = true;
   1016       running = false;
   1017       cancelAnimationFrame(raf);
   1018       clearTimeout(zoneTimer);
   1019       unsubscribe();
   1020       ro.disconnect();
   1021       io?.disconnect();
   1022       zoneRo?.disconnect();
   1023       window.removeEventListener("pointermove", onMove);
   1024       window.removeEventListener("resize", measureSettled);
   1025       cancelAnimationFrame(settleRaf);
   1026       clearTimeout(settleTimer);
   1027       document.removeEventListener("visibilitychange", onVis);
   1028       window.removeEventListener("daemonmodechange", onMode);
   1029       modeMo.disconnect();
   1030       cv.removeEventListener("webglcontextlost", onLost);
   1031       Object.values(buf).forEach((b) => b && gl.deleteBuffer(b));
   1032       gl.deleteProgram(prog);
   1033       gl.deleteShader(vs);
   1034       gl.deleteShader(fs);
   1035     };
   1036 }