File size: 11,875 Bytes
510b5fb
76bbcfa
 
 
 
 
510b5fb
 
 
 
76bbcfa
510b5fb
76bbcfa
 
 
 
 
510b5fb
 
76bbcfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510b5fb
 
76bbcfa
 
510b5fb
76bbcfa
 
 
510b5fb
76bbcfa
 
510b5fb
76bbcfa
 
 
510b5fb
76bbcfa
 
 
 
 
 
 
 
 
 
 
 
510b5fb
76bbcfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510b5fb
76bbcfa
 
 
510b5fb
76bbcfa
 
 
 
 
510b5fb
76bbcfa
 
 
 
 
 
 
 
510b5fb
76bbcfa
 
 
 
 
 
510b5fb
76bbcfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510b5fb
76bbcfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/* ============================================================================
 * a11oy_cathedral.js — "The Sovereign Lattice" front-door hero (vendored Three.js).
 * A luminous gold lattice-core (a11oy) with three governed faculties orbiting it,
 * in deep sovereign space. Cinematic: layered additive bloom, multi-depth starfield,
 * faint nebula, pointer parallax, scroll-driven camera. Live /healthz + Λ.
 * Honesty: locked proven = 5; Λ = Conjecture 1. No fakery; honest fallback.
 * Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
 * ========================================================================== */
import * as THREE from 'three';

const REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const canvas = document.getElementById('scene');
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:false, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.9));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.18;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x05060f, 0.0016);
const camera = new THREE.PerspectiveCamera(46, innerWidth/innerHeight, 0.1, 6000);

// composition: core sits on the right; hero text breathes on the left
const HOME = new THREE.Vector3(46, 16, 250);
const LOOK = new THREE.Vector3(64, 6, 0);
camera.position.copy(HOME);

scene.add(new THREE.AmbientLight(0x2a2942, 0.85));
const keyLight = new THREE.PointLight(0xffe2a0, 2.7, 2200, 1.5); keyLight.position.set(70, 30, 40); scene.add(keyLight);
const rim = new THREE.DirectionalLight(0x6d8fd6, 0.55); rim.position.set(-220, 120, -120); scene.add(rim);
const fill = new THREE.DirectionalLight(0x7fd6d1, 0.18); fill.position.set(120, -80, 120); scene.add(fill);

/* ---- faint nebula: large additive gradient sprites ---- */
function radialSprite(c1, c2, sz, op){
  const cv=document.createElement('canvas'); cv.width=cv.height=256; const g=cv.getContext('2d');
  const grd=g.createRadialGradient(128,128,0,128,128,128);
  grd.addColorStop(0,c1); grd.addColorStop(0.45,c2); grd.addColorStop(1,'rgba(0,0,0,0)');
  g.fillStyle=grd; g.fillRect(0,0,256,256);
  const tex=new THREE.CanvasTexture(cv);
  const s=new THREE.Sprite(new THREE.SpriteMaterial({ map:tex, transparent:true, opacity:op, blending:THREE.AdditiveBlending, depthWrite:false }));
  s.scale.set(sz,sz,1); return s;
}
const neb1=radialSprite('rgba(215,185,107,0.5)','rgba(120,90,30,0.18)',1500,0.5); neb1.position.set(160,40,-500); scene.add(neb1);
const neb2=radialSprite('rgba(60,80,150,0.5)','rgba(30,40,90,0.16)',1900,0.45); neb2.position.set(-380,-120,-820); scene.add(neb2);
const neb3=radialSprite('rgba(127,214,209,0.32)','rgba(20,90,90,0.1)',1000,0.4); neb3.position.set(-180,180,-360); scene.add(neb3);

/* ---- multi-depth starfield ---- */
function starLayer(n, rMin, rMax, size, col, op){
  const pos=new Float32Array(n*3);
  for(let i=0;i<n;i++){ const r=rMin+Math.random()*(rMax-rMin), t=Math.random()*Math.PI*2, p=Math.acos(2*Math.random()-1);
    pos[i*3]=r*Math.sin(p)*Math.cos(t); pos[i*3+1]=r*Math.cos(p); pos[i*3+2]=r*Math.sin(p)*Math.sin(t); }
  const g=new THREE.BufferGeometry(); g.setAttribute('position', new THREE.BufferAttribute(pos,3));
  const pts=new THREE.Points(g, new THREE.PointsMaterial({ color:col, size, sizeAttenuation:true, transparent:true, opacity:op, depthWrite:false }));
  scene.add(pts); return pts;
}
const stars1=starLayer(1700, 700, 1500, 1.1, 0x9aa0d0, 0.55);
const stars2=starLayer(1100, 1400, 2800, 2.2, 0xcdbb8e, 0.5);
const stars3=starLayer(500, 380, 900, 1.5, 0x7fd6d1, 0.32);

/* ---- the sovereign lattice-core (a11oy) ---- */
const core=new THREE.Group(); core.position.copy(LOOK); scene.add(core);

const coreMesh=new THREE.Mesh(new THREE.IcosahedronGeometry(34,5),
  new THREE.MeshStandardMaterial({ color:0xffcf73, emissive:0xe0a536, emissiveIntensity:1.55, roughness:0.34, metalness:0.12, flatShading:true }));
