mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-26 23:51:02 +02:00
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>
60 lines
No EOL
19 KiB
JSON
60 lines
No EOL
19 KiB
JSON
{
|
|
"summary": "Research how to render glowing neural-tendril/dendrite text with synapse nodes + energy burst on the web, return implementable techniques",
|
|
"agentCount": 5,
|
|
"logs": [],
|
|
"result": {
|
|
"spec": {
|
|
"recommendedApproach": "A baked-SVG hybrid, scoped to a single component (`NeuralSign.svelte`) that sits FIXED at the top of the waitlist page above the stats/proof row. Three layers, all driven by ONE pre-generated asset built at compile time:\n\nLAYER 0 (build-time, run once in Node): Grow real dendrites with space-colonization seeded from the rasterized \"VESTIGE\" glyph mask. Sample the glyph alpha to get inside-pixels as attractors + a sparse outside halo for tendrils that burst past the letters; seed roots at the bottom of each stroke; thicken veins by descendant count; drop a synapse <circle> at every junction and tip. Export ONE static `vestige-neural.svg` (branch <path>s + synapse <circle>s, grouped per-letter, with per-branch `--depth` and per-node `--i` custom props baked in) plus a JSON of streak anchor points. This is the ONLY technique that produces true branch topology + synapse nodes instead of faking neuron-ness with blur — and it costs ZERO at runtime because the text is fixed.\n\nLAYER 1 (glow, compositor-only): A stacked feGaussianBlur → feColorMatrix → feMerge SVG <filter> recolored per blur tier (inner green, mid cyan, outer violet) gives the multi-color bloom + hot-white synapse halos for free.\n\nLAYER 2 (life, cheap per-frame): (a) SVG stroke-dashoffset \"draw-on\" staggered by `--depth` so dendrites grow outward from the letter stems on load; (b) CSS @keyframes pulse on synapse <circle>s staggered by `--i` for organic firing; (c) a CSS mask-image gradient SWEEP over a baked radial energy-burst layer behind the letters for streaks; (d) a single gentle feTurbulence+feDisplacementMap \"breathing\" warp gated to desktop + prefers-reduced-motion:no-preference.\n\nOPTIONAL TOP TIER (desktop WebGL2 only, lazy-loaded): a tiny dedicated additive Three.js GPUComputationRenderer particle canvas the size of the sign, homes packed from the baked branch points, curl-noise + spring-to-home, UnrealBloom. Feature-gated behind navigator + canvas size; the SVG baseline IS the product and ships first.\n\nReject the full-GPGPU-as-baseline and the AI-PNG-as-primary routes: GPGPU fails the mobile/graceful-degradation constraint as a baseline, and an AI PNG can't be regenerated deterministically for \"JULY 14TH\"/\"SIGN UP NOW\" or animated per-branch. The baked-SVG core wins both fidelity (real dendrites + synapses) AND clean shipping (one static asset, base-path-safe <img>/inline, 60fps on any phone).",
|
|
"whyThisWins": "It is the only approach that hits reference fidelity (genuine branching dendrites + bright synapse nodes, not blurred neon) while satisfying every hard constraint at once: static GitHub Pages (pure build-time asset, no server), mobile-readable (SVG is resolution-independent and razor-sharp at any DPI), performant (per-frame cost is a handful of compositor ops — dashoffset, opacity, mask-position — with the expensive organic growth paid once offline), and graceful degradation built in by construction (static SVG → +CSS pulses → +turbulence breathing → +optional WebGL particles, each layer additive and independently droppable). It also slots cleanly into THIS codebase: the page already uses `base` from `$app/paths` for subpath assets, already has a fixed full-screen background canvas pattern, and Three.js is already a dependency for the graph cinema, so the optional top tier reuses existing infra. One self-contained component, one baked asset, zero new runtime dependencies for the baseline.",
|
|
"buildSteps": [
|
|
"1. Build-time generator: add `apps/dashboard/scripts/gen-neural-sign.mjs`. Use node-canvas (or @napi-rs/canvas) to render each line ('VESTIGE', 'JULY 14TH', 'SIGN UP NOW') in a heavy font (Inter 900 / Orbitron) to an offscreen canvas; getImageData; collect alpha>128 pixels as attractors (step 3-4px) + a sparse ring of attractors just outside the outline for outward tendrils.",
|
|
"2. Run space colonization (Jason Webb's algorithm) per line: KDBush for nearest-node queries, ATTRACT_DIST~24, KILL_DIST~6, SEG~4 in glyph-pixel space; seed roots at lowest inside-pixel of each stroke; iterate until attractors exhausted. Point-in-polygon clamp against the glyph outline so growth fills letters; allow the outside-ring attractors to pull a few tendrils past the edges.",
|
|
"3. Post-process the branch graph: walk tip→root accumulating descendant count → per-edge stroke-width (thick trunks, thin tips). Mark synapse nodes = junctions (degree>=3) + tips. Compute per-branch depth-from-root.",
|
|
"4. Emit ONE `static/vestige-neural.svg`: a <g> per letter; each branch as <path d> with `stroke-width`, `stroke` cycling green #39ff9d / cyan #22d3ee / violet #b388ff, and inline `style=\"--depth:N\"`; each synapse as <circle r> with `style=\"--i:K\"`. Inline the bloom + turbulence <filter> defs. Also export `vestige-streak-anchors.json` for the burst layer. Commit both to static/.",
|
|
"5. Pre-bake a radial energy-burst PNG (`static/energy-burst.png`) for the streaks-behind layer (transparent, additive-friendly) — either offline Canvas2D additive wedges or one good AI render; it's purely decorative backing.",
|
|
"6. Create `src/lib/components/NeuralSign.svelte`. Inline the SVG (import as raw string via `?raw` so CSS custom props/filters animate) OR <img src={`${base}/vestige-neural.svg`} /> if you don't need per-element animation — inline is required for dashoffset/pulse, so inline it. Wrap in a fixed container above the proof-row.",
|
|
"7. Add the CSS life layer in the component: stroke-dashoffset draw-on keyed off `--depth`, synapse pulse keyed off `--i`, mask-sweep on the .streaks layer, and a `@media (prefers-reduced-motion: reduce)` block that freezes everything to a static glowing state.",
|
|
"8. Gate the breathing turbulence + (optional) WebGL particle canvas behind a desktop/capability check; lazy-import Three.js only when mounting the top tier so mobile never downloads it. Pause all rAF when the sign scrolls out of view (IntersectionObserver).",
|
|
"9. Place <NeuralSign /> as a fixed element at top of `src/routes/waitlist/+page.svelte` above the stats/proof row; ensure z-index sits above the existing memory-field canvas but below interactive nav. Verify with `pnpm --filter @vestige/dashboard build` and a real device/Chrome mobile emulation that it's readable and 60fps.",
|
|
"10. Add the three lines as data so the generator can re-run if copy changes: re-running the script is the ONLY step needed to update the artwork — no runtime change."
|
|
],
|
|
"keyCode": "// === BUILD TIME: scripts/gen-neural-sign.mjs (space colonization, the core) ===\nimport { createCanvas } from '@napi-rs/canvas';\nimport KDBush from 'kdbush';\nconst SEG = 4, ATTRACT = 24, KILL = 6;\nfunction growLine(text, fontPx) {\n const W = text.length * fontPx * 0.75, H = fontPx * 1.4;\n const cv = createCanvas(W, H), ctx = cv.getContext('2d');\n ctx.font = `900 ${fontPx}px Inter`; ctx.fillStyle = '#fff';\n ctx.fillText(text, 0, fontPx);\n const px = ctx.getImageData(0, 0, W, H).data;\n const inside = [], outline = [];\n for (let y = 0; y < H; y += 3) for (let x = 0; x < W; x += 3) {\n const a = px[(y * W + x) * 4 + 3] > 128;\n if (a) { inside.push({ x, y });\n // outline test: any 4-neighbor empty\n const nb = i => px[i * 4 + 3] <= 128;\n if (nb((y*W)+x+1)||nb((y*W)+x-1)||nb(((y+1)*W)+x)||nb(((y-1)*W)+x)) outline.push({x,y}); }\n }\n // attractors = inside pixels + sparse halo just outside (outward tendrils)\n let attractors = inside.slice();\n for (const o of outline) if (Math.random() < 0.15)\n attractors.push({ x: o.x + (Math.random()-0.5)*30, y: o.y + (Math.random()-0.5)*30 });\n // roots: lowest inside-pixel per x-band -> letter stems\n const nodes = [];\n const bands = {};\n for (const p of inside) { const b = (p.x/20)|0; if (!bands[b] || p.y > bands[b].y) bands[b] = p; }\n for (const k in bands) nodes.push({ x: bands[k].x, y: bands[k].y, parent: -1, w: 1 });\n // grow\n for (let it = 0; it < 400 && attractors.length; it++) {\n const idx = new KDBush(nodes.length); for (const n of nodes) idx.add(n.x, n.y); idx.finish();\n const inf = nodes.map(() => []);\n for (const a of attractors) {\n let best = -1, bd = ATTRACT * ATTRACT;\n for (const id of idx.within(a.x, a.y, ATTRACT)) {\n const d = (nodes[id].x-a.x)**2 + (nodes[id].y-a.y)**2; if (d < bd) { bd = d; best = id; } }\n if (best >= 0) inf[best].push(a);\n }\n const fresh = [];\n nodes.forEach((n, i) => {\n if (!inf[i].length) return;\n let dx = 0, dy = 0; for (const a of inf[i]) { dx += a.x-n.x; dy += a.y-n.y; }\n const L = Math.hypot(dx, dy) || 1;\n fresh.push({ x: n.x + SEG*dx/L + (Math.random()-0.5),\n y: n.y + SEG*dy/L + (Math.random()-0.5), parent: i, w: 1 });\n });\n const before = nodes.length; nodes.push(...fresh);\n attractors = attractors.filter(a => !nodes.some((n, i) => i >= before-200 &&\n (n.x-a.x)**2 + (n.y-a.y)**2 < KILL*KILL));\n }\n // thickness via descendant count\n for (let i = nodes.length-1; i > 0; i--) if (nodes[i].parent >= 0)\n nodes[nodes[i].parent].w += nodes[i].w * 0.06;\n return { nodes };\n}\n// emit SVG: per-branch <path> with --depth, synapse <circle> with --i (omitted for brevity)\n\n/* === RUNTIME: NeuralSign.svelte CSS — the cheap \"alive\" layer === */\n/*\n.dendrite { stroke-dasharray: var(--len); stroke-dashoffset: var(--len);\n animation: draw 1.6s ease-out forwards; animation-delay: calc(var(--depth) * 60ms); }\n@keyframes draw { to { stroke-dashoffset: 0; } }\n.synapse { transform-origin: center; animation: fire 2.4s ease-in-out infinite;\n animation-delay: calc(var(--i) * -0.137s); }\n@keyframes fire { 0%,100%{ r:1.5; opacity:.5 } 50%{ r:3; opacity:1 } }\n.streaks { background: url(/vestige/energy-burst.png) center/cover; mix-blend-mode: screen;\n -webkit-mask-image: linear-gradient(120deg,transparent 0 30%,#000 50%,transparent 70%);\n mask-size: 300% 100%; animation: sweep 6s linear infinite; }\n@keyframes sweep { to { mask-position: -200% 0 } }\n@media (prefers-reduced-motion: reduce) {\n .dendrite { stroke-dashoffset: 0; animation: none }\n .synapse, .streaks { animation: none }\n}\n*/\n\n/* === GLOW + BREATHING FILTER (inline in the baked SVG <defs>) === */\n/*\n<filter id=\"bloom\" x=\"-60%\" y=\"-60%\" width=\"220%\" height=\"220%\">\n <feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"3\" result=\"b1\"/>\n <feColorMatrix in=\"b1\" values=\"0 0 0 0 .22 0 0 0 0 1 0 0 0 0 .62 0 0 0 1 0\" result=\"g1\"/>\n <feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"9\" result=\"b2\"/>\n <feColorMatrix in=\"b2\" values=\"0 0 0 0 .13 0 0 0 0 .82 0 0 0 0 1 0 0 0 .8 0\" result=\"g2\"/>\n <feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"18\" result=\"b3\"/>\n <feColorMatrix in=\"b3\" values=\"0 0 0 0 .70 0 0 0 0 .42 0 0 0 0 1 0 0 0 .6 0\" result=\"g3\"/>\n <feMerge><feMergeNode in=\"g3\"/><feMergeNode in=\"g2\"/><feMergeNode in=\"g1\"/>\n <feMergeNode in=\"SourceGraphic\"/></feMerge>\n</filter>\n<filter id=\"breathe\"> // desktop-only, applied before #bloom\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.018 0.03\" numOctaves=\"3\" seed=\"11\" result=\"w\">\n <animate attributeName=\"baseFrequency\" values=\"0.018 0.03;0.022 0.038;0.018 0.03\"\n dur=\"9s\" repeatCount=\"indefinite\"/>\n </feTurbulence>\n <feDisplacementMap in=\"SourceGraphic\" in2=\"w\" scale=\"14\" xChannelSelector=\"R\" yChannelSelector=\"G\"/>\n</filter>\n*/",
|
|
"palette": "Near-black base #05060a (matches existing waitlist memory-field). Tendril stroke colors cycle: green #39ff9d, cyan #22d3ee, violet #b388ff. Bloom tiers recolored via feColorMatrix: inner green (.22/1/.62), mid cyan (.13/.82/1), outer violet (.70/.42/1), hot-white cores via additive overlap. Synapse nodes white-cyan core (#cffff0) bleeding to cyan halo. Energy-burst backing: cyan-green core → violet mid → green-transparent tips. Keep the bg pure dark so mix-blend-mode:screen makes only the luminous strokes/streaks read.",
|
|
"animationDetails": "On mount: dendrites DRAW ON via stroke-dashoffset animating len→0, staggered by `--depth * 60ms` so branches grow outward from each letter's stem to its tips (radiating growth, ~1.6s total). Continuously: synapse <circle>s PULSE (r 1.5↔3, opacity .5↔1) on a 2.4s loop with per-node `--i * -0.137s` negative delay for organic, non-synchronized firing. A narrow bright band SWEEPS across the energy-burst backing layer every 6s via animated mask-position, reading as light pulsing outward behind the letters (mix-blend-mode:screen). On desktop only, a gentle feTurbulence baseFrequency animation (0.018↔0.022 over 9s, scale~14) makes the whole tendril mass BREATHE/writhe like living tissue. Optional WebGL tier: curl-noise particles crawl ALONG the strands and a second emitter shoots streaks outward from outline anchors then springs back. All animation freezes to a static glowing state under prefers-reduced-motion, and rAF/SMIL pauses when the sign scrolls out of the viewport.",
|
|
"mobilePerfNotes": [
|
|
"Baseline is pure SVG + CSS compositor props (dashoffset, opacity, mask-position) — 60fps on low-end phones with zero WebGL; the only one-time cost is initial bloom-blur rasterization.",
|
|
"Do NOT ship feTurbulence/feDisplacementMap on mobile: per-frame Perlin + displacement re-raster is the cost center. Gate it behind a desktop check AND prefers-reduced-motion:no-preference; mobile gets a static seed (no <animate>).",
|
|
"Never download Three.js on mobile: lazy dynamic-import the optional WebGL particle tier only after a capability + viewport-width check passes, so the mobile bundle stays SVG-only.",
|
|
"Cap the synapse count animated on mobile (animate only junction/tip nodes, ~40-80 max) — thousands of simultaneously-animating <circle>s thrash the compositor.",
|
|
"Use IntersectionObserver to pause CSS animations + any rAF when the fixed sign scrolls out of view; the page is long, so this saves battery once the user scrolls past the hero.",
|
|
"Pre-bake the energy-burst as a single PNG so the streak layer is one GPU-composited texture (background-position/mask animation), not per-frame trig.",
|
|
"Inline the SVG (not <img>) so animations work, but keep total path count reasonable (~200-600 branch segments per line) — decimate branches in the generator with a min-length prune so the DOM stays light.",
|
|
"Reference all static assets via `base` from $app/paths (e.g. `${base}/energy-burst.png`) so they resolve under the GitHub Pages /vestige subpath."
|
|
],
|
|
"fallback": "If space-colonization tuning eats too much time before the July 14 launch, ship the SVG glow+life layer on the GLYPH OUTLINE itself: render VESTIGE as a single-stroke vector path (opentype.js getPath / text outline), apply the SAME bloom + breathe filters and the SAME dashoffset draw-on + mask sweep, and scatter a hand-placed set of synapse <circle>s along the strokes. This drops the true branch topology but keeps the alive/glowing/synapse/energy-streak reference look with an hour of work instead of a day, and is a drop-in upgrade path (swap the outline <path>s for the grown dendrite <path>s later, same component, same CSS). Ultimate fallback: a static glowing PNG <img> behind prefers-reduced-motion / no-JS, so the sign always renders something on-brand."
|
|
},
|
|
"sources": [
|
|
"https://tympanus.net/codrops/2022/11/08/3d-typing-effects-with-three-js/",
|
|
"https://medium.com/@jason.webb/space-colonization-algorithm-in-javascript-6f683b743dc5",
|
|
"https://github.com/jasonwebb/2d-diffusion-limited-aggregation-experiments/blob/master/README.md",
|
|
"https://css-tricks.com/svg-line-animation-works/",
|
|
"https://tympanus.net/codrops/2025/12/10/simulating-life-in-the-browser-creating-a-living-particle-system-for-the-untillabs-website/",
|
|
"https://code.tutsplus.com/how-to-generate-shockingly-good-2d-lightning-effects--gamedev-2681t",
|
|
"https://github.com/mapbox/tiny-sdf (also SDF text rationale: https://github.com/Chlumsky/msdfgen)",
|
|
"https://inspirnathan.com/posts/65-glow-shader-in-shadertoy/ (glow=0.01/d, clamp, additive) + sdSegment: https://iquilezles.org/articles/distfunctions2d/",
|
|
"https://www.shadertoy.com/view/ldjXDD (FBM Lightning Bolt) + https://godotshaders.com/shader/2d-lightning-electric-arc-plasma/",
|
|
"GLSL flow (fract/mix): https://frontend.horse/articles/making-dynamic-animations-with-shaders/ • SVG chunky-gradient: https://gist.github.com/shshaw/e87dbdfbe9c327b5bfdca2319f709172",
|
|
"https://github.com/aadebdeb/study-three.js/blob/master/gpgpu-particles-with-curl-noise.html • https://al-ro.github.io/projects/embers/ (curl noise = Bridson 2007, divergence-free)",
|
|
"Selective bloom: https://waelyasmina.net/articles/unreal-bloom-selective-threejs-post-processing/ + https://threejs.org/examples/webgl_postprocessing_unreal_bloom_selective.html • SVG multi-blur+flicker: https://dev.to/ninjasoards/make-a-flickering-neon-svg-animation-from-scratch-w-illustrator-react-emotion-39gm",
|
|
"https://blakecrosley.com/blog/glsl-shader-lab (atan/length polar + sin(N*angle) spokes + exp(-k*radius) glow); corona/ray references https://www.shadertoy.com/view/4Xt3Rs and https://www.shadertoy.com/view/lljGDt",
|
|
"https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-13-volumetric-light-scattering-post-process and https://github.com/Erkaman/glsl-godrays",
|
|
"https://tympanus.net/codrops/2024/12/19/crafting-a-dreamy-particle-effect-with-three-js-and-gpgpu/ and https://github.com/NewKrok/three-particles (trail/ribbon renderer + WebGPU compute, 50k-350k particles)",
|
|
"https://codepen.io/Nasdav/pen/LjjbLE (rotating spokes via transforms + globalCompositeOperation) and https://www.cssscript.com/canvas-particle-animation/ (trail/motion-streak params)",
|
|
"https://medium.com/@jason.webb/space-colonization-algorithm-in-javascript-6f683b743dc5 (full JS algorithm) and https://github.com/jasonwebb/2d-space-colonization-experiments (canvas impl + SVG export via 'e' key, vein thickening + opacity blending)",
|
|
"https://9elements.com/blog/creating-an-animated-svg-neon-light-effect/ (exact feOffset->feGaussianBlur->feColorMatrix->feMerge neon filter with per-tier color matrices + step-end flicker keyframes) and https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/feColorMatrix",
|
|
"https://tympanus.net/codrops/2019/02/19/svg-filter-effects-creating-texture-with-feturbulence/ (feTurbulence+feDisplacementMap distortion, GSAP baseFrequency animation, fractalNoise vs turbulence, baseFrequency 0.02-0.2 guidance) and https://codepen.io/BeardedBear/pen/abvEKpz (live animated feTurbulence playground)",
|
|
"https://www.sabatino.dev/recreating-the-ios-16-shimmer-effect/ (masked-image shimmer, mask-size 300% + animated mask-position) and https://www.smashingmagazine.com/2024/01/css-blurry-shimmer-effect/ (compositor-thread performance of gradient/position-animated shimmer)",
|
|
"https://transparify.app/blog/midjourney-transparent-background (white-bg vs black-bg alpha recovery preserving neon glow + soft edges; standard premultiplied-alpha algebra) and https://geekycuriosity.substack.com/p/skip-the-background-remover-ideogram (Ideogram native transparent text generation, handles letterforms well)"
|
|
]
|
|
}
|
|
} |