File size: 15,294 Bytes
453b484
dcf704e
453b484
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
dcf704e
 
 
453b484
dcf704e
 
 
453b484
dcf704e
 
394e14f
dcf704e
 
 
 
394e14f
dcf704e
 
453b484
 
 
 
 
 
 
 
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
 
 
 
 
 
 
 
 
 
 
 
 
 
dcf704e
453b484
 
 
dcf704e
453b484
 
 
 
dcf704e
453b484
dcf704e
453b484
 
dcf704e
 
 
453b484
 
dcf704e
453b484
 
 
 
 
 
 
 
 
 
 
 
 
dcf704e
453b484
 
dcf704e
 
 
 
453b484
 
dcf704e
453b484
 
dcf704e
453b484
dcf704e
 
453b484
 
 
dcf704e
 
 
 
 
 
 
 
 
 
453b484
dcf704e
 
 
 
 
 
 
453b484
dcf704e
 
 
 
 
 
453b484
dcf704e
 
 
 
 
453b484
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
 
 
 
dcf704e
453b484
 
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
dcf704e
 
 
 
 
453b484
 
 
dcf704e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453b484
dcf704e
 
 
453b484
dcf704e
453b484
 
 
 
 
 
 
 
dcf704e
 
 
453b484
 
dcf704e
453b484
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/* ============================================================================
 * a11oy_landing.js — the living-proof hero for the a11oy front door (v2).
 *
 * THE MOTIF (NVIDIA technical-sublime + True Anomaly operational COP, restrained
 * like Anthropic): a holographic PROOF LATTICE / signed-receipt constellation.
 * It encodes REAL state, not decorative noise:
 *   • NODES   = the recent receipts from GET /api/a11oy/v1/ledger. Each instanced
 *               node is one real receipt {seq, action, receipt_id}; the newest
 *               (chain head) sits innermost and brightest.
 *   • EDGES   = the SHA3-256 hash-chain links between consecutive receipts —
 *               the append-only ledger drawn as a lattice.
 *   • PULSE   = a token traveling the chain (genesis → head). When the live lake
 *               (GET /api/lake/v1/health.total_receipts) actually GROWS between
 *               polls, a brighter pulse fires to the head — a genuinely fresh
 *               signed receipt. No growth → only the ambient chain traversal of
 *               the receipts that already exist. We never fake a "new signature".
 *   • GLOW    = the governance core's intensity is tied to the advisory Λ from
 *               GET /api/a11oy/v1/lambda/org. Λ is Conjecture 1 (advisory bound),
 *               so the glow is a mood, never a pass/fail oracle.
 *
 * HONEST DEGRADE: if the ledger is unreachable we do NOT invent receipts or a
 * fake flow. The scene falls back to a calm, still lattice (dim wireframe core +
 * sparse static dust), reports dataState:"unreachable", and emits no pulses.
 *
 * SOVEREIGN: Three.js r160 (MIT) only, vendored in-image under /hero/vendor3d
 * (importmap in a11oy_landing.html). 0 runtime CDN. Bloom is faked with additive
 * fresnel + a layered halo sprite (no postprocessing dependency = GPU-light).
 *
 * KANCHAY palette: --void #080c14, --proof #3af4c8, --lattice #5b8dee. Calm and
 * deep-near-black; the lattice whispers, it never floods. Purple is banned.
 *
 * PERF: instanced nodes (one draw call), capped DPR, <13ms/frame target. Honors
 * prefers-reduced-motion (one static frame, no polling) and pauses when the hero
 * scrolls out of view (IntersectionObserver). Live figures are also fetched and
 * labelled by a11oy_landing.html — this module owns only the canvas.
 *
 * Doctrine v11: Λ = Conjecture 1 (advisory, NOT a theorem). locked-proven = 8.
 * Nothing here fabricates a number; every visual quantity is real or honestly off.
 * ========================================================================== */
import * as THREE from "three";

const REDUCED = window.matchMedia &&
  window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const MOBILE = Math.min(window.innerWidth, window.innerHeight) < 720 ||
  /Mobi|Android/i.test(navigator.userAgent);

// KANCHAY palette, held at restrained brightness so the deep void dominates.
const VOID    = 0x080c14;
const PROOF   = new THREE.Color(0x3af4c8); // --proof  : freshest receipt / head
const LATTICE = new THREE.Color(0x5b8dee); // --lattice: older receipts / chain
const DUSK    = new THREE.Color(0x101a2e); // near-void: ambient dust, denied glow

