buildmymcpserver/apps/web/components/particle-hero/shaders.ts

309 lines
11 KiB
TypeScript
Raw Normal View History

feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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;
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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;
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:05:28 +02:00
feat: particle cloud (no discrete dots) + geo-IP country preselect on login Two coordinated polish moves the owner asked for. ## 1. Hero particle field — "no white dots, just a glow that follows the mouse and is always in motion" Previous tuning (uPointSize 2.8, uBaseAlpha 0.6) gave discrete indigo dots that additively saturated to near-white in dense clusters. The owner wanted no granular dots visible at all — a continuous indigo cloud that the cursor pulls toward itself. Changes: - **Render fragment**: replaced the anti-aliased disc SDF (`smoothstep(0.5, 0.42, d)` — hard edge) with a Gaussian falloff (`exp(-d * d * 6.0)` — smooth blob, no edge). Each particle is now a soft volume that blends seamlessly with neighbours. - **Sim fragment**: replaced the outward-gradient ring push with a mouse-halo attraction. Particles drift toward an ideal radius (~0.20) around the cursor, with exp-bell falloff so they don't collapse onto the cursor or feel influenced from across the canvas. `ringField()` helper is now unused but kept for future use. - **JS uniforms**: `uPointSize` 2.8→14 (256-tier) / 3.6→20 (128-tier); `uBaseAlpha` 0.6→0.055. Individual particles are below the perception threshold for "dot" but 65k of them additively composite into a continuous cloud. With the much lower per-particle alpha, the cumulative brightness never saturates to white. - **ParticleField tick loop**: asymmetric ring-active fade — `alpha = 0.14` ramping in (fast cursor response), `0.012` decaying out (slow glow trail after the pointer moves away). Matches the brief "glow longer + attractive to mouse but always in motion". - **ParticleHero index.tsx**: added an always-on indigo radial gradient behind the WebGL canvas, so the hero never reads as visually empty between frames — the canvas additively paints the dynamic cloud on top. Removed the white-dot stipple from the static fallback (it was the most likely source of the "weisse punkte" complaint for any visitor on the fallback path). ## 2. SMS login — pre-select country picker from visitor's geo-IP The country picker on `/login` previously defaulted to `'CH'` for everyone. Visitors from DE / AT / US / etc. had to manually scroll to their dial code — small friction but it sits on the highest-stakes conversion step in the funnel. - **New API route** `apps/api/src/routes/geo.ts` → `GET /v1/geo/country` returns `{ country: 'CH' | 'DE' | … | null }` by reading Cloudflare's `CF-IPCountry` header. Public, no auth — reading a 2-letter country code from a geo-IP header isn't PII under GDPR / DSG. `'XX'` and `'T1'` (CF's "unknown" + Tor) are normalised to `null`. Outside CF (dev), header is missing → null. - **Login page** picks up the result in the existing `useEffect`, guards against codes not in our country list, and calls `setCountry` to override the `'CH'` default. Stays at `'CH'` if the detection fails or the visitor is on a Tor exit. Verified live: the endpoint returns `{"country":"DE"}` from CF's German edge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:17:20 +02:00
// --- Mouse halo pull (attraction, not repulsion) ---
// Particles are drawn toward a soft halo orbiting the cursor —
// strongest at ~0.20 distance, fading both closer and farther.
// Closer-fade prevents the cloud from collapsing onto the cursor;
// farther-fade keeps the influence local. The result is a moving
// bright spot that follows the pointer with a continuous breathing
// ring of indigo around it, rather than the old outward push that
// hollowed the cloud where the cursor sat.
vec2 toMouse = uRingPos - pos;
float distToMouse = length(toMouse) + 0.001;
float halo = exp(-pow(distToMouse - 0.20, 2.0) * 22.0);
vec2 ringVel = (toMouse / distToMouse) * halo * 0.05 * uRingActive * uMotionScale;
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: particle cloud (no discrete dots) + geo-IP country preselect on login Two coordinated polish moves the owner asked for. ## 1. Hero particle field — "no white dots, just a glow that follows the mouse and is always in motion" Previous tuning (uPointSize 2.8, uBaseAlpha 0.6) gave discrete indigo dots that additively saturated to near-white in dense clusters. The owner wanted no granular dots visible at all — a continuous indigo cloud that the cursor pulls toward itself. Changes: - **Render fragment**: replaced the anti-aliased disc SDF (`smoothstep(0.5, 0.42, d)` — hard edge) with a Gaussian falloff (`exp(-d * d * 6.0)` — smooth blob, no edge). Each particle is now a soft volume that blends seamlessly with neighbours. - **Sim fragment**: replaced the outward-gradient ring push with a mouse-halo attraction. Particles drift toward an ideal radius (~0.20) around the cursor, with exp-bell falloff so they don't collapse onto the cursor or feel influenced from across the canvas. `ringField()` helper is now unused but kept for future use. - **JS uniforms**: `uPointSize` 2.8→14 (256-tier) / 3.6→20 (128-tier); `uBaseAlpha` 0.6→0.055. Individual particles are below the perception threshold for "dot" but 65k of them additively composite into a continuous cloud. With the much lower per-particle alpha, the cumulative brightness never saturates to white. - **ParticleField tick loop**: asymmetric ring-active fade — `alpha = 0.14` ramping in (fast cursor response), `0.012` decaying out (slow glow trail after the pointer moves away). Matches the brief "glow longer + attractive to mouse but always in motion". - **ParticleHero index.tsx**: added an always-on indigo radial gradient behind the WebGL canvas, so the hero never reads as visually empty between frames — the canvas additively paints the dynamic cloud on top. Removed the white-dot stipple from the static fallback (it was the most likely source of the "weisse punkte" complaint for any visitor on the fallback path). ## 2. SMS login — pre-select country picker from visitor's geo-IP The country picker on `/login` previously defaulted to `'CH'` for everyone. Visitors from DE / AT / US / etc. had to manually scroll to their dial code — small friction but it sits on the highest-stakes conversion step in the funnel. - **New API route** `apps/api/src/routes/geo.ts` → `GET /v1/geo/country` returns `{ country: 'CH' | 'DE' | … | null }` by reading Cloudflare's `CF-IPCountry` header. Public, no auth — reading a 2-letter country code from a geo-IP header isn't PII under GDPR / DSG. `'XX'` and `'T1'` (CF's "unknown" + Tor) are normalised to `null`. Outside CF (dev), header is missing → null. - **Login page** picks up the result in the existing `useEffect`, guards against codes not in our country list, and calls `setCountry` to override the `'CH'` default. Stays at `'CH'` if the detection fails or the visitor is on a Tor exit. Verified live: the endpoint returns `{"country":"DE"}` from CF's German edge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:17:20 +02:00
// Soft Gaussian blob — no hard disc edge. Combined with the bigger
// uPointSize on the JS side (14-20px vs the old 2.8) and the much
// lower uBaseAlpha (0.05 vs 0.6), individual particles disappear
// into a continuous indigo cloud. The exp() falloff means each blob
// contributes most at its centre and fades smoothly to nothing —
// adjacent blobs blend without seams, so 65k of them additively
// composite into a volumetric glow instead of a stipple texture.
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:05:28 +02:00
float d = length(gl_PointCoord - 0.5);
feat: particle cloud (no discrete dots) + geo-IP country preselect on login Two coordinated polish moves the owner asked for. ## 1. Hero particle field — "no white dots, just a glow that follows the mouse and is always in motion" Previous tuning (uPointSize 2.8, uBaseAlpha 0.6) gave discrete indigo dots that additively saturated to near-white in dense clusters. The owner wanted no granular dots visible at all — a continuous indigo cloud that the cursor pulls toward itself. Changes: - **Render fragment**: replaced the anti-aliased disc SDF (`smoothstep(0.5, 0.42, d)` — hard edge) with a Gaussian falloff (`exp(-d * d * 6.0)` — smooth blob, no edge). Each particle is now a soft volume that blends seamlessly with neighbours. - **Sim fragment**: replaced the outward-gradient ring push with a mouse-halo attraction. Particles drift toward an ideal radius (~0.20) around the cursor, with exp-bell falloff so they don't collapse onto the cursor or feel influenced from across the canvas. `ringField()` helper is now unused but kept for future use. - **JS uniforms**: `uPointSize` 2.8→14 (256-tier) / 3.6→20 (128-tier); `uBaseAlpha` 0.6→0.055. Individual particles are below the perception threshold for "dot" but 65k of them additively composite into a continuous cloud. With the much lower per-particle alpha, the cumulative brightness never saturates to white. - **ParticleField tick loop**: asymmetric ring-active fade — `alpha = 0.14` ramping in (fast cursor response), `0.012` decaying out (slow glow trail after the pointer moves away). Matches the brief "glow longer + attractive to mouse but always in motion". - **ParticleHero index.tsx**: added an always-on indigo radial gradient behind the WebGL canvas, so the hero never reads as visually empty between frames — the canvas additively paints the dynamic cloud on top. Removed the white-dot stipple from the static fallback (it was the most likely source of the "weisse punkte" complaint for any visitor on the fallback path). ## 2. SMS login — pre-select country picker from visitor's geo-IP The country picker on `/login` previously defaulted to `'CH'` for everyone. Visitors from DE / AT / US / etc. had to manually scroll to their dial code — small friction but it sits on the highest-stakes conversion step in the funnel. - **New API route** `apps/api/src/routes/geo.ts` → `GET /v1/geo/country` returns `{ country: 'CH' | 'DE' | … | null }` by reading Cloudflare's `CF-IPCountry` header. Public, no auth — reading a 2-letter country code from a geo-IP header isn't PII under GDPR / DSG. `'XX'` and `'T1'` (CF's "unknown" + Tor) are normalised to `null`. Outside CF (dev), header is missing → null. - **Login page** picks up the result in the existing `useEffect`, guards against codes not in our country list, and calls `setCountry` to override the `'CH'` default. Stays at `'CH'` if the detection fails or the visitor is on a Tor exit. Verified live: the endpoint returns `{"country":"DE"}` from CF's German edge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:17:20 +02:00
float a = exp(-d * d * 6.0);
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:05:28 +02:00
if (a <= 0.001) discard;
feat: particle cloud (no discrete dots) + geo-IP country preselect on login Two coordinated polish moves the owner asked for. ## 1. Hero particle field — "no white dots, just a glow that follows the mouse and is always in motion" Previous tuning (uPointSize 2.8, uBaseAlpha 0.6) gave discrete indigo dots that additively saturated to near-white in dense clusters. The owner wanted no granular dots visible at all — a continuous indigo cloud that the cursor pulls toward itself. Changes: - **Render fragment**: replaced the anti-aliased disc SDF (`smoothstep(0.5, 0.42, d)` — hard edge) with a Gaussian falloff (`exp(-d * d * 6.0)` — smooth blob, no edge). Each particle is now a soft volume that blends seamlessly with neighbours. - **Sim fragment**: replaced the outward-gradient ring push with a mouse-halo attraction. Particles drift toward an ideal radius (~0.20) around the cursor, with exp-bell falloff so they don't collapse onto the cursor or feel influenced from across the canvas. `ringField()` helper is now unused but kept for future use. - **JS uniforms**: `uPointSize` 2.8→14 (256-tier) / 3.6→20 (128-tier); `uBaseAlpha` 0.6→0.055. Individual particles are below the perception threshold for "dot" but 65k of them additively composite into a continuous cloud. With the much lower per-particle alpha, the cumulative brightness never saturates to white. - **ParticleField tick loop**: asymmetric ring-active fade — `alpha = 0.14` ramping in (fast cursor response), `0.012` decaying out (slow glow trail after the pointer moves away). Matches the brief "glow longer + attractive to mouse but always in motion". - **ParticleHero index.tsx**: added an always-on indigo radial gradient behind the WebGL canvas, so the hero never reads as visually empty between frames — the canvas additively paints the dynamic cloud on top. Removed the white-dot stipple from the static fallback (it was the most likely source of the "weisse punkte" complaint for any visitor on the fallback path). ## 2. SMS login — pre-select country picker from visitor's geo-IP The country picker on `/login` previously defaulted to `'CH'` for everyone. Visitors from DE / AT / US / etc. had to manually scroll to their dial code — small friction but it sits on the highest-stakes conversion step in the funnel. - **New API route** `apps/api/src/routes/geo.ts` → `GET /v1/geo/country` returns `{ country: 'CH' | 'DE' | … | null }` by reading Cloudflare's `CF-IPCountry` header. Public, no auth — reading a 2-letter country code from a geo-IP header isn't PII under GDPR / DSG. `'XX'` and `'T1'` (CF's "unknown" + Tor) are normalised to `null`. Outside CF (dev), header is missing → null. - **Login page** picks up the result in the existing `useEffect`, guards against codes not in our country list, and calls `setCountry` to override the `'CH'` default. Stays at `'CH'` if the detection fails or the visitor is on a Tor exit. Verified live: the endpoint returns `{"country":"DE"}` from CF's German edge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:17:20 +02:00
// Velocity-driven mix kept, but with the new low base alpha the
// green tint is barely visible — by design. The cloud is calm.
feat(web): hero redesign — cycling step rotator + full-width video section Restructures the landing page above-the-fold into two distinct sections: 1. **Hero — left copy + cycling tile, no static stack of three blocks** New `<HeroStepRotator>` (Framer Motion client component) shows ONE tile centred in the column, cycling prompt.txt → build.log → claude_desktop_config.json every 3.5s. Auto-advance pauses on hover and exposes a 3-dot tablist so users can jump to any step. The active dot grows wide with an accent glow. Mouse interaction: spring-smoothed 3D tilt on rotateX/rotateY plus a radial glow that translates toward the cursor — both driven by motion values, so the transforms stay on the GPU compositor instead of re-rendering on every mousemove. `useReducedMotion()` strips the tilt + glow translation and collapses the page transition to an instant cross-fade (the rotation itself still advances — it's content, not decoration). Hero padding tightened (py-12/14/16 vs py-14/20/28) so the video section below is teased above the fold. New scroll cue ("see it run" + animated chevron) sits at the bottom of the hero, anchored to #flow. 2. **Flow video — full-width edge-to-edge under the hero (new section)** The hero.mp4 / hero.webm pair moves out of the "How it works" section into its own #flow section. No max-w wrapper — it spans the viewport with `w-full aspect-video`, so on a 1080p monitor the video gets the full 1920px width. Adds a subtle radial vignette so the black edges blend into the page chrome. 3. **"How it works" — now lean** Video removed (it's the flow section now). Just the three textual cards as supporting copy. Adds `framer-motion@11.18.2` to apps/web/package.json. Build passes typecheck + Next.js production build with no new warnings; LCP path is untouched since the rotator is client-hydrated after first paint and Framer Motion is tree-shaken to the components we import. Note: visitors with `prefers-reduced-motion: reduce` will still see the video's poster instead of autoplay — Chrome blocks the network fetch entirely for autoplay media when reduced-motion is set. The flow video remains visible for the rest, and the step rotator continues to cycle its content (with instant cross-fade instead of slide+scale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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);
}
`;