vestige/docs/launch/node-engine-spec.json
Sam Valladares cfe8d03d36 feat(landing): full-viewport GPU node-engine hero + waitlist + dashboard auth removal
The /launch landing page (bleeding-edge waitlist hero):
- Full-viewport WebGL node engine (src/lib/hero/nodeEngine.ts): 40k GPU particles
  on a two-FBO GPUComputationRenderer running an 18s looping cinematic — particles
  stream in from the screen edges, slam together and EXPLODE at center, reform into
  a brain / graph constellation / neural lattice, dissolve back out, loop. Glowing
  additive particles + UnrealBloom, fills the whole screen edge to edge.
- Key GPGPU fix: custom DataTexture shape targets read black in GPUComputationRenderer
  (three.js #15882), so shape targets are computed PROCEDURALLY in-shader from the
  per-particle seed. Position integrates raw per-frame velocity (Codrops pattern),
  texel-center reference attribute.
- AmbientField (god-ray glow + parallax starfield), phantomBrain (deterministic
  seed-from-identity memory graph), LandingHero/neuralFlow (WebGPU Cinema-storm
  reuse + WebGL fallback, kept as alternates). Manifesto copy, seed input, email
  capture. No em-dashes. SSR off + prerender for the WebGL route.

Waitlist backend (Supabase, owned data):
- supabase/migrations/0001_waitlist.sql (RLS-locked table, dedup), Edge Function
  waitlist-join (CORS, validation, honeypot, dedup, Resend confirmation), setup doc.

Dashboard auth removed (reverts the launch-polish auth wall that 401'd the dashboard
against itself): mod.rs/state.rs/websocket.rs back to no-token, api.ts/websocket.ts/
+layout.svelte stripped of the token client. Pending-review UX + Memory PR gating kept.

Research/spec docs under docs/launch/. Frontend typechecks 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:54:51 -05:00

12 lines
No EOL
19 KiB
JSON

{
"concept": "A NEW self-contained full-viewport WebGL 'node engine' for the Vestige landing hero, in its own files under apps/dashboard/src/lib/hero/ (do NOT touch src/lib/graph/cinema -- that is the protected Memory Cinema). It is a THREE.Points cloud of ~40,000 particles (200x200 GPGPU sim texture) driven by two ping-pong FBOs (position + velocity) via GPUComputationRenderer (verified present at three/examples/jsm/misc/GPUComputationRenderer.js in r172; EffectComposer/UnrealBloomPass already used in src/lib/graph/scene.ts). ONE looping normalized clock uPhaseT in [0,1) runs the whole 4-beat cinematic and the SAME velocity/position shaders survive all beats -- only a per-phase FORCE MIX changes. Particles spawn OFF-screen on the true frustum rectangle (all four edges/corners, aspect-tracked), stream inward on curl-noise tendrils (Beat 1), accelerate into a critically-damped center slam and get a one-frame gaussian radial blast impulse = the EXPLODE plus an expanding screen-space shockwave post pass (Beat 2), then spring onto a per-particle shape target (brain -> memory-graph constellation -> neural lattice, cycled each loop) with a staggered per-particle wipe (Beat 3), breathe, and dissolve back to the edge-spawn shell to seamlessly restart (Beat 4). Rendered additively with velocity-stretched soft sprites; per-particle base color is kept LOW (x0.38) so dense overlap stays violet/cyan/emerald instead of clipping to white, then UnrealBloom with a threshold ABOVE the particle base so only hot cores bloom. Headline is a separate DOM/CSS layer on top with a radial scrim, so center can be busy yet never washes the text. Palette matches the dashboard (violet 0x6366f1/0xa855f7, cyan ~0x22d3ee, emerald ~0x34d399, near-black 0x05050f). Reduced-motion + 3 perf tiers + DPR clamp <=2 + FPS watchdog keep it 60fps and static-shippable on GitHub Pages (pure WebGL2). Cited demos: Codrops 'Crafting a Dreamy Particle Effect with Three.js and GPGPU' (Dec 2024) two-pass spring/damp core; Three.js Journey 'Particles Morphing Shader' + github.com/MisterPrada/morph-particles staggered morph; Codrops 'Surface Sampling in Three.js' (2021) MeshSurfaceSampler targets; cabbibo/glsl-curl-noise divergence-free curl; Halisavakis shockwave post; Loopspeed FBO multi-target mix.",
"timeline": "ONE uniform drives everything. JS each frame: const LOOP = 18.0; uPhaseT = (clock.getElapsedTime() % LOOP) / LOOP; At wrap (detect prevPhase > phase) advance shapeIndex = (shapeIndex+1)%3 and bind the next target texture to texTarget, and (re)set uBlastTime so the gaussian gate is fresh next cycle.\n\nPhase windows on uPhaseT (18s loop -> seconds in parens):\n 0.00-0.20 STREAM (0.0-3.6s): particles released from edges in hash-staggered waves; high uFlow curl, low uAttract, target = converge point vec3(0). Activation act = smoothstep(0, 0.5, mod(uTime - delay, LOOP)).\n 0.20-0.235 SLAM/EXPLODE (3.6-4.23s): convergence pull peaks (ease-in pow(tConv,3)); the instant uPhaseT crosses 0.20 JS sets uBlastTime = uTime ONCE; velocity pass adds gaussian one-shot radial impulse pulse=exp(-age*age*60); uDamping drops 0.85->0.60; uAttract ~0.\n 0.235-0.55 REFORM (4.23-9.9s): uExplode decays to 0, uAttract ramps 0.002->0.06 toward texTarget, curl turb fades 0.6->0.02; per-particle localProg = smoothstep(delay, delay+0.4, (uPhaseT-0.235)/0.315) gives the assembling wipe.\n 0.55-0.82 HOLD/BREATHE (9.9-14.76s): uAttract high, curl ~0.03 shimmer, gl_PointSize *= 1.0+0.05*sin(uTime*1.4); shape sits still and alive.\n 0.82-1.00 DISSOLVE (14.76-18.0s): uAttract->0, uFlow up, gentle outward bias + curl pushes particles to the edge-spawn shell (texOrigin); since dissolve target == next-loop spawn shell, the loop is SEAMLESS (no cut).\n\nDerive all weights inside the sim shader with smoothstep windows (ease-in/out, no pops):\n float wStream = 1.0 - smoothstep(0.18, 0.21, uPhaseT);\n float pulse = smoothstep(0.200, 0.215, uPhaseT) * (1.0 - smoothstep(0.215, 0.245, uPhaseT));\n float wReform = smoothstep(0.235, 0.55, uPhaseT) * (1.0 - smoothstep(0.80, 0.86, uPhaseT));\n float wDissolve = smoothstep(0.82, 1.00, uPhaseT);\n float uAttract = mix(0.002, 0.06, wReform);\n float uFlow = mix(0.35, 0.05, wReform) + wDissolve*0.40 + wStream*0.10;\n float uDamping = mix(0.86, 0.60, pulse);\n float uExplode = pulse * 6.0;\nImportant detail (MiniMax skill): use mod(uTime - delay, LOOP) for per-particle phase, NOT floor(uTime/LOOP)*LOOP (the latter adds a one-cycle startup dead time).",
"cameraAndViewport": "Single full-bleed canvas pinned behind the hero: CSS position:fixed/absolute inset:0; width:100vw; height:100vh; z-index:0; the H1/sub/CTA sit in a sibling layer z-index:10 with pointer-events passing through the canvas. The canvas element gets display:block so there is no inline-gap, and the renderer fills clientWidth x clientHeight.\n\nRenderer/camera setup:\n renderer = new THREE.WebGLRenderer({ antialias:false, alpha:true, powerPreference:'high-performance' });\n renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // HARD DPR clamp -- #1 cause of <60fps on a full-frame canvas\n renderer.setClearColor(0x05050f, 1);\n renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0; // saturates the dense core instead of whiting out\n const camera = new THREE.PerspectiveCamera(55, aspect, 0.1, 100); camera.position.set(0,0,14);\n\nFit-to-viewport (the cure for the 'contained ball in the middle' failure): compute the world half-extents the camera actually sees at the particle plane z=0 and spawn/dissolve on a rectangle slightly LARGER than that frustum:\n const vFOV = camera.fov*Math.PI/180;\n const h = 2*Math.tan(vFOV/2)*Math.abs(camera.position.z); // visible height at z=0\n const w = h*camera.aspect; // visible width at z=0\n uFrustum.value.set(w, h); // fed to sim + render so dissolve targets scale with the frame\nSpawn shell half-extents = frustum*0.625 then pushed out *1.15 so streaks ENTER from beyond the edge (point sprites are culled by center, so spawning just past +/-1 avoids edge-pop). Spread target z across [-camZ*0.6, +camZ*0.4] so the frame reads volumetric (near/far size-attenuation glow gradient), not a flat sheet. Recompute w/h/uFrustum and renderer size on every resize (ResizeObserver on the container). Keep PerspectiveCamera (NOT ortho) so gl_PointSize = uSize*uDpr / -mvPosition.z gives the depth glow gradient. Coverage comes from frustum math, never from zoom.",
"gpgpuDesign": "GPUComputationRenderer with TWO variables, RGBA32F, sized TEX x TEX (TEX=200 -> 40,000 particles).\n\nTextures / channels:\n texturePosition: rgb = world xyz, a = per-particle SEED in [0,1] (baked once, used for delay/role/jitter).\n textureVelocity: rgb = world velocity, a = spare (store role or local-progress cache).\n texOrigin (DataTexture, static uniform): rgb = the edge-spawn shell point for this particle, a = perimeterT in [0,1) for clockwise sweep. Used as STREAM source and DISSOLVE destination.\n texTarget (DataTexture, static uniform, swapped per loop): rgb = this particle's slot in the current shape (brain/constellation/lattice), a = node-vs-edge role flag (0=edge particle dim/small, 1=node particle bright/large).\n uRandom is not needed separately -- seed lives in position.a.\n\nDependencies: positionVar.material depends on [positionVar, velocityVar]; velocityVar.material depends on [positionVar, velocityVar]. Set wrapS/wrapT = ClampToEdgeWrapping, minFilter/magFilter = NearestFilter on ALL FBOs and DataTextures (texel = particle; bilinear would smear neighbors).\n\nGeometry for render: a BufferGeometry of TEX*TEX dummy vertices, each carrying a 'reference' vec2 attribute = the uv into the sim textures: reference[i] = ( (i%TEX)/TEX, floor(i/TEX)/TEX ). Plus an 'aCorner' is NOT needed if you render as gl_Points (cheaper, chosen here); velocity-stretch is done by modulating gl_PointSize + an elongated sprite mask in the fragment, which is enough at 40k and avoids 4x the vertex work of expanded quads.\n\nInit (fillTexture): position init = copy of texOrigin (everyone starts at the edge shell); velocity init = vec3(0); position.a = Math.random() seed. computeRenderer.init() then check getError().\n\nPer frame order: 1) set uniforms (uTime, uPhaseT, uBlastTime). 2) gpuCompute.compute(). 3) read velocityVar/positionVar current render targets via getCurrentRenderTarget(var).texture, assign to render material uniforms texturePosition/textureVelocity. 4) composer.render() (RenderPass -> shockwave ShaderPass -> UnrealBloom).",
"positionShader": "// === simPosition.frag (GPUComputationRenderer; 'resolution' + texturePosition/textureVelocity auto-injected) ===\nuniform float uDt;\nvoid main() {\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n vec4 posTex = texture2D(texturePosition, uv);\n vec3 pos = posTex.xyz;\n float seed = posTex.a; // preserve per-particle seed across frames\n vec3 vel = texture2D(textureVelocity, uv).xyz;\n // Symplectic Euler: velocity pass already integrated forces this step.\n pos += vel * uDt;\n gl_FragColor = vec4(pos, seed); // carry seed forward untouched\n}\n\nNotes: uDt is the clamped frame delta (clamp to <=1/30 so a tab-stall never explodes the sim). No target logic lives here -- position is pure integration; all forces are in the velocity pass so damping/impulse/spring stay in one place (the Codrops Dec-2024 two-pass split).",
"velocityShader": "// === simVelocity.frag (GPUComputationRenderer) ===\n// Auto-injected by GPUComputationRenderer: texturePosition, textureVelocity, resolution.\nuniform float uTime, uPhaseT, uDt, uBlastTime;\nuniform vec2 uFrustum;\nuniform sampler2D texOrigin; // edge-spawn shell (STREAM src / DISSOLVE dst)\nuniform sampler2D texTarget; // current shape slot (brain/constellation/lattice)\n#define PI 3.14159265\n\n// ---- simplex noise snoise(vec3) : paste Ashima/webgl-noise 3D simplex here (standard 80-line impl) ----\nvec3 snoiseVec3(vec3 p){ return vec3( snoise(p),\n snoise(p+vec3(17.1, 9.2, 3.3)),\n snoise(p+vec3(101.7,5.4,71.2)) ); }\nvec3 curlNoise(vec3 p){ // cabbibo/glsl-curl-noise, divergence-free\n const float e=0.1; vec3 dx=vec3(e,0,0),dy=vec3(0,e,0),dz=vec3(0,0,e);\n vec3 px0=snoiseVec3(p-dx),px1=snoiseVec3(p+dx);\n vec3 py0=snoiseVec3(p-dy),py1=snoiseVec3(p+dy);\n vec3 pz0=snoiseVec3(p-dz),pz1=snoiseVec3(p+dz);\n float x=py1.z-py0.z-pz1.y+pz0.y;\n float y=pz1.x-pz0.x-px1.z+px0.z;\n float z=px1.y-px0.y-py1.x+py0.x;\n return normalize(vec3(x,y,z)/(2.0*e));\n}\nfloat hash11(float p){ return fract(sin(p*127.1)*43758.5453); }\n\nvoid main(){\n vec2 uv = gl_FragCoord.xy / resolution.xy;\n vec4 pT = texture2D(texturePosition, uv);\n vec3 pos = pT.xyz; float seed = pT.a;\n vec3 vel = texture2D(textureVelocity, uv).xyz;\n vec3 originPos = texture2D(texOrigin, uv).xyz;\n float perimT = texture2D(texOrigin, uv).a;\n vec4 tgtTex = texture2D(texTarget, uv);\n vec3 shapePos = tgtTex.xyz;\n\n // ---- per-phase weights (smoothstep windows, no pops) ----\n float wStream = 1.0 - smoothstep(0.18, 0.21, uPhaseT);\n float pulse = smoothstep(0.200, 0.215, uPhaseT) * (1.0 - smoothstep(0.215, 0.245, uPhaseT));\n float wReform = smoothstep(0.235, 0.55, uPhaseT) * (1.0 - smoothstep(0.80, 0.86, uPhaseT));\n float wDissolve = smoothstep(0.82, 1.00, uPhaseT);\n\n // ---- STREAM: hash-staggered release toward center, curl tendrils ----\n float delay = 0.10*hash11(seed*91.7) + 0.08*perimT; // random + clockwise sweep\n float act = smoothstep(0.0, 0.06, uPhaseT - delay); // 0 until this particle's turn\n vec3 toCenter = -pos; // center = vec3(0)\n float dC = length(toCenter);\n vec3 dirC = toCenter / max(dC, 1e-4);\n // ease-in convergence: slow drift -> violent collapse as we approach the slam (uPhaseT->0.20)\n float tConv = clamp(uPhaseT/0.20, 0.0, 1.0);\n float pull = pow(tConv, 3.0);\n vel += dirC * (0.01 + 0.12*pull) * act * wStream * smoothstep(0.0, 6.0, dC);\n\n // ---- EXPLODE: one-shot gaussian radial blast (the SLAM), core launches hardest ----\n float age = uTime - uBlastTime;\n float gate = exp(-age*age*60.0); // ~1 frame wide -> reads as impact not push\n vec3 fromC = pos;\n float r = max(length(fromC), 1e-4);\n vec3 outDir= fromC / r;\n float falloff = 1.0/(1.0 + r*r*4.0); // inverse-square-ish\n vel += outDir * 6.0 * falloff * gate;\n vel += (snoiseVec3(pos*31.0)) * 0.06 * gate; // jitter so the front is not a perfect sphere\n\n // ---- REFORM/HOLD: spring onto shape target with staggered per-particle wipe ----\n float localProg = smoothstep(delay, delay+0.40, (uPhaseT-0.235)/0.315); // assembling sweep\n float kAttract = mix(0.002, 0.06, wReform) * localProg;\n vec3 toShape = shapePos - pos; float dS = length(toShape);\n vel += (toShape / max(dS,1e-4)) * kAttract * dS; // Hookean: stronger when far\n\n // ---- DISSOLVE: fade spring, ramp curl + outward bias toward the edge shell ----\n vec3 toEdge = originPos - pos;\n vel += toEdge * (0.01 * wDissolve); // gently retarget to next-loop spawn\n vel += normalize(pos + 1e-4) * 0.004 * wDissolve; // outward bias fills viewport\n\n // ---- curl turbulence: strong on stream/explode, ~0 once formed (keeps brain crisp) ----\n float turb = (0.35*wStream + 0.18*gate + 0.40*wDissolve) + mix(0.04, 0.015, wReform);\n vel += curlNoise(pos*0.30 + uTime*0.10) * turb * uDt * 12.0;\n\n // ---- damping + velocity cap (stable slam) ----\n float damping = mix(0.86, 0.60, pulse);\n vel *= damping;\n float vmax = 3.0; float v = length(vel); if (v > vmax) vel *= vmax / v;\n\n gl_FragColor = vec4(vel, 1.0);\n}",
"renderShader": "// ===================== render VERTEX shader =====================\nuniform sampler2D texturePosition;\nuniform sampler2D textureVelocity;\nuniform float uSize, uDpr, uPhaseT, uTime;\nattribute vec2 reference; // uv into the sim textures (baked per vertex)\nvarying float vSpeed;\nvarying float vRole; // node(1) vs edge(0) from target.a -- but we pass seed-based hue too\nvarying float vSeed;\nvarying vec3 vWorld;\nvoid main(){\n vec4 posTex = texture2D(texturePosition, reference);\n vec3 pos = posTex.xyz; vSeed = posTex.a;\n vec3 vel = texture2D(textureVelocity, reference).xyz;\n vSpeed = length(vel);\n vWorld = pos;\n vec4 mv = modelViewMatrix * vec4(pos, 1.0);\n // breathe during HOLD (0.55-0.82) only\n float breathe = 1.0 + 0.05*sin(uTime*1.4) * step(0.55,uPhaseT) * step(uPhaseT,0.82);\n // velocity-stretch: fast edge streamers get bigger (comet feel); settled dots stay small\n float stretch = 1.0 + clamp(vSpeed*0.6, 0.0, 2.5);\n gl_PointSize = uSize * uDpr * breathe * stretch / max(-mv.z, 0.1);\n gl_Position = projectionMatrix * mv;\n}\n\n// ===================== render FRAGMENT shader =====================\nprecision highp float;\nuniform vec3 uViolet; // 0.39,0.40,0.95 (0x6366f1)\nuniform vec3 uCyan; // 0.13,0.83,0.93 (0x22d3ee)\nuniform vec3 uEmerald; // 0.20,0.83,0.60 (0x34d399)\nuniform vec2 uTextCenterNDC; // headline center in [0,1] screen uv (~0.5,0.5)\nuniform vec2 uResolution;\nvarying float vSpeed; varying float vSeed; varying vec3 vWorld;\nvoid main(){\n // soft round sprite -- NO hard disc, so additive overlap accumulates as glow not a white plate\n vec2 q = gl_PointCoord - 0.5;\n float d = length(q);\n float a = exp(-d*d*5.0); // gaussian falloff sprite\n if (a < 0.01) discard;\n // palette: split hue by seed so violet/cyan/emerald separate SPATIALLY (never average to white)\n vec3 col = mix(uViolet, uCyan, smoothstep(0.0,0.5,vSeed));\n col = mix(col, uEmerald, smoothstep(0.5,1.0,vSeed));\n // fast particles flash slightly hotter (toward cyan) so the slam reads energetic\n col = mix(col, uCyan, clamp(vSpeed*0.12, 0.0, 0.5));\n // LOW base so N-overlap stays in-gamut and HUED (this is the anti-white-out lever)\n col *= 0.38;\n // dim behind the headline bounding box so text stays legible even at the explosion peak\n vec2 sUv = gl_FragCoord.xy / uResolution;\n float textMask = smoothstep(0.0, 0.22, length(sUv - uTextCenterNDC));\n float alpha = a * (0.55 + 0.45*textMask);\n gl_FragColor = vec4(col * alpha, alpha); // AdditiveBlending, depthWrite:false\n}\n\nMaterial: new THREE.ShaderMaterial({ blending: THREE.AdditiveBlending, depthWrite:false, depthTest:false, transparent:true, uniforms:{...} }). Render to the composer; UnrealBloom strength 0.7, radius 0.45, THRESHOLD 0.55 -- the 0.38 base sits BELOW threshold so only hot cores/the slam bloom, keeping the center violet/cyan not a white disc. ACESFilmic tone mapping on the renderer finishes the anti-clip.",
"targetShapes": "Build THREE target DataTextures once at boot, all in the SAME texel order the particles read (texel i -> particle i), all RGBA FloatType, NearestFilter, ClampToEdge. Channel a = role (1=node bright/large, 0=edge/fill dim/small).\n\nSHAPE A -- BRAIN (organic, mesh-sampled): load a low-poly brain GLB (Draco) OR fall back to two merged icospheres squashed into hemispheres. Use three/addons/math/MeshSurfaceSampler:\n import { MeshSurfaceSampler } from 'three/addons/math/MeshSurfaceSampler.js';\n const sampler = new MeshSurfaceSampler(brainMesh).build();\n const p=new THREE.Vector3(), n=new THREE.Vector3();\n for(let i=0;i<N;i++){ sampler.sample(p,n); p.addScaledVector(n, -Math.random()*0.15); // push inward => volumetric fill\n data[i*4]=p.x*S; data[i*4+1]=p.y*S; data[i*4+2]=p.z*S; data[i*4+3]= Math.random()<0.5?1.0:0.0; }\n Scale S so the brain spans ~70% of uFrustum height.\n\nSHAPE B -- MEMORY-GRAPH CONSTELLATION (no mesh): Fibonacci-sphere nodes + edge particles painting connections.\n const GA=Math.PI*(3.0-Math.sqrt(5.0)); const M=600; // node count\n for(let k=0;k<M;k++){ const y=1-(k/(M-1))*2, r=Math.sqrt(1-y*y), t=GA*k; nodes[k]=new THREE.Vector3(Math.cos(t)*r,y,Math.sin(t)*r).multiplyScalar(R); }\n // precompute edges: connect a->b if dist<edgeRadius. Then assign particles:\n // ~35% NODE particles: target = nodes[k] (+ small jitter), role=1\n // ~65% EDGE particles: target = mix(nodes[a], nodes[b], seed) along a chosen edge, role=0\n This turns a point cloud into a READABLE network (nodes bright, filaments faint).\n\nSHAPE C -- NEURAL LATTICE (crystalline): hash-jittered 3D grid.\n cell = floor(gridIndex); center = (cell+0.5)*cellSize - half; jitter = (hash3(cell)-0.5)*0.55*cellSize;\n target = center + jitter; role: particles near a grid LINE (one axis ~integer) = node(1), interior = fill(0).\n Edge particles lerp along lattice struts (mix of two adjacent cell centers by seed) for visible connectivity.\n\nSWITCHING: keep targetTextures = [brainTex, constellationTex, latticeTex]. On loop wrap (prevPhase>phase) set velocityVar.material.uniforms.texTarget.value = targetTextures[++shapeIndex % 3] and renderMaterial likewise if it samples target. Because every particle keeps its texel index, the morph from edge-shell -> shape is clean point-to-point; no re-sort needed. (Loopspeed FBO multi-target mix pattern.)",
"buildSteps": null,
"perfNotes": null
}