core.add(coreMesh);

const latticeA=new THREE.Mesh(new THREE.IcosahedronGeometry(44,2),
  new THREE.MeshBasicMaterial({ color:0xffe6a8, wireframe:true, transparent:true, opacity:0.26 }));
core.add(latticeA);
const latticeB=new THREE.Mesh(new THREE.IcosahedronGeometry(56,1),
  new THREE.MeshBasicMaterial({ color:0xd7b96b, wireframe:true, transparent:true, opacity:0.12 }));
core.add(latticeB);

// layered additive bloom halos
for(const [r,o] of [[64,0.16],[92,0.09],[140,0.05]]){
  const h=new THREE.Mesh(new THREE.SphereGeometry(r,32,32),
    new THREE.MeshBasicMaterial({ color:0xffd98a, transparent:true, opacity:o, side:THREE.BackSide, blending:THREE.AdditiveBlending, depthWrite:false }));
  core.add(h);
}
const flare=radialSprite('rgba(255,222,150,0.95)','rgba(224,165,54,0.35)',360,0.9); core.add(flare);

// inner sparks orbiting inside the core
const sparkN=80, sparkPos=new Float32Array(sparkN*3), sparkSeed=[];
for(let i=0;i<sparkN;i++){ sparkSeed.push({a:Math.random()*Math.PI*2,b:Math.random()*Math.PI,r:20+Math.random()*22,s:0.4+Math.random()}); }
const sparkGeo=new THREE.BufferGeometry(); sparkGeo.setAttribute('position',new THREE.BufferAttribute(sparkPos,3));
core.add(new THREE.Points(sparkGeo, new THREE.PointsMaterial({ color:0xfff0c8, size:1.7, transparent:true, opacity:0.85, blending:THREE.AdditiveBlending, depthWrite:false })));

/* ---- three orbiting faculties ---- */
const FAC=[
  {color:0x7fd6d1, angle:0.2,  rx:120, rz:120, tilt:0.12},
  {color:0xd7b96b, angle:2.25, rx:138, rz:128, tilt:-0.22},
  {color:0xe58e54, angle:4.2,  rx:128, rz:140, tilt:0.30},
];
const facBodies=[];
FAC.forEach((f)=>{
  const g=new THREE.Group();
  const m=new THREE.Mesh(new THREE.IcosahedronGeometry(11,2),
    new THREE.MeshStandardMaterial({ color:f.color, emissive:f.color, emissiveIntensity:0.7, roughness:0.45, metalness:0.25 }));
  g.add(m);
  const ring=new THREE.Mesh(new THREE.TorusGeometry(17,0.5,8,72),
    new THREE.MeshBasicMaterial({ color:f.color, transparent:true, opacity:0.4 })); ring.rotation.x=Math.PI/2; g.add(ring);
  g.add(radialSprite(`rgba(${(f.color>>16)&255},${(f.color>>8)&255},${f.color&255},0.8)`,'rgba(0,0,0,0)',70,0.7));
  scene.add(g);
  // curved tether (core -> faculty) as a tube we rebuild each frame
  const tetherMat=new THREE.MeshBasicMaterial({ color:f.color, transparent:true, opacity:0.16, blending:THREE.AdditiveBlending, depthWrite:false });
  let tetherMesh=null;
  facBodies.push({ f, g, m, tetherMat, get tether(){return tetherMesh;}, set tether(v){tetherMesh=v;} });
});

/* ---- live mesh: /healthz + Λ ---- */
const lamDot=document.getElementById('lam-dot'), lamText=document.getElementById('lam-text');
async function getJSON(url,ms){ const c=new AbortController(); const t=setTimeout(()=>c.abort(),ms||7000);
  try{ const r=await fetch(url,{signal:c.signal,headers:{accept:'application/json'}}); if(!r.ok) throw 0; return await r.json(); } finally{ clearTimeout(t); } }
let lambda=null, alive=false;
async function poll(){
  try{ const h=await getJSON('/healthz',7000); alive=!!(h&&(h.status==='ok'||h.ok)); }catch(_){ alive=false; }
  try{ const l=await getJSON('/api/a11oy/v1/lambda',7000); if(l&&typeof l.lambda==='number') lambda=l.lambda; }catch(_){}
  paint();
}
function paint(){
  if(!lamDot) return;
  if(alive){ lamDot.classList.add('live'); }else{ lamDot.classList.remove('live'); }
  if(lambda!==null) lamText.innerHTML = `<b>Λ ${lambda.toFixed(3)}</b> · live trust score · 13-axis`;
  else lamText.textContent = alive ? 'mesh live · Λ reconnecting…' : 'live mesh offline · honest fallback';
}
paint(); poll(); setInterval(poll, 8000);

/* ---- pointer parallax + scroll ---- */
let px=0, py=0, tpx=0, tpy=0;
if(!REDUCED) addEventListener('pointermove',(e)=>{ tpx=(e.clientX/innerWidth-0.5); tpy=(e.clientY/innerHeight-0.5); }, {passive:true});
let scrollP=0;
function onScroll(){ const max=document.body.scrollHeight-innerHeight; scrollP = max>0 ? Math.min(1, scrollY/max) : 0; }
addEventListener('scroll', onScroll, {passive:true}); onScroll();

