2026-05-27 12:05:28 +02:00
|
|
|
/**
|
|
|
|
|
* GLSL shaders for the particle-field hero.
|
|
|
|
|
*
|
|
|
|
|
* Shaders are exported as tagged-template strings with a leading
|
|
|
|
|
* `/* glsl *\/` comment marker so future syntax highlighters or
|
|
|
|
|
* static analysers can pick them up without us adding a webpack loader.
|
|
|
|
|
*
|
|
|
|
|
* Conventions:
|
|
|
|
|
* - All positions live in clip-space-like coordinates: x, y ∈ [-1, +1].
|
|
|
|
|
* - Position texture is RGBA32F:
|
|
|
|
|
* r = x
|
|
|
|
|
* g = y
|
|
|
|
|
* b = scale (per-particle render size jitter)
|
|
|
|
|
* a = velocity magnitude (used for color tint)
|
|
|
|
|
* - Simulation runs in a fullscreen quad pass — each fragment = one particle.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const simplexNoise = /* glsl */ `
|
|
|
|
|
// 2D simplex noise by Ian McEwan / Ashima Arts — public domain.
|
|
|
|
|
// Used both for idle drift in the sim and for organic distortion of
|
|
|
|
|
// the cursor-tracking ring.
|
|
|
|
|
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
|
|
|
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
|
|
|
vec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }
|
|
|
|
|
|
|
|
|
|
float snoise(vec2 v) {
|
|
|
|
|
const vec4 C = vec4(
|
|
|
|
|
0.211324865405187,
|
|
|
|
|
0.366025403784439,
|
|
|
|
|
-0.577350269189626,
|
|
|
|
|
0.024390243902439
|
|
|
|
|
);
|
|
|
|
|
vec2 i = floor(v + dot(v, C.yy));
|
|
|
|
|
vec2 x0 = v - i + dot(i, C.xx);
|
|
|
|
|
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
|
|
|
vec4 x12 = x0.xyxy + C.xxzz;
|
|
|
|
|
x12.xy -= i1;
|
|
|
|
|
i = mod289(i);
|
|
|
|
|
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0))
|
|
|
|
|
+ i.x + vec3(0.0, i1.x, 1.0));
|
|
|
|
|
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
|
|
|
|
|
m = m * m;
|
|
|
|
|
m = m * m;
|
|
|
|
|
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
|
|
|
vec3 h = abs(x) - 0.5;
|
|
|
|
|
vec3 ox = floor(x + 0.5);
|
|
|
|
|
vec3 a0 = x - ox;
|
|
|
|
|
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
|
|
|
|
|
vec3 g;
|
|
|
|
|
g.x = a0.x * x0.x + h.x * x0.y;
|
|
|
|
|
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
|
|
|
return 130.0 * dot(m, g);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sim vertex shader — trivial fullscreen pass.
|
|
|
|
|
* Writes through clip-space UVs so the fragment shader receives one
|
|
|
|
|
* fragment per particle in the position texture.
|
|
|
|
|
*/
|
|
|
|
|
export const simVertex = /* glsl */ `
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
void main() {
|
|
|
|
|
vUv = uv;
|
|
|
|
|
gl_Position = vec4(position, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sim fragment shader — the actual integrator.
|
|
|
|
|
*
|
|
|
|
|
* Inputs:
|
|
|
|
|
* uPrev — previous-frame position texture (ping-pong source)
|
|
|
|
|
* uTime — elapsed seconds
|
|
|
|
|
* uDelta — clamped frame delta (seconds), guards against tab-switch spikes
|
|
|
|
|
* uRingPos — mouse position in clip space, smoothed
|
|
|
|
|
* uRingRadius— current ring radius (clip-space units)
|
|
|
|
|
* uRingWidth — base ring thickness (clip-space units)
|
|
|
|
|
* uRingActive— 0..1 fade so the ring softly vanishes when the mouse leaves
|
|
|
|
|
* uMotionScale— global multiplier on drift + ring-push velocity. 1.0 is
|
|
|
|
|
* default; the prefers-reduced-motion path passes 0.5 so
|
|
|
|
|
* the field reads as calm without removing interaction.
|
|
|
|
|
* Pointer *position* is not scaled — the ring still
|
|
|
|
|
* tracks the cursor at full fidelity.
|
|
|
|
|
*
|
|
|
|
|
* Per-particle dynamics:
|
|
|
|
|
* 1. Idle drift: rotational simplex-noise velocity field — feels like
|
|
|
|
|
* slow oceanic currents rather than random brownian jitter.
|
|
|
|
|
* 2. Ring push: three overlapping smoothstep bands at slightly offset
|
|
|
|
|
* radii, with the radius input distorted by simplex noise and a
|
|
|
|
|
* polar sin/cos wave. The gradient of the resulting field is
|
|
|
|
|
* applied as an outward push, so particles get gently shoved as
|
|
|
|
|
* the ring sweeps over them.
|
|
|
|
|
* 3. Containment: a very soft spring pulls particles back toward the
|
|
|
|
|
* origin if they drift past the field edge — prevents particles
|
|
|
|
|
* from escaping to infinity on long sessions.
|
|
|
|
|
* 4. Damping: every frame velocity decays so the field returns to a
|
|
|
|
|
* calm steady state when the mouse is idle.
|
|
|
|
|
*/
|
|
|
|
|
export const simFragment = /* glsl */ `
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
|
|
uniform sampler2D uPrev;
|
|
|
|
|
uniform float uTime;
|
|
|
|
|
uniform float uDelta;
|
|
|
|
|
uniform vec2 uRingPos;
|
|
|
|
|
uniform float uRingRadius;
|
|
|
|
|
uniform float uRingWidth;
|
|
|
|
|
uniform float uRingActive;
|
|
|
|
|
uniform float uMotionScale;
|
|
|
|
|
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
${simplexNoise}
|
|
|
|
|
|
|
|
|
|
// Organic ring field — value peaks ON the ring, falls off either side.
|
|
|
|
|
// Three overlapping smoothstep bands with simplex-noise + polar-wave
|
|
|
|
|
// distortion to keep the boundary breathing instead of geometric.
|
|
|
|
|
float ringField(vec2 p) {
|
|
|
|
|
vec2 d = p - uRingPos;
|
|
|
|
|
float r = length(d);
|
|
|
|
|
float ang = atan(d.y, d.x);
|
|
|
|
|
|
feat(web): mobile-fit hero tiles + voluminous calmer particle field + FAQ accordion
Three coordinated polish items requested:
1. **Hero step-rotator tiles fit mobile without horizontal scroll.**
The previous snippets contained a 50+ char `Live at https://notion-x9.mcp.buildmymcpserver.com` URL that overflowed the ~295 px text area on a 375 px viewport. Rewrote all three snippets to be naturally short — same product story, no full URLs. The <pre> drops `overflow-x-auto` and gains `whitespace-pre-wrap break-words` so any token that does exceed the column wraps gracefully instead of forcing a scrollbar.
2. **ParticleHero — more volumetric, slower, steadier at load-in.**
The "stuttery / too fast" feedback came from two issues compounding: tiny dots (1.8 px on 256-tier, with 0.42 base alpha) gave the eye too few pixels to track between frames, so individual particles read as snapping rather than drifting; and the simplex-noise drift evolved at 0.08 time-scale with 0.045 velocity, fast enough that frame-to-frame deltas exceeded a tracked particle's diameter.
Render uniforms tuned:
- `uPointSize` 1.8 → 2.8 (256-tier), 2.4 → 3.6 (128-tier)
- `uBaseAlpha` 0.42 → 0.60
Simulation shader tuned:
- Drift noise time scale 0.08 → 0.045 (the most impactful single change — particles now move at half the previous speed)
- Drift velocity magnitude 0.045 → 0.028
- Ring breathing noise time scale 0.35 → 0.22
- Ring polar-wave time scales 1.2 / 0.7 → 0.7 / 0.42
Net effect: same number of particles (65k) but each individually larger, brighter, and moving more slowly. The cumulative additive bloom is denser without the jitter that read as visual stutter.
3. **FAQ collapsed into a native `<details>` accordion.**
Crawlers and screen readers still see every Q+A in the SSR'd HTML — `<details><summary>...</summary><p>answer</p></details>` is the standard semantic pattern for disclosure widgets. Users see one question at a time and expand on demand, which keeps the page from feeling like an endless wall of marketing text below the fold.
Container narrowed `max-w-6xl` → `max-w-3xl` for accordion typography (long-form prose reads better single-column). The default WebKit disclosure-triangle marker is suppressed with `list-none` + `[&_summary::-webkit-details-marker]:hidden`, and a `lucide-react` `ChevronDown` icon rotates 180° via `group-open:rotate-180` to indicate state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:35:03 +02:00
|
|
|
// Breathing distortion of the radius itself. Time scale tuned
|
|
|
|
|
// down (was 0.35 → 0.22) so the ring "breathes" rather than
|
|
|
|
|
// pulsates, which used to read as visual stutter on slow drift.
|
|
|
|
|
float noise = snoise(p * 4.0 + uTime * 0.22) * 0.05;
|
2026-05-27 12:05:28 +02:00
|
|
|
// Polar wave — a slow rippling around the circumference.
|
feat(web): mobile-fit hero tiles + voluminous calmer particle field + FAQ accordion
Three coordinated polish items requested:
1. **Hero step-rotator tiles fit mobile without horizontal scroll.**
The previous snippets contained a 50+ char `Live at https://notion-x9.mcp.buildmymcpserver.com` URL that overflowed the ~295 px text area on a 375 px viewport. Rewrote all three snippets to be naturally short — same product story, no full URLs. The <pre> drops `overflow-x-auto` and gains `whitespace-pre-wrap break-words` so any token that does exceed the column wraps gracefully instead of forcing a scrollbar.
2. **ParticleHero — more volumetric, slower, steadier at load-in.**
The "stuttery / too fast" feedback came from two issues compounding: tiny dots (1.8 px on 256-tier, with 0.42 base alpha) gave the eye too few pixels to track between frames, so individual particles read as snapping rather than drifting; and the simplex-noise drift evolved at 0.08 time-scale with 0.045 velocity, fast enough that frame-to-frame deltas exceeded a tracked particle's diameter.
Render uniforms tuned:
- `uPointSize` 1.8 → 2.8 (256-tier), 2.4 → 3.6 (128-tier)
- `uBaseAlpha` 0.42 → 0.60
Simulation shader tuned:
- Drift noise time scale 0.08 → 0.045 (the most impactful single change — particles now move at half the previous speed)
- Drift velocity magnitude 0.045 → 0.028
- Ring breathing noise time scale 0.35 → 0.22
- Ring polar-wave time scales 1.2 / 0.7 → 0.7 / 0.42
Net effect: same number of particles (65k) but each individually larger, brighter, and moving more slowly. The cumulative additive bloom is denser without the jitter that read as visual stutter.
3. **FAQ collapsed into a native `<details>` accordion.**
Crawlers and screen readers still see every Q+A in the SSR'd HTML — `<details><summary>...</summary><p>answer</p></details>` is the standard semantic pattern for disclosure widgets. Users see one question at a time and expand on demand, which keeps the page from feeling like an endless wall of marketing text below the fold.
Container narrowed `max-w-6xl` → `max-w-3xl` for accordion typography (long-form prose reads better single-column). The default WebKit disclosure-triangle marker is suppressed with `list-none` + `[&_summary::-webkit-details-marker]:hidden`, and a `lucide-react` `ChevronDown` icon rotates 180° via `group-open:rotate-180` to indicate state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:35:03 +02:00
|
|
|
// Same calming pass on both phases.
|
|
|
|
|
float wave = sin(ang * 5.0 + uTime * 0.7) * 0.012
|
|
|
|
|
+ cos(ang * 3.0 - uTime * 0.42) * 0.010;
|
2026-05-27 12:05:28 +02:00
|
|
|
float rr = r + noise + wave;
|
|
|
|
|
|
|
|
|
|
// Three bands of different thickness at slightly offset radii.
|
|
|
|
|
float w = uRingWidth;
|
|
|
|
|
float b1 = smoothstep(uRingRadius - w * 0.30, uRingRadius, rr)
|
|
|
|
|
* (1.0 - smoothstep(uRingRadius, uRingRadius + w * 0.30, rr));
|
|
|
|
|
float b2 = smoothstep(uRingRadius - w * 0.80, uRingRadius - w * 0.15, rr)
|
|
|
|
|
* (1.0 - smoothstep(uRingRadius - w * 0.15, uRingRadius + w * 0.65, rr));
|
|
|
|
|
float b3 = smoothstep(uRingRadius - w * 1.40, uRingRadius - w * 0.50, rr)
|
|
|
|
|
* (1.0 - smoothstep(uRingRadius - w * 0.50, uRingRadius + w * 1.20, rr));
|
|
|
|
|
|
|
|
|
|
return (b1 * 1.0 + b2 * 0.55 + b3 * 0.30) * uRingActive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec4 prev = texture2D(uPrev, vUv);
|
|
|
|
|
vec2 pos = prev.xy;
|
|
|
|
|
float scale = prev.z;
|
|
|
|
|
float velPrev = prev.w;
|
|
|
|
|
|
|
|
|
|
// --- Idle drift: rotational simplex-noise current ---
|
|
|
|
|
// Time is scaled by uMotionScale so reduced-motion users get a
|
|
|
|
|
// calmer field that evolves at half speed.
|
feat(web): mobile-fit hero tiles + voluminous calmer particle field + FAQ accordion
Three coordinated polish items requested:
1. **Hero step-rotator tiles fit mobile without horizontal scroll.**
The previous snippets contained a 50+ char `Live at https://notion-x9.mcp.buildmymcpserver.com` URL that overflowed the ~295 px text area on a 375 px viewport. Rewrote all three snippets to be naturally short — same product story, no full URLs. The <pre> drops `overflow-x-auto` and gains `whitespace-pre-wrap break-words` so any token that does exceed the column wraps gracefully instead of forcing a scrollbar.
2. **ParticleHero — more volumetric, slower, steadier at load-in.**
The "stuttery / too fast" feedback came from two issues compounding: tiny dots (1.8 px on 256-tier, with 0.42 base alpha) gave the eye too few pixels to track between frames, so individual particles read as snapping rather than drifting; and the simplex-noise drift evolved at 0.08 time-scale with 0.045 velocity, fast enough that frame-to-frame deltas exceeded a tracked particle's diameter.
Render uniforms tuned:
- `uPointSize` 1.8 → 2.8 (256-tier), 2.4 → 3.6 (128-tier)
- `uBaseAlpha` 0.42 → 0.60
Simulation shader tuned:
- Drift noise time scale 0.08 → 0.045 (the most impactful single change — particles now move at half the previous speed)
- Drift velocity magnitude 0.045 → 0.028
- Ring breathing noise time scale 0.35 → 0.22
- Ring polar-wave time scales 1.2 / 0.7 → 0.7 / 0.42
Net effect: same number of particles (65k) but each individually larger, brighter, and moving more slowly. The cumulative additive bloom is denser without the jitter that read as visual stutter.
3. **FAQ collapsed into a native `<details>` accordion.**
Crawlers and screen readers still see every Q+A in the SSR'd HTML — `<details><summary>...</summary><p>answer</p></details>` is the standard semantic pattern for disclosure widgets. Users see one question at a time and expand on demand, which keeps the page from feeling like an endless wall of marketing text below the fold.
Container narrowed `max-w-6xl` → `max-w-3xl` for accordion typography (long-form prose reads better single-column). The default WebKit disclosure-triangle marker is suppressed with `list-none` + `[&_summary::-webkit-details-marker]:hidden`, and a `lucide-react` `ChevronDown` icon rotates 180° via `group-open:rotate-180` to indicate state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:35:03 +02:00
|
|
|
// Noise-evolution speed dropped from 0.08 → 0.045 (almost halved)
|
|
|
|
|
// and velocity magnitude from 0.045 → 0.028. The combination kills
|
|
|
|
|
// the perceived stutter — each particle now moves slowly enough
|
|
|
|
|
// that the eye tracks individual motion as smooth drift rather
|
|
|
|
|
// than as jerky per-frame teleportation.
|
2026-05-27 12:05:28 +02:00
|
|
|
float driftTime = uTime * uMotionScale;
|
feat(web): mobile-fit hero tiles + voluminous calmer particle field + FAQ accordion
Three coordinated polish items requested:
1. **Hero step-rotator tiles fit mobile without horizontal scroll.**
The previous snippets contained a 50+ char `Live at https://notion-x9.mcp.buildmymcpserver.com` URL that overflowed the ~295 px text area on a 375 px viewport. Rewrote all three snippets to be naturally short — same product story, no full URLs. The <pre> drops `overflow-x-auto` and gains `whitespace-pre-wrap break-words` so any token that does exceed the column wraps gracefully instead of forcing a scrollbar.
2. **ParticleHero — more volumetric, slower, steadier at load-in.**
The "stuttery / too fast" feedback came from two issues compounding: tiny dots (1.8 px on 256-tier, with 0.42 base alpha) gave the eye too few pixels to track between frames, so individual particles read as snapping rather than drifting; and the simplex-noise drift evolved at 0.08 time-scale with 0.045 velocity, fast enough that frame-to-frame deltas exceeded a tracked particle's diameter.
Render uniforms tuned:
- `uPointSize` 1.8 → 2.8 (256-tier), 2.4 → 3.6 (128-tier)
- `uBaseAlpha` 0.42 → 0.60
Simulation shader tuned:
- Drift noise time scale 0.08 → 0.045 (the most impactful single change — particles now move at half the previous speed)
- Drift velocity magnitude 0.045 → 0.028
- Ring breathing noise time scale 0.35 → 0.22
- Ring polar-wave time scales 1.2 / 0.7 → 0.7 / 0.42
Net effect: same number of particles (65k) but each individually larger, brighter, and moving more slowly. The cumulative additive bloom is denser without the jitter that read as visual stutter.
3. **FAQ collapsed into a native `<details>` accordion.**
Crawlers and screen readers still see every Q+A in the SSR'd HTML — `<details><summary>...</summary><p>answer</p></details>` is the standard semantic pattern for disclosure widgets. Users see one question at a time and expand on demand, which keeps the page from feeling like an endless wall of marketing text below the fold.
Container narrowed `max-w-6xl` → `max-w-3xl` for accordion typography (long-form prose reads better single-column). The default WebKit disclosure-triangle marker is suppressed with `list-none` + `[&_summary::-webkit-details-marker]:hidden`, and a `lucide-react` `ChevronDown` icon rotates 180° via `group-open:rotate-180` to indicate state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:35:03 +02:00
|
|
|
float n1 = snoise(pos * 1.6 + vec2(driftTime * 0.045, 0.0));
|
|
|
|
|
float n2 = snoise(pos * 1.6 + vec2(0.0, driftTime * 0.045) + 53.7);
|
|
|
|
|
vec2 driftVel = vec2(-n2, n1) * 0.028 * uMotionScale; // curl-like rotation
|
2026-05-27 12:05:28 +02:00
|
|
|
|
feat(video): v10 hero video with mute toggle — voice + bg music
Ships the long-form (71.5 s) hero video to the marketing /flow section
along with the iteration trail of architectural visual fixes the owner
worked through over the last sprint.
## Video composition (remotion/)
Eight phases driven by the 71.47 s voice-over in `audio.mp3` plus the
`Sub-bass Lullaby.wav` background music (ducked to 0.16 with fade in /
fade out). Every scene was rebuilt for v10 with concrete fixes:
- **HookScene** (12 s) — adds FloatingChaos overlay: a docker-compose
excerpt, an oauth_callback.ts snippet, an .env file with a yellow
squiggle warning ("in git history since v0.3.1"), and a live-ticking
502 retry toast. Tangle now reads as a developer's desktop right
before they give up, not as four icons drifting.
- **PromptScene** (12.2 s) — 6.5 s post-typing dead-zone replaced with
the parse beat: three sequential highlights on the prompt text
(MCP server / searches / Notion workspace), three chips below the
input (intent / tool / secret → vault), three-stat summary panel
(tools · 2, secrets · 1, targets · 3). At local frame 250 (≈ 21 s
global, on the voice line "the prompt path and the secret path
never cross") a mini two-rail diagram with an explicit X-marker
ring lands, visualising the architectural promise the moment it's
spoken.
- **SecretsScene** (15.2 s) — kept the arrow-fork + AES-256 stamp +
env-var injection beats; added the lock-snap flash at frame 66,
pinned the vault at full opacity throughout, and added a dashed
vault → container connector so the secret's provenance is visible.
The "what the AI sees" panel is now 680 px wide with an eye icon,
four corner viewfinder brackets around the prompt text, and three
explicit denied lines (no secrets / no environment variables / no
tokens).
- **BuildScene** (7.2 s) — unchanged beats: streaming log, server
card emerges with code + 🔒 NOTION_API_KEY slot pills, isolated-
container caption, <60s countdown.
- **IsolationScene** (14 s) — completely restructured. Orbit-and-dock
chips that collided with the card and with the tokens-only badge
are replaced by a clean vertical chip column at x=760: read-only
filesystem · dropped capabilities · no new privileges · 512 MB
memory cap · 0.5 CPU limit · ✓ your token only (last in green).
A vault graphic now sits below the server card with a dashed arrow
up into its env slot so the architecture story is complete in one
frame. PKCE jargon removed: "OAuth 2.1 · PKCE" → "only your token
gets in" with a small "oauth 2.1 · proof-key flow" subtitle for
the curious. Handshake stages simplified to your client → verified
→ scoped token. Final settlement arrow in success-green curves
from the scoped-token pill back into the card.
- **LibraryScene** (7 s) — cards enlarged from 340×180 to 400×220
with 36 px gaps. The "templates carry code, not credentials"
sub-caption was pulled (felt on-the-nose; the detached lock and
empty NOTION_API_KEY=? slot carry the story visually).
- **DiscoveryScene** (3 s) — the most-iterated scene. Earlier
versions had a fake "1,200+ developers building" fork counter
(pulled — solo-founder, hadn't earned). Replaced with a two-lane
architecture diagram that visualises "no paths cross" literally:
top lane prompt → AI → code, bottom lane vault → encrypted →
env, both converging at the server box on the right. v10
refinements: all seven boxes visible from frame 0 (no late
server arrival), a parallel glow tour walks across both lanes
simultaneously, a dashed vertical divider with a "no shared
node" chip pinned in the middle, and the closing line "One
sentence in. Live server out." slides down from above and lands
centred while the diagram fades to 0.12 opacity behind it —
no overlap.
- **LogoLockup** (1.7 s) — wordmark + fade-to-black for a clean
loop seam.
The Subtitle / CAPTIONS layer added in v7 was pulled wholesale —
owner found the kinetic-typography overlay aggressive and noted
that technical terms (PKCE etc.) created friction with no payoff.
Scene visuals and voice now carry the whole story; the Subtitle
component file is retained for possible future use.
Render pipeline (`render:mp4` / `render:webm` / `render:poster` in
remotion/package.json) is unchanged. The MP4 is post-processed to
H.264 Main / yuv420p / TV-range with faststart + AAC audio. The
WebM is re-encoded at VP9 CRF 38 / Opus 64k to stay under the 3 MB
budget. Final artefacts in apps/web/public/videos/: 2.59 MB mp4,
2.99 MB webm, 62 KB poster.
## Web integration (apps/web/components/hero-video.tsx)
New client component wraps the <video> element and pins a frosted-
glass mute toggle bottom-right of the player. Why not native
`controls`: the browser chrome fights the section's design vocabulary
and we only need one affordance — unmute — so we render exactly
that. The toggle's icon flips between VolumeX (currently muted) and
Volume2 (currently unmuted), accent colour switches indigo when sound
is on. Initial state is muted so autoplay still fires; on unmute we
call .play() defensively because mobile Safari pauses on
muted-property changes mid-playback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:31:10 +02:00
|
|
|
// --- Ring push: gradient of the ring field, pointing outward ---
|
|
|
|
|
float h = 0.003;
|
|
|
|
|
float fx0 = ringField(pos - vec2(h, 0.0));
|
|
|
|
|
float fx1 = ringField(pos + vec2(h, 0.0));
|
|
|
|
|
float fy0 = ringField(pos - vec2(0.0, h));
|
|
|
|
|
float fy1 = ringField(pos + vec2(0.0, h));
|
|
|
|
|
vec2 grad = vec2(fx1 - fx0, fy1 - fy0) / (2.0 * h);
|
|
|
|
|
float fieldHere = ringField(pos);
|
|
|
|
|
// Push along gradient — particles get nudged away from the ring crest.
|
|
|
|
|
// Magnitude is scaled by uMotionScale so reduced-motion users get a
|
|
|
|
|
// softer shove while the ring position still tracks at full fidelity.
|
|
|
|
|
vec2 ringVel = grad * fieldHere * 0.55 * uMotionScale;
|
2026-05-27 12:05:28 +02:00
|
|
|
|
|
|
|
|
// --- Soft containment toward origin if particle escaped ---
|
|
|
|
|
float r = length(pos);
|
|
|
|
|
vec2 containVel = vec2(0.0);
|
|
|
|
|
if (r > 1.05) {
|
|
|
|
|
containVel = -normalize(pos) * (r - 1.05) * 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Integrate ---
|
|
|
|
|
vec2 vel = driftVel + ringVel + containVel;
|
|
|
|
|
vec2 next = pos + vel * uDelta * 60.0; // normalise to 60fps reference
|
|
|
|
|
|
|
|
|
|
// Velocity magnitude for color tint — EMA so flash decays gracefully.
|
|
|
|
|
float velMag = length(vel);
|
|
|
|
|
float velOut = mix(velPrev, velMag, 0.20);
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(next, scale, velOut);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render vertex shader — one vertex per particle, sampled from the
|
|
|
|
|
* position texture. The vertex's `position` attribute is unused;
|
|
|
|
|
* instead `aIndexUv` carries the (u, v) coordinate of this particle
|
|
|
|
|
* inside the position texture, and we read the actual position from
|
|
|
|
|
* `uPositions`.
|
|
|
|
|
*
|
|
|
|
|
* `gl_PointSize` is scaled by per-particle `scale` (z channel) and the
|
|
|
|
|
* device pixel ratio so the disc stays the same physical size on
|
|
|
|
|
* retina displays.
|
|
|
|
|
*/
|
|
|
|
|
export const renderVertex = /* glsl */ `
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
|
|
uniform sampler2D uPositions;
|
|
|
|
|
uniform float uPointSize;
|
|
|
|
|
uniform float uDpr;
|
|
|
|
|
|
|
|
|
|
attribute vec2 aIndexUv;
|
|
|
|
|
|
|
|
|
|
varying float vVel;
|
|
|
|
|
varying float vScale;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec4 p = texture2D(uPositions, aIndexUv);
|
|
|
|
|
vScale = p.z;
|
|
|
|
|
vVel = p.w;
|
|
|
|
|
|
|
|
|
|
// Position is already clip-space xy in [-1, +1]; pin z = 0.
|
|
|
|
|
gl_Position = vec4(p.xy, 0.0, 1.0);
|
|
|
|
|
gl_PointSize = uPointSize * p.z * uDpr;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render fragment shader — anti-aliased disc + velocity-based tint.
|
|
|
|
|
*
|
|
|
|
|
* Color: most of the field stays calm indigo at low opacity; particles
|
|
|
|
|
* that just got shoved by the ring (high velocity) flash toward a
|
|
|
|
|
* success-green tint. Output is premultiplied so additive blending
|
|
|
|
|
* gives the bloom-like glow without needing a post-process pass.
|
|
|
|
|
*/
|
|
|
|
|
export const renderFragment = /* glsl */ `
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
|
|
uniform vec3 uColorCalm; // indigo
|
|
|
|
|
uniform vec3 uColorHot; // success-green
|
|
|
|
|
uniform float uBaseAlpha;
|
|
|
|
|
|
|
|
|
|
varying float vVel;
|
|
|
|
|
varying float vScale;
|
|
|
|
|
|
|
|
|
|
void main() {
|
feat(video): v10 hero video with mute toggle — voice + bg music
Ships the long-form (71.5 s) hero video to the marketing /flow section
along with the iteration trail of architectural visual fixes the owner
worked through over the last sprint.
## Video composition (remotion/)
Eight phases driven by the 71.47 s voice-over in `audio.mp3` plus the
`Sub-bass Lullaby.wav` background music (ducked to 0.16 with fade in /
fade out). Every scene was rebuilt for v10 with concrete fixes:
- **HookScene** (12 s) — adds FloatingChaos overlay: a docker-compose
excerpt, an oauth_callback.ts snippet, an .env file with a yellow
squiggle warning ("in git history since v0.3.1"), and a live-ticking
502 retry toast. Tangle now reads as a developer's desktop right
before they give up, not as four icons drifting.
- **PromptScene** (12.2 s) — 6.5 s post-typing dead-zone replaced with
the parse beat: three sequential highlights on the prompt text
(MCP server / searches / Notion workspace), three chips below the
input (intent / tool / secret → vault), three-stat summary panel
(tools · 2, secrets · 1, targets · 3). At local frame 250 (≈ 21 s
global, on the voice line "the prompt path and the secret path
never cross") a mini two-rail diagram with an explicit X-marker
ring lands, visualising the architectural promise the moment it's
spoken.
- **SecretsScene** (15.2 s) — kept the arrow-fork + AES-256 stamp +
env-var injection beats; added the lock-snap flash at frame 66,
pinned the vault at full opacity throughout, and added a dashed
vault → container connector so the secret's provenance is visible.
The "what the AI sees" panel is now 680 px wide with an eye icon,
four corner viewfinder brackets around the prompt text, and three
explicit denied lines (no secrets / no environment variables / no
tokens).
- **BuildScene** (7.2 s) — unchanged beats: streaming log, server
card emerges with code + 🔒 NOTION_API_KEY slot pills, isolated-
container caption, <60s countdown.
- **IsolationScene** (14 s) — completely restructured. Orbit-and-dock
chips that collided with the card and with the tokens-only badge
are replaced by a clean vertical chip column at x=760: read-only
filesystem · dropped capabilities · no new privileges · 512 MB
memory cap · 0.5 CPU limit · ✓ your token only (last in green).
A vault graphic now sits below the server card with a dashed arrow
up into its env slot so the architecture story is complete in one
frame. PKCE jargon removed: "OAuth 2.1 · PKCE" → "only your token
gets in" with a small "oauth 2.1 · proof-key flow" subtitle for
the curious. Handshake stages simplified to your client → verified
→ scoped token. Final settlement arrow in success-green curves
from the scoped-token pill back into the card.
- **LibraryScene** (7 s) — cards enlarged from 340×180 to 400×220
with 36 px gaps. The "templates carry code, not credentials"
sub-caption was pulled (felt on-the-nose; the detached lock and
empty NOTION_API_KEY=? slot carry the story visually).
- **DiscoveryScene** (3 s) — the most-iterated scene. Earlier
versions had a fake "1,200+ developers building" fork counter
(pulled — solo-founder, hadn't earned). Replaced with a two-lane
architecture diagram that visualises "no paths cross" literally:
top lane prompt → AI → code, bottom lane vault → encrypted →
env, both converging at the server box on the right. v10
refinements: all seven boxes visible from frame 0 (no late
server arrival), a parallel glow tour walks across both lanes
simultaneously, a dashed vertical divider with a "no shared
node" chip pinned in the middle, and the closing line "One
sentence in. Live server out." slides down from above and lands
centred while the diagram fades to 0.12 opacity behind it —
no overlap.
- **LogoLockup** (1.7 s) — wordmark + fade-to-black for a clean
loop seam.
The Subtitle / CAPTIONS layer added in v7 was pulled wholesale —
owner found the kinetic-typography overlay aggressive and noted
that technical terms (PKCE etc.) created friction with no payoff.
Scene visuals and voice now carry the whole story; the Subtitle
component file is retained for possible future use.
Render pipeline (`render:mp4` / `render:webm` / `render:poster` in
remotion/package.json) is unchanged. The MP4 is post-processed to
H.264 Main / yuv420p / TV-range with faststart + AAC audio. The
WebM is re-encoded at VP9 CRF 38 / Opus 64k to stay under the 3 MB
budget. Final artefacts in apps/web/public/videos/: 2.59 MB mp4,
2.99 MB webm, 62 KB poster.
## Web integration (apps/web/components/hero-video.tsx)
New client component wraps the <video> element and pins a frosted-
glass mute toggle bottom-right of the player. Why not native
`controls`: the browser chrome fights the section's design vocabulary
and we only need one affordance — unmute — so we render exactly
that. The toggle's icon flips between VolumeX (currently muted) and
Volume2 (currently unmuted), accent colour switches indigo when sound
is on. Initial state is muted so autoplay still fires; on unmute we
call .play() defensively because mobile Safari pauses on
muted-property changes mid-playback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:31:10 +02:00
|
|
|
// Disc SDF — anti-aliased round dot.
|
2026-05-27 12:05:28 +02:00
|
|
|
float d = length(gl_PointCoord - 0.5);
|
feat(video): v10 hero video with mute toggle — voice + bg music
Ships the long-form (71.5 s) hero video to the marketing /flow section
along with the iteration trail of architectural visual fixes the owner
worked through over the last sprint.
## Video composition (remotion/)
Eight phases driven by the 71.47 s voice-over in `audio.mp3` plus the
`Sub-bass Lullaby.wav` background music (ducked to 0.16 with fade in /
fade out). Every scene was rebuilt for v10 with concrete fixes:
- **HookScene** (12 s) — adds FloatingChaos overlay: a docker-compose
excerpt, an oauth_callback.ts snippet, an .env file with a yellow
squiggle warning ("in git history since v0.3.1"), and a live-ticking
502 retry toast. Tangle now reads as a developer's desktop right
before they give up, not as four icons drifting.
- **PromptScene** (12.2 s) — 6.5 s post-typing dead-zone replaced with
the parse beat: three sequential highlights on the prompt text
(MCP server / searches / Notion workspace), three chips below the
input (intent / tool / secret → vault), three-stat summary panel
(tools · 2, secrets · 1, targets · 3). At local frame 250 (≈ 21 s
global, on the voice line "the prompt path and the secret path
never cross") a mini two-rail diagram with an explicit X-marker
ring lands, visualising the architectural promise the moment it's
spoken.
- **SecretsScene** (15.2 s) — kept the arrow-fork + AES-256 stamp +
env-var injection beats; added the lock-snap flash at frame 66,
pinned the vault at full opacity throughout, and added a dashed
vault → container connector so the secret's provenance is visible.
The "what the AI sees" panel is now 680 px wide with an eye icon,
four corner viewfinder brackets around the prompt text, and three
explicit denied lines (no secrets / no environment variables / no
tokens).
- **BuildScene** (7.2 s) — unchanged beats: streaming log, server
card emerges with code + 🔒 NOTION_API_KEY slot pills, isolated-
container caption, <60s countdown.
- **IsolationScene** (14 s) — completely restructured. Orbit-and-dock
chips that collided with the card and with the tokens-only badge
are replaced by a clean vertical chip column at x=760: read-only
filesystem · dropped capabilities · no new privileges · 512 MB
memory cap · 0.5 CPU limit · ✓ your token only (last in green).
A vault graphic now sits below the server card with a dashed arrow
up into its env slot so the architecture story is complete in one
frame. PKCE jargon removed: "OAuth 2.1 · PKCE" → "only your token
gets in" with a small "oauth 2.1 · proof-key flow" subtitle for
the curious. Handshake stages simplified to your client → verified
→ scoped token. Final settlement arrow in success-green curves
from the scoped-token pill back into the card.
- **LibraryScene** (7 s) — cards enlarged from 340×180 to 400×220
with 36 px gaps. The "templates carry code, not credentials"
sub-caption was pulled (felt on-the-nose; the detached lock and
empty NOTION_API_KEY=? slot carry the story visually).
- **DiscoveryScene** (3 s) — the most-iterated scene. Earlier
versions had a fake "1,200+ developers building" fork counter
(pulled — solo-founder, hadn't earned). Replaced with a two-lane
architecture diagram that visualises "no paths cross" literally:
top lane prompt → AI → code, bottom lane vault → encrypted →
env, both converging at the server box on the right. v10
refinements: all seven boxes visible from frame 0 (no late
server arrival), a parallel glow tour walks across both lanes
simultaneously, a dashed vertical divider with a "no shared
node" chip pinned in the middle, and the closing line "One
sentence in. Live server out." slides down from above and lands
centred while the diagram fades to 0.12 opacity behind it —
no overlap.
- **LogoLockup** (1.7 s) — wordmark + fade-to-black for a clean
loop seam.
The Subtitle / CAPTIONS layer added in v7 was pulled wholesale —
owner found the kinetic-typography overlay aggressive and noted
that technical terms (PKCE etc.) created friction with no payoff.
Scene visuals and voice now carry the whole story; the Subtitle
component file is retained for possible future use.
Render pipeline (`render:mp4` / `render:webm` / `render:poster` in
remotion/package.json) is unchanged. The MP4 is post-processed to
H.264 Main / yuv420p / TV-range with faststart + AAC audio. The
WebM is re-encoded at VP9 CRF 38 / Opus 64k to stay under the 3 MB
budget. Final artefacts in apps/web/public/videos/: 2.59 MB mp4,
2.99 MB webm, 62 KB poster.
## Web integration (apps/web/components/hero-video.tsx)
New client component wraps the <video> element and pins a frosted-
glass mute toggle bottom-right of the player. Why not native
`controls`: the browser chrome fights the section's design vocabulary
and we only need one affordance — unmute — so we render exactly
that. The toggle's icon flips between VolumeX (currently muted) and
Volume2 (currently unmuted), accent colour switches indigo when sound
is on. Initial state is muted so autoplay still fires; on unmute we
call .play() defensively because mobile Safari pauses on
muted-property changes mid-playback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:31:10 +02:00
|
|
|
float a = smoothstep(0.5, 0.42, d);
|
2026-05-27 12:05:28 +02:00
|
|
|
if (a <= 0.001) discard;
|
|
|
|
|
|
feat(video): v10 hero video with mute toggle — voice + bg music
Ships the long-form (71.5 s) hero video to the marketing /flow section
along with the iteration trail of architectural visual fixes the owner
worked through over the last sprint.
## Video composition (remotion/)
Eight phases driven by the 71.47 s voice-over in `audio.mp3` plus the
`Sub-bass Lullaby.wav` background music (ducked to 0.16 with fade in /
fade out). Every scene was rebuilt for v10 with concrete fixes:
- **HookScene** (12 s) — adds FloatingChaos overlay: a docker-compose
excerpt, an oauth_callback.ts snippet, an .env file with a yellow
squiggle warning ("in git history since v0.3.1"), and a live-ticking
502 retry toast. Tangle now reads as a developer's desktop right
before they give up, not as four icons drifting.
- **PromptScene** (12.2 s) — 6.5 s post-typing dead-zone replaced with
the parse beat: three sequential highlights on the prompt text
(MCP server / searches / Notion workspace), three chips below the
input (intent / tool / secret → vault), three-stat summary panel
(tools · 2, secrets · 1, targets · 3). At local frame 250 (≈ 21 s
global, on the voice line "the prompt path and the secret path
never cross") a mini two-rail diagram with an explicit X-marker
ring lands, visualising the architectural promise the moment it's
spoken.
- **SecretsScene** (15.2 s) — kept the arrow-fork + AES-256 stamp +
env-var injection beats; added the lock-snap flash at frame 66,
pinned the vault at full opacity throughout, and added a dashed
vault → container connector so the secret's provenance is visible.
The "what the AI sees" panel is now 680 px wide with an eye icon,
four corner viewfinder brackets around the prompt text, and three
explicit denied lines (no secrets / no environment variables / no
tokens).
- **BuildScene** (7.2 s) — unchanged beats: streaming log, server
card emerges with code + 🔒 NOTION_API_KEY slot pills, isolated-
container caption, <60s countdown.
- **IsolationScene** (14 s) — completely restructured. Orbit-and-dock
chips that collided with the card and with the tokens-only badge
are replaced by a clean vertical chip column at x=760: read-only
filesystem · dropped capabilities · no new privileges · 512 MB
memory cap · 0.5 CPU limit · ✓ your token only (last in green).
A vault graphic now sits below the server card with a dashed arrow
up into its env slot so the architecture story is complete in one
frame. PKCE jargon removed: "OAuth 2.1 · PKCE" → "only your token
gets in" with a small "oauth 2.1 · proof-key flow" subtitle for
the curious. Handshake stages simplified to your client → verified
→ scoped token. Final settlement arrow in success-green curves
from the scoped-token pill back into the card.
- **LibraryScene** (7 s) — cards enlarged from 340×180 to 400×220
with 36 px gaps. The "templates carry code, not credentials"
sub-caption was pulled (felt on-the-nose; the detached lock and
empty NOTION_API_KEY=? slot carry the story visually).
- **DiscoveryScene** (3 s) — the most-iterated scene. Earlier
versions had a fake "1,200+ developers building" fork counter
(pulled — solo-founder, hadn't earned). Replaced with a two-lane
architecture diagram that visualises "no paths cross" literally:
top lane prompt → AI → code, bottom lane vault → encrypted →
env, both converging at the server box on the right. v10
refinements: all seven boxes visible from frame 0 (no late
server arrival), a parallel glow tour walks across both lanes
simultaneously, a dashed vertical divider with a "no shared
node" chip pinned in the middle, and the closing line "One
sentence in. Live server out." slides down from above and lands
centred while the diagram fades to 0.12 opacity behind it —
no overlap.
- **LogoLockup** (1.7 s) — wordmark + fade-to-black for a clean
loop seam.
The Subtitle / CAPTIONS layer added in v7 was pulled wholesale —
owner found the kinetic-typography overlay aggressive and noted
that technical terms (PKCE etc.) created friction with no payoff.
Scene visuals and voice now carry the whole story; the Subtitle
component file is retained for possible future use.
Render pipeline (`render:mp4` / `render:webm` / `render:poster` in
remotion/package.json) is unchanged. The MP4 is post-processed to
H.264 Main / yuv420p / TV-range with faststart + AAC audio. The
WebM is re-encoded at VP9 CRF 38 / Opus 64k to stay under the 3 MB
budget. Final artefacts in apps/web/public/videos/: 2.59 MB mp4,
2.99 MB webm, 62 KB poster.
## Web integration (apps/web/components/hero-video.tsx)
New client component wraps the <video> element and pins a frosted-
glass mute toggle bottom-right of the player. Why not native
`controls`: the browser chrome fights the section's design vocabulary
and we only need one affordance — unmute — so we render exactly
that. The toggle's icon flips between VolumeX (currently muted) and
Volume2 (currently unmuted), accent colour switches indigo when sound
is on. Initial state is muted so autoplay still fires; on unmute we
call .play() defensively because mobile Safari pauses on
muted-property changes mid-playback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:31:10 +02:00
|
|
|
// Velocity-driven mix: pin to indigo for typical drift, lerp toward
|
|
|
|
|
// green only on real shoves. The 0.04..0.18 band is roughly where
|
|
|
|
|
// ring pushes live; idle drift stays below 0.03.
|
2026-05-27 12:05:28 +02:00
|
|
|
float t = smoothstep(0.04, 0.18, vVel);
|
|
|
|
|
vec3 col = mix(uColorCalm, uColorHot, t);
|
|
|
|
|
|
|
|
|
|
float alpha = uBaseAlpha * a * (0.6 + 0.4 * vScale);
|
|
|
|
|
// Premultiplied alpha — pairs with THREE.AdditiveBlending.
|
|
|
|
|
gl_FragColor = vec4(col * alpha, alpha);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Init fragment — runs once into both ping-pong targets to seed the
|
|
|
|
|
* starting field. Uses a tempered random distribution: uniform in the
|
|
|
|
|
* disc, with a small radial bias toward the edges so the field doesn't
|
|
|
|
|
* look like a bullseye on first frame.
|
|
|
|
|
*/
|
|
|
|
|
export const initFragment = /* glsl */ `
|
|
|
|
|
precision highp float;
|
|
|
|
|
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
${simplexNoise}
|
|
|
|
|
|
|
|
|
|
// Tiny hash for per-particle deterministic randoms.
|
|
|
|
|
float hash(vec2 p) {
|
|
|
|
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
float r1 = hash(vUv);
|
|
|
|
|
float r2 = hash(vUv + 17.3);
|
|
|
|
|
float r3 = hash(vUv + 91.7);
|
|
|
|
|
|
|
|
|
|
// Polar-uniform disc with a soft outward bias.
|
|
|
|
|
float angle = r1 * 6.28318;
|
|
|
|
|
float radius = sqrt(r2) * 1.0;
|
|
|
|
|
vec2 pos = vec2(cos(angle), sin(angle)) * radius;
|
|
|
|
|
|
|
|
|
|
// Slight horizontal stretch so the field reads as a wide hero band,
|
|
|
|
|
// not a perfect circle.
|
|
|
|
|
pos.x *= 1.25;
|
|
|
|
|
|
|
|
|
|
float scale = 0.55 + r3 * 0.85;
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(pos, scale, 0.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|