Spaces:
Running
chore(sync): mirror front-door files to Space (hf-sync)
Browse filesAutomated front-door sync from szl-holdings/a11oy main via hf-sync.
Added/updated: cathedral.html, cathedral_genius.html, console/docs.html, console/index.html, console/pricing.html, console/throne-room.html, console/throne-room.js, pages/api-keys.html, pages/audit.html, pages/ayni.html, pages/brain-dual.html, pages/brain-jack.html, pages/brain.html, pages/chaski.html, pages/codex-kernel.html, pages/company.html, pages/compliance.html, pages/console.html, pages/counter-uas.html, pages/cued-engagement.html, pages/docs.html, pages/evidence.html, pages/gap-report.html, pages/harvest.html, pages/hatun-mcp.html, pages/hub.html, pages/integrations.html, pages/landing.html, pages/mesh.html, pages/observability.html, pages/operator_organ.html, pages/pricing.html, pages/run-all.html, pages/sdk.html, pages/security.html, pages/status.html, pages/substrate.html, pages/superpowers.html, pages/throne-room.html, pages/throne-room.js, pages/uds.html, pages/upgrades.html, pages/wallpa.html, pages/warhacker.html, pages/wasi-rikuq.html, pages/wires.html, static/a11oy_cathedral.js, static/cathedral_app.js, static/shared/szl_codename_sanitizer.js, static/shared/szl_holo3d.js, static/shared/szl_label_engine.js, static/shared/szl_receipt_cosign.js
Deleted (gone from GitHub main): (none)
Keeps the served front-door (pages/*.html, console/*.html) identical
to GitHub main so an HF factory rebuild never drops a GitHub edit or
keeps serving a page that was deleted on GitHub.
- static/shared/szl_holo3d.js +723 -0
|
@@ -0,0 +1,723 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
* © 2026 SZL Holdings. szl_holo3d.js — the SZL 3D / HOLOGRAPHIC SUBSTRATE KIT.
|
| 3 |
+
* =====================================================================================
|
| 4 |
+
* THE reusable holographic substrate every SZL frontier surface renders on.
|
| 5 |
+
* Vendored locally, served at /static/shared/szl_holo3d.js — ZERO RUNTIME CDN.
|
| 6 |
+
* Byte-identical across a11oy and killinchu (shared-source drift guard enforced).
|
| 7 |
+
*
|
| 8 |
+
* DESIGN INTENT (Surface 10 of FRONTIER_RESEARCH.md): a defense-grade dark-glass
|
| 9 |
+
* holographic scene shell with a node/edge GRAPH renderer (proof-DAGs, factory
|
| 10 |
+
* graphs, ontology organisms), a lightweight GLOBE renderer, a Λ-driven TRUST
|
| 11 |
+
* SPHERE primitive, a SIGNED-PULSE edge animation (light flows along an edge when
|
| 12 |
+
* a receipt is signed), and a time-slider replay hook.
|
| 13 |
+
*
|
| 14 |
+
* RENDERER STRATEGY (honest, air-gap-clean, 0-CDN):
|
| 15 |
+
* - SELF-CONTAINED: the kit ships its OWN minimal WebGL2 renderer (no hard
|
| 16 |
+
* dependency on three.js). This guarantees byte-identical behaviour on BOTH
|
| 17 |
+
* apps regardless of whether a heavy 3D vendor bundle is present.
|
| 18 |
+
* - WebGL2 BASELINE: feature-detected; the default render path.
|
| 19 |
+
* - WebGPU PROGRESSIVE ENHANCEMENT: feature-detected (navigator.gpu). When
|
| 20 |
+
* present we expose it via caps.webgpu = true and (ROADMAP) a WGSL compute
|
| 21 |
+
* layout path; we DO NOT claim a WebGPU pipeline we have not built — the
|
| 22 |
+
* baseline WebGL2 path renders identically. Honestly labelled ROADMAP.
|
| 23 |
+
* - CPU / OLD-GPU FALLBACK: if WebGL2 is unavailable the kit renders an HONEST
|
| 24 |
+
* 2D <canvas> projection (never a blank canvas) with a visible
|
| 25 |
+
* "2D FALLBACK" badge. Graph, trust sphere, globe and signed-pulse all have
|
| 26 |
+
* a 2D path so the surface is always legible.
|
| 27 |
+
*
|
| 28 |
+
* DOCTRINE: 0 runtime CDN; Λ = Conjecture 1 (a bounded score < 1.0, NOT a
|
| 29 |
+
* theorem); trust < 100%; honest SAMPLE labels on demo data; never claim live
|
| 30 |
+
* data; 0 visible character codenames. window.SZLLabels drives honest badges.
|
| 31 |
+
*
|
| 32 |
+
* Citations (build references, not runtime calls):
|
| 33 |
+
* three.js / R3F — https://github.com/pmndrs/react-three-fiber
|
| 34 |
+
* deck.gl (vis.gl)— https://github.com/visgl/deck.gl
|
| 35 |
+
* CesiumJS — https://github.com/CesiumGS/cesium
|
| 36 |
+
*
|
| 37 |
+
* Public API: window.SZLHolo
|
| 38 |
+
* SZLHolo.version
|
| 39 |
+
* SZLHolo.capabilities() -> { webgl2, webgpu, mode, dpr }
|
| 40 |
+
* SZLHolo.createScene(mountEl, opts) -> Scene
|
| 41 |
+
* Scene.addGraph(spec) -> GraphHandle (nodes/edges DAG)
|
| 42 |
+
* Scene.addGlobe(spec) -> GlobeHandle (lightweight globe)
|
| 43 |
+
* Scene.addTrustSphere(spec) -> TrustSphereHandle (morph by Λ)
|
| 44 |
+
* Scene.signPulse(edgeId, opts) -> animate light along an edge
|
| 45 |
+
* Scene.timeSlider(opts) -> replay hook (onTime callback)
|
| 46 |
+
* Scene.setLambda(value) -> drive global Λ (< 1.0)
|
| 47 |
+
* Scene.start() / Scene.stop() / Scene.dispose()
|
| 48 |
+
* =====================================================================================
|
| 49 |
+
*/
|
| 50 |
+
(function (root, factory) {
|
| 51 |
+
"use strict";
|
| 52 |
+
var mod = factory();
|
| 53 |
+
if (root) { root.SZLHolo = mod; }
|
| 54 |
+
if (typeof module !== "undefined" && module.exports) { module.exports = mod; }
|
| 55 |
+
})(typeof self !== "undefined" ? self : (typeof window !== "undefined" ? window : null), function () {
|
| 56 |
+
"use strict";
|
| 57 |
+
|
| 58 |
+
var VERSION = "1.0.0";
|
| 59 |
+
|
| 60 |
+
/* ----------------------------------------------------------------------- *
|
| 61 |
+
* Honest palette — Palantir/Datadog-style dark glass, defense-grade.
|
| 62 |
+
* ----------------------------------------------------------------------- */
|
| 63 |
+
var PALETTE = {
|
| 64 |
+
bg0: [0.020, 0.027, 0.043], // deep space navy
|
| 65 |
+
bg1: [0.043, 0.063, 0.098], // glass tint
|
| 66 |
+
grid: "rgba(90,140,200,0.10)",
|
| 67 |
+
node: "#7fd4ff",
|
| 68 |
+
nodeLock: "#ffd479",
|
| 69 |
+
edge: "rgba(120,170,230,0.45)",
|
| 70 |
+
pulse: "#aef6ff",
|
| 71 |
+
text: "#cfe6ff",
|
| 72 |
+
ok: "#46d39a",
|
| 73 |
+
warn: "#ffcf5c",
|
| 74 |
+
bad: "#ff6b6b"
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
/* Λ -> colour. Λ is a bounded score in [0,1) (Conjecture 1; NOT a theorem).
|
| 78 |
+
* Lower Λ = healthier (green); higher = riskier (red). Trust is never 100%. */
|
| 79 |
+
function lambdaColor(lam) {
|
| 80 |
+
lam = clamp01(lam == null ? 0.5 : lam);
|
| 81 |
+
// green (low) -> amber (mid) -> red (high)
|
| 82 |
+
var r, g, b;
|
| 83 |
+
if (lam < 0.5) { var t = lam / 0.5; r = lerp(70, 255, t); g = lerp(211, 207, t); b = lerp(154, 92, t); }
|
| 84 |
+
else { var u = (lam - 0.5) / 0.5; r = lerp(255, 255, u); g = lerp(207, 107, u); b = lerp(92, 107, u); }
|
| 85 |
+
return "rgb(" + (r | 0) + "," + (g | 0) + "," + (b | 0) + ")";
|
| 86 |
+
}
|
| 87 |
+
function clamp01(x) { return x < 0 ? 0 : (x > 1 ? 1 : x); }
|
| 88 |
+
function lerp(a, b, t) { return a + (b - a) * t; }
|
| 89 |
+
|
| 90 |
+
/* ----------------------------------------------------------------------- *
|
| 91 |
+
* Capability detection. WebGL2 baseline, WebGPU enhancement, 2D fallback.
|
| 92 |
+
* ----------------------------------------------------------------------- */
|
| 93 |
+
function detectCaps() {
|
| 94 |
+
var webgl2 = false, webgpu = false;
|
| 95 |
+
try {
|
| 96 |
+
var c = document.createElement("canvas");
|
| 97 |
+
var gl = c.getContext("webgl2");
|
| 98 |
+
webgl2 = !!gl;
|
| 99 |
+
if (gl && gl.getExtension) { /* probe ok */ }
|
| 100 |
+
} catch (e) { webgl2 = false; }
|
| 101 |
+
try { webgpu = (typeof navigator !== "undefined" && !!navigator.gpu); } catch (e2) { webgpu = false; }
|
| 102 |
+
var dpr = (typeof window !== "undefined" && window.devicePixelRatio) ? Math.min(window.devicePixelRatio, 2) : 1;
|
| 103 |
+
var mode = webgl2 ? "webgl2" : "2d";
|
| 104 |
+
return { webgl2: webgl2, webgpu: webgpu, mode: mode, dpr: dpr,
|
| 105 |
+
// WebGPU is feature-detected but the compute-layout pipeline is ROADMAP;
|
| 106 |
+
// we honestly do NOT switch the render path to an unbuilt WebGPU path.
|
| 107 |
+
webgpuPipeline: false };
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* ----------------------------------------------------------------------- *
|
| 111 |
+
* Honest label badge helper (reuses window.SZLLabels when present).
|
| 112 |
+
* ----------------------------------------------------------------------- */
|
| 113 |
+
function badgeHTML(key, text) {
|
| 114 |
+
try {
|
| 115 |
+
if (typeof window !== "undefined" && window.SZLLabels && window.SZLLabels.badgeHTML) {
|
| 116 |
+
return window.SZLLabels.badgeHTML(key, { text: text });
|
| 117 |
+
}
|
| 118 |
+
} catch (e) {}
|
| 119 |
+
// honest inline fallback if the shared label engine is not loaded
|
| 120 |
+
var label = text || key;
|
| 121 |
+
return '<span class="szl-holo-badge" data-k="' + esc(key) + '">' + esc(label) + "</span>";
|
| 122 |
+
}
|
| 123 |
+
function esc(s) {
|
| 124 |
+
return String(s == null ? "" : s)
|
| 125 |
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
| 126 |
+
.replace(/"/g, """).replace(/'/g, "'");
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* small style sheet injected once (self-styled, 0 external font/css) */
|
| 130 |
+
var STYLE_ID = "szl-holo3d-style";
|
| 131 |
+
function ensureStyle(doc) {
|
| 132 |
+
if (!doc || doc.getElementById(STYLE_ID)) { return; }
|
| 133 |
+
var css =
|
| 134 |
+
".szl-holo-root{position:relative;width:100%;height:100%;min-height:240px;" +
|
| 135 |
+
"background:radial-gradient(120% 120% at 50% 0%,#0a1326 0%,#050a14 60%,#03060d 100%);" +
|
| 136 |
+
"overflow:hidden;border-radius:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:" + PALETTE.text + ";}" +
|
| 137 |
+
".szl-holo-root canvas{display:block;width:100%;height:100%;}" +
|
| 138 |
+
".szl-holo-hud{position:absolute;top:8px;left:10px;z-index:5;font-size:11px;letter-spacing:.04em;" +
|
| 139 |
+
"text-shadow:0 1px 3px rgba(0,0,0,.8);pointer-events:none;}" +
|
| 140 |
+
".szl-holo-hud .row{display:flex;gap:6px;align-items:center;margin-bottom:3px;flex-wrap:wrap;}" +
|
| 141 |
+
".szl-holo-badge{display:inline-block;padding:1px 6px;border-radius:6px;font-size:10px;font-weight:600;" +
|
| 142 |
+
"background:rgba(70,150,230,.18);border:1px solid rgba(120,180,240,.35);color:#bfe0ff;}" +
|
| 143 |
+
".szl-holo-badge[data-k='SAMPLE']{background:rgba(90,160,240,.16);}" +
|
| 144 |
+
".szl-holo-badge[data-k='2DFALLBACK']{background:rgba(255,180,90,.18);border-color:rgba(255,190,110,.4);color:#ffd9a8;}" +
|
| 145 |
+
".szl-holo-lambda{position:absolute;bottom:8px;left:10px;z-index:5;font-size:11px;pointer-events:none;}" +
|
| 146 |
+
".szl-holo-slider{position:absolute;bottom:8px;right:10px;left:auto;z-index:6;width:42%;max-width:260px;}" +
|
| 147 |
+
".szl-holo-slider input{width:100%;}" +
|
| 148 |
+
".szl-holo-slider label{font-size:10px;display:block;margin-bottom:2px;opacity:.85;}";
|
| 149 |
+
var el = doc.createElement("style");
|
| 150 |
+
el.id = STYLE_ID;
|
| 151 |
+
el.textContent = css;
|
| 152 |
+
(doc.head || doc.documentElement).appendChild(el);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* ----------------------------------------------------------------------- *
|
| 156 |
+
* Minimal 3x perspective math (no matrix lib needed for our point cloud).
|
| 157 |
+
* We project 3D points to 2D with a simple rotating camera; both the WebGL2
|
| 158 |
+
* path and the 2D fallback use the SAME projection so visuals match.
|
| 159 |
+
* ----------------------------------------------------------------------- */
|
| 160 |
+
function project(p, cam, w, h) {
|
| 161 |
+
// rotate around Y then X
|
| 162 |
+
var cy = Math.cos(cam.yaw), sy = Math.sin(cam.yaw);
|
| 163 |
+
var cx = Math.cos(cam.pitch), sx = Math.sin(cam.pitch);
|
| 164 |
+
var x = p[0], y = p[1], z = p[2];
|
| 165 |
+
var x1 = x * cy + z * sy;
|
| 166 |
+
var z1 = -x * sy + z * cy;
|
| 167 |
+
var y1 = y * cx - z1 * sx;
|
| 168 |
+
var z2 = y * sx + z1 * cx;
|
| 169 |
+
var d = cam.dist;
|
| 170 |
+
var zc = z2 + d;
|
| 171 |
+
var f = (cam.fov * Math.min(w, h)) / (zc <= 0.05 ? 0.05 : zc);
|
| 172 |
+
return {
|
| 173 |
+
x: w / 2 + x1 * f,
|
| 174 |
+
y: h / 2 - y1 * f,
|
| 175 |
+
z: zc,
|
| 176 |
+
s: f / cam.fov // relative scale for size/alpha
|
| 177 |
+
};
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* ----------------------------------------------------------------------- *
|
| 181 |
+
* Scene — the holographic shell + all primitives.
|
| 182 |
+
* ----------------------------------------------------------------------- */
|
| 183 |
+
function Scene(mountEl, opts) {
|
| 184 |
+
opts = opts || {};
|
| 185 |
+
this.mount = mountEl;
|
| 186 |
+
this.doc = (mountEl && mountEl.ownerDocument) || (typeof document !== "undefined" ? document : null);
|
| 187 |
+
this.caps = detectCaps();
|
| 188 |
+
this.opts = opts;
|
| 189 |
+
this.lambda = (typeof opts.lambda === "number") ? clamp01(opts.lambda) : 0.42;
|
| 190 |
+
this.cam = { yaw: 0.6, pitch: -0.35, dist: 3.2, fov: 1.7, autoRotate: opts.autoRotate !== false };
|
| 191 |
+
this.graphs = [];
|
| 192 |
+
this.globes = [];
|
| 193 |
+
this.spheres = [];
|
| 194 |
+
this.pulses = [];
|
| 195 |
+
this._running = false;
|
| 196 |
+
this._raf = null;
|
| 197 |
+
this._lastT = 0;
|
| 198 |
+
this._t0 = (typeof performance !== "undefined" ? performance.now() : Date.now());
|
| 199 |
+
this._replay = null; // { from,to,value,playing }
|
| 200 |
+
this._build();
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
Scene.prototype._build = function () {
|
| 204 |
+
var doc = this.doc;
|
| 205 |
+
ensureStyle(doc);
|
| 206 |
+
var root = doc.createElement("div");
|
| 207 |
+
root.className = "szl-holo-root";
|
| 208 |
+
root.setAttribute("data-szl-holo", VERSION);
|
| 209 |
+
this.root = root;
|
| 210 |
+
|
| 211 |
+
var canvas = doc.createElement("canvas");
|
| 212 |
+
canvas.setAttribute("aria-label", "SZL holographic scene");
|
| 213 |
+
this.canvas = canvas;
|
| 214 |
+
root.appendChild(canvas);
|
| 215 |
+
|
| 216 |
+
// HUD with honest mode + SAMPLE badge (demo data is SAMPLE)
|
| 217 |
+
var hud = doc.createElement("div");
|
| 218 |
+
hud.className = "szl-holo-hud";
|
| 219 |
+
var modeBadge = this.caps.webgl2 ? badgeHTML("EXPERIMENTAL", "WEBGL2") : badgeHTML("2DFALLBACK", "2D FALLBACK");
|
| 220 |
+
var gpuBadge = this.caps.webgpu ? badgeHTML("EXPERIMENTAL", "WEBGPU·ROADMAP") : "";
|
| 221 |
+
hud.innerHTML =
|
| 222 |
+
'<div class="row">' + (this.opts.title ? '<strong>' + esc(this.opts.title) + '</strong>' : '') +
|
| 223 |
+
' ' + modeBadge + ' ' + gpuBadge + '</div>' +
|
| 224 |
+
'<div class="row" data-szl-holo-labels>' + badgeHTML("SAMPLE", "SAMPLE DATA") + '</div>';
|
| 225 |
+
this.hud = hud;
|
| 226 |
+
root.appendChild(hud);
|
| 227 |
+
|
| 228 |
+
// Λ readout (Conjecture 1, < 1.0) — never claims a theorem, never 100%.
|
| 229 |
+
var lam = doc.createElement("div");
|
| 230 |
+
lam.className = "szl-holo-lambda";
|
| 231 |
+
this.lamEl = lam;
|
| 232 |
+
root.appendChild(lam);
|
| 233 |
+
this._renderLambdaReadout();
|
| 234 |
+
|
| 235 |
+
this.mount.appendChild(root);
|
| 236 |
+
|
| 237 |
+
// renderer init
|
| 238 |
+
if (this.caps.webgl2) {
|
| 239 |
+
this._initGL();
|
| 240 |
+
}
|
| 241 |
+
this.ctx2d = canvas.getContext("2d"); // always available for HUD/2D fallback layer
|
| 242 |
+
this._resize();
|
| 243 |
+
var self = this;
|
| 244 |
+
this._onResize = function () { self._resize(); };
|
| 245 |
+
if (typeof window !== "undefined") { window.addEventListener("resize", this._onResize); }
|
| 246 |
+
};
|
| 247 |
+
|
| 248 |
+
Scene.prototype._renderLambdaReadout = function () {
|
| 249 |
+
var col = lambdaColor(this.lambda);
|
| 250 |
+
this.lamEl.innerHTML =
|
| 251 |
+
'Λ <span style="color:' + col + ';font-weight:700">' + this.lambda.toFixed(3) + '</span> ' +
|
| 252 |
+
'<span style="opacity:.6">· Conjecture 1 (<1.0) · trust <100%</span>';
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
Scene.prototype._initGL = function () {
|
| 256 |
+
var gl = null;
|
| 257 |
+
try { gl = this.canvas.getContext("webgl2", { antialias: true, alpha: false, premultipliedAlpha: false }); }
|
| 258 |
+
catch (e) { gl = null; }
|
| 259 |
+
if (!gl) { this.caps.webgl2 = false; this.caps.mode = "2d"; return; }
|
| 260 |
+
this.gl = gl;
|
| 261 |
+
// point + line shader (instanced-free, simple gl.POINTS / gl.LINES)
|
| 262 |
+
var vs =
|
| 263 |
+
"#version 300 es\n" +
|
| 264 |
+
"layout(location=0) in vec3 aPos;\n" +
|
| 265 |
+
"layout(location=1) in vec4 aColor;\n" +
|
| 266 |
+
"layout(location=2) in float aSize;\n" +
|
| 267 |
+
"uniform mat4 uMVP;\n" +
|
| 268 |
+
"out vec4 vColor;\n" +
|
| 269 |
+
"void main(){ gl_Position = uMVP * vec4(aPos,1.0); gl_PointSize = aSize; vColor = aColor; }\n";
|
| 270 |
+
var fs =
|
| 271 |
+
"#version 300 es\nprecision highp float;\n" +
|
| 272 |
+
"in vec4 vColor; out vec4 frag;\n" +
|
| 273 |
+
"void main(){\n" +
|
| 274 |
+
" vec2 d = gl_PointCoord - vec2(0.5);\n" +
|
| 275 |
+
" float r = length(d);\n" +
|
| 276 |
+
" float a = smoothstep(0.5,0.1,r);\n" +
|
| 277 |
+
" frag = vec4(vColor.rgb, vColor.a * max(a, 0.18));\n" +
|
| 278 |
+
"}\n";
|
| 279 |
+
this.prog = this._linkProgram(gl, vs, fs);
|
| 280 |
+
this.uMVP = gl.getUniformLocation(this.prog, "uMVP");
|
| 281 |
+
this.vbo = gl.createBuffer();
|
| 282 |
+
gl.enable(gl.BLEND);
|
| 283 |
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE); // additive glow
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
Scene.prototype._linkProgram = function (gl, vsSrc, fsSrc) {
|
| 287 |
+
function sh(type, src) {
|
| 288 |
+
var s = gl.createShader(type);
|
| 289 |
+
gl.shaderSource(s, src); gl.compileShader(s);
|
| 290 |
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
| 291 |
+
// honest degrade: drop to 2D rather than throw
|
| 292 |
+
return null;
|
| 293 |
+
}
|
| 294 |
+
return s;
|
| 295 |
+
}
|
| 296 |
+
var v = sh(gl.VERTEX_SHADER, vsSrc), f = sh(gl.FRAGMENT_SHADER, fsSrc);
|
| 297 |
+
if (!v || !f) { this.caps.webgl2 = false; this.caps.mode = "2d"; return null; }
|
| 298 |
+
var p = gl.createProgram();
|
| 299 |
+
gl.attachShader(p, v); gl.attachShader(p, f); gl.linkProgram(p);
|
| 300 |
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) { this.caps.webgl2 = false; this.caps.mode = "2d"; return null; }
|
| 301 |
+
return p;
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
Scene.prototype._resize = function () {
|
| 305 |
+
var dpr = this.caps.dpr;
|
| 306 |
+
var w = this.root.clientWidth || 640, h = this.root.clientHeight || 360;
|
| 307 |
+
this.W = w; this.H = h;
|
| 308 |
+
this.canvas.width = Math.max(1, (w * dpr) | 0);
|
| 309 |
+
this.canvas.height = Math.max(1, (h * dpr) | 0);
|
| 310 |
+
if (this.gl) { this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); }
|
| 311 |
+
};
|
| 312 |
+
|
| 313 |
+
/* -------- primitives ------------------------------------------------ */
|
| 314 |
+
|
| 315 |
+
// GRAPH: { nodes:[{id,pos:[x,y,z],label,locked,lambda}], edges:[{id,from,to}] }
|
| 316 |
+
Scene.prototype.addGraph = function (spec) {
|
| 317 |
+
spec = spec || {};
|
| 318 |
+
var nodes = (spec.nodes || []).map(function (n, i) {
|
| 319 |
+
return {
|
| 320 |
+
id: n.id != null ? n.id : ("n" + i),
|
| 321 |
+
pos: n.pos || sphereLayout(i, (spec.nodes || []).length, 1.1),
|
| 322 |
+
label: n.label || "",
|
| 323 |
+
locked: !!n.locked,
|
| 324 |
+
lambda: (typeof n.lambda === "number") ? clamp01(n.lambda) : null
|
| 325 |
+
};
|
| 326 |
+
});
|
| 327 |
+
var index = {}; nodes.forEach(function (n, i) { index[n.id] = i; });
|
| 328 |
+
var edges = (spec.edges || []).map(function (e, i) {
|
| 329 |
+
return { id: e.id != null ? e.id : ("e" + i), from: index[e.from], to: index[e.to],
|
| 330 |
+
fromId: e.from, toId: e.to };
|
| 331 |
+
}).filter(function (e) { return e.from != null && e.to != null; });
|
| 332 |
+
var h = { kind: "graph", nodes: nodes, edges: edges, index: index };
|
| 333 |
+
this.graphs.push(h);
|
| 334 |
+
return h;
|
| 335 |
+
};
|
| 336 |
+
|
| 337 |
+
// GLOBE: lightweight wireframe sphere + lat/long graticule + optional points.
|
| 338 |
+
// Honest: this is a procedural SAMPLE globe, NOT a CesiumJS tile basemap.
|
| 339 |
+
// A real basemap (CesiumJS local tiles / three-globe) is ROADMAP and must be
|
| 340 |
+
// vendored 0-CDN before any tile call is made.
|
| 341 |
+
Scene.prototype.addGlobe = function (spec) {
|
| 342 |
+
spec = spec || {};
|
| 343 |
+
var pts = (spec.points || []).map(function (p, i) {
|
| 344 |
+
var la = (p.lat || 0) * Math.PI / 180, lo = (p.lon || 0) * Math.PI / 180;
|
| 345 |
+
var r = 1.0;
|
| 346 |
+
return { pos: [r * Math.cos(la) * Math.cos(lo), r * Math.sin(la), r * Math.cos(la) * Math.sin(lo)],
|
| 347 |
+
label: p.label || "", lambda: (typeof p.lambda === "number") ? clamp01(p.lambda) : 0.4 };
|
| 348 |
+
});
|
| 349 |
+
var h = { kind: "globe", radius: spec.radius || 1.0, points: pts,
|
| 350 |
+
sample: true, basemap: "PROCEDURAL_SAMPLE" };
|
| 351 |
+
this.globes.push(h);
|
| 352 |
+
return h;
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
// TRUST SPHERE: a sphere whose radius/colour morph with Λ (Conjecture 1).
|
| 356 |
+
// Lower Λ -> larger, greener (more trust); higher Λ -> smaller, redder.
|
| 357 |
+
Scene.prototype.addTrustSphere = function (spec) {
|
| 358 |
+
spec = spec || {};
|
| 359 |
+
var h = { kind: "sphere",
|
| 360 |
+
center: spec.center || [0, 0, 0],
|
| 361 |
+
baseR: spec.radius || 0.6,
|
| 362 |
+
lambda: (typeof spec.lambda === "number") ? clamp01(spec.lambda) : this.lambda,
|
| 363 |
+
label: spec.label || "TRUST",
|
| 364 |
+
segs: spec.segments || 16 };
|
| 365 |
+
this.spheres.push(h);
|
| 366 |
+
return h;
|
| 367 |
+
};
|
| 368 |
+
|
| 369 |
+
// SIGNED-PULSE: animate a light packet flowing along an edge (receipt signed).
|
| 370 |
+
Scene.prototype.signPulse = function (edgeRef, opts) {
|
| 371 |
+
opts = opts || {};
|
| 372 |
+
var graph = this.graphs[0];
|
| 373 |
+
var edge = null;
|
| 374 |
+
if (graph) {
|
| 375 |
+
for (var i = 0; i < graph.edges.length; i++) {
|
| 376 |
+
if (graph.edges[i].id === edgeRef || i === edgeRef) { edge = graph.edges[i]; break; }
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
if (!edge && graph && graph.edges.length) { edge = graph.edges[0]; }
|
| 380 |
+
if (!edge) { return null; }
|
| 381 |
+
var p = { graph: graph, edge: edge, t: 0, dur: opts.duration || 1200,
|
| 382 |
+
color: opts.color || PALETTE.pulse, started: this._now(), loop: !!opts.loop };
|
| 383 |
+
this.pulses.push(p);
|
| 384 |
+
return p;
|
| 385 |
+
};
|
| 386 |
+
|
| 387 |
+
// TIME-SLIDER replay hook. Injects a slider; calls opts.onTime(normalized,absolute).
|
| 388 |
+
Scene.prototype.timeSlider = function (opts) {
|
| 389 |
+
opts = opts || {};
|
| 390 |
+
var doc = this.doc, self = this;
|
| 391 |
+
var box = doc.createElement("div");
|
| 392 |
+
box.className = "szl-holo-slider";
|
| 393 |
+
var from = opts.from != null ? opts.from : 0;
|
| 394 |
+
var to = opts.to != null ? opts.to : 100;
|
| 395 |
+
box.innerHTML = '<label>REPLAY ' + badgeHTML("SAMPLE", "SAMPLE") + '</label>' +
|
| 396 |
+
'<input type="range" min="0" max="1000" value="1000" step="1" aria-label="time replay">';
|
| 397 |
+
var input = box.querySelector("input");
|
| 398 |
+
input.addEventListener("input", function () {
|
| 399 |
+
var n = (+input.value) / 1000;
|
| 400 |
+
self._replay = { from: from, to: to, value: from + (to - from) * n, norm: n };
|
| 401 |
+
if (typeof opts.onTime === "function") { opts.onTime(n, self._replay.value); }
|
| 402 |
+
});
|
| 403 |
+
this.root.appendChild(box);
|
| 404 |
+
this._replay = { from: from, to: to, value: to, norm: 1 };
|
| 405 |
+
return { el: box, set: function (n) { input.value = String(Math.round(clamp01(n) * 1000)); input.dispatchEvent(new Event("input")); } };
|
| 406 |
+
};
|
| 407 |
+
|
| 408 |
+
Scene.prototype.setLambda = function (v) {
|
| 409 |
+
this.lambda = clamp01(v);
|
| 410 |
+
// trust sphere(s) without an explicit lambda track the global Λ
|
| 411 |
+
for (var i = 0; i < this.spheres.length; i++) {
|
| 412 |
+
if (this.spheres[i]._tracksGlobal !== false && this.spheres[i].lambda == null) {
|
| 413 |
+
this.spheres[i].lambda = this.lambda;
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
this._renderLambdaReadout();
|
| 417 |
+
return this.lambda;
|
| 418 |
+
};
|
| 419 |
+
|
| 420 |
+
Scene.prototype._now = function () { return (typeof performance !== "undefined" ? performance.now() : Date.now()); };
|
| 421 |
+
|
| 422 |
+
/* -------- run loop -------------------------------------------------- */
|
| 423 |
+
Scene.prototype.start = function () {
|
| 424 |
+
if (this._running) { return; }
|
| 425 |
+
this._running = true;
|
| 426 |
+
var self = this;
|
| 427 |
+
function loop(t) {
|
| 428 |
+
if (!self._running) { return; }
|
| 429 |
+
self._frame(t || self._now());
|
| 430 |
+
self._raf = (typeof requestAnimationFrame !== "undefined")
|
| 431 |
+
? requestAnimationFrame(loop) : setTimeout(function () { loop(self._now()); }, 33);
|
| 432 |
+
}
|
| 433 |
+
this._raf = (typeof requestAnimationFrame !== "undefined")
|
| 434 |
+
? requestAnimationFrame(loop) : setTimeout(function () { loop(self._now()); }, 33);
|
| 435 |
+
return this;
|
| 436 |
+
};
|
| 437 |
+
Scene.prototype.stop = function () {
|
| 438 |
+
this._running = false;
|
| 439 |
+
if (this._raf != null) {
|
| 440 |
+
if (typeof cancelAnimationFrame !== "undefined") { cancelAnimationFrame(this._raf); }
|
| 441 |
+
else { clearTimeout(this._raf); }
|
| 442 |
+
this._raf = null;
|
| 443 |
+
}
|
| 444 |
+
return this;
|
| 445 |
+
};
|
| 446 |
+
|
| 447 |
+
Scene.prototype._frame = function (t) {
|
| 448 |
+
var dt = this._lastT ? (t - this._lastT) : 16;
|
| 449 |
+
this._lastT = t;
|
| 450 |
+
if (this.cam.autoRotate) { this.cam.yaw += dt * 0.00018; }
|
| 451 |
+
this._drawCount = 0;
|
| 452 |
+
if (this.caps.webgl2 && this.gl && this.prog) {
|
| 453 |
+
this._renderGL(t);
|
| 454 |
+
}
|
| 455 |
+
// 2D overlay always draws labels/pulses/slider markers; in fallback it draws everything.
|
| 456 |
+
this._render2D(t);
|
| 457 |
+
// expose a draw count so verifiers can confirm >0 draws
|
| 458 |
+
this.root.setAttribute("data-szl-draws", String(this._drawCount));
|
| 459 |
+
};
|
| 460 |
+
|
| 461 |
+
/* Build the full point/line set in scene space (shared by GL + 2D). */
|
| 462 |
+
Scene.prototype._collectGeometry = function (t) {
|
| 463 |
+
var pts = []; // {pos, color, size, label}
|
| 464 |
+
var lines = []; // {a, b, color}
|
| 465 |
+
var self = this;
|
| 466 |
+
var replayN = this._replay ? this._replay.norm : 1;
|
| 467 |
+
|
| 468 |
+
// graphs
|
| 469 |
+
this.graphs.forEach(function (g) {
|
| 470 |
+
g.edges.forEach(function (e) {
|
| 471 |
+
var a = g.nodes[e.from], b = g.nodes[e.to];
|
| 472 |
+
if (!a || !b) { return; }
|
| 473 |
+
lines.push({ a: a.pos, b: b.pos, color: PALETTE.edge });
|
| 474 |
+
});
|
| 475 |
+
g.nodes.forEach(function (n, i) {
|
| 476 |
+
// replay: reveal nodes progressively by index
|
| 477 |
+
var reveal = (i / Math.max(1, g.nodes.length - 1)) <= (replayN + 0.0001);
|
| 478 |
+
if (!reveal) { return; }
|
| 479 |
+
var col = n.locked ? PALETTE.nodeLock : (n.lambda != null ? lambdaColor(n.lambda) : PALETTE.node);
|
| 480 |
+
pts.push({ pos: n.pos, color: col, size: n.locked ? 16 : 12, label: n.label });
|
| 481 |
+
});
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
// globes
|
| 485 |
+
this.globes.forEach(function (gl) {
|
| 486 |
+
var R = gl.radius, rings = 10, seg = 22;
|
| 487 |
+
for (var i = 1; i < rings; i++) {
|
| 488 |
+
var lat = (i / rings - 0.5) * Math.PI;
|
| 489 |
+
var prev = null;
|
| 490 |
+
for (var j = 0; j <= seg; j++) {
|
| 491 |
+
var lon = (j / seg) * Math.PI * 2;
|
| 492 |
+
var p = [R * Math.cos(lat) * Math.cos(lon), R * Math.sin(lat), R * Math.cos(lat) * Math.sin(lon)];
|
| 493 |
+
if (prev) { lines.push({ a: prev, b: p, color: "rgba(110,160,220,0.18)" }); }
|
| 494 |
+
prev = p;
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
for (var k = 0; k < seg; k++) {
|
| 498 |
+
var lon2 = (k / seg) * Math.PI * 2; var prev2 = null;
|
| 499 |
+
for (var m = 0; m <= rings; m++) {
|
| 500 |
+
var lat2 = (m / rings - 0.5) * Math.PI;
|
| 501 |
+
var q = [R * Math.cos(lat2) * Math.cos(lon2), R * Math.sin(lat2), R * Math.cos(lat2) * Math.sin(lon2)];
|
| 502 |
+
if (prev2) { lines.push({ a: prev2, b: q, color: "rgba(110,160,220,0.12)" }); }
|
| 503 |
+
prev2 = q;
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
gl.points.forEach(function (p) {
|
| 507 |
+
pts.push({ pos: p.pos, color: lambdaColor(p.lambda), size: 14, label: p.label });
|
| 508 |
+
});
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
+
// trust spheres — radius morphs with Λ (lower Λ = larger/greener)
|
| 512 |
+
this.spheres.forEach(function (s) {
|
| 513 |
+
var lam = (s.lambda != null) ? s.lambda : self.lambda;
|
| 514 |
+
var R = s.baseR * (1.25 - 0.5 * lam); // morph
|
| 515 |
+
var col = lambdaColor(lam);
|
| 516 |
+
var rings = s.segs, seg = s.segs;
|
| 517 |
+
for (var i = 0; i <= rings; i++) {
|
| 518 |
+
var lat = (i / rings - 0.5) * Math.PI; var prev = null;
|
| 519 |
+
for (var j = 0; j <= seg; j++) {
|
| 520 |
+
var lon = (j / seg) * Math.PI * 2;
|
| 521 |
+
var p = [s.center[0] + R * Math.cos(lat) * Math.cos(lon),
|
| 522 |
+
s.center[1] + R * Math.sin(lat),
|
| 523 |
+
s.center[2] + R * Math.cos(lat) * Math.sin(lon)];
|
| 524 |
+
if (prev) { lines.push({ a: prev, b: p, color: hexA(col, 0.30) }); }
|
| 525 |
+
prev = p;
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
pts.push({ pos: s.center, color: col, size: 10 + 14 * (1 - lam), label: s.label + " Λ" + lam.toFixed(2) });
|
| 529 |
+
});
|
| 530 |
+
|
| 531 |
+
// signed pulses — moving light packet along an edge
|
| 532 |
+
var alive = [];
|
| 533 |
+
this.pulses.forEach(function (p) {
|
| 534 |
+
var age = self._now() - p.started;
|
| 535 |
+
var f = age / p.dur;
|
| 536 |
+
if (f >= 1) { if (p.loop) { p.started = self._now(); f = 0; } else { return; } }
|
| 537 |
+
var a = p.graph.nodes[p.edge.from], b = p.graph.nodes[p.edge.to];
|
| 538 |
+
if (!a || !b) { return; }
|
| 539 |
+
var pos = [lerp(a.pos[0], b.pos[0], f), lerp(a.pos[1], b.pos[1], f), lerp(a.pos[2], b.pos[2], f)];
|
| 540 |
+
pts.push({ pos: pos, color: p.color, size: 18, label: "" , pulse: true });
|
| 541 |
+
// glowing trail
|
| 542 |
+
for (var q = 1; q <= 4; q++) {
|
| 543 |
+
var tf = Math.max(0, f - q * 0.05);
|
| 544 |
+
pts.push({ pos: [lerp(a.pos[0], b.pos[0], tf), lerp(a.pos[1], b.pos[1], tf), lerp(a.pos[2], b.pos[2], tf)],
|
| 545 |
+
color: hexA(p.color, 0.5 - q * 0.1), size: 14 - q * 2, pulse: true });
|
| 546 |
+
}
|
| 547 |
+
alive.push(p);
|
| 548 |
+
});
|
| 549 |
+
this.pulses = alive;
|
| 550 |
+
|
| 551 |
+
return { pts: pts, lines: lines };
|
| 552 |
+
};
|
| 553 |
+
|
| 554 |
+
Scene.prototype._renderGL = function (t) {
|
| 555 |
+
var gl = this.gl, geo = this._collectGeometry(t);
|
| 556 |
+
gl.clearColor(PALETTE.bg0[0], PALETTE.bg0[1], PALETTE.bg0[2], 1);
|
| 557 |
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
| 558 |
+
var mvp = this._mvp();
|
| 559 |
+
gl.useProgram(this.prog);
|
| 560 |
+
gl.uniformMatrix4fv(this.uMVP, false, mvp);
|
| 561 |
+
|
| 562 |
+
// lines
|
| 563 |
+
var lineArr = [];
|
| 564 |
+
geo.lines.forEach(function (ln) {
|
| 565 |
+
var c = parseColor(ln.color);
|
| 566 |
+
lineArr.push(ln.a[0], ln.a[1], ln.a[2], c[0], c[1], c[2], c[3], 2.0);
|
| 567 |
+
lineArr.push(ln.b[0], ln.b[1], ln.b[2], c[0], c[1], c[2], c[3], 2.0);
|
| 568 |
+
});
|
| 569 |
+
this._drawArray(gl, lineArr, gl.LINES);
|
| 570 |
+
this._drawCount += geo.lines.length;
|
| 571 |
+
|
| 572 |
+
// points
|
| 573 |
+
var ptArr = [];
|
| 574 |
+
geo.pts.forEach(function (pt) {
|
| 575 |
+
var c = parseColor(pt.color);
|
| 576 |
+
ptArr.push(pt.pos[0], pt.pos[1], pt.pos[2], c[0], c[1], c[2], c[3], pt.size * 1.0);
|
| 577 |
+
});
|
| 578 |
+
this._drawArray(gl, ptArr, gl.POINTS);
|
| 579 |
+
this._drawCount += geo.pts.length;
|
| 580 |
+
this._lastGeo = geo; // for 2D label overlay
|
| 581 |
+
};
|
| 582 |
+
|
| 583 |
+
Scene.prototype._drawArray = function (gl, arr, mode) {
|
| 584 |
+
if (!arr.length) { return; }
|
| 585 |
+
var data = new Float32Array(arr);
|
| 586 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
|
| 587 |
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
|
| 588 |
+
var stride = 8 * 4;
|
| 589 |
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
|
| 590 |
+
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.FLOAT, false, stride, 3 * 4);
|
| 591 |
+
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 1, gl.FLOAT, false, stride, 7 * 4);
|
| 592 |
+
gl.drawArrays(mode, 0, arr.length / 8);
|
| 593 |
+
};
|
| 594 |
+
|
| 595 |
+
// Build a perspective * view matrix consistent with our project() camera.
|
| 596 |
+
Scene.prototype._mvp = function () {
|
| 597 |
+
var cam = this.cam;
|
| 598 |
+
var aspect = this.canvas.width / Math.max(1, this.canvas.height);
|
| 599 |
+
var f = 1.0 / Math.tan(cam.fov / 2);
|
| 600 |
+
var near = 0.1, far = 100;
|
| 601 |
+
var proj = [
|
| 602 |
+
f / aspect, 0, 0, 0,
|
| 603 |
+
0, f, 0, 0,
|
| 604 |
+
0, 0, (far + near) / (near - far), -1,
|
| 605 |
+
0, 0, (2 * far * near) / (near - far), 0
|
| 606 |
+
];
|
| 607 |
+
var cy = Math.cos(cam.yaw), sy = Math.sin(cam.yaw);
|
| 608 |
+
var cx = Math.cos(cam.pitch), sx = Math.sin(cam.pitch);
|
| 609 |
+
var ry = [cy, 0, -sy, 0, 0, 1, 0, 0, sy, 0, cy, 0, 0, 0, 0, 1];
|
| 610 |
+
var rx = [1, 0, 0, 0, 0, cx, sx, 0, 0, -sx, cx, 0, 0, 0, 0, 1];
|
| 611 |
+
var trans = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -cam.dist, 1];
|
| 612 |
+
return mul(proj, mul(trans, mul(rx, ry)));
|
| 613 |
+
};
|
| 614 |
+
|
| 615 |
+
/* 2D layer: in fallback mode renders the full scene; otherwise draws labels. */
|
| 616 |
+
Scene.prototype._render2D = function (t) {
|
| 617 |
+
var ctx = this.ctx2d; if (!ctx) { return; }
|
| 618 |
+
var dpr = this.caps.dpr, w = this.canvas.width, h = this.canvas.height;
|
| 619 |
+
if (!this.caps.webgl2) {
|
| 620 |
+
// FULL 2D FALLBACK — never a blank canvas.
|
| 621 |
+
var grd = ctx.createRadialGradient(w / 2, h * 0.2, 10, w / 2, h * 0.2, h);
|
| 622 |
+
grd.addColorStop(0, "#0a1326"); grd.addColorStop(1, "#03060d");
|
| 623 |
+
ctx.fillStyle = grd; ctx.fillRect(0, 0, w, h);
|
| 624 |
+
// faint grid
|
| 625 |
+
ctx.strokeStyle = PALETTE.grid; ctx.lineWidth = 1;
|
| 626 |
+
for (var gx = 0; gx < w; gx += 40 * dpr) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, h); ctx.stroke(); }
|
| 627 |
+
for (var gy = 0; gy < h; gy += 40 * dpr) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(w, gy); ctx.stroke(); }
|
| 628 |
+
var geo = this._collectGeometry(t);
|
| 629 |
+
var self = this;
|
| 630 |
+
ctx.lineWidth = 1.4 * dpr;
|
| 631 |
+
geo.lines.forEach(function (ln) {
|
| 632 |
+
var pa = project(ln.a, self.cam, w, h), pb = project(ln.b, self.cam, w, h);
|
| 633 |
+
ctx.strokeStyle = ln.color; ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.lineTo(pb.x, pb.y); ctx.stroke();
|
| 634 |
+
self._drawCount++;
|
| 635 |
+
});
|
| 636 |
+
geo.pts.slice().sort(function (a, b) {
|
| 637 |
+
return project(b.pos, self.cam, w, h).z - project(a.pos, self.cam, w, h).z;
|
| 638 |
+
}).forEach(function (pt) {
|
| 639 |
+
var p = project(pt.pos, self.cam, w, h);
|
| 640 |
+
var r = Math.max(2, pt.size * 0.5 * dpr * p.s);
|
| 641 |
+
var glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 2.4);
|
| 642 |
+
glow.addColorStop(0, pt.color); glow.addColorStop(1, "rgba(0,0,0,0)");
|
| 643 |
+
ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(p.x, p.y, r * 2.4, 0, Math.PI * 2); ctx.fill();
|
| 644 |
+
ctx.fillStyle = pt.color; ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
|
| 645 |
+
if (pt.label) {
|
| 646 |
+
ctx.fillStyle = PALETTE.text; ctx.font = (11 * dpr) + "px ui-monospace,monospace";
|
| 647 |
+
ctx.fillText(pt.label, p.x + r + 3, p.y + 3);
|
| 648 |
+
}
|
| 649 |
+
self._drawCount++;
|
| 650 |
+
});
|
| 651 |
+
return;
|
| 652 |
+
}
|
| 653 |
+
// WebGL2 mode: overlay node labels on a transparent 2D layer is not possible
|
| 654 |
+
// on the same canvas (GL owns it). Labels are shown via the HUD; here we just
|
| 655 |
+
// keep the draw count honest from the GL pass. (No second canvas to stay 0-CDN
|
| 656 |
+
// and simple.)
|
| 657 |
+
};
|
| 658 |
+
|
| 659 |
+
Scene.prototype.dispose = function () {
|
| 660 |
+
this.stop();
|
| 661 |
+
if (typeof window !== "undefined" && this._onResize) { window.removeEventListener("resize", this._onResize); }
|
| 662 |
+
if (this.root && this.root.parentNode) { this.root.parentNode.removeChild(this.root); }
|
| 663 |
+
this.gl = null; this.ctx2d = null;
|
| 664 |
+
};
|
| 665 |
+
|
| 666 |
+
/* ----------------------------------------------------------------------- *
|
| 667 |
+
* helpers
|
| 668 |
+
* ----------------------------------------------------------------------- */
|
| 669 |
+
function sphereLayout(i, n, r) {
|
| 670 |
+
// fibonacci sphere for pleasing graph node distribution
|
| 671 |
+
n = Math.max(1, n);
|
| 672 |
+
var phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
| 673 |
+
var theta = Math.PI * (1 + Math.sqrt(5)) * i;
|
| 674 |
+
return [r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta)];
|
| 675 |
+
}
|
| 676 |
+
function parseColor(c) {
|
| 677 |
+
// returns [r,g,b,a] in 0..1
|
| 678 |
+
if (Array.isArray(c)) { return [c[0], c[1], c[2], c[3] == null ? 1 : c[3]]; }
|
| 679 |
+
c = String(c).trim();
|
| 680 |
+
var m;
|
| 681 |
+
if ((m = c.match(/^#([0-9a-f]{6})$/i))) {
|
| 682 |
+
var n = parseInt(m[1], 16);
|
| 683 |
+
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];
|
| 684 |
+
}
|
| 685 |
+
if ((m = c.match(/^rgba?\(([^)]+)\)$/i))) {
|
| 686 |
+
var p = m[1].split(",").map(function (x) { return parseFloat(x); });
|
| 687 |
+
return [(p[0] || 0) / 255, (p[1] || 0) / 255, (p[2] || 0) / 255, p[3] == null ? 1 : p[3]];
|
| 688 |
+
}
|
| 689 |
+
return [0.5, 0.8, 1, 1];
|
| 690 |
+
}
|
| 691 |
+
function hexA(c, a) {
|
| 692 |
+
var p = parseColor(c);
|
| 693 |
+
return "rgba(" + (p[0] * 255 | 0) + "," + (p[1] * 255 | 0) + "," + (p[2] * 255 | 0) + "," + a + ")";
|
| 694 |
+
}
|
| 695 |
+
function mul(a, b) {
|
| 696 |
+
var out = new Array(16);
|
| 697 |
+
for (var r = 0; r < 4; r++) {
|
| 698 |
+
for (var c = 0; c < 4; c++) {
|
| 699 |
+
var s = 0;
|
| 700 |
+
for (var k = 0; k < 4; k++) { s += a[k * 4 + c] * b[r * 4 + k]; }
|
| 701 |
+
out[r * 4 + c] = s;
|
| 702 |
+
}
|
| 703 |
+
}
|
| 704 |
+
return out;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* ----------------------------------------------------------------------- *
|
| 708 |
+
* public module surface
|
| 709 |
+
* ----------------------------------------------------------------------- */
|
| 710 |
+
return {
|
| 711 |
+
version: VERSION,
|
| 712 |
+
palette: PALETTE,
|
| 713 |
+
lambdaColor: lambdaColor,
|
| 714 |
+
capabilities: function () { return detectCaps(); },
|
| 715 |
+
createScene: function (mountEl, opts) {
|
| 716 |
+
if (!mountEl) { throw new Error("SZLHolo.createScene requires a mount element"); }
|
| 717 |
+
return new Scene(mountEl, opts || {});
|
| 718 |
+
},
|
| 719 |
+
// expose for advanced lanes (F2-F5) that build their own scene types
|
| 720 |
+
_Scene: Scene,
|
| 721 |
+
_helpers: { sphereLayout: sphereLayout, project: project }
|
| 722 |
+
};
|
| 723 |
+
});
|