mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
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>
This commit is contained in:
parent
335a42f341
commit
cfe8d03d36
26 changed files with 3002 additions and 266 deletions
55
docs/launch/bleeding-edge-landing-research.md
Normal file
55
docs/launch/bleeding-edge-landing-research.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Vestige bleeding-edge landing page — verified 2025/2026 research
|
||||
|
||||
Synthesized from a 6-lens web scour (web-tech, award landings, viral waitlists,
|
||||
why-devs-share, interactive heroes, shareable artifacts) + adversarial
|
||||
fact-check. Every item below was verified as genuinely current for late 2025 /
|
||||
2026 and shippable on a static SvelteKit + GitHub Pages site (Svelte 5, Kit 2,
|
||||
adapter-static, Three.js r172, Tailwind v4, Vite 6).
|
||||
|
||||
## The throughline (what all 6 lenses agree on)
|
||||
The 2025/2026 winning waitlist for a skeptical dev audience is:
|
||||
**a live in-page product embed as the hero + a manifesto + a SHAREABLE ARTIFACT
|
||||
made of the visitor's own data + real (not fake) scarcity.**
|
||||
Vestige is uniquely positioned: its artifact (a cinematic render of a memory
|
||||
graph) is more shareable than any generic "#1,204 in line" OG card — and it's
|
||||
already built.
|
||||
|
||||
## Bleeding-edge web tech (shippable, static)
|
||||
- **Three.js WebGPURenderer + TSL (r171+), now 100% browsers** — Safari 26 shipped WebGPU Sep 2025. First time you can ship WebGPU-first to everyone (with WebGL2 fallback from the SAME TSL source). Headline claim: "150,000 GPU-compute particles, one shader, every browser."
|
||||
- **TSL compute particles 50k–350k @ framerate** — net-new in 2025/2026; ~150x over CPU. Memory Cinema's 150k sits dead-center in the sweet spot.
|
||||
- **TSL post-processing (r183 RenderPipeline)** — runtime-swappable bloom/dotScreen; glow the dream geometry, hot-swap effects per scene.
|
||||
- **Paper Shaders (zero-dep canvas shaders)** — cheap mesh/grain gradient backdrops below the fold so the heavy WebGPU canvas is only the hero.
|
||||
- **Real-time ASCII / dithering / halftone post-FX** — a defining late-2025/2026 aesthetic (Codrops "Efecto" Jan 2026). Run the Agent Black Box through a dithered/CRT pass so an agent's "thoughts" read off a screen.
|
||||
- **CSS scroll-driven animations (`animation-timeline: scroll()/view()`)** — JS-free scrollytelling, Interop 2026 target. Drive all non-canvas reveals off this so the GPU budget stays for the hero.
|
||||
- **View Transitions API (same-document Baseline Oct 2025)** — morph hero → graph → dream → Black Box on scroll, app-like, off-main-thread.
|
||||
- **Motion (motion.dev), MIT, WAAPI off-thread** — 2.5–6x faster than GSAP, no Webflow license issue. Use for all DOM/UI motion so it never janks the canvas.
|
||||
|
||||
## What makes award-winning dev landings 1-of-1 (2025/2026)
|
||||
- **Product UI rendered INSIDE the WebGPU pass** (Igloo Inc, @pmndrs/uikit) — labels/wordmark as SDF text in the same pass as particles, sharing grain/aberration/depth. DOM-over-canvas looks "pasted on"; this doesn't.
|
||||
- **Scroll = camera = narrative** (worldbuilding, not parallax) with offscreen render-pass culling for perf.
|
||||
- **Cursor-reactive POST-processing warp** (whole composited frame bends toward cursor; scroll velocity smears particles) — generic "spotlight follows cursor" is 2022.
|
||||
- **Live spec-sheet hero / big real numbers, NOT logo soup** (Evil Martians 100-page study) — the 2026 dev crowd distrusts "trusted by" walls. Lead with live HUD: "561 stars · 43 countries · 150,000 particles."
|
||||
- **Kinetic/variable-font typography** driven by scroll velocity (wordmark "tightens" as you dive into the graph).
|
||||
- **Switchable one-canvas, three-lens hero** — Vestige has exactly 3 surfaces: Dream (Cinema) / Graph (force-directed) / Black Box (agent replay). One canvas, three toggles.
|
||||
|
||||
## Viral waitlist mechanics (2024–2026, verified)
|
||||
- **The share artifact = the product's OUTPUT, not an OG counter card** — Suno song / v0 gen / Lovable URL. For Vestige: a **6-second rendered loop of the VISITOR'S OWN seeded memory graph** flying through Cinema's dream worlds.
|
||||
- **Manus tradeable invite codes** — "scarcity that creates content"; access itself is the artifact. Give each signup 2–3 unique founder codes that unlock a rendered Cinema world.
|
||||
- **Robinhood skip-the-line position jump** — now table-stakes SKELETON; layer the novel artifact on top. Tie milestones to Vestige-native unlocks (new dream geometry / longer render), NOT "free month."
|
||||
- **REAL scarcity beats fake** — Superhuman's own builder now warns against fake scarcity; deceptive.design lists it as a dark pattern. Frame the cap truthfully: "each Cinema render is real WebGPU work; seats open in batches as render capacity scales" (literally true).
|
||||
- **Police leaderboard fraud from minute one** — #1 self-inflicted failure with a taste-driven HN/X crowd. Require GitHub OAuth or email verification before a referral counts; dedupe by verified identity.
|
||||
- **Manifesto-first, capture-second** (Arc Browser pattern) — "Your agent forgets everything. Your memory should be yours, local, and beautiful." then scroll INTO the live demo.
|
||||
- **Compounding milestone unlocks** — each referral milestone spawns a NEW shareable (new dream world / longer render: "I just unlocked the gyroid world"), not one-shot share-to-join.
|
||||
|
||||
## Personalized shareable artifact — how to build it (all client-side, fits local-first)
|
||||
- **Raycast Wrapped 2025 pattern: fully client-side "Copy as Image" from LOCAL data** — exactly Vestige's local-first positioning; a server OG pipeline would CONTRADICT it.
|
||||
- **WebGPU/WebGL canvas → media via `captureStream()` + `MediaRecorder`** (works for Three.js AND WebGPU) — MemoryCinema's sandbox already renders to a canvas host with camera-only fallback. Wire capture for a shareable clip.
|
||||
- **`canvas.toBlob` → PNG** for the still card; **HTML-in-Canvas `drawElement()`** (Chromium 147+) to composite a stat overlay onto the live canvas where supported.
|
||||
- **Seed-from-identity deterministic art** — on the waitlist (no real memory yet) seed a one-of-one "phantom brain" from the visitor's GitHub handle / email hash / typed memory. Unique before they do anything.
|
||||
- **Build-time dynamic OG** via Satori + resvg-js (or @ethercorps/sveltekit-og) so the shared LINK preview is a stunning Cinema frame, even though the page is static.
|
||||
|
||||
## The two pieces that CANNOT be static (the only hosted bits)
|
||||
1. **Referral state + position counter** — needs a tiny API/edge fn (GetWaitlist/LaunchList, or our own Supabase fn — already started).
|
||||
2. **Per-user OG/share-card** if done server-side — but the Raycast pattern lets us do it CLIENT-side instead, staying local-first.
|
||||
|
||||
WebGPU caveat: default-on in Safari Sep 2025 / Firefox Jul 2025 — always ship the WebGL2/Canvas/pre-baked WebP fallback so share never silently breaks.
|
||||
36
docs/launch/living-brain-hero-spec.json
Normal file
36
docs/launch/living-brain-hero-spec.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"concept": "A 65,536-particle GPU field that inhales the word \"VESTIGE\" out of chaos, exhales it into a dense, breathing, iridescent brain whose bright tracts trace real memory-graph pathways, then lets the cursor part the cortex like liquid and watch it heal \u2014 a living second brain that is a function of Sam's data, not a copyable starfield.",
|
||||
"entranceAnimation": "Timeline driven by one CPU uniform uFormProgress (GSAP easeOutExpo, 0->1 over 0.0-1.2s) plus uDissolve and a word/brain target swap. Beat 0 (0-0.9s): the same 65k points are seeded onto a canvas-rendered \"VESTIGE\" wordmask (pick random pixels where alpha>0.5 -> targetA texture); a critically-damped spring (k ramps 0->14*uFormProgress, damping 0.86 so it overshoots ~3% and settles) snaps the chaos cloud into legible, screenshot-able text while low-amp curl keeps it alive. Beat 1 (0.9-1.4s): uDissolve 0->1 disintegrates the letters left-to-right via a Perlin-remapped per-glyph mask (step(uDissolve, remap)) with wind+rise+curl drift \u2014 the IDENTICAL particles are conserved, never respawned, so the dust that was \"V\" becomes specific neurons. Beat 2 (1.4-3.0s): the freed particles re-target the brain point cloud (targetB = baked memory-graph projection) through the same spring; a per-particle simplex-of-target-position delay (delay = (snoise(targetPos*scale)*0.5+0.5)*0.6) makes formation SWEEP across the cortex like a propagating thought instead of popping as a blob, with a size-pop (sin(p*PI)*1.8) and an emissive dissolve-frontier band (uEdgeBoost>1.0) that UnrealBloom catches as a moving rim of neural ignition. At t=3s the brain is formed, breathing, and the first idle synapse fires.",
|
||||
"brainCoreSpec": "Confine an analytic two-lobe brain SDF and make flow run TANGENT to it (curl of an SDF-gradient-modulated noise potential is divergence-free, so streamlines parallel the shell \u2014 particles glide along the cortex, never pierce it).\n\nfloat sdEllipsoid(vec3 p, vec3 r){ float k0=length(p/r); float k1=length(p/(r*r)); return k0*(k0-1.0)/k1; }\nfloat brainSDF(vec3 p){\n float L = sdEllipsoid(p-vec3(-0.32,0.0,0.0), vec3(0.85,0.95,1.15));\n float R = sdEllipsoid(p-vec3( 0.32,0.0,0.0), vec3(0.85,0.95,1.15));\n float d = min(L,R);\n d += 0.06*sin(8.0*p.x)*sin(7.0*p.y)*sin(6.0*p.z); // sulci/gyri wrinkles\n return d;\n}\nvec3 sdfNormal(vec3 p){ const vec2 e=vec2(1.0,-1.0)*0.004;\n return normalize(e.xyy*brainSDF(p+e.xyy)+e.yyx*brainSDF(p+e.yyx)+e.yxy*brainSDF(p+e.yxy)+e.xxx*brainSDF(p+e.xxx)); }\nvec3 potential(vec3 p){ return sdfNormal(p)*snoise(p*1.6 + uTime*0.15); } // tangential when curl'd\nvec3 curlNoise(vec3 p){ const float e=0.012; vec3 dx=vec3(e,0,0),dy=vec3(0,e,0),dz=vec3(0,0,e);\n float x=potential(p+dy).z-potential(p-dy).z-(potential(p+dz).y-potential(p-dz).y);\n float y=potential(p+dz).x-potential(p-dz).x-(potential(p+dx).z-potential(p-dx).z);\n float z=potential(p+dx).y-potential(p-dx).y-(potential(p+dy).x-potential(p-dy).x);\n return vec3(x,y,z)/(2.0*e); }\n\nDENSITY (the anti-starfield): each particle carries life in the FBO .w; decrement, and on death reseed onto the shell with center-weighted radius rad=pow(hash(uv),1.5) (mix(0.2,1.0,rad)), then snap p -= sdfNormal(p)*brainSDF(p)*0.85. Additive overlap of thousands of short-lived particles funneling through the same sulcus builds visible glowing neural tracts and a dense core \u2014 not evenly spaced dots. For the moat, replace the procedural shell seed with the baked memory-graph: targetB texture = PCA/UMAP(memory_embeddings)->[-1,1]^3, per-particle density weighted by importance_score, color by recency, so the densest bright pathways ARE Sam's most-connected memories.\n\nBREATHING: float pulse=0.5+0.5*sin(uTime*2.0); restore=6.0+3.0*pulse (shell tightens on systole); flow += -sdfNormal(p)*brainSDF(p)*restore; and film thickness += 60.0*pulse so hue breathes with the beat (~0.32 Hz heartbeat).",
|
||||
"liquidCursorSpec": "Four forces in ONE velocity FBO pass, all gated by pointer SPEED (not position) so a still cursor barely perturbs and a fast swipe carves a healing hole. JS feeds uMouse (UV pos), uMouseVel (delta), uMouseSpeed (EMA-smoothed): const dx=mx-pmx,dy=my-pmy; let raw=hypot(dx*aspect,dy); speed=speed*0.85+raw*0.15; speed*=0.92 (idle decay->heal); uMouseSpeed=min(speed*gain,cap). On no-pointer (mobile), drive a Lissajous ghost cursor (mx=0.5+0.3*cos(t*0.7), my=0.5+0.3*sin(t*1.1)) so the brain parts itself.\n\nIn the velocity FS, accumulate into accel then velocity-Verlet integrate:\nvec2 q=(pos.xy-uMouse); q.x*=uAspect; float fall=exp(-dot(q,q)/(uRadius*uRadius)); // Gaussian splat, soft edge\nvec2 rdir=normalize(pos.xy-uMouse+1e-5);\naccel.xy += rdir*fall*uPushK*uMouseSpeed; // (1) repulsion splat, uPushK~0.0023..0.02\nvec2 tangent=vec2(-rdir.y,rdir.x); // 90deg rotate == cross with +Z\nfloat swirlSign=sign(uMouseVel.x*rdir.y - uMouseVel.y*rdir.x);\naccel.xy += tangent*swirlSign*fall*uSwirlK*uMouseSpeed; // (2) vortex swirl, uSwirlK~1x uPushK -> flick spins eddy\naccel.xyz += (uTargetPos - pos.xyz)*uSpringK; // (3) HEAL: Hooke pull to brain-home, uSpringK~0.003..0.02\naccel.xyz += curlNoise(pos)*uFlowK; // (4) always-on ambient brain flow (the SDF curl above)\nvel.xyz = (vel.xyz + accel*uDt) * uDamping; // damping 0.7..0.92; crit-damp target ~1-2*sqrt(uSpringK)\nvel.xy *= min(1.0, uMaxSpeed/length(vel.xy)); // per-axis clamp = stable at any dt\n// position FS: pos.xyz += vel.xyz*uDt;\n\nThe heal returns to uTargetPos = the brain/memory-graph home texture, so it re-forms the actual graph, not a generic sphere. Combining soft speed-gated splat + signed swirl + brain-home spring + ambient SDF-curl in one Verlet pass is what reads as parting LIQUID with surface tension; public demos ship only one of these.",
|
||||
"colorSpec": "Physical thin-film structural color so it reads as living tissue/soap-film, not HSV rainbow dots. Constructive-interference wavelength w = 2*n*d*cos\u03b8 where film thickness d is DRIVEN by curl/velocity magnitude (fast neural traffic = thicker film = shifted hue) and \u03b8 is the view angle vs the SDF normal \u2014 so color is a function of MOTION and VIEW, shimmering as it pulses and parallaxes. Convert wavelength->RGB with the verified spectral_zucconi6 six-parabola fit (NOT naive HSV):\n\nvec3 bump3y(vec3 x, vec3 yo){ vec3 y=1.0-x*x; return max(y-yo,0.0); }\nvec3 spectral_zucconi6(float w){ float x=clamp((w-400.0)/300.0,0.0,1.0);\n const vec3 c1=vec3(3.54585104,2.93225262,2.41593945), x1=vec3(0.69549072,0.49228336,0.27699880), y1=vec3(0.02312639,0.15225084,0.52607955);\n const vec3 c2=vec3(3.90307140,3.21182957,3.96587128), x2=vec3(0.11748627,0.86755042,0.66077860), y2=vec3(0.84897130,0.88445281,0.73949448);\n return bump3y(c1*(x-x1),y1)+bump3y(c2*(x-x2),y2); }\n// render frag:\nfloat cosTheta = abs(dot(vViewDir, vSdfNormal));\nfloat thickness = 280.0 + vCurlMag*180.0 + 60.0*pulse; // nm; curlMag from sim, breathes with heartbeat\nfloat w = clamp(2.0*1.35*thickness*cosTheta, 400.0, 700.0); // n=1.35 film IOR\nvec3 iridescent = spectral_zucconi6(w);\n\nFRESNEL RIM (backlit-organ silhouette): float fres=pow(1.0-clamp(dot(vSdfNormal,vViewDir),0.0,1.0),2.5); vec3 rim=mix(vec3(0.15,0.10,0.45),iridescent,fres); float brightness=0.25+1.6*fres (edges ~7x brighter than core). Core stays indigo, grazing-angle silhouette flares into the full spectrum tracing the sulci wrinkles. Because rim/iridescence both use the SAME analytic SDF normal as the sim, the glow precisely traces YOUR organic shape \u2014 a clone with different seeds blows out to white or reads sparse.",
|
||||
"shaderChanges": [
|
||||
"POSITION FBO shader: change from `position += velocity` Euler to velocity-Verlet half-step \u2014 read updated velocity texture, `pos.xyz += vel.xyz * uDt` only; carry life in .w, decrement by uDelta*0.05, and on life<0 call sampleShellPoint(uv) to reseed center-weighted onto the brain SDF shell (this is what creates density turnover and bright tracts).",
|
||||
"ADD a dedicated VELOCITY FBO shader (GPUComputationRenderer two-variable position+velocity dependency, matching the house pattern): accumulate the 4 forces (Gaussian speed-gated repulsion splat, signed tangential swirl, brain-home Hooke spring to uTargetPos, ambient SDF-curl) into accel, then `vel = (vel + accel*uDt)*uDamping` with a per-axis speed clamp. Add the breathing term `-sdfNormal(p)*brainSDF(p)*(6.0+3.0*pulse)` and the firing-ripple impulse from uEvent.",
|
||||
"ADD brainSDF/sdfNormal/potential/curlNoise GLSL (replacing the freefloating box curl) so flow is tangent to the cortex \u2014 this is the single biggest starfield->organ move; reuse the same sdfNormal in BOTH the velocity sim and the render vertex shader.",
|
||||
"RENDER vertex shader: pass vSdfNormal=sdfNormal(pos), vViewDir=normalize(cameraPosition-worldPos), vCurlMag=length(flow) (write curl magnitude from sim into an unused channel), vAlpha=clamp(speed*1.2,0.04,0.8); gl_PointSize = uSize*(0.6+2.0*vAlpha)*(300.0/-mvPosition.z) so near/fast particles dominate density. Add per-particle staggered-reveal delay from simplex(targetPos) and size-pop sin(p*PI) for the entrance.",
|
||||
"RENDER fragment shader: compute spectral_zucconi6 thin-film iridescence (thickness from vCurlMag+pulse), Fresnel rim mix, soft gaussian sprite `float soft=exp(-3.0*dot(uv,uv)); if(dot(uv,uv)>1.0) discard;`, premultiplied low energy `vec3 col=rim*brightness; float energy=soft*vAlpha*0.6; gl_FragColor=vec4(col*energy, energy)` so additive OVERLAP builds dense colored mass instead of blowing out to white. Add the dissolve-frontier emissive band (uEdgeBoost>1.0) for the entrance sweep.",
|
||||
"MATERIAL (JS): THREE.ShaderMaterial { blending:AdditiveBlending, depthTest:true, depthWrite:false, transparent:true } \u2014 order-independent additive sum, no per-frame CPU depth sort (keeps 65k at 60fps).",
|
||||
"UnrealBloom retune in scene.ts: strength ~0.85, radius ~0.55, threshold ~0.62 \u2014 threshold high enough that only accumulated sulci hot-spots + rim bloom into a continuous membrane while the dim shell stays as fine texture (not a lens flare)."
|
||||
],
|
||||
"buildSteps": [
|
||||
"Create apps/dashboard/src/lib/graph/neuralFlow.ts as the WebGL2 GPGPU hero (separate from CPU particles.ts and from the WebGPU Memory Cinema storm.ts \u2014 DO NOT touch MemoryCinema/cinema/*). Import GPUComputationRenderer from 'three/examples/jsm/misc/GPUComputationRenderer.js' (confirmed present at three@0.172).",
|
||||
"Set up GPUComputationRenderer with 256x256 = 65,536 particles, two variables texturePosition (xyz+life) and texturevelocity (xyz+curlMag); wire setVariableDependencies([position,velocity]) both ways. Seed initial positions as a scattered curl cloud and bake targetA (VESTIGE wordmask via offscreen canvas, alpha>0.5 pixels) + targetB (brain SDF shell points, or for real users a precomputed PCA/UMAP DataTexture of memory_embeddings with importance_score density).",
|
||||
"Author the position + velocity GLSL fragment shaders per shaderChanges: brainSDF/curlNoise block, 4-force velocity-Verlet accumulation, center-weighted reseed, breathing pulse, firing-ripple uEvent.",
|
||||
"Author the render ShaderMaterial (vertex passes SDF normal/viewDir/curlMag/staggered-delay; fragment does spectral_zucconi6 + Fresnel rim + soft sprite + premultiplied energy). Set AdditiveBlending, depthWrite:false. Attach to a THREE.Points reading the position texture.",
|
||||
"Wire the entrance timeline: GSAP (or rAF lerp) drives uFormProgress 0->1 (easeOutExpo 1.2s), uDissolve for the word->dust beat, and a target swap A->B at ~1.4s; expose uEdgeBoost for the dissolve-frontier glow. Land at formed+breathing+first-fire by t=3s.",
|
||||
"Wire the input layer: pointermove -> uMouse/uMouseVel/uMouseSpeed with EMA(0.85/0.15) + idle decay(0.92) + aspect correction; no-pointer -> Lissajous ghost cursor. Wire firing events: on Vestige retrieve/create (or a Poisson timer weighted by importance_score) set uEvent=vec4(nodePos, uTime) so a bright iridescent ripple travels the cortex.",
|
||||
"Retune UnrealBloomPass in scene.ts (strength 0.85 / radius 0.55 / threshold 0.62). Mount neuralFlow as the hero, swap out / keep the legacy starfield behind a feature flag.",
|
||||
"Add graceful degradation: detect WebGL2; on low-end (devicePixelRatio cap, frame-time probe) drop to 128x128=16,384 particles and skip the second glow-halo sprite; ensure static-shippable (no server, all textures baked at build or hydrated from a shipped JSON of memory-graph coords). Verify 60fps live in the running app and capture proof.",
|
||||
"Run gates: pnpm --filter @vestige/dashboard check && build; load the page and confirm formation hook, liquid cursor, and 60fps via the running app before claiming done."
|
||||
],
|
||||
"perfNotes": [
|
||||
"65,536 = 256x256 FBO is the sweet spot; depthWrite:false + AdditiveBlending gives order-independent transparency so NO per-frame CPU depth sort (a sort at 65k would blow the frame budget). Keep this \u2014 it is the 60fps enabler.",
|
||||
"brainSDF is evaluated multiple times per particle (sdfNormal does 4 SDF taps, curlNoise does 6 potential evals = up to ~24 SDF calls/particle/frame). Keep the SDF cheap (2 ellipsoids + one sin product); do NOT add raymarched detail. If frame-time spikes, drop curlNoise epsilon taps from 6 to a 3-tap variant (al-ro style cross(grad0,grad1)).",
|
||||
"Reuse the SAME sdfNormal result between sim and render by writing it to a spare texture channel rather than recomputing in the vertex shader, if profiling shows the vertex SDF eval is hot.",
|
||||
"velocity-Verlet + per-axis speed clamp (vel *= min(1.0, uMaxSpeed/length(vel))) is mandatory: naive Euler explodes the spring+repulsion on dt spikes (tab refocus / slow frame) and breaks graceful degradation. Pass real dt, clamped to ~33ms max.",
|
||||
"Degrade path: WebGL2-only (ships everywhere per three@0.172); on devicePixelRatio>2 or measured <50fps, halve to 128x128 (16,384), drop the glow-halo second sprite, and lower bloom radius \u2014 keeps the brain alive on mobile/low-end.",
|
||||
"Bloom is the most expensive post pass; threshold 0.62 limits the bloomed pixel count to rim+hot tracts only. Do not lower threshold below ~0.55 or the whole field blooms and both perf and the colored-density read collapse to white.",
|
||||
"Premultiplied low per-particle energy (soft*vAlpha*0.6) is load-bearing for BOTH look and perf: it keeps additive overlap building hue instead of saturating, so you don't need HDR tonemapping gymnastics to keep the iridescence visible in dense zones."
|
||||
]
|
||||
}
|
||||
60
docs/launch/neural-sign-spec.json
Normal file
60
docs/launch/neural-sign-spec.json
Normal file
File diff suppressed because one or more lines are too long
12
docs/launch/node-engine-spec.json
Normal file
12
docs/launch/node-engine-spec.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
92
docs/launch/particle-shapes-spec.json
Normal file
92
docs/launch/particle-shapes-spec.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"summary": "Scour the web for unexpected jaw-dropping procedural particle-morph shapes, return implementable GLSL/math for the Vestige hero",
|
||||
"agentCount": 5,
|
||||
"logs": [],
|
||||
"result": {
|
||||
"pick": {
|
||||
"chosenShapes": [
|
||||
{
|
||||
"name": "Aizawa Strange Attractor (chaotic-thought butterfly)",
|
||||
"whyChosen": "The single highest 'how did they DO that' shape. 40k particles self-organize from noise into a glowing toroidal shell pierced by an axial spike - a sculpture no primitive can fake. Deterministic chaos pools particles where the orbit lingers, giving free non-uniform nebula density that beats any Perlin shading. It is the literal 'watch your agent think' money shot: same seed = same orbit, yet the path looks unpredictable. Closed-form per-seed via fixed-step iteration, no CPU LUT or mesh needed.",
|
||||
"glslSnippet": "// Aizawa attractor - iterate the ODE from a seed-jittered start.\n// Each particle lands on a different part of the same attracting set,\n// and varying the step count spreads density along the orbit.\nvec3 shapeAizawa(float a, float b, float c, float S){\n // tiny jittered start ball near origin (seeds decorrelated by hashing)\n vec3 p = (vec3(fract(a*43.0+0.13), fract(b*91.7+0.41), fract(c*57.3+0.77)) - 0.5) * 0.12;\n const float A=0.95, B=0.7, C=0.6, D=3.5, E=0.25, F=0.1, dt=0.01;\n int iters = 260 + int(a*140.0); // 260..400 -> spread along manifold\n for(int i=0;i<400;i++){\n if(i>=iters) break; // GLSL ES: constant loop bound + break\n float dx = (p.z - B)*p.x - D*p.y;\n float dy = D*p.x + (p.z - B)*p.y;\n float dz = C + A*p.z - p.z*p.z*p.z/3.0\n - (p.x*p.x + p.y*p.y)*(1.0 + E*p.z)\n + F*p.z*p.x*p.x*p.x;\n p += vec3(dx,dy,dz)*dt;\n }\n // attractor lives roughly in [-1.5,1.5]; 0.42*S fills ~70% of frustum height\n return p * (0.42 * S);\n // PALETTE: color by length(velocity) of the final step ->\n // violet (slow shell) -> cyan -> gold (fast axial spike).\n}",
|
||||
"seedsNeeded": "a (drives iteration count + start jitter), b, c (start jitter). All three used.",
|
||||
"scaleNote": "Attractor spans ~[-1.5,1.5]. Multiply by 0.42*S so the full body fills ~70% of frustum height. Cap loop at 400 for 40k @ 60fps; 260-400 iters per particle is the sweet spot for even density."
|
||||
},
|
||||
{
|
||||
"name": "Hopf Fibration (linked memories, interlocking rings)",
|
||||
"whyChosen": "The most 'premium math' object you can ship: nested Villarceau circles threaded through each other so every ring links every other exactly once, yet none collide. Reads as impossible 3D chainmail of light that rotates into self-similar tori. Devs who know it gasp; devs who don't think it's an alien artifact. Perfect on-brand metaphor for a fully-connected memory graph that never tangles. Pure closed-form from seeds, surface-coherent.",
|
||||
"glslSnippet": "// Hopf fibration: seed -> a base point on S^2 (which fiber), then a phase\n// along that fiber circle; lift to S^3 and stereographically project to R^3.\n// Quantizing the base point to a few values yields a few BIG clean linked\n// rings (more legible than a full fill).\nvec3 shapeHopf(float a, float b, float c, float S){\n const float TAU = 6.28318530718;\n // quantize base point so we get ~12 bold interlocking rings, not mush\n float ringId = floor(a * 12.0);\n float cosT = 2.0*fract(ringId*0.61803398875) - 1.0; // base colatitude, spread\n float baseTheta= acos(clamp(cosT,-1.0,1.0));\n float basePhi = TAU * fract(ringId*0.7548776662);\n vec3 bp = vec3(sin(baseTheta)*cos(basePhi),\n sin(baseTheta)*sin(basePhi),\n cos(baseTheta)); // (x,y,z) on S^2\n float t = TAU * b; // phase along the fiber circle\n float k = 1.0 / sqrt(2.0*(1.0 + bp.z) + 1e-4);\n vec4 P = vec4((1.0+bp.z)*cos(t),\n bp.x*sin(t) - bp.y*cos(t),\n bp.x*cos(t) + bp.y*sin(t),\n (1.0+bp.z)*sin(t)) * k; // point on S^3\n vec3 q = P.xyz / (1.0 - P.w + 1e-3); // stereographic S^3 -> R^3\n // jitter slightly along c so each ring has a little tube thickness\n q += (vec3(fract(c*71.3),fract(c*131.7),fract(c*197.1))-0.5)*0.04;\n return q * (0.28 * S);\n}",
|
||||
"seedsNeeded": "a (picks which of ~12 rings), b (phase along the fiber circle), c (tube-thickness jitter). All three used.",
|
||||
"scaleNote": "Stereographic output ranges widely; 0.28*S tames it to ~70% frustum. The 1e-3 guard on (1-P.w) prevents the projection blowing up at the far pole. Use ~8-16 quantized rings for legibility - a full continuous fill reads as fog."
|
||||
},
|
||||
{
|
||||
"name": "(p,q) Torus Knot tube (the reasoning loop)",
|
||||
"whyChosen": "A glowing rope that winds a torus and ties itself into a perfect trefoil - clean, logo-grade, obviously computed-not-drawn. Inflating the core curve into a particle TUBE (Frenet frame + uniform disc fill) makes it read as a solid living rope under additive bloom, not a thin wire. Bump (p,q) each loop (2,3 -> 3,2 -> 3,4 -> 5,2) and the SAME code morphs through a family of distinct knots: free variety. On-brand as 'the agent threading recall back through prior context.'",
|
||||
"glslSnippet": "// (p,q) torus knot, inflated into a volumetric tube via a Frenet-ish frame.\n// a -> position along the curve; b,c -> uniform fill of the tube cross-section.\nvec3 shapeTorusKnot(float a, float b, float c, float S){\n const float TAU = 6.28318530718;\n const float P = 3.0, Q = 7.0; // (3,7) braid; swap per loop for variety\n const float R = 1.0, rMinor = 0.45, rTube = 0.16;\n float t = a * TAU;\n float cq = cos(Q*t), sq = sin(Q*t);\n float ring = R + rMinor*cq;\n vec3 C = vec3(ring*cos(P*t), ring*sin(P*t), rMinor*sq); // core curve\n // analytic tangent dC/dt\n vec3 T = vec3(-rMinor*Q*sq*cos(P*t) - ring*P*sin(P*t),\n -rMinor*Q*sq*sin(P*t) + ring*P*cos(P*t),\n rMinor*Q*cq);\n T = normalize(T);\n vec3 N = normalize(cross(T, vec3(0.0,0.0,1.0)));\n vec3 B = cross(T, N);\n float ang = b * TAU;\n float rad = sqrt(c) * rTube; // sqrt -> uniform disc (no center clump)\n vec3 pos = C + rad*(cos(ang)*N + sin(ang)*B);\n return pos * (0.55 * S);\n}",
|
||||
"seedsNeeded": "a (along the knot), b (tube angle), c (radial fill, sqrt for uniform disc). All three used.",
|
||||
"scaleNote": "Knot body spans roughly [-1.45,1.45]; 0.55*S fills ~70% height. Keep P,Q coprime (3&7, 2&5, 3&4) or the curve degenerates. rTube=0.16 gives a solid rope; drop to 0.05 for a bare hypnotic wire."
|
||||
},
|
||||
{
|
||||
"name": "DNA Double Helix (memory as encoded sequence)",
|
||||
"whyChosen": "Instantly legible at thumbnail size - twin spiraling ribbons with rungs ladder-stepping between them, the universal 'intelligent / alive' signal. Photographs perfectly for a hero still and rotates beautifully. Provides recognizable, calm contrast to the abstract chaos/topology shapes (variety). Built-in palette story: strand A cyan, strand B violet, base-pair rungs gold. On-brand: event strand + interpretation strand bound by link base-pairs.",
|
||||
"glslSnippet": "// DNA double helix: two antiparallel sugar-phosphate strands + base-pair rungs.\n// a -> height along the axis; b classifies particle into strandA / strandB / rung;\n// c -> radial thickness so strands read as ribbons, not lines.\nvec3 shapeDNA(float a, float b, float c, float S){\n const float TAU = 6.28318530718;\n const float TWISTS = 6.0; // full turns over the length\n const float HEIGHT = 2.0;\n const float RADIUS = 0.55;\n float yy = (a - 0.5) * HEIGHT;\n float ang = a * TWISTS * TAU;\n vec3 strandA = vec3(cos(ang)*RADIUS, yy, sin(ang)*RADIUS);\n vec3 strandB = vec3(cos(ang+3.14159)*RADIUS, yy, sin(ang+3.14159)*RADIUS);\n vec3 pos;\n if(b < 0.42){ // strand A (cyan)\n pos = strandA;\n } else if(b < 0.84){ // strand B (violet)\n pos = strandB;\n } else { // base-pair rung (gold), interpolate across the ladder\n float k = fract(b * 53.0);\n pos = mix(strandA, strandB, k);\n }\n // ribbon thickness: jitter in a small ball, sqrt for even fill\n float rad = sqrt(c) * 0.06;\n float ja = fract(c*131.7)*TAU, jb = fract(c*71.3)*3.14159;\n pos += rad * vec3(sin(jb)*cos(ja), cos(jb), sin(jb)*sin(ja));\n return pos * (0.62 * S);\n}",
|
||||
"seedsNeeded": "a (height + twist angle), b (strandA / strandB / rung classifier), c (ribbon thickness jitter). All three used.",
|
||||
"scaleNote": "Height = 2.0, radius = 0.55 -> bounding box ~1.1 wide x 2.0 tall. 0.62*S puts the tall axis at ~70% frustum height (this one is portrait-oriented, which frames well for a hero). Tune TWISTS=6 for a classic look; raise to 8-10 for a tighter coil."
|
||||
},
|
||||
{
|
||||
"name": "Spiral Galaxy (galaxy of memories)",
|
||||
"whyChosen": "The most universally read 'whoa' shape - 40k points snap into a barred spiral with a luminous gold core and trailing violet dust arms, like a Hubble plate assembling. Reads even at thumbnail size and needs zero math literacy from the viewer, so it converts the broadest audience. Exponential vertical falloff gives a real galactic-disc thinness, not a flat pancake. On-brand: each particle a stored memory orbiting the bright working-context core.",
|
||||
"glslSnippet": "// Barred spiral galaxy: log-spiral arms + exponential disc thickness + bright core.\n// a -> which arm + per-arm scatter; b -> radius (sqrt for centre concentration);\n// c -> vertical disc jitter and core bulge.\nvec3 shapeGalaxy(float a, float b, float c, float S){\n const float TAU = 6.28318530718;\n const float N_ARMS = 4.0;\n const float TWIST = 2.6; // arm winding\n float arm = floor(a * N_ARMS);\n float t = sqrt(b); // 0..1 radial, sqrt concentrates centre\n float r = t;\n float jit = (fract(a*97.13) - 0.5) * 0.35 * (1.0 - t); // arms tighten outward\n float ang = arm * TAU / N_ARMS + t * TWIST * TAU + jit;\n // disc thickness: thick bulge at centre, thin at rim\n float yy = (fract(c*53.7) - 0.5) * 0.14 * exp(-2.5*r);\n vec3 pos = vec3(cos(ang)*r, yy, sin(ang)*r);\n // central bulge: a fraction of particles form the spherical core\n if(b < 0.06){\n float rr = pow(fract(c*191.3), 0.5) * 0.12;\n float th = fract(a*311.7)*TAU, ph = acos(2.0*fract(c*131.1)-1.0);\n pos = vec3(rr*sin(ph)*cos(th), rr*cos(ph), rr*sin(ph)*sin(th));\n }\n return pos * (1.30 * S);\n // PALETTE: gold core (small r) -> emerald mid-disc -> violet/cyan rim by r.\n}",
|
||||
"seedsNeeded": "a (arm index + angular scatter), b (radius; <0.06 -> core bulge), c (disc thickness + core sphere jitter). All three used.",
|
||||
"scaleNote": "Disc radius normalized to ~1.0; 1.30*S fills ~70% frustum width for the flat disc (it presents widescreen). Tilt the whole shape ~25deg on the engine side so the spiral reads as a disc, not a ring. exp(-2.5*r) keeps the rim a razor-thin sheet."
|
||||
},
|
||||
{
|
||||
"name": "3D Superformula Supershape (the morph engine)",
|
||||
"whyChosen": "ONE equation, four knobs -> starfish, sea-urchin, faceted crystal, blooming flower, bloated pollen. Because it is the same target with different params, you get an entire alien bestiary nearly free, and morphing BETWEEN parameter sets is buttery since the topology is shared - perfect connective tissue between the 'serious' shapes. A dev watching it shapeshift instantly realizes there is no mesh: it is pure procedural magic. On-brand: one generator, infinite forms = one memory engine producing emergent structure.",
|
||||
"glslSnippet": "// Gielis superformula, spherical product of two superformulas (surface-sampled).\n// a -> longitude theta, b -> latitude phi, c -> thin surface-shell jitter.\n// Swap the (m,n1,n2,n3) set per loop to morph through the bestiary.\nfloat superR(float ang, float m, float n1, float n2, float n3){\n float t = m * ang * 0.25;\n float c1 = pow(abs(cos(t)), n2);\n float c2 = pow(abs(sin(t)), n3);\n return pow(c1 + c2 + 1e-6, -1.0/n1);\n}\nvec3 shapeSupershape(float a, float b, float c, float S){\n const float PI = 3.14159265359;\n float theta = (a*2.0 - 1.0) * PI; // -PI..PI\n float phi = (b - 0.5) * PI; // -PI/2..PI/2\n // spiky sea-urchin: m=7,n1=0.2,n2=1.7,n3=1.7\n // (crystal m=4,n1=60,n2=55,n3=30 | star m=5,n1=0.1,n2=1.7,n3=1.7)\n float m=7.0, n1=0.2, n2=1.7, n3=1.7;\n float r1 = superR(theta, m, n1, n2, n3);\n float r2 = superR(phi, m, n1, n2, n3);\n vec3 pos = vec3(r1*cos(theta) * r2*cos(phi),\n r1*sin(theta) * r2*cos(phi),\n r2*sin(phi));\n // tame extreme spikes so framing stays stable, then add a thin shell jitter\n pos = normalize(pos) * pow(length(pos), 0.6);\n pos *= (1.0 + (c - 0.5)*0.04);\n return pos * (0.85 * S);\n}",
|
||||
"seedsNeeded": "a (longitude), b (latitude), c (surface-shell thickness). All three used. (superR is a free helper - paste once.)",
|
||||
"scaleNote": "After the normalize()*pow(len,0.6) spike-taming the body sits in ~[-1.6,1.6]; 0.85*S -> ~70% height. The +1e-6 inside superR and abs() on cos/sin keep pow() real for odd exponents on negatives. Lerp the (m,n1,n2,n3) tuple over a loop for a living bloom."
|
||||
},
|
||||
{
|
||||
"name": "VESTIGE text beat (baked-mask glyph cloud)",
|
||||
"whyChosen": "A skeptical dev expects abstract blobs; spelling the product name out of 40k live particles is the single biggest 'wait, how' moment - recognizable signal emerging from noise - and it states the value prop in one beat (REMEMBER assembles, FORGET dissolves to dust). It also signals 'this team can actually drive the GPU.' The production-correct method is zero-rejection: bake every inside-pixel of the rasterized word into the position texture once on the CPU, so each particle already owns a guaranteed on-glyph target. The snippet is the SHADER-side fallback that rejection-samples a bound alpha mask when you prefer a pure-GPU path.",
|
||||
"glslSnippet": "// TEXT beat. PREFERRED (zero rejection): on the CPU, draw the word to a canvas,\n// read alpha, collect every pixel with a>0.5 into inside[], and for particle i\n// write target = vec3((px/W-0.5)*S*aspect, -(py/H-0.5)*S, (hash-0.5)*depth)\n// straight into the position FBO. No per-frame cost.\n//\n// SHADER fallback below: sample a bound alpha mask uMask (NearestFilter) and\n// relax outside particles toward the glyph along the alpha gradient.\nuniform sampler2D uMask; // word rasterized to alpha; e.g. 1024x256\nuniform float uAspect; // mask width/height\nvec3 shapeText(float a, float b, float c, float S){\n const float DEPTH = 0.10;\n vec2 uv = vec2(a, b); // candidate glyph UV\n // a few relaxation steps: walk toward denser alpha so strays land on letters\n for(int i=0;i<4;i++){\n float al = texture2D(uMask, uv).a;\n if(al > 0.5) break;\n float e = 1.0/256.0;\n float gx = texture2D(uMask, uv+vec2(e,0.0)).a - texture2D(uMask, uv-vec2(e,0.0)).a;\n float gy = texture2D(uMask, uv+vec2(0.0,e)).a - texture2D(uMask, uv-vec2(0.0,e)).a;\n vec2 g = vec2(gx,gy);\n uv += normalize(g + 1e-5) * 0.03; // climb the alpha field\n }\n vec3 pos = vec3((uv.x - 0.5) * uAspect, // centre + correct aspect\n -(uv.y - 0.5), // flip: texture v is top-down\n (c - 0.5) * DEPTH); // slab depth so it isn't flat\n return pos * (0.95 * S);\n // PALETTE: glyph core gold/emerald; edge/stray particles cyan->violet,\n // driven by step(uProgress, noise) for the assemble/dissolve dust.\n}",
|
||||
"seedsNeeded": "a, b (candidate glyph UV / which inside-pixel), c (slab depth). All three used. Requires uniforms uMask (alpha atlas of the word) + uAspect.",
|
||||
"scaleNote": "0.95*S sizes the word to ~70% frustum width (text reads best wide). uAspect = mask_w/mask_h keeps letters un-stretched. The 4-step alpha-climb is a cheap fallback; for crisp edges and zero runtime cost, prefer the CPU-baked inside[] -> position-FBO path described in the header comment (the nicoptere/barradeau method)."
|
||||
}
|
||||
]
|
||||
},
|
||||
"sources": [
|
||||
"https://www.songho.ca/opengl/gl_torus.html",
|
||||
"https://www.algosome.com/articles/aizawa-attractor-chaos.html",
|
||||
"https://en.wikipedia.org/wiki/Thomas%27_cyclically_symmetric_attractor",
|
||||
"https://paulbourke.net/geometry/supershape/",
|
||||
"https://analyticphysics.com/Higher%20Dimensions/Visualizing%20Calabi-Yau%20Manifolds.htm",
|
||||
"https://nilesjohnson.net/hopf.html",
|
||||
"https://mathworld.wolfram.com/KleinBottle.html",
|
||||
"https://paulbourke.net/geometry/sphericalh/",
|
||||
"https://www.shadertoy.com/view/tfdBDf",
|
||||
"https://en.wikipedia.org/wiki/Lorenz_system",
|
||||
"https://en.wikipedia.org/wiki/List_of_chaotic_maps",
|
||||
"https://paulbourke.net/geometry/supershape/",
|
||||
"https://kenbrakke.com/evolver/examples/periodic/gyroid/gyroid.html",
|
||||
"https://en.wikipedia.org/wiki/Torus_knot",
|
||||
"https://ykob.github.io/sketch-threejs/sketch/dna.html",
|
||||
"https://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/",
|
||||
"https://www.dynamicmath.xyz/strange-attractors/",
|
||||
"https://en.wikipedia.org/wiki/Hopf_fibration",
|
||||
"https://en.wikipedia.org/wiki/Gyroid",
|
||||
"https://en.wikipedia.org/wiki/Torus_knot",
|
||||
"https://paulbourke.net/geometry/supershape/",
|
||||
"https://en.wikipedia.org/wiki/Forgetting_curve",
|
||||
"https://en.wikipedia.org/wiki/Space_colonization",
|
||||
"https://github.com/nicoptere/FBO and https://barradeau.com/blog/?p=621 (baked image->FBO positions); https://tympanus.net/codrops/2026/01/28/webgpu-gommage-effect-dissolving-msdf-text-into-dust-and-petals-with-three-js-tsl/ and https://github.com/Chlumsky/msdfgen (MSDF >0.5 inside test, Jan 2026)",
|
||||
"https://www.algosome.com/articles/aizawa-attractor-chaos.html (Aizawa eqs); https://www.geeks3d.com/20140630/lorenz-attractor-butterfly-effect/ (Lorenz GLSL); https://github.com/BrutPitt/glChAoS.P (256M-particle GPU attractor reference)",
|
||||
"https://paulbourke.net/geometry/supershape/ (exact 3D spherical-product eqs); https://github.com/Softwave/glsl-superformula (GLSL function); https://en.wikipedia.org/wiki/Superformula",
|
||||
"https://en.wikipedia.org/wiki/Gyroid (G = sinXcosY+sinYcosZ+sinZcosX); https://alessandromastrofini.it/2022/03/29/gyroid-and-minimal-surfaces-for-cad/ (TPMS for CAD, gradient/level-set form)",
|
||||
"https://paulbourke.net/geometry/sphericalh/ (r=f(theta,phi) spherical harmonic surface, the m0..m7 exponent form); https://github.com/sebh/HLSL-Spherical-Harmonics (shader SH functions)",
|
||||
"https://www.4rknova.com/blog/2025/09/01/mandelbulb (2025 DE walkthrough, power-8); http://2008.sub.blue/projects/mandelbulb.html (Beddard, the canonical DE formula); https://en.wikipedia.org/wiki/Mandelbulb",
|
||||
"https://www.songho.ca/opengl/gl_torus.html (torus knot + trefoil parametric); https://rwoodley.org/posts/12-TorusKnots.html (p,q math); https://threejs.org/docs/pages/TorusKnotGeometry.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
83
docs/launch/waitlist-setup.md
Normal file
83
docs/launch/waitlist-setup.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Waitlist capture — setup (10 minutes)
|
||||
|
||||
The public waitlist form (`/dashboard/waitlist`) is already built. It POSTs JSON
|
||||
to `VITE_WAITLIST_ENDPOINT`. This guide stands up the storage backend: a
|
||||
Supabase table + Edge Function that stores every signup and (optionally) emails a
|
||||
confirmation.
|
||||
|
||||
Owned data, queryable Postgres, free tier handles launch-day volume.
|
||||
|
||||
## 1. Create the project
|
||||
|
||||
1. Create a project at <https://supabase.com> (free tier is fine for launch).
|
||||
2. Grab the **Project Ref** (Settings → General) and the
|
||||
**service-role key** (Settings → API). The service-role key is a secret —
|
||||
never commit it or expose it client-side.
|
||||
|
||||
## 2. Deploy the schema + function
|
||||
|
||||
```sh
|
||||
supabase link --project-ref <your-project-ref>
|
||||
supabase db push # applies supabase/migrations/0001_waitlist.sql
|
||||
supabase functions deploy waitlist-join # deploys supabase/functions/waitlist-join
|
||||
```
|
||||
|
||||
## 3. Configure function secrets
|
||||
|
||||
`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected automatically.
|
||||
Email confirmation is optional — set these to enable it (reuses Resend):
|
||||
|
||||
```sh
|
||||
supabase secrets set RESEND_API_KEY=<your-resend-key>
|
||||
supabase secrets set WAITLIST_FROM_EMAIL="Vestige <waitlist@yourdomain>"
|
||||
# Optional: lock CORS to your sites (comma-separated). Omit for "*".
|
||||
supabase secrets set WAITLIST_ALLOWED_ORIGIN="https://samvallad33.github.io,https://vestige.dev"
|
||||
```
|
||||
|
||||
## 4. Point the form at the function
|
||||
|
||||
The function URL looks like:
|
||||
`https://<project-ref>.functions.supabase.co/waitlist-join`
|
||||
|
||||
Set it at build time for the dashboard:
|
||||
|
||||
```sh
|
||||
# apps/dashboard/.env (or your Pages build env)
|
||||
VITE_WAITLIST_ENDPOINT=https://<project-ref>.functions.supabase.co/waitlist-join
|
||||
```
|
||||
|
||||
Rebuild the dashboard (`pnpm --filter @vestige/dashboard build`) and redeploy.
|
||||
|
||||
## 5. Verify
|
||||
|
||||
```sh
|
||||
curl -X POST https://<project-ref>.functions.supabase.co/waitlist-join \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"email":"you@example.com","name":"You","plan":"solo","priority":"sync"}'
|
||||
# -> {"ok":true,"deduped":false}
|
||||
|
||||
# Re-run the same command:
|
||||
# -> {"ok":true,"deduped":true} (idempotent; no duplicate row)
|
||||
```
|
||||
|
||||
## 6. Export the list on launch day
|
||||
|
||||
In the Supabase SQL editor:
|
||||
|
||||
```sql
|
||||
select email, name, plan, priority, notes, created_at
|
||||
from public.waitlist
|
||||
order by created_at desc;
|
||||
```
|
||||
|
||||
Or download a CSV from the Table Editor. That list is the asset that converts
|
||||
to paid over the weeks after the spike.
|
||||
|
||||
## Notes
|
||||
|
||||
- **RLS is on, anon can do nothing.** Only the service-role key inside the
|
||||
function can insert. The public anon key (if ever used) has zero table access.
|
||||
- **Honeypot + email validation** run both client-side (in the form) and
|
||||
server-side (in the function).
|
||||
- **Dedup** is enforced by a unique index on the lowercased email; a re-submit
|
||||
returns `{ok:true,deduped:true}` instead of erroring or duplicating.
|
||||
Loading…
Add table
Add a link
Reference in a new issue