Spaces:
Running
feat(roster): ship video-game-style 3D character-select roster
Browse filesReplaces the flat 5-tile grid + static AI-illustration hero banner with a
live Three.js (r160) character-select scene: five procedural low-poly heroes
(AMARU, SENTRA, ROSIE, KILLINCHU, A11OY) on colored rim-lit daises in an arc,
UDS NPCs orbiting in the background, KHIPU-style particle motes, slow camera
carousel orbit. Hover reveals animated superpower panels + activates each
hero's prop and dims the rest; click opens the hero's HF Space. Fully
procedural geometry + canvas-painted UDS swag (zero external assets). WebGL
fallback to the existing 5-tile SVG grid. prefers-reduced-motion honored.
Files: index.html, js/{main,heroes,uds_npcs,hover_panel,superpower_icons,canvas_decals}.js, css/style.css
Author: Yachay <yachay@szlholdings.dev>
Co-Authored-By: Perplexity Computer Agent <agent@perplexity.ai>
Signed-off-by: Yachay <yachay@szlholdings.dev>
ADDITIVE only; no force push. Doctrine v11 LOCKED 749/14/163 unchanged.
Lambda stays Conjecture 1. SLSA L1 honest. README frontmatter untouched.
- css/style.css +85 -0
- index.html +50 -568
- js/canvas_decals.js +195 -0
- js/heroes.js +219 -0
- js/hover_panel.js +84 -0
- js/main.js +344 -0
- js/superpower_icons.js +105 -0
- js/uds_npcs.js +64 -0
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* style.css — minimal, mostly defaults. SZL palette: deep purple + gold. */
|
| 2 |
+
:root{
|
| 3 |
+
--purple-deep:#12091f; --purple-mid:#2d1b4e; --gold:#d4af37; --gold-light:#e8cc6a;
|
| 4 |
+
--text-main:#e8e0f0; --text-muted:#a090c0; --text-dim:#7060a0;
|
| 5 |
+
--font-head:'Cinzel',Georgia,serif; --font-mono:'JetBrains Mono','Fira Code',monospace;
|
| 6 |
+
}
|
| 7 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
| 8 |
+
html,body{height:100%;}
|
| 9 |
+
body{
|
| 10 |
+
background:var(--purple-deep);color:var(--text-main);
|
| 11 |
+
font-family:'Inter',system-ui,-apple-system,sans-serif;line-height:1.6;
|
| 12 |
+
-webkit-font-smoothing:antialiased;overflow-x:hidden;
|
| 13 |
+
background-image:radial-gradient(circle at 50% -10%,rgba(61,40,120,0.5),transparent 55%),
|
| 14 |
+
radial-gradient(circle at 90% 10%,rgba(212,175,55,0.07),transparent 45%);
|
| 15 |
+
}
|
| 16 |
+
.skip{position:absolute;left:-9999px;top:0.5rem;z-index:99;padding:.5rem 1rem;background:var(--gold);color:#150b22;font-weight:700;border-radius:4px;}
|
| 17 |
+
.skip:focus{left:1rem;}
|
| 18 |
+
|
| 19 |
+
/* Header / footer banding */
|
| 20 |
+
.roster-header,.roster-footer{text-align:center;font-family:var(--font-mono);letter-spacing:.18em;
|
| 21 |
+
text-transform:uppercase;color:var(--gold-light);padding:1.1rem 1rem .6rem;font-size:clamp(.62rem,1.6vw,.86rem);}
|
| 22 |
+
.roster-footer{color:var(--text-muted);padding:.7rem 1rem 1.4rem;font-size:clamp(.56rem,1.4vw,.74rem);}
|
| 23 |
+
.roster-footer b{color:var(--gold);}
|
| 24 |
+
.tagline{text-align:center;font-family:var(--font-head);color:var(--gold);
|
| 25 |
+
font-size:clamp(1.1rem,3vw,1.8rem);padding:.4rem 1rem 0;}
|
| 26 |
+
.tagline small{display:block;font-family:var(--font-mono);text-transform:none;letter-spacing:.04em;
|
| 27 |
+
font-size:.62rem;color:var(--text-dim);margin-top:.4rem;}
|
| 28 |
+
|
| 29 |
+
/* The 3D stage */
|
| 30 |
+
#stage{position:relative;width:100%;height:min(70vh,640px);min-height:420px;}
|
| 31 |
+
#roster{display:block;width:100%;height:100%;}
|
| 32 |
+
#loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
| 33 |
+
font-family:var(--font-mono);color:var(--text-dim);font-size:.8rem;letter-spacing:.1em;}
|
| 34 |
+
|
| 35 |
+
/* Holographic hover panel */
|
| 36 |
+
.hover-panel{position:absolute;transform:translate(-50%,-100%);min-width:240px;max-width:300px;
|
| 37 |
+
padding:.8rem .9rem;pointer-events:none;opacity:0;transition:opacity .18s ease-out;z-index:40;
|
| 38 |
+
background:linear-gradient(180deg,rgba(18,9,31,.92),rgba(18,9,31,.78));
|
| 39 |
+
border:1px solid var(--hc,#d4af37);border-radius:10px;
|
| 40 |
+
box-shadow:0 0 24px -4px var(--hc,#d4af37),inset 0 0 18px -8px var(--hc,#d4af37);
|
| 41 |
+
backdrop-filter:blur(6px);}
|
| 42 |
+
.hover-panel.hp-visible{opacity:1;}
|
| 43 |
+
.hover-panel::after{content:'';position:absolute;left:50%;bottom:-7px;transform:translateX(-50%);
|
| 44 |
+
width:0;height:0;border:7px solid transparent;border-top-color:var(--hc,#d4af37);}
|
| 45 |
+
.hp-name{font-family:var(--font-mono);font-weight:700;font-size:1.05rem;letter-spacing:.08em;}
|
| 46 |
+
.hp-role{font-size:.62rem;color:var(--text-muted);margin:.1rem 0 .5rem;}
|
| 47 |
+
.hp-head{font-family:var(--font-mono);font-size:.6rem;letter-spacing:.22em;color:var(--text-dim);
|
| 48 |
+
border-top:1px solid rgba(255,255,255,.12);padding-top:.4rem;margin-bottom:.4rem;}
|
| 49 |
+
.hp-list{list-style:none;display:flex;flex-direction:column;gap:.4rem;}
|
| 50 |
+
.hp-line{display:flex;align-items:center;gap:.5rem;font-family:var(--font-mono);font-size:.72rem;
|
| 51 |
+
color:var(--text-main);min-height:22px;opacity:.25;}
|
| 52 |
+
.hp-line.hp-shown{opacity:1;}
|
| 53 |
+
.hp-line.hp-pulse{animation:hp-pulse 1.4s ease-in-out infinite;}
|
| 54 |
+
@keyframes hp-pulse{0%,100%{filter:none;}50%{filter:drop-shadow(0 0 5px var(--hc));}}
|
| 55 |
+
.sp-icon{display:inline-flex;width:22px;height:22px;flex:0 0 22px;}
|
| 56 |
+
.sp-static *{animation:none !important;}
|
| 57 |
+
.hp-text{white-space:nowrap;overflow:hidden;}
|
| 58 |
+
|
| 59 |
+
/* Accessible focus proxies (visually hidden but focusable) */
|
| 60 |
+
.a11y-heroes{position:absolute;width:1px;height:1px;overflow:hidden;}
|
| 61 |
+
.a11y-heroes button{width:1px;height:1px;}
|
| 62 |
+
.a11y-hero:focus{position:fixed;left:8px;top:8px;width:auto;height:auto;z-index:200;
|
| 63 |
+
padding:.4rem .7rem;background:var(--gold);color:#150b22;border:0;border-radius:4px;
|
| 64 |
+
font-family:var(--font-mono);font-size:.7rem;}
|
| 65 |
+
|
| 66 |
+
/* SVG fallback grid (WebGL unavailable) */
|
| 67 |
+
#fallback[hidden]{display:none;}
|
| 68 |
+
#fallback{max-width:1100px;margin:1rem auto;padding:0 1rem;}
|
| 69 |
+
.fb-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;}
|
| 70 |
+
.fb-cell{display:flex;flex-direction:column;align-items:center;text-decoration:none;
|
| 71 |
+
background:#0a0e14;border:1px solid rgba(212,175,55,.18);border-radius:14px;padding:.9rem;
|
| 72 |
+
transition:transform .2s,border-color .2s;}
|
| 73 |
+
.fb-cell:hover,.fb-cell:focus-visible{transform:translateY(-4px);border-color:var(--gold);}
|
| 74 |
+
.fb-cell img{width:100%;aspect-ratio:1/1;border-radius:12px;}
|
| 75 |
+
.fb-name{font-family:var(--font-head);color:var(--gold);margin-top:.5rem;}
|
| 76 |
+
.fb-role{font-family:var(--font-mono);font-size:.6rem;color:var(--text-dim);
|
| 77 |
+
text-transform:uppercase;letter-spacing:.06em;text-align:center;}
|
| 78 |
+
@media(max-width:900px){.fb-grid{grid-template-columns:repeat(3,1fr);}}
|
| 79 |
+
@media(max-width:560px){.fb-grid{grid-template-columns:repeat(2,1fr);}}
|
| 80 |
+
|
| 81 |
+
@media(prefers-reduced-motion:reduce){
|
| 82 |
+
.hover-panel{transition:none;}
|
| 83 |
+
.hp-line.hp-pulse{animation:none;}
|
| 84 |
+
.sp-anim *{animation:none !important;}
|
| 85 |
+
}
|
|
@@ -2,579 +2,61 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8"/>
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover
|
| 6 |
-
<meta name="
|
| 7 |
-
<
|
| 8 |
-
<meta name="
|
| 9 |
-
<title
|
| 10 |
-
<meta
|
| 11 |
-
<meta property="og:title" content="SZL Holdings — Governed Agentic Mesh"/>
|
| 12 |
-
<meta property="og:description" content="Five organs, ten sub-organs. Every call leaves a receipt. Doctrine v11 LOCKED · 749/14/163."/>
|
| 13 |
<meta property="og:image" content="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png"/>
|
| 14 |
<meta property="og:type" content="website"/>
|
| 15 |
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
| 16 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
| 17 |
-
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500;600
|
| 18 |
-
<style>
|
| 19 |
-
/* =========================================================
|
| 20 |
-
SZL SHOWCASE — inherits amaru-platform design language
|
| 21 |
-
Palette: deep purple #1a0d2e · gold #d4af37
|
| 22 |
-
========================================================= */
|
| 23 |
-
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
| 24 |
-
:root{
|
| 25 |
-
--purple-deep:#1a0d2e; --purple-mid:#2d1b4e; --purple-light:#3d2878;
|
| 26 |
-
--gold:#d4af37; --gold-light:#e8cc6a; --gold-dim:#b8952e;
|
| 27 |
-
--text-main:#e8e0f0; --text-muted:#a090c0; --text-dim:#7060a0;
|
| 28 |
-
--border:rgba(212,175,55,0.18); --glass:rgba(45,27,78,0.55);
|
| 29 |
-
--coral:#ff7a59; --cyan:#00d4ff; --green:#3ddc84; --ocean:#0099cc;
|
| 30 |
-
--font-head:'Cinzel','Palatino Linotype',Georgia,serif;
|
| 31 |
-
--font-body:'Inter',system-ui,-apple-system,sans-serif;
|
| 32 |
-
--font-mono:'JetBrains Mono','Fira Code',monospace;
|
| 33 |
-
--radius:12px; --radius-lg:20px; --content-max:1180px;
|
| 34 |
-
}
|
| 35 |
-
html{scroll-behavior:smooth;font-size:16px;}
|
| 36 |
-
body{background:var(--purple-deep);color:var(--text-main);font-family:var(--font-body);line-height:1.7;-webkit-font-smoothing:antialiased;
|
| 37 |
-
background-image:radial-gradient(circle at 20% -10%,rgba(61,40,120,0.45),transparent 45%),radial-gradient(circle at 90% 10%,rgba(212,175,55,0.06),transparent 40%);}
|
| 38 |
-
h1,h2,h3,h4{font-family:var(--font-head);letter-spacing:0.03em;line-height:1.2;}
|
| 39 |
-
h1{font-size:clamp(2.4rem,6vw,4.2rem);color:var(--gold);}
|
| 40 |
-
h2{font-size:clamp(1.6rem,3.5vw,2.5rem);color:var(--gold-light);margin-bottom:0.4rem;}
|
| 41 |
-
h3{font-size:1.1rem;color:var(--gold);margin-bottom:0.35rem;}
|
| 42 |
-
h4{font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.12em;}
|
| 43 |
-
p{margin-bottom:1rem;color:var(--text-main);}
|
| 44 |
-
a{color:var(--gold);text-decoration:none;border-bottom:1px solid transparent;transition:border-color .2s,color .2s;}
|
| 45 |
-
a:hover{border-color:var(--gold);}
|
| 46 |
-
code,.mono{font-family:var(--font-mono);font-size:0.85em;background:rgba(212,175,55,0.08);padding:0.12em 0.45em;border-radius:5px;color:var(--gold-light);}
|
| 47 |
-
strong{color:var(--gold-light);font-weight:600;}
|
| 48 |
-
.container{max-width:var(--content-max);margin:0 auto;padding:0 1.6rem;}
|
| 49 |
-
section{padding:5rem 0;position:relative;}
|
| 50 |
-
section+section{border-top:1px solid var(--border);}
|
| 51 |
-
.gold-bar{width:54px;height:3px;background:linear-gradient(90deg,var(--gold),transparent);margin:0.6rem 0 2rem;border-radius:2px;}
|
| 52 |
-
.section-label{font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.2em;text-transform:uppercase;color:var(--gold-dim);}
|
| 53 |
-
.skip{position:absolute;left:-9999px;top:1rem;z-index:9999;padding:0.5rem 1.25rem;background:var(--gold);color:var(--purple-deep);font-weight:700;border-radius:4px;}
|
| 54 |
-
.skip:focus{left:1rem;}
|
| 55 |
-
/* NAV */
|
| 56 |
-
nav{position:sticky;top:0;z-index:100;background:rgba(26,13,46,0.82);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);}
|
| 57 |
-
.nav-inner{max-width:var(--content-max);margin:0 auto;padding:0.7rem 1.6rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;}
|
| 58 |
-
.nav-logo{display:flex;align-items:center;gap:0.5rem;font-family:var(--font-head);font-weight:700;color:var(--gold);font-size:1.05rem;}
|
| 59 |
-
.nav-links{display:flex;gap:1.05rem;flex-wrap:wrap;font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.08em;text-transform:uppercase;}
|
| 60 |
-
.nav-links a{color:var(--text-muted);border:none;}
|
| 61 |
-
.nav-links a:hover{color:var(--gold-light);}
|
| 62 |
-
/* HERO */
|
| 63 |
-
#hero{padding-top:4rem;}
|
| 64 |
-
.hero-eyebrow{font-family:var(--font-mono);font-size:0.75rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:1rem;}
|
| 65 |
-
.pill{display:inline-block;font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.05em;padding:0.32rem 0.85rem;border-radius:999px;border:1px solid var(--border);background:var(--glass);color:var(--text-muted);margin:0.2rem 0.3rem 0.2rem 0;}
|
| 66 |
-
.pill.warn{border-color:rgba(255,122,89,0.4);color:#ffb39e;}
|
| 67 |
-
.pill.gold{border-color:var(--gold);color:var(--gold-light);}
|
| 68 |
-
.hero-tagline{font-size:clamp(1.05rem,2.2vw,1.4rem);color:var(--text-main);max-width:760px;margin:1.1rem auto 1.4rem;}
|
| 69 |
-
.hero-grid{display:grid;grid-template-columns:1.05fr 0.95fr;gap:2.5rem;align-items:center;}
|
| 70 |
-
.hero-img{width:100%;max-width:540px;border-radius:var(--radius-lg);border:1px solid var(--border);background:#fff;}
|
| 71 |
-
.ctas{display:flex;gap:0.8rem;flex-wrap:wrap;margin-top:1.6rem;}
|
| 72 |
-
.btn{font-family:var(--font-body);font-weight:600;font-size:0.92rem;padding:0.7rem 1.3rem;border-radius:var(--radius);border:1px solid var(--border);transition:transform .15s,box-shadow .15s;}
|
| 73 |
-
.btn-primary{background:linear-gradient(135deg,var(--gold),var(--gold-dim));color:var(--purple-deep);border-color:var(--gold);}
|
| 74 |
-
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(212,175,55,0.25);border-color:var(--gold);}
|
| 75 |
-
.btn-secondary{background:var(--glass);color:var(--text-main);}
|
| 76 |
-
.btn-secondary:hover{transform:translateY(-2px);border-color:var(--gold);}
|
| 77 |
-
/* ORGAN GRID */
|
| 78 |
-
.organ-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1.2rem;margin-top:1.5rem;}
|
| 79 |
-
.organ-card{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.2rem 1rem;text-align:center;transition:transform .18s,border-color .18s,box-shadow .18s;display:flex;flex-direction:column;}
|
| 80 |
-
.organ-card:hover{transform:translateY(-4px);border-color:var(--accent,var(--gold));box-shadow:0 12px 30px rgba(0,0,0,0.35);}
|
| 81 |
-
.organ-card img{width:118px;height:118px;border-radius:50%;margin:0 auto 0.7rem;border:2px solid var(--accent,var(--gold));}
|
| 82 |
-
.organ-name{font-family:var(--font-head);font-size:1.25rem;color:var(--gold);margin-bottom:0.15rem;}
|
| 83 |
-
.organ-role{font-size:0.82rem;color:var(--text-muted);min-height:2.4em;}
|
| 84 |
-
.organ-quote{font-family:var(--font-mono);font-size:0.72rem;color:var(--text-main);background:rgba(255,255,255,0.04);border-radius:8px;padding:0.5rem;margin:0.7rem 0;min-height:3.4em;}
|
| 85 |
-
.organ-facts{font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);letter-spacing:0.02em;margin-bottom:0.8rem;}
|
| 86 |
-
.organ-card a.go{margin-top:auto;font-size:0.8rem;font-weight:600;}
|
| 87 |
-
/* SUB-ORGAN GRID */
|
| 88 |
-
.sub-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:0.8rem;margin-top:1.5rem;}
|
| 89 |
-
.sub-card{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius);padding:0.85rem;font-size:0.8rem;}
|
| 90 |
-
.sub-card b{color:var(--gold-light);font-size:0.92rem;}
|
| 91 |
-
.sub-card .where{display:block;font-family:var(--font-mono);font-size:0.66rem;color:var(--text-dim);margin-top:0.4rem;letter-spacing:0.04em;}
|
| 92 |
-
/* PhD STRIP */
|
| 93 |
-
.phd-note{font-size:0.85rem;color:var(--text-muted);background:rgba(255,122,89,0.07);border:1px solid rgba(255,122,89,0.25);border-radius:var(--radius);padding:0.7rem 1rem;margin-bottom:1.4rem;}
|
| 94 |
-
.phd-strip{display:flex;gap:1rem;overflow-x:auto;padding:0.6rem 0.2rem 1.2rem;scroll-snap-type:x mandatory;}
|
| 95 |
-
.phd-badge{flex:0 0 220px;scroll-snap-align:start;background:var(--glass);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.1rem;text-align:center;transition:transform .18s,border-color .18s;}
|
| 96 |
-
.phd-badge:hover{transform:translateY(-4px);border-color:var(--gold);}
|
| 97 |
-
.phd-badge img{width:120px;height:120px;border-radius:50%;margin:0 auto 0.6rem;display:block;}
|
| 98 |
-
.phd-badge .title{font-family:var(--font-head);color:var(--gold);font-size:1.05rem;margin-bottom:0.4rem;}
|
| 99 |
-
.phd-badge .ship{font-size:0.76rem;color:var(--text-muted);line-height:1.5;}
|
| 100 |
-
/* STATE TABLE */
|
| 101 |
-
table.state{width:100%;border-collapse:collapse;margin-top:1.2rem;font-size:0.88rem;}
|
| 102 |
-
table.state th,table.state td{text-align:left;padding:0.6rem 0.8rem;border-bottom:1px solid var(--border);}
|
| 103 |
-
table.state th{font-family:var(--font-mono);font-size:0.7rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--gold-dim);}
|
| 104 |
-
table.state td:nth-child(2){font-family:var(--font-mono);color:var(--gold-light);}
|
| 105 |
-
/* FOOTER */
|
| 106 |
-
footer{padding:3rem 0 4rem;border-top:1px solid var(--border);font-size:0.85rem;color:var(--text-muted);}
|
| 107 |
-
footer .links{display:flex;flex-wrap:wrap;gap:0.6rem 1.2rem;margin:1rem 0;font-family:var(--font-mono);font-size:0.8rem;}
|
| 108 |
-
footer .built{font-family:var(--font-mono);font-size:0.74rem;color:var(--text-dim);margin-top:1rem;letter-spacing:0.03em;}
|
| 109 |
-
/* RESPONSIVE */
|
| 110 |
-
@media(max-width:980px){
|
| 111 |
-
.hero-grid{grid-template-columns:1fr;text-align:center;}
|
| 112 |
-
.hero-img{margin:0 auto;}
|
| 113 |
-
.ctas{justify-content:center;}
|
| 114 |
-
.organ-grid{grid-template-columns:repeat(2,1fr);}
|
| 115 |
-
.sub-grid{grid-template-columns:repeat(2,1fr);}
|
| 116 |
-
}
|
| 117 |
-
@media(max-width:560px){
|
| 118 |
-
.organ-grid{grid-template-columns:1fr;}
|
| 119 |
-
.sub-grid{grid-template-columns:1fr;}
|
| 120 |
-
}
|
| 121 |
-
/* =========================================================
|
| 122 |
-
AMBIENT EMOJI LAYER (Wave4 — additive)
|
| 123 |
-
5 character emojis hanging around the page edges, animated.
|
| 124 |
-
Additive layer: does NOT touch the banner or the organ-card
|
| 125 |
-
painterly avatars. Uses transform/opacity (GPU compositor).
|
| 126 |
-
z-index 40: above the page background, below nav (100) + skip (9999).
|
| 127 |
-
========================================================= */
|
| 128 |
-
.emoji-layer{position:fixed;inset:0;z-index:40;pointer-events:none;overflow:hidden;}
|
| 129 |
-
/* outer wrapper = resting spot + slow drift; inner img = breathing + hover pulse */
|
| 130 |
-
.ambient-emoji{position:fixed;width:84px;height:84px;pointer-events:auto;}
|
| 131 |
-
.ae-inner{display:block;width:100%;height:100%;opacity:0.9;
|
| 132 |
-
filter:drop-shadow(0 4px 14px rgba(0,0,0,0.45));will-change:transform,opacity;
|
| 133 |
-
transition:transform .25s ease,filter .25s ease,opacity .25s ease;}
|
| 134 |
-
/* fixed resting spots — corners/edges, clear of the top-center banner */
|
| 135 |
-
.ae-a11oy{top:88px;left:22px;} /* upper-left, below nav */
|
| 136 |
-
.ae-rosie{top:118px;right:26px;} /* upper-right */
|
| 137 |
-
.ae-amaru{top:46%;left:14px;} /* mid-left edge */
|
| 138 |
-
.ae-sentra{top:54%;right:16px;} /* mid-right edge */
|
| 139 |
-
.ae-vessels{bottom:42px;right:40px;} /* lower-right */
|
| 140 |
-
/* hover + keyboard focus: non-motion cue (brighten) survives reduced-motion */
|
| 141 |
-
.ambient-emoji:hover .ae-inner,.ambient-emoji:focus-visible .ae-inner{opacity:1;
|
| 142 |
-
filter:drop-shadow(0 6px 20px rgba(212,175,55,0.55));}
|
| 143 |
-
.ambient-emoji:focus-visible{outline:2px solid var(--gold);outline-offset:4px;border-radius:50%;}
|
| 144 |
-
/* shrink + tuck on small screens so emojis never block reading */
|
| 145 |
-
@media(max-width:760px){
|
| 146 |
-
.ambient-emoji{width:54px;height:54px;opacity:0.8;}
|
| 147 |
-
.ae-amaru,.ae-sentra{display:none;}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/* Motion enabled for users who have NOT requested reduced motion */
|
| 151 |
-
@media(prefers-reduced-motion:no-preference){
|
| 152 |
-
@keyframes ae-breathe{0%,100%{transform:translateY(0) scale(1);opacity:0.88;}
|
| 153 |
-
50%{transform:translateY(-6px) scale(1.05);opacity:1;}}
|
| 154 |
-
@keyframes ae-drift-a{0%{transform:translate3d(0,0,0);}25%{transform:translate3d(10px,-8px,0);}
|
| 155 |
-
50%{transform:translate3d(4px,8px,0);}75%{transform:translate3d(-8px,4px,0);}100%{transform:translate3d(0,0,0);}}
|
| 156 |
-
@keyframes ae-drift-b{0%{transform:translate3d(0,0,0);}30%{transform:translate3d(-12px,6px,0);}
|
| 157 |
-
60%{transform:translate3d(6px,-10px,0);}100%{transform:translate3d(0,0,0);}}
|
| 158 |
-
@keyframes ae-pulse{0%{transform:scale(1);}40%{transform:scale(1.16);}100%{transform:scale(1);}}
|
| 159 |
-
/* idle breathing on the inner image; slow 30s drift on the wrapper */
|
| 160 |
-
.ae-inner{animation:ae-breathe 5.5s ease-in-out infinite;}
|
| 161 |
-
.ambient-emoji{animation:ae-drift-a 30s ease-in-out infinite;}
|
| 162 |
-
.ae-a11oy .ae-inner{animation-delay:-0.2s;}
|
| 163 |
-
.ae-rosie .ae-inner{animation-delay:-1.4s;}
|
| 164 |
-
.ae-amaru .ae-inner{animation-delay:-2.6s;}
|
| 165 |
-
.ae-sentra .ae-inner{animation-delay:-3.8s;}
|
| 166 |
-
.ae-vessels .ae-inner{animation-delay:-4.9s;}
|
| 167 |
-
.ae-rosie,.ae-sentra{animation-name:ae-drift-b;}
|
| 168 |
-
.ambient-emoji:hover .ae-inner,.ambient-emoji:focus-visible .ae-inner{animation:ae-pulse .6s ease-in-out;}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
@media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important;}
|
| 172 |
-
.ae-inner{animation:none!important;}.ambient-emoji{animation:none!important;}}
|
| 173 |
-
|
| 174 |
-
/* =========================================================
|
| 175 |
-
MISSION ROOM — joint cyber-ops action band (Wave4, additive)
|
| 176 |
-
Replaces the static team picture. Six characters, each an
|
| 177 |
-
animated WebP looping its own cyber task. The looping is baked
|
| 178 |
-
into each WebP; CSS adds just a very slight ambient float that
|
| 179 |
-
is fully disabled under prefers-reduced-motion. A static
|
| 180 |
-
first-frame WebP is stacked underneath and revealed when motion
|
| 181 |
-
is reduced (the animated layer is hidden in that case).
|
| 182 |
-
========================================================= */
|
| 183 |
-
#mission-room{padding:2.4rem 0 1.6rem;border-top:none;text-align:center;}
|
| 184 |
-
.mr-head{font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.2rem;}
|
| 185 |
-
.mr-band{display:flex;justify-content:center;align-items:flex-end;gap:0.3rem;flex-wrap:wrap;
|
| 186 |
-
padding:0.6rem 0 0.4rem;margin:0 auto;max-width:1140px;}
|
| 187 |
-
.mr-unit{position:relative;width:clamp(120px,15vw,178px);height:clamp(120px,15vw,178px);
|
| 188 |
-
display:flex;align-items:flex-end;justify-content:center;}
|
| 189 |
-
.mr-unit img{display:block;width:100%;height:100%;object-fit:contain;
|
| 190 |
-
filter:drop-shadow(0 6px 16px rgba(0,0,0,0.45));}
|
| 191 |
-
/* the static layer sits beneath the moving layer; hidden by default */
|
| 192 |
-
.mr-static{position:absolute;inset:0;opacity:0;}
|
| 193 |
-
.mr-cap{font-family:var(--font-mono);font-size:0.74rem;letter-spacing:0.14em;
|
| 194 |
-
color:var(--text-muted);margin-top:0.5rem;}
|
| 195 |
-
.mr-cap b{color:var(--gold-light);font-weight:500;}
|
| 196 |
-
@media(max-width:760px){
|
| 197 |
-
.mr-band{gap:0.2rem;}
|
| 198 |
-
.mr-unit{width:clamp(92px,29vw,120px);height:clamp(92px,29vw,120px);}
|
| 199 |
-
}
|
| 200 |
-
/* ambient float for motion-OK users; offset per unit so the band is alive, not in lock-step */
|
| 201 |
-
@media(prefers-reduced-motion:no-preference){
|
| 202 |
-
@keyframes mr-bob{0%,100%{transform:translateY(0);}50%{transform:translateY(-7px);}}
|
| 203 |
-
@keyframes mr-blip{0%,92%,100%{opacity:0;transform:scale(0.6);}96%{opacity:0.9;transform:scale(1);}}
|
| 204 |
-
.mr-unit{animation:mr-bob 4.6s ease-in-out infinite;}
|
| 205 |
-
.mr-unit:nth-child(1){animation-delay:-0.1s;}
|
| 206 |
-
.mr-unit:nth-child(2){animation-delay:-1.3s;animation-duration:5.1s;}
|
| 207 |
-
.mr-unit:nth-child(3){animation-delay:-2.4s;animation-duration:4.2s;}
|
| 208 |
-
.mr-unit:nth-child(4){animation-delay:-3.0s;animation-duration:5.4s;}
|
| 209 |
-
.mr-unit:nth-child(5){animation-delay:-1.9s;animation-duration:4.8s;}
|
| 210 |
-
.mr-unit:nth-child(6){animation-delay:-3.7s;animation-duration:5.0s;}
|
| 211 |
-
/* occasional sparkle blip near a couple of units */
|
| 212 |
-
.mr-unit::after{content:"";position:absolute;top:8%;right:10%;width:7px;height:7px;border-radius:50%;
|
| 213 |
-
background:var(--gold-light);box-shadow:0 0 8px var(--gold-light);opacity:0;pointer-events:none;}
|
| 214 |
-
.mr-unit:nth-child(1)::after{animation:mr-blip 7.5s ease-in-out infinite;}
|
| 215 |
-
.mr-unit:nth-child(4)::after{animation:mr-blip 9.2s ease-in-out infinite 2s;}
|
| 216 |
-
}
|
| 217 |
-
/* reduced motion: kill float + sparkle, hide the animated frame, show the still */
|
| 218 |
-
@media(prefers-reduced-motion:reduce){
|
| 219 |
-
.mr-unit{animation:none!important;}
|
| 220 |
-
.mr-unit::after{display:none!important;}
|
| 221 |
-
.mr-anim{opacity:0!important;}
|
| 222 |
-
.mr-static{opacity:1!important;}
|
| 223 |
-
}
|
| 224 |
-
</style>
|
| 225 |
-
|
| 226 |
-
<style id="szl-mobile-card-safety">
|
| 227 |
-
/* SZL org-card mobile safety net (ADDITIVE — Yachay). Overrides via later cascade. */
|
| 228 |
-
html, body { -webkit-tap-highlight-color: transparent; }
|
| 229 |
-
body { max-width: 100vw; overflow-x: hidden; }
|
| 230 |
-
img, canvas, svg, video, iframe { max-width: 100%; height: auto; }
|
| 231 |
-
/* intermediate tablet/large-phone layout: 5-col -> 2-col before the 560px 1-col fallback */
|
| 232 |
-
@media (max-width: 900px) and (min-width: 561px) {
|
| 233 |
-
.organ-grid, .sub-grid { grid-template-columns: repeat(2, 1fr) !important; }
|
| 234 |
-
}
|
| 235 |
-
@media (max-width: 560px) {
|
| 236 |
-
body { font-size: 16px; }
|
| 237 |
-
h1 { font-size: 24px; line-height: 1.2; }
|
| 238 |
-
a, button, [role="button"] { min-height: 44px; }
|
| 239 |
-
.organ-grid, .sub-grid { gap: 0.9rem; }
|
| 240 |
-
}
|
| 241 |
-
</style>
|
| 242 |
</head>
|
| 243 |
<body>
|
| 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 |
-
<script type="module">
|
| 287 |
-
import * as THREE from 'three';
|
| 288 |
-
|
| 289 |
-
const SZL_MOBILE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
| 290 |
-
const SZL_REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 291 |
-
const PR = Math.min(window.devicePixelRatio || 1, SZL_MOBILE ? 1.5 : 2);
|
| 292 |
-
const GOLD = 0xd4a444, GOLD_HI = 0xe8cc6a, WHITE = 0xffffff, SLATE = 0x0a0e14;
|
| 293 |
-
|
| 294 |
-
const cells = [];
|
| 295 |
-
|
| 296 |
-
function makeScene(canvas, organ){
|
| 297 |
-
const renderer = new THREE.WebGLRenderer({
|
| 298 |
-
canvas, antialias: !SZL_MOBILE, alpha: true,
|
| 299 |
-
powerPreference: SZL_MOBILE ? 'low-power' : 'high-performance'
|
| 300 |
-
});
|
| 301 |
-
renderer.setPixelRatio(PR);
|
| 302 |
-
// WebGL is live for this cell — drop the SVG poster fallback so the real 3D shows cleanly.
|
| 303 |
-
canvas.style.backgroundImage = 'none';
|
| 304 |
-
const scene = new THREE.Scene();
|
| 305 |
-
scene.background = null;
|
| 306 |
-
const cam = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
| 307 |
-
cam.position.set(0, 0.4, 6.2);
|
| 308 |
-
|
| 309 |
-
scene.add(new THREE.AmbientLight(0x404a5e, 1.1));
|
| 310 |
-
const key = new THREE.DirectionalLight(GOLD_HI, 1.6); key.position.set(3,4,5); scene.add(key);
|
| 311 |
-
const rim = new THREE.DirectionalLight(0x6688ff, 0.7); rim.position.set(-4,-2,-3); scene.add(rim);
|
| 312 |
-
const pt = new THREE.PointLight(WHITE, 0.9, 20); pt.position.set(0,0,4); scene.add(pt);
|
| 313 |
-
|
| 314 |
-
const root = new THREE.Group(); scene.add(root);
|
| 315 |
-
const goldMat = new THREE.MeshStandardMaterial({color:GOLD, metalness:0.85, roughness:0.32, emissive:0x2a1d08, emissiveIntensity:0.4});
|
| 316 |
-
const slateMat = new THREE.MeshStandardMaterial({color:0x1b2230, metalness:0.6, roughness:0.5});
|
| 317 |
-
const emitMat = new THREE.MeshStandardMaterial({color:WHITE, emissive:WHITE, emissiveIntensity:1.4, metalness:0.2, roughness:0.3});
|
| 318 |
-
const goldEmit = new THREE.MeshStandardMaterial({color:GOLD_HI, emissive:GOLD, emissiveIntensity:1.1});
|
| 319 |
-
|
| 320 |
-
const extras = {}; // per-organ animated handles
|
| 321 |
-
|
| 322 |
-
if(organ==='amaru'){
|
| 323 |
-
// serpent (tube) coiled around a glowing crystal core
|
| 324 |
-
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.85,0), emitMat.clone());
|
| 325 |
-
core.material.color.setHex(GOLD_HI); core.material.emissive.setHex(GOLD);
|
| 326 |
-
root.add(core); extras.core = core;
|
| 327 |
-
const pts=[]; const turns=3.2, N=160;
|
| 328 |
-
for(let i=0;i<=N;i++){const t=i/N; const a=t*Math.PI*2*turns; const r=1.5-0.35*t;
|
| 329 |
-
pts.push(new THREE.Vector3(Math.cos(a)*r, (t-0.5)*3.0, Math.sin(a)*r));}
|
| 330 |
-
const tube = new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts),200,0.16,10,false), goldMat.clone());
|
| 331 |
-
root.add(tube);
|
| 332 |
-
const head = new THREE.Mesh(new THREE.ConeGeometry(0.26,0.6,8), goldEmit.clone());
|
| 333 |
-
const hp = pts[pts.length-1]; head.position.copy(hp); root.add(head);
|
| 334 |
-
const eyeL=new THREE.Mesh(new THREE.SphereGeometry(0.05,8,8),emitMat); eyeL.position.copy(hp).add(new THREE.Vector3(0.12,0.1,0.12)); root.add(eyeL);
|
| 335 |
-
}
|
| 336 |
-
else if(organ==='sentra'){
|
| 337 |
-
// icosahedron shield with hex panels + threat dots absorbed
|
| 338 |
-
const shield = new THREE.Mesh(new THREE.IcosahedronGeometry(1.5,1),
|
| 339 |
-
new THREE.MeshStandardMaterial({color:0x16202f, metalness:0.7, roughness:0.35, flatShading:true}));
|
| 340 |
-
root.add(shield);
|
| 341 |
-
const wire = new THREE.LineSegments(new THREE.WireframeGeometry(shield.geometry),
|
| 342 |
-
new THREE.LineBasicMaterial({color:GOLD})); root.add(wire);
|
| 343 |
-
const lambda = new THREE.Mesh(new THREE.TorusGeometry(0.55,0.07,8,30), goldEmit.clone());
|
| 344 |
-
root.add(lambda); extras.lambda=lambda;
|
| 345 |
-
const threats=[]; const tg=new THREE.SphereGeometry(0.08,8,8);
|
| 346 |
-
const tm=new THREE.MeshStandardMaterial({color:0xff5a44,emissive:0xff5a44,emissiveIntensity:1.2});
|
| 347 |
-
for(let i=0;i<8;i++){const m=new THREE.Mesh(tg,tm.clone());
|
| 348 |
-
m.userData={a:Math.random()*6.28, r:2.4+Math.random()*0.6, sp:0.6+Math.random()*0.5};
|
| 349 |
-
root.add(m); threats.push(m);} extras.threats=threats;
|
| 350 |
-
}
|
| 351 |
-
else if(organ==='rosie'){
|
| 352 |
-
// torus + sphere stack (head + HUD rings) + pulsing data lines
|
| 353 |
-
const head = new THREE.Mesh(new THREE.SphereGeometry(0.7,24,24), slateMat.clone());
|
| 354 |
-
head.material.color.setHex(0x1d2636); head.position.y=0.2; root.add(head);
|
| 355 |
-
const visor = new THREE.Mesh(new THREE.TorusGeometry(0.55,0.09,10,30), goldEmit.clone());
|
| 356 |
-
visor.rotation.x=Math.PI/2; visor.position.y=0.2; root.add(visor);
|
| 357 |
-
const rings=[];
|
| 358 |
-
for(let i=0;i<3;i++){const r=new THREE.Mesh(new THREE.TorusGeometry(1.0+i*0.42,0.025,8,48),
|
| 359 |
-
new THREE.MeshStandardMaterial({color:GOLD,emissive:GOLD,emissiveIntensity:0.6,transparent:true,opacity:0.8-i*0.18}));
|
| 360 |
-
r.rotation.x=Math.PI/2.1; r.position.y=-0.1; root.add(r); rings.push(r);} extras.rings=rings;
|
| 361 |
-
const base=new THREE.Mesh(new THREE.CylinderGeometry(0.9,1.05,0.18,32), slateMat.clone()); base.position.y=-1.25; root.add(base);
|
| 362 |
-
// pulsing data lines (vertical)
|
| 363 |
-
const lines=[]; for(let i=0;i<6;i++){const a=i/6*6.28;
|
| 364 |
-
const m=new THREE.Mesh(new THREE.BoxGeometry(0.05,1.2,0.05),goldEmit.clone());
|
| 365 |
-
m.position.set(Math.cos(a)*1.3,-0.55,Math.sin(a)*1.3); root.add(m); lines.push(m);} extras.lines=lines;
|
| 366 |
-
}
|
| 367 |
-
else if(organ==='killinchu'){
|
| 368 |
-
// low-poly kestrel over faceted terrain + 53 drone-dots circling
|
| 369 |
-
const terrain=new THREE.Mesh(new THREE.ConeGeometry(2.3,0.7,3,1),
|
| 370 |
-
new THREE.MeshStandardMaterial({color:0x14202c,metalness:0.4,roughness:0.7,flatShading:true}));
|
| 371 |
-
terrain.position.y=-1.6; terrain.rotation.y=0.4; root.add(terrain);
|
| 372 |
-
const bird=new THREE.Group(); bird.position.y=0.5; root.add(bird); extras.bird=bird;
|
| 373 |
-
const body=new THREE.Mesh(new THREE.ConeGeometry(0.22,1.0,6),goldMat.clone());
|
| 374 |
-
body.rotation.x=Math.PI/2; bird.add(body);
|
| 375 |
-
const wgeo=new THREE.BufferGeometry(); const wv=new Float32Array([0,0,0, 1.5,0.15,-0.5, 1.3,-0.05,0.4]);
|
| 376 |
-
wgeo.setAttribute('position',new THREE.BufferAttribute(wv,3)); wgeo.computeVertexNormals();
|
| 377 |
-
const wmat=new THREE.MeshStandardMaterial({color:GOLD_HI,metalness:0.7,roughness:0.4,side:THREE.DoubleSide,flatShading:true});
|
| 378 |
-
const wingL=new THREE.Mesh(wgeo,wmat); const wingR=new THREE.Mesh(wgeo,wmat); wingR.scale.x=-1;
|
| 379 |
-
bird.add(wingL); bird.add(wingR); extras.wingL=wingL; extras.wingR=wingR;
|
| 380 |
-
const eye=new THREE.Mesh(new THREE.SphereGeometry(0.06,8,8),emitMat); eye.position.set(0.08,0.05,0.5); bird.add(eye);
|
| 381 |
-
// 53 drone-dots
|
| 382 |
-
const dg=new THREE.SphereGeometry(0.04,6,6);
|
| 383 |
-
const dm=new THREE.MeshStandardMaterial({color:WHITE,emissive:0x88ccff,emissiveIntensity:1.0});
|
| 384 |
-
const drones=new THREE.InstancedMesh(dg,dm,53); const dummy=new THREE.Object3D(); const dd=[];
|
| 385 |
-
for(let i=0;i<53;i++){dd.push({a:Math.random()*6.28, r:1.6+Math.random()*0.9, y:-0.3+Math.random()*1.4, sp:0.4+Math.random()*0.6});}
|
| 386 |
-
root.add(drones); extras.drones=drones; extras.dd=dd; extras.dummy=dummy;
|
| 387 |
-
}
|
| 388 |
-
else if(organ==='a11oy'){
|
| 389 |
-
// 16-node icosahedron knot-graph (Khipu cords) + edge pulses
|
| 390 |
-
const ico=new THREE.IcosahedronGeometry(1.5,0);
|
| 391 |
-
const pos=ico.attributes.position; const nodes=[]; const seen=new Set();
|
| 392 |
-
for(let i=0;i<pos.count;i++){const v=new THREE.Vector3().fromBufferAttribute(pos,i);
|
| 393 |
-
const k=v.toArray().map(n=>n.toFixed(2)).join(','); if(seen.has(k))continue; seen.add(k); nodes.push(v);}
|
| 394 |
-
const ng=new THREE.SphereGeometry(0.13,12,12);
|
| 395 |
-
nodes.forEach(v=>{const m=new THREE.Mesh(ng,goldEmit.clone()); m.position.copy(v); root.add(m);});
|
| 396 |
-
// knotted edges
|
| 397 |
-
const edgeMat=new THREE.LineBasicMaterial({color:GOLD,transparent:true,opacity:0.6});
|
| 398 |
-
const segs=[]; for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){
|
| 399 |
-
if(nodes[i].distanceTo(nodes[j])<1.95){segs.push(nodes[i],nodes[j]);}}
|
| 400 |
-
const eg=new THREE.BufferGeometry().setFromPoints(segs);
|
| 401 |
-
root.add(new THREE.LineSegments(eg, edgeMat));
|
| 402 |
-
// pulse traveling along edges
|
| 403 |
-
const pulse=new THREE.Mesh(new THREE.SphereGeometry(0.1,10,10),emitMat); root.add(pulse);
|
| 404 |
-
extras.pulse=pulse; extras.segs=segs;
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
// touch / mouse rotate (additive — auto-rotate continues unless reduced motion)
|
| 408 |
-
let drag=false, px=0, py=0, vy=0, vx=0;
|
| 409 |
-
const dn=e=>{drag=true; const p=e.touches?e.touches[0]:e; px=p.clientX; py=p.clientY;};
|
| 410 |
-
const mv=e=>{if(!drag)return; const p=e.touches?e.touches[0]:e;
|
| 411 |
-
vy=(p.clientX-px)*0.01; vx=(p.clientY-py)*0.01; root.rotation.y+=vy; root.rotation.x+=vx;
|
| 412 |
-
px=p.clientX; py=p.clientY; if(e.touches)e.preventDefault();};
|
| 413 |
-
const up=()=>{drag=false;};
|
| 414 |
-
canvas.addEventListener('mousedown',dn); canvas.addEventListener('mousemove',mv); window.addEventListener('mouseup',up);
|
| 415 |
-
canvas.addEventListener('touchstart',dn,{passive:true}); canvas.addEventListener('touchmove',mv,{passive:false}); canvas.addEventListener('touchend',up);
|
| 416 |
-
|
| 417 |
-
function resize(){const w=canvas.clientWidth||220, h=canvas.clientHeight||220;
|
| 418 |
-
if(canvas.width!==w*PR||canvas.height!==h*PR){renderer.setSize(w,h,false); cam.aspect=w/h; cam.updateProjectionMatrix();}}
|
| 419 |
-
|
| 420 |
-
cells.push({renderer,scene,cam,root,organ,extras,resize});
|
| 421 |
-
}
|
| 422 |
-
|
| 423 |
-
// Resilient init: if WebGL is unavailable or the module/importmap is stripped by a
|
| 424 |
-
// host sanitizer (e.g. the HF org-card iframe), each canvas keeps its inline SVG
|
| 425 |
-
// poster background so the mesh is NEVER an empty box. Live 3D paints over it when supported.
|
| 426 |
-
document.querySelectorAll('#szl-hero-grid canvas').forEach(function(c){
|
| 427 |
-
try { makeScene(c, c.dataset.organ); }
|
| 428 |
-
catch(err){ /* keep poster fallback for this organ */ }
|
| 429 |
-
});
|
| 430 |
-
|
| 431 |
-
let t0=performance.now();
|
| 432 |
-
function loop(now){
|
| 433 |
-
requestAnimationFrame(loop);
|
| 434 |
-
if(document.hidden) return;
|
| 435 |
-
const t=(now-t0)/1000;
|
| 436 |
-
for(const c of cells){
|
| 437 |
-
c.resize();
|
| 438 |
-
if(!SZL_REDUCED) c.root.rotation.y += 0.0045;
|
| 439 |
-
const e=c.extras;
|
| 440 |
-
if(c.organ==='amaru' && e.core){ e.core.rotation.y+=0.02; e.core.scale.setScalar(1+Math.sin(t*2)*0.06); }
|
| 441 |
-
if(c.organ==='sentra'){ if(e.lambda)e.lambda.rotation.z+=0.02;
|
| 442 |
-
(e.threats||[]).forEach(m=>{m.userData.r-=m.userData.sp*0.012; if(m.userData.r<0.6){m.userData.r=2.6;}
|
| 443 |
-
m.userData.a+=0.02; m.position.set(Math.cos(m.userData.a)*m.userData.r, Math.sin(m.userData.a*1.3)*0.6, Math.sin(m.userData.a)*m.userData.r);}); }
|
| 444 |
-
if(c.organ==='rosie'){ (e.rings||[]).forEach((r,i)=>r.rotation.z+=0.01*(i+1));
|
| 445 |
-
(e.lines||[]).forEach((l,i)=>{l.scale.y=0.6+0.5*(0.5+0.5*Math.sin(t*3+i)); l.material.emissiveIntensity=0.6+0.6*(0.5+0.5*Math.sin(t*3+i));}); }
|
| 446 |
-
if(c.organ==='killinchu'){ if(e.bird)e.bird.position.y=0.5+Math.sin(t*1.5)*0.12;
|
| 447 |
-
if(e.wingL){const f=Math.sin(t*5)*0.5; e.wingL.rotation.z=f; e.wingR.rotation.z=-f;}
|
| 448 |
-
if(e.drones){const dummy=e.dummy; e.dd.forEach((d,i)=>{d.a+=d.sp*0.01;
|
| 449 |
-
dummy.position.set(Math.cos(d.a)*d.r,d.y+Math.sin(t+d.a)*0.1,Math.sin(d.a)*d.r);
|
| 450 |
-
dummy.updateMatrix(); e.drones.setMatrixAt(i,dummy.matrix);}); e.drones.instanceMatrix.needsUpdate=true;} }
|
| 451 |
-
if(c.organ==='a11oy' && e.pulse && e.segs.length){
|
| 452 |
-
const seg=Math.floor(t*0.7)%(e.segs.length/2); const a=e.segs[seg*2], b=e.segs[seg*2+1];
|
| 453 |
-
const f=(t*0.7)%1; e.pulse.position.lerpVectors(a,b,f); }
|
| 454 |
-
c.renderer.render(c.scene,c.cam);
|
| 455 |
-
}
|
| 456 |
-
}
|
| 457 |
-
requestAnimationFrame(loop);
|
| 458 |
-
</script>
|
| 459 |
-
<!-- ================= /SZL 3D HERO FIGURES ================= -->
|
| 460 |
-
|
| 461 |
-
<!-- BANNER — UNTOUCHED (5-hero painterly artwork is sacred) -->
|
| 462 |
-
<section id="banner" style="padding:0;border-top:none;">
|
| 463 |
-
<img src="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png" alt="SZL Holdings — the five-hero governed agentic mesh team" style="display:block;width:100%;height:auto;"/>
|
| 464 |
-
</section>
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
<!-- flag block: five hero portraits as links + org name + icon links + footer -->
|
| 469 |
-
<main id="main">
|
| 470 |
-
<section style="padding:3.5rem 0;text-align:center;">
|
| 471 |
-
<div class="container">
|
| 472 |
-
|
| 473 |
-
<!-- chibi portrait row replaced by 3D heroes (above) + live mesh panel (below) — Yachay -->
|
| 474 |
-
|
| 475 |
-
<h1 style="margin-bottom:1.4rem;">SZL Holdings</h1>
|
| 476 |
-
|
| 477 |
-
<div style="display:flex;justify-content:center;gap:1.6rem;font-size:1.8rem;">
|
| 478 |
-
<a href="https://orcid.org/0009-0001-0110-4173" aria-label="ORCID" title="ORCID">✦</a>
|
| 479 |
-
<a href="https://github.com/szl-holdings" aria-label="GitHub" title="GitHub">⌬</a>
|
| 480 |
-
<a href="https://huggingface.co/spaces/SZLHOLDINGS/uds-demo" aria-label="UDS Demo" title="UDS Demo">▶</a>
|
| 481 |
-
</div>
|
| 482 |
-
|
| 483 |
-
</div>
|
| 484 |
-
</section>
|
| 485 |
-
</main>
|
| 486 |
-
<!-- ============ LIVE MESH + LATEST RECEIPTS (ADDITIVE — Yachay) ============
|
| 487 |
-
Polls /healthz of each flagship live (badge flips green/red) and pulls the
|
| 488 |
-
5 most recent Khipu receipts across the mesh. Apache-2.0. Doctrine v11 LOCKED. -->
|
| 489 |
-
<section id="szl-live-mesh" style="padding:2.2rem 0;">
|
| 490 |
-
<div class="container">
|
| 491 |
-
<p class="mr-head" style="font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.2rem;text-align:center;">Live Mesh · /healthz</p>
|
| 492 |
-
<div id="szl-mesh-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.7rem;margin:1rem 0 0;">
|
| 493 |
-
<div class="szl-mesh-cell" data-organ="amaru"><span class="szl-mesh-name">amaru</span><span class="szl-mesh-badge" data-code="…">…</span></div>
|
| 494 |
-
<div class="szl-mesh-cell" data-organ="sentra"><span class="szl-mesh-name">sentra</span><span class="szl-mesh-badge" data-code="…">…</span></div>
|
| 495 |
-
<div class="szl-mesh-cell" data-organ="rosie"><span class="szl-mesh-name">rosie</span><span class="szl-mesh-badge" data-code="…">…</span></div>
|
| 496 |
-
<div class="szl-mesh-cell" data-organ="killinchu"><span class="szl-mesh-name">killinchu</span><span class="szl-mesh-badge" data-code="…">…</span></div>
|
| 497 |
-
<div class="szl-mesh-cell" data-organ="a11oy"><span class="szl-mesh-name">a11oy</span><span class="szl-mesh-badge" data-code="…">…</span></div>
|
| 498 |
-
</div>
|
| 499 |
-
<p id="szl-receipt-counter" style="text-align:center;font-family:var(--font-mono);font-size:0.8rem;color:var(--gold-light);margin-top:1.1rem;">signed receipts across the mesh: <b data-receipts>—</b></p>
|
| 500 |
-
<div style="max-width:760px;margin:1.2rem auto 0;">
|
| 501 |
-
<p class="mr-head" style="font-family:var(--font-mono);font-size:0.7rem;letter-spacing:0.14em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.4rem;text-align:center;">Latest signed receipts</p>
|
| 502 |
-
<ul id="szl-receipt-list" style="list-style:none;padding:0;margin:0;font-family:var(--font-mono);font-size:0.72rem;color:var(--text-muted);">
|
| 503 |
-
<li style="opacity:0.6;text-align:center;padding:0.5rem;">polling Khipu DAG…</li>
|
| 504 |
-
</ul>
|
| 505 |
-
</div>
|
| 506 |
-
</div>
|
| 507 |
-
</section>
|
| 508 |
-
<style id="szl-live-mesh-style">
|
| 509 |
-
.szl-mesh-cell{background:var(--glass,rgba(45,27,78,0.55));border:1px solid var(--border,rgba(212,175,55,0.18));border-radius:12px;padding:0.7rem 0.4rem;text-align:center;display:flex;flex-direction:column;gap:0.4rem;min-height:44px;}
|
| 510 |
-
.szl-mesh-name{font-family:var(--font-mono,monospace);font-size:0.72rem;color:var(--text-muted,#a090c0);letter-spacing:0.04em;}
|
| 511 |
-
.szl-mesh-badge{font-family:var(--font-mono,monospace);font-size:0.78rem;font-weight:600;padding:0.18rem 0.4rem;border-radius:6px;background:rgba(255,255,255,0.06);color:var(--text-dim,#7060a0);}
|
| 512 |
-
.szl-mesh-badge.ok{background:rgba(61,220,132,0.16);color:#3ddc84;}
|
| 513 |
-
.szl-mesh-badge.down{background:rgba(255,90,68,0.16);color:#ff7a59;}
|
| 514 |
-
#szl-receipt-list li{padding:0.4rem 0.6rem;border-bottom:1px solid var(--border,rgba(212,175,55,0.12));overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 515 |
-
#szl-receipt-list li b{color:var(--gold-light,#e8cc6a);}
|
| 516 |
-
@media(max-width:900px) and (min-width:561px){#szl-mesh-grid{grid-template-columns:repeat(3,1fr);}}
|
| 517 |
-
@media(max-width:560px){#szl-mesh-grid{grid-template-columns:repeat(2,1fr);}}
|
| 518 |
-
</style>
|
| 519 |
-
<script>
|
| 520 |
-
(function(){
|
| 521 |
-
var ORGANS=["amaru","sentra","rosie","killinchu","a11oy"];
|
| 522 |
-
var receipts=[];
|
| 523 |
-
function base(o){return "https://szlholdings-"+o+".hf.space";}
|
| 524 |
-
function pollHealth(){
|
| 525 |
-
ORGANS.forEach(function(o){
|
| 526 |
-
var cell=document.querySelector('.szl-mesh-cell[data-organ="'+o+'"] .szl-mesh-badge');
|
| 527 |
-
if(!cell)return;
|
| 528 |
-
fetch(base(o)+"/healthz",{mode:"cors",cache:"no-store"}).then(function(r){
|
| 529 |
-
cell.textContent=r.status; cell.dataset.code=r.status;
|
| 530 |
-
cell.className="szl-mesh-badge "+(r.ok?"ok":"down");
|
| 531 |
-
}).catch(function(){ cell.textContent="—"; cell.className="szl-mesh-badge down"; });
|
| 532 |
-
});
|
| 533 |
-
}
|
| 534 |
-
function pollReceipts(){
|
| 535 |
-
var total=0, got=0;
|
| 536 |
-
ORGANS.forEach(function(o){
|
| 537 |
-
// metrics counter for running total
|
| 538 |
-
fetch(base(o)+"/metrics",{mode:"cors",cache:"no-store"}).then(function(r){return r.text();}).then(function(t){
|
| 539 |
-
var m=t.match(/(\w*receipts_total)\s+(\d+)/);
|
| 540 |
-
if(m){ total+=parseInt(m[2],10); }
|
| 541 |
-
}).catch(function(){}).finally(function(){
|
| 542 |
-
got++; if(got>=ORGANS.length){var b=document.querySelector('[data-receipts]'); if(b&&total>0)b.textContent=total.toLocaleString();}
|
| 543 |
-
});
|
| 544 |
-
// latest receipts
|
| 545 |
-
fetch(base(o)+"/api/"+o+"/v2/khipu/lmdb/tail?n=2",{mode:"cors",cache:"no-store"}).then(function(r){return r.json();}).then(function(j){
|
| 546 |
-
var arr=(j&&j.receipts)||j&&j.tail||[];
|
| 547 |
-
arr.forEach(function(rec){ receipts.push({organ:o, id:(rec.id||rec.hash||rec.receipt_id||"").toString().slice(0,16), ts:rec.ts||rec.timestamp||""}); });
|
| 548 |
-
renderReceipts();
|
| 549 |
-
}).catch(function(){});
|
| 550 |
-
});
|
| 551 |
-
}
|
| 552 |
-
function renderReceipts(){
|
| 553 |
-
var ul=document.getElementById("szl-receipt-list"); if(!ul)return;
|
| 554 |
-
var latest=receipts.slice(-5).reverse();
|
| 555 |
-
if(!latest.length)return;
|
| 556 |
-
ul.innerHTML=latest.map(function(r){return '<li><b>'+r.organ+'</b> · '+(r.id||'receipt')+' <span style="opacity:0.6;">'+(r.ts||'')+'</span></li>';}).join("");
|
| 557 |
-
}
|
| 558 |
-
pollHealth(); pollReceipts();
|
| 559 |
-
setInterval(pollHealth,5000); setInterval(pollReceipts,15000);
|
| 560 |
-
})();
|
| 561 |
-
</script>
|
| 562 |
-
<!-- ============ /LIVE MESH + LATEST RECEIPTS ============ -->
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
<footer style="text-align:center;padding:1.5rem 0;border-top:1px solid var(--border);">
|
| 566 |
-
<!-- Stills strip: the previous static team picture, relocated here from below the banner
|
| 567 |
-
when the live Mission Room action band replaced it. Kept as a reference still. -->
|
| 568 |
-
<!-- team-portrait still removed: replaced by 3D heroes (Yachay) -->
|
| 569 |
-
<span class="mono" style="font-size:0.72rem;color:var(--text-dim);">— szl-holdings/.github @ d304951 · Doctrine v11 LOCKED · 749 declarations · 14 unique axioms · 163 sorries · ORCID 0009-0001-0110-4173</span>
|
| 570 |
-
</footer>
|
| 571 |
-
|
| 572 |
-
<!-- AMBIENT EMOJI LAYER (Wave4, additive) — 5 character emojis hanging around
|
| 573 |
-
the page edges via position:fixed. Decorative (aria-hidden), placed at the
|
| 574 |
-
end of <body> so that if a host strips the <style> block (e.g. the HF org-card
|
| 575 |
-
HTML sanitizer), these small 64px images fall to the page bottom instead of
|
| 576 |
-
overlaying content; explicit width/height attributes keep them small even
|
| 577 |
-
with CSS removed. Animation is gated behind prefers-reduced-motion: no-preference. -->
|
| 578 |
-
<!-- ambient chibi emoji layer removed: replaced by 3D heroes (Yachay) -->
|
| 579 |
</body>
|
| 580 |
</html>
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
| 6 |
+
<meta name="theme-color" content="#12091f"/>
|
| 7 |
+
<title>SZL Holdings — Five Organs · Live 3D Character Roster</title>
|
| 8 |
+
<meta name="description" content="SZL Holdings character-select roster: five live organs as 3D heroes in UDS swag. Hover reveals superpowers; click opens each Space. Doctrine v11 LOCKED 749/14/163."/>
|
| 9 |
+
<meta property="og:title" content="SZL Holdings — Live 3D Character Roster"/>
|
| 10 |
+
<meta property="og:description" content="The Mesh · Five Organs · Live 3D · Character Roster · UDS Edition."/>
|
|
|
|
|
|
|
| 11 |
<meta property="og:image" content="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png"/>
|
| 12 |
<meta property="og:type" content="website"/>
|
| 13 |
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
| 14 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"/>
|
| 16 |
+
<link rel="stylesheet" href="css/style.css"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
+
<a class="skip" href="#stage">Skip to character roster</a>
|
| 20 |
+
|
| 21 |
+
<header class="roster-header">THE MESH · FIVE ORGANS · LIVE 3D · CHARACTER ROSTER</header>
|
| 22 |
+
|
| 23 |
+
<!-- 3D character-select stage. Replaces both the old flat 5-tile grid and the
|
| 24 |
+
static AI-illustration hero banner. -->
|
| 25 |
+
<main id="stage" aria-label="SZL Holdings five-organ 3D character roster">
|
| 26 |
+
<canvas id="roster" aria-hidden="true"></canvas>
|
| 27 |
+
<div id="loading">INITIALIZING ROSTER…</div>
|
| 28 |
+
<!-- Hidden focusable proxies for keyboard nav (Tab cycles, Enter opens Space) -->
|
| 29 |
+
<div id="a11y-heroes" class="a11y-heroes" aria-label="Hero select"></div>
|
| 30 |
+
|
| 31 |
+
<!-- WebGL fallback: the existing 5-tile SVG grid (graceful degradation) -->
|
| 32 |
+
<section id="fallback" hidden aria-label="SZL Holdings five organs (static)">
|
| 33 |
+
<div class="fb-grid">
|
| 34 |
+
<a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/amaru" aria-label="AMARU — cortex · serpent. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMTEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgb3BhY2l0eT0iMC45NSI+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjE5MCIgcng9IjU4IiByeT0iMTciLz48ZWxsaXBzZSBjeD0iMTI4IiBjeT0iMTUwIiByeD0iNTAiIHJ5PSIxNSIvPjxlbGxpcHNlIGN4PSIxMjgiIGN5PSIxMTIiIHJ4PSI0MCIgcnk9IjEyIi8+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjgwIiByeD0iMjgiIHJ5PSI5Ii8+PC9nPjxwb2x5Z29uIHBvaW50cz0iMTI4LDk4IDE1MCwxMzIgMTI4LDE1NiAxMDYsMTMyIiBmaWxsPSIjZThjYzZhIi8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNjAgMTQ0LDg0IDEyOCw5OCAxMTIsODQiIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNDIiIGN5PSI3NCIgcj0iNCIgZmlsbD0iI2Y0ZWVkZSIvPjwvc3ZnPg==" alt="amaru emblem"/><span class="fb-name">AMARU</span><span class="fb-role">cortex · serpent</span></a>
|
| 35 |
+
<a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/sentra" aria-label="SENTRA — immune · shield. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIwLjkiPjxwb2x5Z29uIHBvaW50cz0iMTI4LDU2IDE5NiwxMDQgMTcwLDE4MiA4NiwxODIgNjAsMTA0Ii8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNTYgMTcwLDE4MiA2MCwxMDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCw1NiA4NiwxODIgMTk2LDEwNCIvPjxsaW5lIHgxPSI2MCIgeTE9IjEwNCIgeDI9IjE5NiIgeTI9IjEwNCIvPjxsaW5lIHgxPSIxMjgiIHkxPSI1NiIgeDI9IjEyOCIgeTI9IjE4MiIvPjwvZz48cG9seWdvbiBwb2ludHM9IjEwMCwxMTggMTU2LDExOCAxMjgsMTUwIiBmaWxsPSIjZDRhNDQ0IiBvcGFjaXR5PSIwLjU1Ii8+PGNpcmNsZSBjeD0iNjAiIGN5PSIxMzgiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSIyMDAiIGN5PSIxMjAiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSI3OCIgY3k9IjkyIiByPSI1IiBmaWxsPSIjNjY4OGZmIi8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTIwIiByPSIzNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjQiIG9wYWNpdHk9IjAuOCIvPjwvc3ZnPg==" alt="sentra emblem"/><span class="fb-name">SENTRA</span><span class="fb-role">immune · shield</span></a>
|
| 36 |
+
<a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/rosie" aria-label="ROSIE — operator · console. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48Y2lyY2xlIGN4PSIxMjgiIGN5PSIxMjgiIHI9IjQ0IiBmaWxsPSIjMTExNjFmIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMyIvPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjEyOCIgcj0iMjIiIGZpbGw9IiNmNGVlZGUiIG9wYWNpdHk9IjAuOTIiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iNjAiIHkxPSIxNzAiIHgyPSI2MCIgeTI9IjEyMCIvPjxsaW5lIHgxPSIxMDAiIHkxPSIxODAiIHgyPSIxMDAiIHkyPSIxMTAiLz48bGluZSB4MT0iMTU2IiB5MT0iMTgwIiB4Mj0iMTU2IiB5Mj0iMTEwIi8+PGxpbmUgeDE9IjE5NiIgeTE9IjE3MCIgeDI9IjE5NiIgeTI9IjEyMCIvPjwvZz48cmVjdCB4PSI1NiIgeT0iMTc2IiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjgiIHJ4PSI0IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTI4IiByPSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjEuNiIgb3BhY2l0eT0iMC41Ii8+PC9zdmc+" alt="rosie emblem"/><span class="fb-name">ROSIE</span><span class="fb-role">operator · console</span></a>
|
| 37 |
+
<a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/killinchu" aria-label="KILLINCHU — kestrel · drone. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48cGF0aCBkPSJNNzAgMTQwIFExMjggOTYgMTg2IDE0MCBRMTUwIDEyOCAxMjggMTI4IFExMDYgMTI4IDcwIDE0MFoiIGZpbGw9IiNkNGE0NDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCwxMTIgMTM0LDEyOCAxMjIsMTI4IiBmaWxsPSIjZThjYzZhIi8+PGcgZmlsbD0iI2Y0ZWVkZSIgb3BhY2l0eT0iMC45Ij48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyMCIgcj0iMy4yIi8+PGNpcmNsZSBjeD0iOTIiIGN5PSI5NiIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTYwIiBjeT0iOTIiIHI9IjMiLz48Y2lyY2xlIGN4PSIxOTYiIGN5PSIxMTgiIHI9IjMuNCIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE2MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTAwIiBjeT0iMTY4IiByPSIzIi8+PGNpcmNsZSBjeD0iMjAwIiBjeT0iMTUwIiByPSIyLjQiLz48Y2lyY2xlIGN4PSI1OCIgY3k9IjE1OCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iNzgiIHI9IjIuNCIvPjwvZz48ZyBmaWxsPSIjNjY4OGZmIiBvcGFjaXR5PSIwLjgiPjxjaXJjbGUgY3g9IjEyMCIgY3k9IjE1MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTcwIiBjeT0iMTQwIiByPSIyLjYiLz48L2c+PC9zdmc+" alt="killinchu emblem"/><span class="fb-name">KILLINCHU</span><span class="fb-role">kestrel · drone</span></a>
|
| 38 |
+
<a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/a11oy" aria-label="A11OY — router · wires. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi4yIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSIxODYiIHkyPSIxMDQiLz48bGluZSB4MT0iMTg2IiB5MT0iMTA0IiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PGxpbmUgeDE9IjE5NiIgeTE9IjE2OCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSIxNDAiIHkxPSIxOTYiIHgyPSI4NiIgeTI9IjE4NCIvPjxsaW5lIHgxPSI4NiIgeTE9IjE4NCIgeDI9IjY0IiB5Mj0iMTI4Ii8+PGxpbmUgeDE9IjY0IiB5MT0iMTI4IiB4Mj0iMTI4IiB5Mj0iNzIiLz48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSI5NiIgeTI9Ijk2Ii8+PGxpbmUgeDE9Ijk2IiB5MT0iOTYiIHgyPSIxNTAiIHkyPSIxNDAiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTg2IiB5Mj0iMTA0Ii8+PGxpbmUgeDE9IjE1MCIgeTE9IjE0MCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSI5NiIgeTE9Ijk2IiB4Mj0iNjQiIHkyPSIxMjgiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PC9nPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjcyIiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTg2IiBjeT0iMTA0IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTk2IiBjeT0iMTY4IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTQwIiBjeT0iMTk2IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iODYiIGN5PSIxODQiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyOCIgcj0iNyIgZmlsbD0iI2Q0YTQ0NCIvPjxjaXJjbGUgY3g9Ijk2IiBjeT0iOTYiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIxNDAiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI5NiIgY3k9Ijk2IiByPSI3IiBmaWxsPSIjZjRlZWRlIi8+PC9zdmc+" alt="a11oy emblem"/><span class="fb-name">A11OY</span><span class="fb-role">router · wires</span></a>
|
| 39 |
+
</div>
|
| 40 |
+
</section>
|
| 41 |
+
</main>
|
| 42 |
+
|
| 43 |
+
<p class="tagline">Governed Agentic Mesh
|
| 44 |
+
<small>Provable by math · signed by receipts · runs on your hardware · UDS Edition</small>
|
| 45 |
+
</p>
|
| 46 |
+
|
| 47 |
+
<footer class="roster-footer">
|
| 48 |
+
Doctrine v11 LOCKED · <b>749 / 14 / 163</b> · Λ Conjecture 1 · SLSA L1 honest · UDS Edition
|
| 49 |
+
</footer>
|
| 50 |
+
|
| 51 |
+
<script type="importmap">
|
| 52 |
+
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
|
| 53 |
+
</script>
|
| 54 |
+
<script type="module" src="js/main.js"></script>
|
| 55 |
+
<noscript>
|
| 56 |
+
<p style="text-align:center;color:#a090c0;font-family:monospace;padding:1rem">
|
| 57 |
+
JavaScript is required for the live 3D roster. Visit each organ:
|
| 58 |
+
<a href="https://huggingface.co/SZLHOLDINGS">SZLHOLDINGS</a>.
|
| 59 |
+
</p>
|
| 60 |
+
</noscript>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</body>
|
| 62 |
</html>
|
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// canvas_decals.js
|
| 2 |
+
// Procedural canvas-painted textures for UDS swag — zero external assets.
|
| 3 |
+
// Every texture is generated with the 2D Canvas API and wrapped in a THREE.CanvasTexture.
|
| 4 |
+
// This keeps the scene asset-free (fast load, no licensing risk, clearly readable).
|
| 5 |
+
import * as THREE from 'three';
|
| 6 |
+
|
| 7 |
+
const DPR = 2; // supersample the decal canvases for crisp text
|
| 8 |
+
|
| 9 |
+
function makeCanvas(w, h) {
|
| 10 |
+
const c = document.createElement('canvas');
|
| 11 |
+
c.width = w * DPR;
|
| 12 |
+
c.height = h * DPR;
|
| 13 |
+
const ctx = c.getContext('2d');
|
| 14 |
+
ctx.scale(DPR, DPR);
|
| 15 |
+
return { c, ctx, w, h };
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function texFrom(c) {
|
| 19 |
+
const t = new THREE.CanvasTexture(c);
|
| 20 |
+
t.anisotropy = 4;
|
| 21 |
+
t.colorSpace = THREE.SRGBColorSpace;
|
| 22 |
+
t.needsUpdate = true;
|
| 23 |
+
return t;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Draw a small emblem glyph for a hero. Keeps shapes simple + iconic.
|
| 27 |
+
function drawEmblem(ctx, kind, cx, cy, r, color) {
|
| 28 |
+
ctx.save();
|
| 29 |
+
ctx.translate(cx, cy);
|
| 30 |
+
ctx.strokeStyle = color;
|
| 31 |
+
ctx.fillStyle = color;
|
| 32 |
+
ctx.lineWidth = Math.max(2, r * 0.14);
|
| 33 |
+
ctx.lineCap = 'round';
|
| 34 |
+
ctx.lineJoin = 'round';
|
| 35 |
+
switch (kind) {
|
| 36 |
+
case 'serpent': { // AMARU — golden serpent S-curve
|
| 37 |
+
ctx.beginPath();
|
| 38 |
+
for (let i = 0; i <= 40; i++) {
|
| 39 |
+
const t = i / 40;
|
| 40 |
+
const a = t * Math.PI * 2.6;
|
| 41 |
+
const x = Math.sin(a) * r * 0.62;
|
| 42 |
+
const y = (t - 0.5) * r * 1.9;
|
| 43 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
| 44 |
+
}
|
| 45 |
+
ctx.stroke();
|
| 46 |
+
ctx.beginPath(); ctx.arc(Math.sin(0) * r * 0.62, -0.5 * r * 1.9, r * 0.16, 0, 7); ctx.fill();
|
| 47 |
+
break;
|
| 48 |
+
}
|
| 49 |
+
case 'shield': { // SENTRA — gate-glyph shield
|
| 50 |
+
ctx.beginPath();
|
| 51 |
+
ctx.moveTo(0, -r);
|
| 52 |
+
ctx.lineTo(r * 0.82, -r * 0.45);
|
| 53 |
+
ctx.lineTo(r * 0.62, r * 0.85);
|
| 54 |
+
ctx.lineTo(0, r);
|
| 55 |
+
ctx.lineTo(-r * 0.62, r * 0.85);
|
| 56 |
+
ctx.lineTo(-r * 0.82, -r * 0.45);
|
| 57 |
+
ctx.closePath(); ctx.stroke();
|
| 58 |
+
// gate bars inside
|
| 59 |
+
for (let i = -1; i <= 1; i++) {
|
| 60 |
+
ctx.beginPath();
|
| 61 |
+
ctx.moveTo(-r * 0.4, i * r * 0.32);
|
| 62 |
+
ctx.lineTo(r * 0.4, i * r * 0.32);
|
| 63 |
+
ctx.stroke();
|
| 64 |
+
}
|
| 65 |
+
break;
|
| 66 |
+
}
|
| 67 |
+
case 'console': { // ROSIE — console rune (terminal prompt)
|
| 68 |
+
ctx.strokeRect(-r * 0.9, -r * 0.7, r * 1.8, r * 1.4);
|
| 69 |
+
ctx.beginPath();
|
| 70 |
+
ctx.moveTo(-r * 0.5, -r * 0.2);
|
| 71 |
+
ctx.lineTo(-r * 0.1, r * 0.05);
|
| 72 |
+
ctx.lineTo(-r * 0.5, r * 0.3);
|
| 73 |
+
ctx.stroke();
|
| 74 |
+
ctx.beginPath();
|
| 75 |
+
ctx.moveTo(r * 0.05, r * 0.3);
|
| 76 |
+
ctx.lineTo(r * 0.55, r * 0.3);
|
| 77 |
+
ctx.stroke();
|
| 78 |
+
break;
|
| 79 |
+
}
|
| 80 |
+
case 'falcon': { // KILLINCHU — falcon / kestrel in flight
|
| 81 |
+
ctx.beginPath();
|
| 82 |
+
ctx.moveTo(-r, -r * 0.1);
|
| 83 |
+
ctx.quadraticCurveTo(-r * 0.35, -r * 0.65, 0, -r * 0.15);
|
| 84 |
+
ctx.quadraticCurveTo(r * 0.35, -r * 0.65, r, -r * 0.1);
|
| 85 |
+
ctx.quadraticCurveTo(r * 0.35, r * 0.1, 0, r * 0.55);
|
| 86 |
+
ctx.quadraticCurveTo(-r * 0.35, r * 0.1, -r, -r * 0.1);
|
| 87 |
+
ctx.closePath(); ctx.fill();
|
| 88 |
+
break;
|
| 89 |
+
}
|
| 90 |
+
case 'router': { // A11OY — neural-net / multi-arrow router node
|
| 91 |
+
ctx.beginPath(); ctx.arc(0, 0, r * 0.28, 0, 7); ctx.stroke();
|
| 92 |
+
const dirs = [[0, -1], [0.87, 0.5], [-0.87, 0.5]];
|
| 93 |
+
for (const [dx, dy] of dirs) {
|
| 94 |
+
ctx.beginPath();
|
| 95 |
+
ctx.moveTo(dx * r * 0.3, dy * r * 0.3);
|
| 96 |
+
ctx.lineTo(dx * r, dy * r);
|
| 97 |
+
ctx.stroke();
|
| 98 |
+
// arrowhead
|
| 99 |
+
ctx.beginPath();
|
| 100 |
+
ctx.arc(dx * r, dy * r, r * 0.12, 0, 7);
|
| 101 |
+
ctx.fill();
|
| 102 |
+
}
|
| 103 |
+
break;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
ctx.restore();
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Build the wrap-around torso texture: dark fabric, name stencil on chest,
|
| 110 |
+
// UDS monogram patch on right shoulder, emblem on chest center.
|
| 111 |
+
// The texture wraps a cylinder; chest faces +Z, so we paint chest at U≈0.5.
|
| 112 |
+
export function makeTorsoTexture({ name, stencil, emblem, color, swatch }) {
|
| 113 |
+
const W = 512, H = 512;
|
| 114 |
+
const { c, ctx } = makeCanvas(W, H);
|
| 115 |
+
// base fabric
|
| 116 |
+
const g = ctx.createLinearGradient(0, 0, 0, H);
|
| 117 |
+
g.addColorStop(0, shade(swatch, -0.45));
|
| 118 |
+
g.addColorStop(1, shade(swatch, -0.7));
|
| 119 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 120 |
+
|
| 121 |
+
// subtle vertical seams
|
| 122 |
+
ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 2;
|
| 123 |
+
for (let x = 64; x < W; x += 128) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
|
| 124 |
+
|
| 125 |
+
// chest panel (centered at U=0.5 -> x=W/2)
|
| 126 |
+
const cx = W / 2;
|
| 127 |
+
// emblem on chest center
|
| 128 |
+
drawEmblem(ctx, emblem, cx, H * 0.42, 56, color);
|
| 129 |
+
|
| 130 |
+
// name stencil under emblem
|
| 131 |
+
ctx.fillStyle = '#f4f4f6';
|
| 132 |
+
ctx.font = 'bold 30px "JetBrains Mono", monospace';
|
| 133 |
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 134 |
+
ctx.fillText(stencil, cx, H * 0.66);
|
| 135 |
+
// colored underline
|
| 136 |
+
ctx.fillStyle = color; ctx.fillRect(cx - 120, H * 0.72, 240, 4);
|
| 137 |
+
|
| 138 |
+
// UDS shoulder patch — right shoulder sits near U≈0.78 -> x≈0.78*W
|
| 139 |
+
drawUDSPatch(ctx, W * 0.80, H * 0.16, 70, color);
|
| 140 |
+
|
| 141 |
+
return texFrom(c);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// A reusable UDS monogram patch (also used by grey NPCs).
|
| 145 |
+
export function drawUDSPatch(ctx, cx, cy, size, accent = '#9aa3ad') {
|
| 146 |
+
ctx.save();
|
| 147 |
+
ctx.translate(cx, cy);
|
| 148 |
+
// patch backing
|
| 149 |
+
ctx.fillStyle = '#1b2128';
|
| 150 |
+
roundRect(ctx, -size * 0.7, -size * 0.55, size * 1.4, size * 1.1, 8); ctx.fill();
|
| 151 |
+
ctx.strokeStyle = accent; ctx.lineWidth = 3;
|
| 152 |
+
roundRect(ctx, -size * 0.7, -size * 0.55, size * 1.4, size * 1.1, 8); ctx.stroke();
|
| 153 |
+
// UDS text
|
| 154 |
+
ctx.fillStyle = '#e8edf2';
|
| 155 |
+
ctx.font = `bold ${Math.round(size * 0.5)}px "JetBrains Mono", monospace`;
|
| 156 |
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 157 |
+
ctx.fillText('UDS', 0, -size * 0.05);
|
| 158 |
+
ctx.fillStyle = accent;
|
| 159 |
+
ctx.font = `${Math.round(size * 0.16)}px "JetBrains Mono", monospace`;
|
| 160 |
+
ctx.fillText('UNITED DEFENSE', 0, size * 0.32);
|
| 161 |
+
ctx.restore();
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Plain grey UDS NPC torso texture.
|
| 165 |
+
export function makeNPCTexture() {
|
| 166 |
+
const W = 256, H = 256;
|
| 167 |
+
const { c, ctx } = makeCanvas(W, H);
|
| 168 |
+
const g = ctx.createLinearGradient(0, 0, 0, H);
|
| 169 |
+
g.addColorStop(0, '#3a4048'); g.addColorStop(1, '#262b31');
|
| 170 |
+
ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
|
| 171 |
+
drawUDSPatch(ctx, W / 2, H * 0.42, 48, '#7f8a95');
|
| 172 |
+
ctx.fillStyle = '#b9c2cb';
|
| 173 |
+
ctx.font = '600 16px "JetBrains Mono", monospace';
|
| 174 |
+
ctx.textAlign = 'center';
|
| 175 |
+
ctx.fillText('PARADE REST', W / 2, H * 0.78);
|
| 176 |
+
return texFrom(c);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function roundRect(ctx, x, y, w, h, r) {
|
| 180 |
+
ctx.beginPath();
|
| 181 |
+
ctx.moveTo(x + r, y);
|
| 182 |
+
ctx.arcTo(x + w, y, x + w, y + h, r);
|
| 183 |
+
ctx.arcTo(x + w, y + h, x, y + h, r);
|
| 184 |
+
ctx.arcTo(x, y + h, x, y, r);
|
| 185 |
+
ctx.arcTo(x, y, x + w, y, r);
|
| 186 |
+
ctx.closePath();
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
function shade(hex, amt) {
|
| 190 |
+
const c = new THREE.Color(hex);
|
| 191 |
+
const hsl = {}; c.getHSL(hsl);
|
| 192 |
+
hsl.l = Math.max(0, Math.min(1, hsl.l + amt));
|
| 193 |
+
c.setHSL(hsl.h, hsl.s * 0.8, hsl.l);
|
| 194 |
+
return '#' + c.getHexString();
|
| 195 |
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// heroes.js
|
| 2 |
+
// The 5 SZL heroes: stylized low-poly humanoids built from primitives
|
| 3 |
+
// (capsule torso/limbs, sphere head, cone stance) — deliberately PS1-era,
|
| 4 |
+
// readable, fast, fully procedural. UDS swag painted via canvas_decals.
|
| 5 |
+
import * as THREE from 'three';
|
| 6 |
+
import { makeTorsoTexture } from './canvas_decals.js';
|
| 7 |
+
|
| 8 |
+
export const HEROES = [
|
| 9 |
+
{
|
| 10 |
+
id: 'amaru', name: 'AMARU', role: 'Cognitive Cortex — the brain',
|
| 11 |
+
color: '#f5c518', stencil: 'AMARU · CORTEX', emblem: 'serpent',
|
| 12 |
+
swag: 'Gold UDS shoulder patch · golden serpent emblem',
|
| 13 |
+
url: 'https://huggingface.co/spaces/SZLHOLDINGS/amaru',
|
| 14 |
+
prop: 'brain',
|
| 15 |
+
powers: [
|
| 16 |
+
{ label: 'Dual-stream routing', icon: 'amaru_dualstream' },
|
| 17 |
+
{ label: '7-chakra memory', icon: 'amaru_memory' },
|
| 18 |
+
{ label: 'Forward-model prediction', icon: 'amaru_forward' },
|
| 19 |
+
{ label: 'Lineage DAG', icon: 'amaru_lineage' },
|
| 20 |
+
],
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
id: 'sentra', name: 'SENTRA', role: 'Immune Sentinel — the gates',
|
| 24 |
+
color: '#c8324e', stencil: 'SENTRA · SHIELD', emblem: 'shield',
|
| 25 |
+
swag: 'Crimson UDS tactical vest · gate-glyph shield emblem',
|
| 26 |
+
url: 'https://huggingface.co/spaces/SZLHOLDINGS/sentra',
|
| 27 |
+
prop: 'runes',
|
| 28 |
+
powers: [
|
| 29 |
+
{ label: '8 fail-CLOSED gates', icon: 'sentra_gates' },
|
| 30 |
+
{ label: 'DSSE signing', icon: 'sentra_dsse' },
|
| 31 |
+
{ label: 'Conduction-aphasia detector', icon: 'sentra_conduction' },
|
| 32 |
+
{ label: 'Cosign verify', icon: 'sentra_cosign' },
|
| 33 |
+
],
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: 'rosie', name: 'ROSIE', role: 'Executive Operator — multi-organ orchestrator',
|
| 37 |
+
color: '#2ea66c', stencil: 'ROSIE · CONSOLE', emblem: 'console',
|
| 38 |
+
swag: 'Emerald UDS command jacket · console-rune emblem',
|
| 39 |
+
url: 'https://huggingface.co/spaces/SZLHOLDINGS/rosie',
|
| 40 |
+
prop: 'console',
|
| 41 |
+
powers: [
|
| 42 |
+
{ label: 'Multi-LLM ensemble', icon: 'rosie_ensemble' },
|
| 43 |
+
{ label: 'Adaptive routing', icon: 'rosie_routing' },
|
| 44 |
+
{ label: 'Step-3.7-Flash voter', icon: 'rosie_step' },
|
| 45 |
+
{ label: 'Executive UI', icon: 'rosie_exec' },
|
| 46 |
+
],
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
id: 'killinchu', name: 'KILLINCHU', role: 'Kestrel Drone Health — UDS Edition',
|
| 50 |
+
color: '#4ac4d9', stencil: 'KILLINCHU · KESTREL', emblem: 'falcon',
|
| 51 |
+
swag: 'Full UDS field uniform · falcon emblem — the UDS face',
|
| 52 |
+
url: 'https://huggingface.co/spaces/SZLHOLDINGS/killinchu',
|
| 53 |
+
prop: 'hud', isUDSFace: true,
|
| 54 |
+
powers: [
|
| 55 |
+
{ label: 'NOAA space-weather', icon: 'kil_noaa' },
|
| 56 |
+
{ label: 'USGS seismic', icon: 'kil_usgs' },
|
| 57 |
+
{ label: 'MQ-9 drone health', icon: 'kil_mq9' },
|
| 58 |
+
{ label: 'Phase prediction', icon: 'kil_phase' },
|
| 59 |
+
],
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
id: 'a11oy', name: 'A11OY', role: 'Sovereign Router — the wires',
|
| 63 |
+
color: '#8b6cf2', stencil: 'A11OY · ROUTER', emblem: 'router',
|
| 64 |
+
swag: 'Violet UDS sash · neural-net circuit pattern',
|
| 65 |
+
url: 'https://huggingface.co/spaces/SZLHOLDINGS/a11oy',
|
| 66 |
+
prop: 'streams',
|
| 67 |
+
powers: [
|
| 68 |
+
{ label: 'PAC-Bayes governance', icon: 'a11_governance' },
|
| 69 |
+
{ label: 'Multi-LLM router', icon: 'a11_router' },
|
| 70 |
+
{ label: '35 anchor formulas', icon: 'a11_formulas' },
|
| 71 |
+
{ label: 'Khipu chain', icon: 'a11_khipu' },
|
| 72 |
+
],
|
| 73 |
+
},
|
| 74 |
+
];
|
| 75 |
+
|
| 76 |
+
// Build one stylized low-poly humanoid. Returns a THREE.Group with a `.userData`
|
| 77 |
+
// describing animatable parts (torso for breathing, prop group, etc.).
|
| 78 |
+
export function buildHero(hero) {
|
| 79 |
+
const g = new THREE.Group();
|
| 80 |
+
const col = new THREE.Color(hero.color);
|
| 81 |
+
const skin = new THREE.Color('#caa98c');
|
| 82 |
+
|
| 83 |
+
const matBody = new THREE.MeshStandardMaterial({
|
| 84 |
+
map: makeTorsoTexture({
|
| 85 |
+
name: hero.name, stencil: hero.stencil, emblem: hero.emblem,
|
| 86 |
+
color: hero.color, swatch: hero.color,
|
| 87 |
+
}),
|
| 88 |
+
roughness: 0.62, metalness: 0.12, flatShading: true,
|
| 89 |
+
});
|
| 90 |
+
const matLimb = new THREE.MeshStandardMaterial({
|
| 91 |
+
color: col.clone().multiplyScalar(0.45), roughness: 0.7, metalness: 0.1, flatShading: true,
|
| 92 |
+
});
|
| 93 |
+
const matSkin = new THREE.MeshStandardMaterial({ color: skin, roughness: 0.8, flatShading: true });
|
| 94 |
+
const matAccent = new THREE.MeshStandardMaterial({
|
| 95 |
+
color: col, emissive: col, emissiveIntensity: 0.35, roughness: 0.4, metalness: 0.3, flatShading: true,
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
// Torso — capsule. Pivot so breathing scales from the waist.
|
| 99 |
+
const torsoGeo = new THREE.CapsuleGeometry(0.34, 0.7, 4, 10);
|
| 100 |
+
const torso = new THREE.Mesh(torsoGeo, matBody);
|
| 101 |
+
torso.castShadow = true;
|
| 102 |
+
const torsoPivot = new THREE.Group();
|
| 103 |
+
torso.position.y = 0.55;
|
| 104 |
+
torsoPivot.add(torso);
|
| 105 |
+
torsoPivot.position.y = 0.9;
|
| 106 |
+
g.add(torsoPivot);
|
| 107 |
+
|
| 108 |
+
// Head — sphere + a small accent visor band.
|
| 109 |
+
const head = new THREE.Mesh(new THREE.SphereGeometry(0.27, 16, 12), matSkin);
|
| 110 |
+
head.position.y = 0.55 + 0.62;
|
| 111 |
+
head.castShadow = true;
|
| 112 |
+
torsoPivot.add(head);
|
| 113 |
+
const visor = new THREE.Mesh(new THREE.TorusGeometry(0.24, 0.05, 6, 16), matAccent);
|
| 114 |
+
visor.rotation.x = Math.PI / 2.1;
|
| 115 |
+
visor.position.y = 0.55 + 0.66;
|
| 116 |
+
torsoPivot.add(visor);
|
| 117 |
+
|
| 118 |
+
// Shoulders / arms — capsules angled outward (parade-ready stance).
|
| 119 |
+
for (const side of [-1, 1]) {
|
| 120 |
+
const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.1, 0.6, 4, 8), matLimb);
|
| 121 |
+
arm.position.set(side * 0.46, 0.42, 0);
|
| 122 |
+
arm.rotation.z = side * 0.18;
|
| 123 |
+
arm.castShadow = true;
|
| 124 |
+
torsoPivot.add(arm);
|
| 125 |
+
const shoulder = new THREE.Mesh(new THREE.SphereGeometry(0.14, 10, 8), matAccent);
|
| 126 |
+
shoulder.position.set(side * 0.44, 0.78, 0);
|
| 127 |
+
torsoPivot.add(shoulder);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Legs — capsules.
|
| 131 |
+
for (const side of [-1, 1]) {
|
| 132 |
+
const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.13, 0.7, 4, 8), matLimb);
|
| 133 |
+
leg.position.set(side * 0.17, 0.45, 0);
|
| 134 |
+
leg.castShadow = true;
|
| 135 |
+
g.add(leg);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Stance cone — anchors the figure to the dais (the "base").
|
| 139 |
+
const base = new THREE.Mesh(new THREE.ConeGeometry(0.5, 0.3, 16), matLimb);
|
| 140 |
+
base.position.y = 0.12; base.rotation.x = Math.PI;
|
| 141 |
+
g.add(base);
|
| 142 |
+
|
| 143 |
+
// Prop group (weapon/tool) — hidden until hover.
|
| 144 |
+
const prop = buildProp(hero, col);
|
| 145 |
+
prop.visible = false;
|
| 146 |
+
g.add(prop);
|
| 147 |
+
|
| 148 |
+
g.userData = { hero, torsoPivot, prop, baseScale: 1 };
|
| 149 |
+
return g;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Per-hero activated prop. Returns a group positioned around the hero.
|
| 153 |
+
function buildProp(hero, col) {
|
| 154 |
+
const grp = new THREE.Group();
|
| 155 |
+
const mat = new THREE.MeshStandardMaterial({
|
| 156 |
+
color: col, emissive: col, emissiveIntensity: 0.9, transparent: true,
|
| 157 |
+
opacity: 0.85, roughness: 0.3, metalness: 0.4,
|
| 158 |
+
});
|
| 159 |
+
switch (hero.prop) {
|
| 160 |
+
case 'brain': { // glowing brain hemispheres halo above head
|
| 161 |
+
for (const side of [-1, 1]) {
|
| 162 |
+
const hemi = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 10, 0, Math.PI), mat);
|
| 163 |
+
hemi.position.set(side * 0.16, 2.05, 0);
|
| 164 |
+
hemi.rotation.y = side > 0 ? 0 : Math.PI;
|
| 165 |
+
grp.add(hemi);
|
| 166 |
+
}
|
| 167 |
+
const halo = new THREE.Mesh(new THREE.TorusGeometry(0.34, 0.02, 6, 24), mat);
|
| 168 |
+
halo.position.y = 2.05; halo.rotation.x = Math.PI / 2;
|
| 169 |
+
grp.add(halo);
|
| 170 |
+
grp.userData.spin = halo;
|
| 171 |
+
break;
|
| 172 |
+
}
|
| 173 |
+
case 'runes': { // 8 floating shield runes orbit
|
| 174 |
+
const runes = [];
|
| 175 |
+
for (let i = 0; i < 8; i++) {
|
| 176 |
+
const r = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.16, 0.03), mat);
|
| 177 |
+
const a = (i / 8) * Math.PI * 2;
|
| 178 |
+
r.position.set(Math.cos(a) * 0.7, 1.0, Math.sin(a) * 0.7);
|
| 179 |
+
grp.add(r); runes.push(r);
|
| 180 |
+
}
|
| 181 |
+
grp.userData.orbit = runes;
|
| 182 |
+
break;
|
| 183 |
+
}
|
| 184 |
+
case 'console': { // holographic console panel materializes in front
|
| 185 |
+
const panel = new THREE.Mesh(new THREE.PlaneGeometry(0.8, 0.5), mat);
|
| 186 |
+
panel.position.set(0, 1.1, 0.55);
|
| 187 |
+
grp.add(panel);
|
| 188 |
+
for (let i = 0; i < 3; i++) {
|
| 189 |
+
const bar = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.04, 0.01),
|
| 190 |
+
new THREE.MeshStandardMaterial({ color: '#0a160f', emissive: col, emissiveIntensity: 0.6 }));
|
| 191 |
+
bar.position.set(0, 1.2 - i * 0.12, 0.56);
|
| 192 |
+
grp.add(bar);
|
| 193 |
+
}
|
| 194 |
+
break;
|
| 195 |
+
}
|
| 196 |
+
case 'hud': { // holographic drone HUD reticle locks on
|
| 197 |
+
const reticle = new THREE.Mesh(new THREE.RingGeometry(0.28, 0.34, 24), mat);
|
| 198 |
+
reticle.position.set(0, 1.4, 0.5);
|
| 199 |
+
grp.add(reticle);
|
| 200 |
+
const inner = new THREE.Mesh(new THREE.RingGeometry(0.1, 0.12, 16), mat);
|
| 201 |
+
inner.position.set(0, 1.4, 0.5);
|
| 202 |
+
grp.add(inner);
|
| 203 |
+
grp.userData.spin = reticle;
|
| 204 |
+
break;
|
| 205 |
+
}
|
| 206 |
+
case 'streams': { // glowing routing-arrow streams flow from hand
|
| 207 |
+
const streams = [];
|
| 208 |
+
for (let i = 0; i < 5; i++) {
|
| 209 |
+
const s = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.18, 6), mat);
|
| 210 |
+
s.position.set(0.5, 0.7 + i * 0.02, 0.2);
|
| 211 |
+
s.rotation.z = -Math.PI / 2;
|
| 212 |
+
grp.add(s); streams.push(s);
|
| 213 |
+
}
|
| 214 |
+
grp.userData.stream = streams;
|
| 215 |
+
break;
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
return grp;
|
| 219 |
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// hover_panel.js
|
| 2 |
+
// Floating holographic superpower panel. Rendered as a DOM overlay anchored to
|
| 3 |
+
// the hero's screen-projected head position (rises ~60cm above the head in world
|
| 4 |
+
// space). Name in hero color (monospace), SUPERPOWERS header, 4 lines that type
|
| 5 |
+
// out sequentially (30ms/char) then pulse, each with an animated SVG icon.
|
| 6 |
+
import { buildIcon } from './superpower_icons.js';
|
| 7 |
+
|
| 8 |
+
export class HoverPanel {
|
| 9 |
+
constructor(container, reduced) {
|
| 10 |
+
this.container = container;
|
| 11 |
+
this.reduced = reduced;
|
| 12 |
+
this.el = document.createElement('div');
|
| 13 |
+
this.el.className = 'hover-panel';
|
| 14 |
+
this.el.setAttribute('role', 'tooltip');
|
| 15 |
+
this.el.setAttribute('aria-hidden', 'true');
|
| 16 |
+
container.appendChild(this.el);
|
| 17 |
+
this.activeId = null;
|
| 18 |
+
this._timers = [];
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
_clearTimers() { this._timers.forEach(clearTimeout); this._timers = []; }
|
| 22 |
+
|
| 23 |
+
show(hero) {
|
| 24 |
+
if (this.activeId === hero.id) return;
|
| 25 |
+
this.activeId = hero.id;
|
| 26 |
+
this._clearTimers();
|
| 27 |
+
const c = hero.color;
|
| 28 |
+
this.el.style.setProperty('--hc', c);
|
| 29 |
+
this.el.innerHTML = `
|
| 30 |
+
<div class="hp-name" style="color:${c}">${hero.name}</div>
|
| 31 |
+
<div class="hp-role">${hero.role}</div>
|
| 32 |
+
<div class="hp-head">SUPERPOWERS</div>
|
| 33 |
+
<ul class="hp-list"></ul>`;
|
| 34 |
+
const list = this.el.querySelector('.hp-list');
|
| 35 |
+
hero.powers.forEach((p, i) => {
|
| 36 |
+
const li = document.createElement('li');
|
| 37 |
+
li.className = 'hp-line';
|
| 38 |
+
const icon = buildIcon(p.icon, c, this.reduced);
|
| 39 |
+
const txt = document.createElement('span');
|
| 40 |
+
txt.className = 'hp-text';
|
| 41 |
+
li.appendChild(icon);
|
| 42 |
+
li.appendChild(txt);
|
| 43 |
+
list.appendChild(li);
|
| 44 |
+
if (this.reduced) {
|
| 45 |
+
txt.textContent = p.label;
|
| 46 |
+
li.classList.add('hp-shown');
|
| 47 |
+
} else {
|
| 48 |
+
// sequential type-out: 30ms/char, staggered per line
|
| 49 |
+
const startDelay = i * 520;
|
| 50 |
+
this._timers.push(setTimeout(() => this._type(li, txt, p.label), startDelay));
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
this.el.classList.add('hp-visible');
|
| 54 |
+
this.el.setAttribute('aria-hidden', 'false');
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
_type(li, txt, label) {
|
| 58 |
+
li.classList.add('hp-shown');
|
| 59 |
+
let n = 0;
|
| 60 |
+
const step = () => {
|
| 61 |
+
txt.textContent = label.slice(0, n);
|
| 62 |
+
n++;
|
| 63 |
+
if (n <= label.length) {
|
| 64 |
+
this._timers.push(setTimeout(step, 30));
|
| 65 |
+
} else {
|
| 66 |
+
li.classList.add('hp-pulse'); // pulse once typed
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
step();
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
hide() {
|
| 73 |
+
this.activeId = null;
|
| 74 |
+
this._clearTimers();
|
| 75 |
+
this.el.classList.remove('hp-visible');
|
| 76 |
+
this.el.setAttribute('aria-hidden', 'true');
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Position the panel at a screen-space point (px), centered horizontally above it.
|
| 80 |
+
place(x, y) {
|
| 81 |
+
this.el.style.left = x + 'px';
|
| 82 |
+
this.el.style.top = y + 'px';
|
| 83 |
+
}
|
| 84 |
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// main.js — Three.js bootstrap + character-select scene for the SZL org card.
|
| 2 |
+
// Five procedural low-poly heroes on lit daises in a slight arc, slow camera
|
| 3 |
+
// carousel orbit, KHIPU-style particle motes, UDS NPCs in the background.
|
| 4 |
+
// Hover reveals animated superpowers; click opens the hero's HF Space.
|
| 5 |
+
import * as THREE from 'three';
|
| 6 |
+
import { HEROES, buildHero } from './heroes.js';
|
| 7 |
+
import { buildUDSNPCs, animateNPCs } from './uds_npcs.js';
|
| 8 |
+
import { HoverPanel } from './hover_panel.js';
|
| 9 |
+
|
| 10 |
+
const REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 11 |
+
|
| 12 |
+
// ---- WebGL capability gate (graceful degradation to SVG grid) ----
|
| 13 |
+
function webglOK() {
|
| 14 |
+
try {
|
| 15 |
+
if (!window.WebGLRenderingContext) return false;
|
| 16 |
+
const c = document.createElement('canvas');
|
| 17 |
+
return !!(c.getContext('webgl2') || c.getContext('webgl'));
|
| 18 |
+
} catch (e) { return false; }
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const stage = document.getElementById('stage');
|
| 22 |
+
const canvas = document.getElementById('roster');
|
| 23 |
+
const fallback = document.getElementById('fallback');
|
| 24 |
+
|
| 25 |
+
if (!webglOK()) {
|
| 26 |
+
// Show the static SVG 5-tile grid fallback, hide the 3D canvas.
|
| 27 |
+
canvas.style.display = 'none';
|
| 28 |
+
if (fallback) fallback.hidden = false;
|
| 29 |
+
document.getElementById('loading')?.remove();
|
| 30 |
+
} else {
|
| 31 |
+
if (fallback) fallback.hidden = true;
|
| 32 |
+
init();
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function init() {
|
| 36 |
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
| 37 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 38 |
+
renderer.shadowMap.enabled = true;
|
| 39 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 40 |
+
|
| 41 |
+
const scene = new THREE.Scene();
|
| 42 |
+
scene.fog = new THREE.FogExp2(0x12091f, 0.045);
|
| 43 |
+
|
| 44 |
+
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
| 45 |
+
const CAM_RADIUS = 9.2, CAM_HEIGHT = 2.4;
|
| 46 |
+
camera.position.set(0, CAM_HEIGHT, CAM_RADIUS);
|
| 47 |
+
|
| 48 |
+
// ---- Lights ----
|
| 49 |
+
scene.add(new THREE.AmbientLight(0x5a4a7a, 0.55));
|
| 50 |
+
const key = new THREE.DirectionalLight(0xffffff, 0.85);
|
| 51 |
+
key.position.set(4, 9, 6); key.castShadow = true;
|
| 52 |
+
key.shadow.mapSize.set(1024, 1024);
|
| 53 |
+
key.shadow.camera.near = 1; key.shadow.camera.far = 30;
|
| 54 |
+
key.shadow.camera.left = -10; key.shadow.camera.right = 10;
|
| 55 |
+
key.shadow.camera.top = 10; key.shadow.camera.bottom = -10;
|
| 56 |
+
scene.add(key);
|
| 57 |
+
const fill = new THREE.DirectionalLight(0x8b6cf2, 0.3);
|
| 58 |
+
fill.position.set(-6, 4, -4); scene.add(fill);
|
| 59 |
+
|
| 60 |
+
// ---- Ground plane (receives shadows, dark) ----
|
| 61 |
+
const ground = new THREE.Mesh(
|
| 62 |
+
new THREE.CircleGeometry(16, 48),
|
| 63 |
+
new THREE.MeshStandardMaterial({ color: 0x0c0718, roughness: 1, metalness: 0 })
|
| 64 |
+
);
|
| 65 |
+
ground.rotation.x = -Math.PI / 2; ground.position.y = -0.02; ground.receiveShadow = true;
|
| 66 |
+
scene.add(ground);
|
| 67 |
+
|
| 68 |
+
// ---- Heroes on daises in a slight arc, slightly elevated ----
|
| 69 |
+
const heroRoot = new THREE.Group();
|
| 70 |
+
scene.add(heroRoot);
|
| 71 |
+
const heroEntries = [];
|
| 72 |
+
const N = HEROES.length;
|
| 73 |
+
const arcSpan = 2.0; // radians across which heroes are spread
|
| 74 |
+
const arcRadius = 5.0; // distance from arc center
|
| 75 |
+
HEROES.forEach((hero, i) => {
|
| 76 |
+
const t = N === 1 ? 0.5 : i / (N - 1);
|
| 77 |
+
const a = -arcSpan / 2 + t * arcSpan;
|
| 78 |
+
const x = Math.sin(a) * arcRadius;
|
| 79 |
+
const z = -arcRadius + Math.cos(a) * arcRadius * 0.55; // gentle forward arc
|
| 80 |
+
const slot = new THREE.Group();
|
| 81 |
+
slot.position.set(x, 0.25, z); // slightly elevated
|
| 82 |
+
slot.rotation.y = -a * 0.45; // face toward camera center
|
| 83 |
+
|
| 84 |
+
// dais
|
| 85 |
+
const daisMat = new THREE.MeshStandardMaterial({ color: 0x171022, roughness: 0.5, metalness: 0.4 });
|
| 86 |
+
const dais = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 1.05, 0.18, 32), daisMat);
|
| 87 |
+
dais.position.y = -0.09; dais.receiveShadow = true; dais.castShadow = true;
|
| 88 |
+
slot.add(dais);
|
| 89 |
+
// colored rim-light ring on the dais edge
|
| 90 |
+
const rimMat = new THREE.MeshStandardMaterial({
|
| 91 |
+
color: hero.color, emissive: hero.color, emissiveIntensity: 1.0,
|
| 92 |
+
roughness: 0.3, transparent: true, opacity: 0.95,
|
| 93 |
+
});
|
| 94 |
+
const rim = new THREE.Mesh(new THREE.TorusGeometry(0.98, 0.045, 8, 48), rimMat);
|
| 95 |
+
rim.rotation.x = Math.PI / 2; rim.position.y = 0.02;
|
| 96 |
+
slot.add(rim);
|
| 97 |
+
// a point light per dais (cheap; 5 total) for the colored glow
|
| 98 |
+
const rimLight = new THREE.PointLight(new THREE.Color(hero.color), 1.4, 4.5, 2);
|
| 99 |
+
rimLight.position.set(0, 0.6, 0.4);
|
| 100 |
+
slot.add(rimLight);
|
| 101 |
+
|
| 102 |
+
const figure = buildHero(hero);
|
| 103 |
+
slot.add(figure);
|
| 104 |
+
|
| 105 |
+
heroRoot.add(slot);
|
| 106 |
+
heroEntries.push({ hero, slot, figure, rim, rimMat, rimLight, baseRim: 1.0,
|
| 107 |
+
bodyMats: collectBodyMats(figure), hovered: false, scale: 1 });
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// ---- UDS NPCs in the background ----
|
| 111 |
+
const { group: npcGroup, npcs } = buildUDSNPCs(4);
|
| 112 |
+
scene.add(npcGroup);
|
| 113 |
+
|
| 114 |
+
// ---- KHIPU DAG particle motes ----
|
| 115 |
+
const PCOUNT = REDUCED ? 0 : 420;
|
| 116 |
+
let particles = null, pPos = null, pHome = null, pVel = null;
|
| 117 |
+
if (PCOUNT > 0) {
|
| 118 |
+
pPos = new Float32Array(PCOUNT * 3);
|
| 119 |
+
pHome = new Float32Array(PCOUNT * 3);
|
| 120 |
+
pVel = new Float32Array(PCOUNT * 3);
|
| 121 |
+
for (let i = 0; i < PCOUNT; i++) {
|
| 122 |
+
const r = 3 + Math.random() * 7;
|
| 123 |
+
const a = Math.random() * Math.PI * 2;
|
| 124 |
+
const x = Math.cos(a) * r, y = Math.random() * 6 + 0.2, z = Math.sin(a) * r - 2;
|
| 125 |
+
pPos[i * 3] = pHome[i * 3] = x;
|
| 126 |
+
pPos[i * 3 + 1] = pHome[i * 3 + 1] = y;
|
| 127 |
+
pPos[i * 3 + 2] = pHome[i * 3 + 2] = z;
|
| 128 |
+
}
|
| 129 |
+
const pg = new THREE.BufferGeometry();
|
| 130 |
+
pg.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
|
| 131 |
+
const pm = new THREE.PointsMaterial({ color: 0xcda8ff, size: 0.06, transparent: true, opacity: 0.7,
|
| 132 |
+
depthWrite: false, blending: THREE.AdditiveBlending });
|
| 133 |
+
particles = new THREE.Points(pg, pm);
|
| 134 |
+
scene.add(particles);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// ---- Hover panel (DOM overlay) ----
|
| 138 |
+
const panel = new HoverPanel(stage, REDUCED);
|
| 139 |
+
|
| 140 |
+
// ---- Raycaster + invisible hit cylinders per hero ----
|
| 141 |
+
const raycaster = new THREE.Raycaster();
|
| 142 |
+
const pointer = new THREE.Vector2();
|
| 143 |
+
let pointerInside = false;
|
| 144 |
+
const hitMeshes = heroEntries.map((e, i) => {
|
| 145 |
+
const hit = new THREE.Mesh(
|
| 146 |
+
new THREE.CylinderGeometry(0.9, 0.9, 2.6, 8),
|
| 147 |
+
new THREE.MeshBasicMaterial({ visible: false })
|
| 148 |
+
);
|
| 149 |
+
hit.position.y = 1.1;
|
| 150 |
+
hit.userData.index = i;
|
| 151 |
+
e.slot.add(hit);
|
| 152 |
+
return hit;
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
let activeIndex = -1;
|
| 156 |
+
function setActive(idx) {
|
| 157 |
+
if (idx === activeIndex) return;
|
| 158 |
+
activeIndex = idx;
|
| 159 |
+
heroEntries.forEach((e, i) => { e.hovered = (i === idx); });
|
| 160 |
+
if (idx >= 0) {
|
| 161 |
+
panel.show(heroEntries[idx].hero);
|
| 162 |
+
burst(idx);
|
| 163 |
+
stage.style.cursor = 'pointer';
|
| 164 |
+
stage.setAttribute('data-active', heroEntries[idx].hero.id);
|
| 165 |
+
} else {
|
| 166 |
+
panel.hide();
|
| 167 |
+
stage.style.cursor = 'default';
|
| 168 |
+
stage.removeAttribute('data-active');
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// particle energy burst toward a hero then radiate
|
| 173 |
+
let burstUntil = 0, burstTarget = new THREE.Vector3();
|
| 174 |
+
function burst(idx) {
|
| 175 |
+
if (!particles) return;
|
| 176 |
+
burstTarget.copy(heroEntries[idx].slot.position).add(new THREE.Vector3(0, 1.2, 0));
|
| 177 |
+
burstUntil = performance.now() + 900;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// ---- Pointer events ----
|
| 181 |
+
function updatePointer(ev) {
|
| 182 |
+
const rect = canvas.getBoundingClientRect();
|
| 183 |
+
pointer.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
| 184 |
+
pointer.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
| 185 |
+
pointerInside = true;
|
| 186 |
+
}
|
| 187 |
+
canvas.addEventListener('pointermove', updatePointer);
|
| 188 |
+
canvas.addEventListener('pointerleave', () => { pointerInside = false; setActive(-1); });
|
| 189 |
+
canvas.addEventListener('click', () => {
|
| 190 |
+
if (activeIndex >= 0) window.open(heroEntries[activeIndex].hero.url, '_blank', 'noopener');
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
// ---- Keyboard accessibility: Tab cycles, Enter activates ----
|
| 194 |
+
// Hidden focusable proxies announce each hero and drive the 3D highlight.
|
| 195 |
+
const a11yRoot = document.getElementById('a11y-heroes');
|
| 196 |
+
heroEntries.forEach((e, i) => {
|
| 197 |
+
const b = document.createElement('button');
|
| 198 |
+
b.className = 'a11y-hero';
|
| 199 |
+
const labels = e.hero.powers.map(p => p.label.toLowerCase()).join(', ');
|
| 200 |
+
b.setAttribute('aria-label',
|
| 201 |
+
`${e.hero.name} — ${e.hero.role}. Superpowers: ${labels}. Click to open Space.`);
|
| 202 |
+
b.addEventListener('focus', () => setKeyActive(i));
|
| 203 |
+
b.addEventListener('blur', () => { if (kbActive === i) setKeyActive(-1); });
|
| 204 |
+
b.addEventListener('click', () => window.open(e.hero.url, '_blank', 'noopener'));
|
| 205 |
+
a11yRoot.appendChild(b);
|
| 206 |
+
});
|
| 207 |
+
let kbActive = -1;
|
| 208 |
+
function setKeyActive(i) { kbActive = i; setActive(i); }
|
| 209 |
+
|
| 210 |
+
// ---- Resize ----
|
| 211 |
+
function resize() {
|
| 212 |
+
const w = stage.clientWidth, h = stage.clientHeight;
|
| 213 |
+
renderer.setSize(w, h, false);
|
| 214 |
+
camera.aspect = w / h; camera.updateProjectionMatrix();
|
| 215 |
+
}
|
| 216 |
+
window.addEventListener('resize', resize);
|
| 217 |
+
resize();
|
| 218 |
+
|
| 219 |
+
// ---- Animation loop ----
|
| 220 |
+
const clock = new THREE.Clock();
|
| 221 |
+
let camAngle = 0;
|
| 222 |
+
const _proj = new THREE.Vector3();
|
| 223 |
+
|
| 224 |
+
function frame() {
|
| 225 |
+
const dt = clock.getDelta();
|
| 226 |
+
const t = clock.elapsedTime * 1000;
|
| 227 |
+
|
| 228 |
+
// camera slow orbit (~60s period) unless reduced motion
|
| 229 |
+
if (!REDUCED) camAngle = (clock.elapsedTime / 60) * Math.PI * 2;
|
| 230 |
+
const orbitR = CAM_RADIUS;
|
| 231 |
+
camera.position.x = Math.sin(camAngle) * orbitR;
|
| 232 |
+
camera.position.z = Math.cos(camAngle) * orbitR;
|
| 233 |
+
camera.position.y = CAM_HEIGHT;
|
| 234 |
+
camera.lookAt(0, 1.0, -1.2);
|
| 235 |
+
|
| 236 |
+
// hover hit-test (only when pointer is in canvas)
|
| 237 |
+
if (pointerInside) {
|
| 238 |
+
raycaster.setFromCamera(pointer, camera);
|
| 239 |
+
const hits = raycaster.intersectObjects(hitMeshes, false);
|
| 240 |
+
setActive(hits.length ? hits[0].object.userData.index : -1);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// per-hero animation
|
| 244 |
+
heroEntries.forEach((e, i) => {
|
| 245 |
+
const ud = e.figure.userData;
|
| 246 |
+
// breathing: torso scale y ±2% over 3s
|
| 247 |
+
if (!REDUCED) {
|
| 248 |
+
const breathe = 1 + Math.sin(t * 0.00209 + i) * 0.02;
|
| 249 |
+
ud.torsoPivot.scale.y = breathe;
|
| 250 |
+
}
|
| 251 |
+
// hover scale-up to 1.15x over ~200ms (ease toward target)
|
| 252 |
+
const target = e.hovered ? 1.15 : 1.0;
|
| 253 |
+
const lerpK = REDUCED ? 1 : Math.min(1, dt / 0.2);
|
| 254 |
+
e.scale += (target - e.scale) * lerpK;
|
| 255 |
+
e.slot.scale.setScalar(e.scale);
|
| 256 |
+
|
| 257 |
+
// dais rim pulse (HSL lightness) + hover brighten +50%
|
| 258 |
+
const baseI = e.hovered ? 1.5 : 1.0;
|
| 259 |
+
const pulse = REDUCED ? 0 : Math.sin(t * 0.0016 + i * 1.3) * 0.25;
|
| 260 |
+
e.rimMat.emissiveIntensity = baseI + pulse;
|
| 261 |
+
e.rimLight.intensity = (e.hovered ? 2.6 : 1.4) + pulse;
|
| 262 |
+
|
| 263 |
+
// dim non-hovered heroes to 50% when one is active
|
| 264 |
+
const dim = (activeIndex >= 0 && !e.hovered) ? 0.5 : 1.0;
|
| 265 |
+
e.bodyMats.forEach(m => {
|
| 266 |
+
m.transparent = dim < 1;
|
| 267 |
+
m.opacity += (dim - m.opacity) * (REDUCED ? 1 : Math.min(1, dt * 6));
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
// prop activation on hover
|
| 271 |
+
ud.prop.visible = e.hovered;
|
| 272 |
+
if (e.hovered && !REDUCED) animateProp(ud.prop, t);
|
| 273 |
+
});
|
| 274 |
+
|
| 275 |
+
// NPC idle
|
| 276 |
+
animateNPCs(npcs, t, REDUCED);
|
| 277 |
+
|
| 278 |
+
// particle motes drift + burst/converge
|
| 279 |
+
if (particles) {
|
| 280 |
+
const now = performance.now();
|
| 281 |
+
const bursting = now < burstUntil;
|
| 282 |
+
for (let i = 0; i < PCOUNT; i++) {
|
| 283 |
+
const ix = i * 3;
|
| 284 |
+
if (bursting) {
|
| 285 |
+
// converge toward target then radiate (second half)
|
| 286 |
+
const half = burstUntil - 450;
|
| 287 |
+
const k = now < half ? 0.06 : -0.05;
|
| 288 |
+
pPos[ix] += (burstTarget.x - pPos[ix]) * k;
|
| 289 |
+
pPos[ix + 1] += (burstTarget.y - pPos[ix + 1]) * k;
|
| 290 |
+
pPos[ix + 2] += (burstTarget.z - pPos[ix + 2]) * k;
|
| 291 |
+
} else {
|
| 292 |
+
// ease home + gentle float
|
| 293 |
+
pPos[ix] += (pHome[ix] - pPos[ix]) * 0.02;
|
| 294 |
+
pPos[ix + 1] += (pHome[ix + 1] - pPos[ix + 1]) * 0.02 + Math.sin(t * 0.0006 + i) * 0.0009;
|
| 295 |
+
pPos[ix + 2] += (pHome[ix + 2] - pPos[ix + 2]) * 0.02;
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
particles.geometry.attributes.position.needsUpdate = true;
|
| 299 |
+
if (!REDUCED) particles.rotation.y = clock.elapsedTime * 0.01;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// place hover panel above active hero's head (screen projection)
|
| 303 |
+
if (activeIndex >= 0) {
|
| 304 |
+
const e = heroEntries[activeIndex];
|
| 305 |
+
_proj.copy(e.slot.position); _proj.y += 2.6; // ~60cm above head
|
| 306 |
+
_proj.project(camera);
|
| 307 |
+
const rect = canvas.getBoundingClientRect();
|
| 308 |
+
const sx = (_proj.x * 0.5 + 0.5) * rect.width;
|
| 309 |
+
const sy = (-_proj.y * 0.5 + 0.5) * rect.height;
|
| 310 |
+
panel.place(sx, sy);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
renderer.render(scene, camera);
|
| 314 |
+
requestAnimationFrame(frame);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
document.getElementById('loading')?.remove();
|
| 318 |
+
requestAnimationFrame(frame);
|
| 319 |
+
|
| 320 |
+
// expose for smoke tests
|
| 321 |
+
window.__SZL_ROSTER__ = { setActive, heroEntries, HEROES };
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function collectBodyMats(figure) {
|
| 325 |
+
const mats = [];
|
| 326 |
+
figure.traverse(o => { if (o.isMesh && o.material && o.material.map) mats.push(o.material); });
|
| 327 |
+
return mats;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
function animateProp(prop, t) {
|
| 331 |
+
if (prop.userData.spin) prop.userData.spin.rotation.z = t * 0.003;
|
| 332 |
+
if (prop.userData.orbit) {
|
| 333 |
+
prop.userData.orbit.forEach((r, k) => {
|
| 334 |
+
const a = (k / 8) * Math.PI * 2 + t * 0.001;
|
| 335 |
+
r.position.set(Math.cos(a) * 0.7, 1.0 + Math.sin(t * 0.002 + k) * 0.05, Math.sin(a) * 0.7);
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
if (prop.userData.stream) {
|
| 339 |
+
prop.userData.stream.forEach((s, k) => {
|
| 340 |
+
s.position.x = 0.5 + ((t * 0.001 + k * 0.2) % 0.6);
|
| 341 |
+
s.material.opacity = 0.3 + 0.6 * Math.abs(Math.sin(t * 0.003 + k));
|
| 342 |
+
});
|
| 343 |
+
}
|
| 344 |
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// superpower_icons.js
|
| 2 |
+
// Animated SVG icon set, one per superpower. Each factory returns an <svg>
|
| 3 |
+
// element (24x24 viewBox) with embedded CSS/SMIL animation. Color is injected.
|
| 4 |
+
// Animations respect prefers-reduced-motion via the `reduced` flag (freezes).
|
| 5 |
+
|
| 6 |
+
const NS = 'http://www.w3.org/2000/svg';
|
| 7 |
+
|
| 8 |
+
function svg(color, reduced, inner) {
|
| 9 |
+
const s = `<svg xmlns="${NS}" viewBox="0 0 24 24" width="22" height="22" fill="none"
|
| 10 |
+
stroke="${color}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"
|
| 11 |
+
class="${reduced ? 'sp-static' : 'sp-anim'}">${inner}</svg>`;
|
| 12 |
+
const wrap = document.createElement('span');
|
| 13 |
+
wrap.className = 'sp-icon';
|
| 14 |
+
wrap.innerHTML = s;
|
| 15 |
+
return wrap;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Each builder takes (color, reduced) and returns an element.
|
| 19 |
+
const ICONS = {
|
| 20 |
+
// ---- AMARU ----
|
| 21 |
+
amaru_dualstream: (c, r) => svg(c, r, `
|
| 22 |
+
<path d="M3 9 H18"><animate attributeName="stroke-dashoffset" values="0;-12" dur="1s" repeatCount="indefinite"/></path>
|
| 23 |
+
<path d="M14 6 L18 9 L14 12" stroke-dasharray="0"/>
|
| 24 |
+
<path d="M21 15 H6"><animate attributeName="stroke-dashoffset" values="0;12" dur="1s" repeatCount="indefinite"/></path>
|
| 25 |
+
<path d="M10 18 L6 15 L10 12"/>`),
|
| 26 |
+
amaru_memory: (c, r) => {
|
| 27 |
+
let dots = '';
|
| 28 |
+
for (let i = 0; i < 7; i++) { const a = (i / 7) * Math.PI * 2; dots += `<circle cx="${(12 + Math.cos(a) * 8).toFixed(1)}" cy="${(12 + Math.sin(a) * 8).toFixed(1)}" r="1.4" fill="${c}" stroke="none"/>`; }
|
| 29 |
+
return svg(c, r, `<g>${dots}<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="6s" repeatCount="indefinite"/></g>`);
|
| 30 |
+
},
|
| 31 |
+
amaru_forward: (c, r) => svg(c, r, `
|
| 32 |
+
<circle cx="18" cy="12" r="3"/><circle cx="18" cy="12" r="0.8" fill="${c}" stroke="none"/>
|
| 33 |
+
<path d="M3 12 q3 -4 6 0 q3 4 6 0"><animate attributeName="stroke-dashoffset" values="0;-12" dur="1.4s" repeatCount="indefinite"/></path>`),
|
| 34 |
+
amaru_lineage: (c, r) => svg(c, r, `
|
| 35 |
+
<path d="M12 21 V13" stroke-dasharray="14"><animate attributeName="stroke-dashoffset" values="14;0" dur="1.6s" repeatCount="indefinite"/></path>
|
| 36 |
+
<path d="M12 13 L6 6 M12 13 L18 6" stroke-dasharray="11"><animate attributeName="stroke-dashoffset" values="11;0" dur="1.6s" begin="0.4s" repeatCount="indefinite"/></path>
|
| 37 |
+
<circle cx="12" cy="21" r="1.4" fill="${c}" stroke="none"/><circle cx="6" cy="6" r="1.4" fill="${c}" stroke="none"/><circle cx="18" cy="6" r="1.4" fill="${c}" stroke="none"/>`),
|
| 38 |
+
// ---- SENTRA ----
|
| 39 |
+
sentra_gates: (c, r) => {
|
| 40 |
+
let locks = '';
|
| 41 |
+
for (let i = 0; i < 8; i++) { const x = 3 + (i % 4) * 6; const y = i < 4 ? 7 : 16;
|
| 42 |
+
locks += `<g><rect x="${x - 1.5}" y="${y}" width="3" height="3" rx="0.5"/><path d="M${x - 1} ${y} v-1.5 a1 1 0 0 1 2 0 V${y}"><animate attributeName="d" values="M${x - 1} ${y} v-1.5 a1 1 0 0 1 2 0 V${y}; M${x - 1} ${y} v-0.5 a1 1 0 0 1 2 0 V${y}" dur="0.9s" begin="${i * 0.1}s" repeatCount="indefinite"/></path></g>`; }
|
| 43 |
+
return svg(c, r, locks);
|
| 44 |
+
},
|
| 45 |
+
sentra_dsse: (c, r) => svg(c, r, `
|
| 46 |
+
<path d="M4 17 q4 -8 8 -4 q4 4 8 -4" />
|
| 47 |
+
<g opacity="0"><rect x="14" y="13" width="7" height="5" rx="1"/><text x="17.5" y="17" font-size="3" fill="${c}" stroke="none" text-anchor="middle">SIG</text>
|
| 48 |
+
<animate attributeName="opacity" values="0;1;1;0" dur="2s" repeatCount="indefinite"/>
|
| 49 |
+
<animateTransform attributeName="transform" type="scale" values="1.4;1" dur="0.4s" repeatCount="indefinite" additive="sum"/></g>`),
|
| 50 |
+
sentra_conduction: (c, r) => svg(c, r, `
|
| 51 |
+
<path d="M2 12 H7 L9 6 L12 18 L14 12 H22" stroke-dasharray="40"><animate attributeName="stroke-dashoffset" values="40;0;-40" dur="1.6s" repeatCount="indefinite"/></path>`),
|
| 52 |
+
sentra_cosign: (c, r) => svg(c, r, `
|
| 53 |
+
<circle cx="8" cy="12" r="3"/><path d="M11 12 H20 M17 12 V15 M20 12 V16"/>
|
| 54 |
+
<g><animateTransform attributeName="transform" type="rotate" values="0 8 12;90 8 12;0 8 12" dur="1.8s" repeatCount="indefinite"/><line x1="8" y1="12" x2="8" y2="9"/></g>`),
|
| 55 |
+
// ---- ROSIE ----
|
| 56 |
+
rosie_ensemble: (c, r) => {
|
| 57 |
+
let tally = '';
|
| 58 |
+
for (let i = 0; i < 5; i++) tally += `<line x1="${4 + i * 3.5}" y1="6" x2="${4 + i * 3.5}" y2="18" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.3s" begin="${i * 0.25}s" fill="freeze" repeatCount="1"/><animate attributeName="opacity" values="1;1;0" dur="2.5s" begin="2.5s" repeatCount="indefinite"/></line>`;
|
| 59 |
+
return svg(c, r, tally);
|
| 60 |
+
},
|
| 61 |
+
rosie_routing: (c, r) => svg(c, r, `
|
| 62 |
+
<path d="M3 18 H8 L12 6 H16 L21 12"/>
|
| 63 |
+
<circle cx="3" cy="18" r="1.6" fill="${c}" stroke="none"><animateMotion dur="1.8s" repeatCount="indefinite" path="M0 0 H5 L9 -12 H13 L18 -6"/></circle>`),
|
| 64 |
+
rosie_step: (c, r) => svg(c, r, `
|
| 65 |
+
<path d="M13 2 L4 14 H11 L9 22 L20 9 H12 Z" fill="${c}" stroke="none" opacity="0.9"><animate attributeName="opacity" values="0.4;1;0.4" dur="0.9s" repeatCount="indefinite"/></path>`),
|
| 66 |
+
rosie_exec: (c, r) => svg(c, r, `
|
| 67 |
+
<rect x="4" y="14" width="3.5" height="6" rx="0.5"><animate attributeName="height" values="2;6;2" dur="1.6s" repeatCount="indefinite"/><animate attributeName="y" values="18;14;18" dur="1.6s" repeatCount="indefinite"/></rect>
|
| 68 |
+
<rect x="10" y="10" width="3.5" height="10" rx="0.5"><animate attributeName="height" values="4;10;4" dur="1.6s" begin="0.2s" repeatCount="indefinite"/><animate attributeName="y" values="16;10;16" dur="1.6s" begin="0.2s" repeatCount="indefinite"/></rect>
|
| 69 |
+
<rect x="16" y="7" width="3.5" height="13" rx="0.5"><animate attributeName="height" values="6;13;6" dur="1.6s" begin="0.4s" repeatCount="indefinite"/><animate attributeName="y" values="14;7;14" dur="1.6s" begin="0.4s" repeatCount="indefinite"/></rect>`),
|
| 70 |
+
// ---- KILLINCHU ----
|
| 71 |
+
kil_noaa: (c, r) => svg(c, r, `
|
| 72 |
+
<circle cx="5" cy="12" r="2.5" fill="${c}" stroke="none"/>
|
| 73 |
+
<path d="M7 10 L21 6 M7 12 H21 M7 14 L21 18"><animate attributeName="stroke-dashoffset" values="0;-10" dur="1.2s" repeatCount="indefinite"/></path>`),
|
| 74 |
+
kil_usgs: (c, r) => svg(c, r, `
|
| 75 |
+
<path d="M2 12 H6 L8 5 L11 19 L13 9 L15 15 L17 12 H22" stroke-dasharray="42"><animate attributeName="stroke-dashoffset" values="42;0;-42" dur="2s" repeatCount="indefinite"/></path>`),
|
| 76 |
+
kil_mq9: (c, r) => svg(c, r, `
|
| 77 |
+
<g><path d="M2 13 q5 -3 9 -1 q5 -4 11 -2 q-5 2 -9 2 q-5 2 -11 1 Z" fill="${c}" stroke="none" opacity="0.85"/>
|
| 78 |
+
<animateTransform attributeName="transform" type="translate" values="-2 1;2 -1;-2 1" dur="2.4s" repeatCount="indefinite"/></g>`),
|
| 79 |
+
kil_phase: (c, r) => svg(c, r, `
|
| 80 |
+
<path d="M2 12 q3 -7 6 0 q3 7 6 0 q3 -7 6 0" opacity="0.4"/>
|
| 81 |
+
<path d="M2 12 q3 -7 6 0 q3 7 6 0 q3 -7 6 0"><animateTransform attributeName="transform" type="translate" values="0 0;6 0;0 0" dur="2s" repeatCount="indefinite"/></path>`),
|
| 82 |
+
// ---- A11OY ----
|
| 83 |
+
a11_governance: (c, r) => svg(c, r, `
|
| 84 |
+
<g><line x1="4" y1="8" x2="20" y2="8"/><line x1="12" y1="4" x2="12" y2="8"/>
|
| 85 |
+
<path d="M4 8 l-2 5 h4 Z"/><path d="M20 8 l-2 5 h4 Z"/>
|
| 86 |
+
<animateTransform attributeName="transform" type="rotate" values="-8 12 8;6 12 8;0 12 8" dur="2.4s" repeatCount="indefinite"/></g>
|
| 87 |
+
<path d="M9 20 H15"/>`),
|
| 88 |
+
a11_router: (c, r) => svg(c, r, `
|
| 89 |
+
<circle cx="12" cy="12" r="3"/><path d="M12 9 V3 M12 15 V21 M9 12 H3 M15 12 H21 M14 10 l4 -4 M10 14 l-4 4 M14 14 l4 4 M10 10 l-4 -4"><animate attributeName="opacity" values="0.3;1;0.3" dur="1.5s" repeatCount="indefinite"/></path>`),
|
| 90 |
+
a11_formulas: (c, r) => {
|
| 91 |
+
let dots = '';
|
| 92 |
+
for (let i = 0; i < 35; i++) { const x = 3 + (i % 7) * 3; const y = 4 + Math.floor(i / 7) * 4;
|
| 93 |
+
dots += `<circle cx="${x}" cy="${y}" r="0.9" fill="${c}" stroke="none" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.2s" begin="${(i * 0.04).toFixed(2)}s" fill="freeze" repeatCount="1"/></circle>`; }
|
| 94 |
+
return svg(c, r, `<g>${dots}<animate attributeName="opacity" values="1;0.3;1" dur="3s" begin="1.6s" repeatCount="indefinite"/></g>`);
|
| 95 |
+
},
|
| 96 |
+
a11_khipu: (c, r) => svg(c, r, `
|
| 97 |
+
<path d="M3 6 H21" /><path d="M6 6 V18 M12 6 V20 M18 6 V16" stroke-dasharray="16"><animate attributeName="stroke-dashoffset" values="16;0" dur="1.4s" repeatCount="indefinite"/></path>
|
| 98 |
+
<circle cx="6" cy="14" r="1.6" fill="${c}" stroke="none"/><circle cx="12" cy="16" r="1.6" fill="${c}" stroke="none"/><circle cx="18" cy="12" r="1.6" fill="${c}" stroke="none"/>`),
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
export function buildIcon(name, color, reduced) {
|
| 102 |
+
const f = ICONS[name];
|
| 103 |
+
if (f) return f(color, reduced);
|
| 104 |
+
return svg(color, reduced, `<circle cx="12" cy="12" r="6" fill="${color}" stroke="none"/>`);
|
| 105 |
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// uds_npcs.js
|
| 2 |
+
// Background UDS NPC silhouettes — same humanoid template, neutral grey,
|
| 3 |
+
// "UDS" monogram decal, standing at parade rest. They orbit slowly + idle-sway.
|
| 4 |
+
import * as THREE from 'three';
|
| 5 |
+
import { makeNPCTexture } from './canvas_decals.js';
|
| 6 |
+
|
| 7 |
+
let sharedTexture = null;
|
| 8 |
+
|
| 9 |
+
function npcMaterials() {
|
| 10 |
+
if (!sharedTexture) sharedTexture = makeNPCTexture();
|
| 11 |
+
return {
|
| 12 |
+
body: new THREE.MeshStandardMaterial({ map: sharedTexture, roughness: 0.85, metalness: 0.05, flatShading: true }),
|
| 13 |
+
limb: new THREE.MeshStandardMaterial({ color: 0x2c3138, roughness: 0.9, flatShading: true }),
|
| 14 |
+
head: new THREE.MeshStandardMaterial({ color: 0x3c4249, roughness: 0.9, flatShading: true }),
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function buildNPC() {
|
| 19 |
+
const g = new THREE.Group();
|
| 20 |
+
const m = npcMaterials();
|
| 21 |
+
const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.3, 0.6, 4, 8), m.body);
|
| 22 |
+
torso.position.y = 1.05; g.add(torso);
|
| 23 |
+
const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 10), m.head);
|
| 24 |
+
head.position.y = 1.62; g.add(head);
|
| 25 |
+
for (const side of [-1, 1]) {
|
| 26 |
+
// arms at parade rest — hands clasped front, slight inward angle
|
| 27 |
+
const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.08, 0.5, 4, 6), m.limb);
|
| 28 |
+
arm.position.set(side * 0.36, 0.92, 0.08);
|
| 29 |
+
arm.rotation.z = side * 0.12; arm.rotation.x = -0.25;
|
| 30 |
+
g.add(arm);
|
| 31 |
+
const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.11, 0.6, 4, 6), m.limb);
|
| 32 |
+
leg.position.set(side * 0.15, 0.5, 0);
|
| 33 |
+
g.add(leg);
|
| 34 |
+
}
|
| 35 |
+
return g;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Create N NPCs arranged in a back arc. Returns { group, npcs[] } for animation.
|
| 39 |
+
export function buildUDSNPCs(count = 4) {
|
| 40 |
+
const group = new THREE.Group();
|
| 41 |
+
const npcs = [];
|
| 42 |
+
for (let i = 0; i < count; i++) {
|
| 43 |
+
const npc = buildNPC();
|
| 44 |
+
const a = -0.9 + (i / Math.max(1, count - 1)) * 1.8; // spread across back arc
|
| 45 |
+
const radius = 6.5;
|
| 46 |
+
npc.position.set(Math.sin(a) * radius, 0, -radius * 0.55 + Math.cos(a) * 0.4);
|
| 47 |
+
npc.scale.setScalar(0.85);
|
| 48 |
+
npc.rotation.y = -a * 0.5; // face roughly toward center/camera
|
| 49 |
+
npc.userData = { baseAngle: a, radius, phase: Math.random() * Math.PI * 2, baseX: npc.position.x };
|
| 50 |
+
group.add(npc); npcs.push(npc);
|
| 51 |
+
}
|
| 52 |
+
return { group, npcs };
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Slow orbit + idle sway. orbit is subtle so NPCs stay readable in-frame.
|
| 56 |
+
export function animateNPCs(npcs, t, reduced) {
|
| 57 |
+
if (reduced) return;
|
| 58 |
+
for (const npc of npcs) {
|
| 59 |
+
const u = npc.userData;
|
| 60 |
+
npc.position.y = Math.sin(t * 0.0009 + u.phase) * 0.04; // idle bob
|
| 61 |
+
npc.rotation.z = Math.sin(t * 0.0011 + u.phase) * 0.02; // idle sway
|
| 62 |
+
npc.position.x = u.baseX + Math.sin(t * 0.00012 + u.phase) * 0.6; // slow lateral orbit
|
| 63 |
+
}
|
| 64 |
+
}
|