const LEDGER_URL = "/api/a11oy/v1/ledger";
const LAMBDA_URL = "/api/a11oy/v1/lambda/org";
const LAKE_URL   = "/api/lake/v1/health";

// Λ (Conjecture 1) → glow factor. Below the 0.80 advisory band the core is dim;
// near 1.0 it is at full restrained glow. Never implies a "pass".
function lambdaGlow(L) {
  if (typeof L !== "number" || !isFinite(L)) return 0.45; // neutral, no claim
  return Math.max(0.25, Math.min(1.0, (L - 0.80) / 0.20));
}

async function getJSON(url, ms = 3500) {
  const ctl = new AbortController();
  const to = setTimeout(() => ctl.abort(), ms);
  try {
    const r = await fetch(url, { cache: "no-store", signal: ctl.signal });
    if (!r.ok) return null;
    return await r.json();
  } catch (_e) {
    return null;
  } finally {
    clearTimeout(to);
  }
}

export function mountHero(canvas) {
  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({
      canvas, antialias: !MOBILE, alpha: true, powerPreference: "high-performance",
    });
  } catch (e) {
    return { ok: false, reason: String(e) };
  }
  if (!renderer.getContext()) return { ok: false, reason: "no-webgl" };

  const DPR = Math.min(window.devicePixelRatio || 1, MOBILE ? 1.5 : 2);
  renderer.setPixelRatio(DPR);
  renderer.setClearColor(VOID, 0); // transparent — the page's deep void shows through

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(46, 1, 0.1, 100);
  camera.position.set(0, 0, 8.4);

  const root = new THREE.Group();
  scene.add(root);

  // ---- Governance core: fresnel glass icosahedron, glow tied to Λ ------------
  const coreMat = new THREE.ShaderMaterial({
    transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,
    uniforms: {
      uTime: { value: 0 },
      uGlow: { value: 0.45 },  // set from real Λ once fetched
      uProof: { value: new THREE.Vector3(PROOF.r, PROOF.g, PROOF.b) },
      uLattice: { value: new THREE.Vector3(LATTICE.r, LATTICE.g, LATTICE.b) },
    },
    vertexShader: /* glsl */`
      varying vec3 vN; varying vec3 vView; uniform float uTime;
      void main(){
        vN = normalize(normalMatrix * normal);
        vec3 p = position;
        float w = sin(uTime*0.8 + position.y*2.0)*0.03
                + sin(uTime*1.3 + position.x*3.0)*0.02;
        p += normal * w;
        vec4 mv = modelViewMatrix * vec4(p,1.0);
        vView = normalize(-mv.xyz);
        gl_Position = projectionMatrix * mv;
      }`,
    fragmentShader: /* glsl */`
      precision highp float;
      varying vec3 vN; varying vec3 vView;
      uniform float uTime; uniform float uGlow; uniform vec3 uProof; uniform vec3 uLattice;
      void main(){
        float fres = pow(1.0 - max(dot(normalize(vN), normalize(vView)), 0.0), 2.4);
        float breathe = 0.5 + 0.5*sin(uTime*1.4);
        float glow = fres * (0.22 + 0.16*breathe) * uGlow;
        vec3 col = mix(uLattice, uProof, fres);
        gl_FragColor = vec4(col * glow, clamp(glow, 0.0, 0.42));
      }`,
  });
  const core = new THREE.Mesh(new THREE.IcosahedronGeometry(1.35, MOBILE ? 2 : 4), coreMat);
  root.add(core);

  // deny-by-default cage — dim hairlines over the core
  const cage = new THREE.LineSegments(
    new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.42, 1)),
    new THREE.LineBasicMaterial({ color: 0x274063, transparent: true, opacity: 0.16 })
  );
  root.add(cage);

  // ---- Ambient dust (pure decoration, clearly NOT data) ----------------------
  // Sparse, very dim, static-ish depth field. Never labelled as receipts.
  const dustN = REDUCED ? 350 : (MOBILE ? 600 : 1100);
  const dpos = new Float32Array(dustN * 3);
  for (let i = 0; i < dustN; i++) {
    const t = i / dustN, phi = Math.acos(1 - 2 * t);
    const theta = Math.PI * (1 + Math.sqrt(5)) * i, r = 4.5 + Math.random() * 4.0;
    dpos[i*3+0] = Math.sin(phi) * Math.cos(theta) * r;
    dpos[i*3+1] = Math.cos(phi) * r;
    dpos[i*3+2] = Math.sin(phi) * Math.sin(theta) * r;
  }
  const dustGeo = new THREE.BufferGeometry();
  dustGeo.setAttribute("position", new THREE.BufferAttribute(dpos, 3));
  const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({
    color: DUSK, size: 0.6 * DPR, sizeAttenuation: false,
    transparent: true, opacity: 0.22, depthWrite: false, blending: THREE.AdditiveBlending,
  }));
  root.add(dust);

  // ---- Receipt constellation (instanced nodes + chain edges + pulse) ---------
  // Built only from REAL ledger data. Empty until/unless the ledger is reachable.
  const MAX_NODES = 24;
  const nodeGeo = new THREE.SphereGeometry(0.06, 10, 10);
  const nodeMat = new THREE.MeshBasicMaterial({
    transparent: true, opacity: 0.92, blending: THREE.AdditiveBlending, depthWrite: false,
  });
  const nodes = new THREE.InstancedMesh(nodeGeo, nodeMat, MAX_NODES);
  nodes.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
  nodes.count = 0; // nothing claimed until real receipts arrive
  nodes.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(MAX_NODES * 3), 3);
  root.add(nodes);

  const edgeMat = new THREE.LineBasicMaterial({
    color: 0x5b8dee, transparent: true, opacity: 0.0, // raised once edges exist
    blending: THREE.AdditiveBlending, depthWrite: false,
  });
  const edges = new THREE.Line(new THREE.BufferGeometry(), edgeMat);
  root.add(edges);

  const pulse = new THREE.Mesh(
    new THREE.SphereGeometry(0.11, 12, 12),
    new THREE.MeshBasicMaterial({ color: PROOF, transparent: true, opacity: 0.0,
      blending: THREE.AdditiveBlending, depthWrite: false })
  );
  root.add(pulse);

  const _m = new THREE.Matrix4();
  const _v = new THREE.Vector3();
  let chainCurve = null;          // CatmullRom through node positions (genesis→head)
  let nodePositions = [];         // Vector3[]
  let pulseT = 0;                 // 0..1 along the chain
  let pulseBoost = 0;             // brief extra brightness on a genuinely fresh receipt
  let receiptCount = 0;           // honest count actually rendered

  // Lay out N receipts on a gentle helix around the core: genesis outer/low,
  // head inner/high (append-only growth climbs toward the governed core).
  function layout(n) {
    nodePositions = [];
    for (let i = 0; i < n; i++) {
      const f = n > 1 ? i / (n - 1) : 0;          // 0 genesis → 1 head
      const ang = f * Math.PI * 4.0;               // two turns of the helix
      const rad = 3.4 - f * 1.4;                   // spirals inward to the core
      const y = (f - 0.5) * 3.6;                   // climbs upward
      nodePositions.push(new THREE.Vector3(Math.cos(ang) * rad, y, Math.sin(ang) * rad));
    }
    chainCurve = nodePositions.length > 1
      ? new THREE.CatmullRomCurve3(nodePositions, false, "catmullrom", 0.4) : null;
  }

  // Apply REAL receipts to the constellation. `list` = [{seq,action,receipt_id}].
  function applyReceipts(list) {
    const n = Math.min(MAX_NODES, list.length);
    receiptCount = n;
    layout(n);
    for (let i = 0; i < n; i++) {
      const f = n > 1 ? i / (n - 1) : 1;
      _m.makeScale(0.7 + f * 0.9, 0.7 + f * 0.9, 0.7 + f * 0.9); // head a touch larger
      _m.setPosition(nodePositions[i]);
      nodes.setMatrixAt(i, _m);
      const c = LATTICE.clone().lerp(PROOF, f); // older=lattice blue, head=proof teal
      nodes.setColorAt(i, c);
    }
    nodes.count = n;
    nodes.instanceMatrix.needsUpdate = true;
    if (nodes.instanceColor) nodes.instanceColor.needsUpdate = true;

    if (n > 1) {
      const pts = chainCurve.getPoints(Math.max(32, n * 4));
      edges.geometry.dispose();
      edges.geometry = new THREE.BufferGeometry().setFromPoints(pts);
      edgeMat.opacity = 0.20;
      pulse.material.opacity = 0.0; // animated in the frame loop
    } else {
      edgeMat.opacity = 0.0;
    }
  }

  // Calm, still, honestly-empty state — no receipts invented, no flow faked.
  function showUnreachable() {
    receiptCount = 0;
    nodes.count = 0;
    edgeMat.opacity = 0.0;
    pulse.material.opacity = 0.0;
    chainCurve = null;
    cage.material.opacity = 0.22; // the lattice cage carries the quiet, honest mood
  }

  // ---- resize ---------------------------------------------------------------
  function resize() {
    const r = canvas.getBoundingClientRect();
    const w = Math.max(1, r.width), h = Math.max(1, r.height);
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize, { passive: true });

  // ---- pointer parallax -----------------------------------------------------
  let px = 0, py = 0, tx = 0, ty = 0;
  if (!REDUCED) {
    window.addEventListener("pointermove", (e) => {
      tx = (e.clientX / window.innerWidth - 0.5);
      ty = (e.clientY / window.innerHeight - 0.5);
    }, { passive: true });
  }

  const hero = {
    ok: true, renderer, dataState: "loading", receiptCount: 0, lambda: null,
    setLambda(v) {
      hero.lambda = (typeof v === "number") ? v : null;
      coreMat.uniforms.uGlow.value = lambdaGlow(v);
    },
    setReceipts(list) {
      if (Array.isArray(list) && list.length) {
        applyReceipts(list);
        hero.dataState = "live"; hero.receiptCount = receiptCount;
      } else {
        showUnreachable(); hero.dataState = "unreachable"; hero.receiptCount = 0;
      }
    },
    fireFreshReceipt() { pulseT = 0; pulseBoost = 1.0; }, // genuine new signed receipt
  };

  // ---- render loop ----------------------------------------------------------
  const clock = new THREE.Clock();
  let raf = 0, running = true;

  function render(t) {
    coreMat.uniforms.uTime.value = t;
    px += (tx - px) * 0.04; py += (ty - py) * 0.04;
    root.rotation.y = t * 0.10 + px * 0.6;
    root.rotation.x = py * 0.35;
    cage.rotation.y = -t * 0.04;
    dust.rotation.y = t * 0.015;

    // signed-receipt pulse traverses the real chain (only when we have one)
    if (chainCurve) {
      pulseT += 0.0016 + 0.004 * pulseBoost; // fresh receipts travel faster + brighter
      if (pulseT >= 1) { pulseT = 0; pulseBoost = 0; }
      chainCurve.getPointAt(pulseT, _v);
      pulse.position.copy(_v);
      const base = 0.45 + 0.25 * Math.sin(t * 3.0);
      pulse.material.opacity = Math.min(1.0, base + pulseBoost * 0.5);
      const s = 1.0 + pulseBoost * 0.8;
      pulse.scale.setScalar(s);
    }
    renderer.render(scene, camera);
  }

  function frame() {
    if (!running) return;
    render(clock.getElapsedTime());
    raf = requestAnimationFrame(frame);
  }

  // ---- live data: fetch real receipts + Λ; poll lake for genuine growth ------
  async function boot() {
    const [ledger, lam] = await Promise.all([getJSON(LEDGER_URL), getJSON(LAMBDA_URL)]);
    if (lam && typeof lam.lambda_org === "number") hero.setLambda(lam.lambda_org);
    if (ledger && Array.isArray(ledger.receipts) && ledger.receipts.length) {
      hero.setReceipts(ledger.receipts);
    } else {
      hero.setReceipts(null); // honest still state — no fabricated receipts
    }
  }

  let lastLakeTotal = null;
  async function pollLake() {
    const h = await getJSON(LAKE_URL);
    const total = h && typeof h.total_receipts === "number" ? h.total_receipts : null;
    if (total !== null && lastLakeTotal !== null && total > lastLakeTotal) {
      hero.fireFreshReceipt(); // a real new receipt landed in the lake
    }
    if (total !== null) lastLakeTotal = total;
  }

  if (REDUCED) {
    // Static, dignified single frame. Still fetch real data (no animation/polling).
    boot().then(() => render(1.2));
    render(1.2);
  } else {
    boot();
    const io = new IntersectionObserver((ents) => {
      for (const en of ents) {
        if (en.isIntersecting && !running) { running = true; clock.start(); frame(); }
        else if (!en.isIntersecting && running) { running = false; cancelAnimationFrame(raf); }
      }
    }, { threshold: 0.01 });
    io.observe(canvas);
    frame();
    // poll the lake every 15s for genuine receipt growth (honest fresh pulse)
    const poll = setInterval(() => { if (running) pollLake(); }, 15000);
    hero._stopPoll = () => clearInterval(poll);
  }

  return hero;
}