/* ---- intro dolly ---- */
let intro=REDUCED?1:0;

const _v=new THREE.Vector3();
const _look=new THREE.Vector3();
let t=0;
function animate(){
  requestAnimationFrame(animate);
  t+=0.0045;
  if(intro<1){ intro=Math.min(1, intro+0.012); }
  const e=intro<1 ? 1-Math.pow(1-intro,3) : 1; // easeOutCubic

  // core motion
  coreMesh.rotation.y+=0.0016; coreMesh.rotation.x+=0.0005;
  latticeA.rotation.y-=0.0012; latticeA.rotation.x+=0.0007;
  latticeB.rotation.y+=0.0009; latticeB.rotation.z-=0.0006;
  const pulse=1+0.012*Math.sin(t*2.0); core.scale.setScalar(pulse);
  flare.material.opacity=0.78+0.12*Math.sin(t*1.7);

  // inner sparks
  for(let i=0;i<sparkN;i++){ const s=sparkSeed[i]; const a=s.a+t*s.s; const b=s.b+t*0.3*s.s; const r=s.r;
    sparkPos[i*3]=r*Math.sin(b)*Math.cos(a); sparkPos[i*3+1]=r*Math.cos(b); sparkPos[i*3+2]=r*Math.sin(b)*Math.sin(a); }
  sparkGeo.attributes.position.needsUpdate=true;

  // faculties orbit + curved tethers
  facBodies.forEach((fb,i)=>{
    const f=fb.f; const a=f.angle + t*0.22*(i%2?-1:1);
    const x=LOOK.x+Math.cos(a)*f.rx;
    const z=LOOK.z+Math.sin(a)*f.rz;
    const y=LOOK.y+Math.sin(a*1.4+i)*26 + f.tilt*40*Math.cos(a*0.7);
    fb.g.position.set(x,y,z); fb.m.rotation.y+=0.012;
    const mid=new THREE.Vector3((LOOK.x+x)/2,(LOOK.y+y)/2+24,(LOOK.z+z)/2);
    const curve=new THREE.QuadraticBezierCurve3(new THREE.Vector3(LOOK.x,LOOK.y,LOOK.z), mid, new THREE.Vector3(x,y,z));
    if(fb.tether){ fb.tether.geometry.dispose(); core.parent.remove(fb.tether); }
    fb.tether=new THREE.Mesh(new THREE.TubeGeometry(curve,20,0.45,6,false), fb.tetherMat); scene.add(fb.tether);
  });

  // starfield drift
  stars1.rotation.y+=0.00012; stars2.rotation.y-=0.00007; stars3.rotation.y+=0.0002;

  // camera: parallax + gentle idle + scroll framing.
  // We aim LEFT of the core so it sits in the right third (text breathes on the
  // left). As you scroll we push the core toward the right edge (no zoom-in that
  // would swamp the headlines) and pull back slightly so it reads as an accent.
  px+=(tpx-px)*0.04; py+=(tpy-py)*0.04;
  const frameX = 60 + 84*scrollP;        // aim offset: larger -> core further right
  const frameY = 4 + 14*scrollP;
  const camHome=HOME.clone();
  camHome.y += 24*scrollP;               // gentle lift
  camHome.z += 30*scrollP;               // pull back (core smaller) instead of zoom-in
  camHome.x += 14*scrollP;
  const idleX=Math.sin(t*0.5)*5, idleY=Math.cos(t*0.4)*4;
  _v.set(camHome.x + px*32 + idleX, camHome.y - py*22 + idleY, camHome.z);
  camera.position.lerp(_v, e<1?0.06*e+0.02:0.06);
  const look=_look.set(LOOK.x - frameX + px*12, LOOK.y + frameY - py*6, LOOK.z);
  camera.lookAt(look);
  renderer.toneMappingExposure = 1.2 - 0.16*scrollP;

  renderer.render(scene,camera);
  if(!_shown){ _shown=true; requestAnimationFrame(()=>canvas.classList.add('on')); }
}
let _shown=false;
animate();

addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });

/* ---- nav stuck + scroll reveals + boot ---- */
const nav=document.getElementById('nav');
function navState(){ if(scrollY>24) nav.classList.add('stuck'); else nav.classList.remove('stuck'); }
addEventListener('scroll', navState, {passive:true}); navState();

const io=new IntersectionObserver((es)=>{ es.forEach(en=>{ if(en.isIntersecting){ en.target.classList.add('in'); io.unobserve(en.target); } }); }, {threshold:0.16});
document.querySelectorAll('.reveal').forEach((el,i)=>{ el.style.transitionDelay=(Math.min(i,6)*70)+'ms'; io.observe(el); });

/* boot overlay removed: editorial hero paints instantly; the 3D canvas fades in
   on its first rendered frame (see #scene.on). Safety net if WebGL never starts: */
setTimeout(()=>canvas.classList.add('on'), 1800);