// Scroll-driven hand-off transitions between sections. // Rule: NO chapter numbers, NO headline text. Each transition is a single // continuous element-morph that uses the visual vocabulary of the surrounding // sections — so the hero element of one section literally becomes the hero // element of the next. // // 02 → 03 16 spark bars (section 02 chart) ➜ 3 pillars (section 03) // 03 → 04 3 pillars ➜ ladder rails + 6 rungs // 04 → 05 ladder rungs ➜ 8-point capability wheel function useScrollProgress(ref) { const [t, setT] = React.useState(0); React.useEffect(() => { function onScroll() { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const viewH = window.innerHeight; const total = r.height + viewH; const passed = viewH - r.top; setT(Math.max(0, Math.min(1, passed / total))); } onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); }; }, [ref]); return t; } const clamp01 = v => Math.max(0, Math.min(1, v)); const lerp = (a, b, t) => a + (b - a) * t; const easeOut = t => 1 - Math.pow(1 - t, 3); const easeInOut = t => t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2; /* ============================================================ 02 → 03 — Spark bars consolidate into 3 pillars ============================================================ */ function Trans02to03() { const ref = React.useRef(null); const t = useScrollProgress(ref); // 16 bars, exact heights from the section-02 sparkline. const SPARK = [14, 22, 18, 28, 24, 38, 32, 46, 42, 58, 52, 68, 62, 76, 72, 86]; // 3 destination centers (% of canvas width) — match 03 pillar order. const TARGETS = [ { x: 16, color1: '#0D903A', color2: '#074c2a', label: 'PERFORMANCE LADDER' }, { x: 50, color1: '#246BB2', color2: '#164A97', label: 'STANDARD OF EXCELLENCE' }, { x: 84, color1: '#FFBA00', color2: '#8C6100', label: 'SCALE & CONTROL' }, ]; // Which target each bar gravitates to. const targetIdx = SPARK.map((_, i) => i < 5 ? 0 : i < 11 ? 1 : 2); // Pick 3 "winner" bars whose colour swaps to the slab gradient. const WINNERS = [2, 8, 13]; // Three phases. const rise = clamp01(t / 0.22); // 0 → 0.22 bars grow up const tall = clamp01((t - 0.20) / 0.30); // 0.20 → 0.50 bars climb tall const merge = clamp01((t - 0.45) / 0.40); // 0.45 → 0.85 migrate + consolidate const finish = clamp01((t - 0.80) / 0.20); // 0.80 → 1.00 settle into pillars return (
{SPARK.map((h, i) => { const tgt = TARGETS[targetIdx[i]]; const isWinner = WINNERS.includes(i); // Original X (0..1 within the 16-bar strip, centered ~ 10–90%) const baseX = 10 + (i / (SPARK.length - 1)) * 80; // Position lerp: origin → cluster center const x = lerp(baseX, tgt.x, easeInOut(merge)); // Height grows in phases: // 1) rise to its spark height // 2) climb to tall // 3) for winners, expand to full slab; for losers, collapse const sparkH = h / 100; // 0.14 → 0.86 const tallH = lerp(sparkH, 0.85, easeOut(tall)); // → 85% of canvas const winnerH = lerp(tallH, 1.0, easeOut(finish)); // → 100% const loserH = lerp(tallH, 0.0, easeInOut(merge)); // collapses to 0 const h01 = isWinner ? winnerH : loserH; // Width: winners fatten into slabs, losers stay narrow const baseW = 28; const slabW = 240; const w = isWinner ? lerp(baseW, slabW, easeOut(merge)) : baseW * (1 - easeInOut(merge)); // Tilt — winners pick up the diagonal slab clip near the end const tilt = isWinner ? lerp(0, -6, easeOut(finish)) : 0; // Colour blend — winners shift from spark gradient to slab gradient const blend = isWinner ? easeOut(merge) : 0; const grad = `linear-gradient(180deg, color-mix(in oklab, #0FE872 ${(1-blend)*100}%, ${tgt.color1}) 0%, color-mix(in oklab, rgba(255,186,0,0.4) ${(1-blend)*100}%, ${tgt.color2}) 100%)`; // Opacity — losers fade as they collapse const op = isWinner ? 1 : (1 - easeInOut(merge)); // Vertical rise from below const baseRise = clamp01(rise * 2 - i * 0.02); return (
); })} {/* Pillar labels — fade in only after winners have consolidated */} {TARGETS.map((tg, i) => (
0{i+1} {tg.label}
))}
); } /* ============================================================ 03 → 04 — 3 pillars un-tilt into ladder rails + rungs draw ============================================================ */ function Trans03to04() { const ref = React.useRef(null); const t = useScrollProgress(ref); // Phase A: 3 tilted pillars (matching 03 hero) un-tilt + slide const straighten = clamp01(t / 0.35); // Phase B: middle pillar fades, outer 2 become the ladder rails const fade = clamp01((t - 0.30) / 0.20); // Phase C: 6 rungs draw across, one by one const rungs = clamp01((t - 0.40) / 0.50); // Phase D: climbing dot ascends const climb = clamp01((t - 0.45) / 0.50); // Source pillars (tilted, like section 03 slabs) const PILLARS = [ { from: { x: 200, w: 200, tilt: -8, color: '#0D903A', color2: '#074c2a' }, to: { x: 200, w: 6, tilt: 0, color: '#FFBA00', color2: '#FFBA00' } }, { from: { x: 400, w: 200, tilt: -8, color: '#246BB2', color2: '#164A97' }, to: { x: 400, w: 4, tilt: 0, color: '#246BB2', color2: '#164A97' } }, { from: { x: 600, w: 200, tilt: -8, color: '#FFBA00', color2: '#8C6100' }, to: { x: 600, w: 6, tilt: 0, color: '#FFBA00', color2: '#FFBA00' } }, ]; const RUNGS = ['Case', 'Sim', 'Mimic', 'Shadow', 'Supervise', 'Independent']; return (
{/* Pillars → rails (interpolated) */} {PILLARS.map((p, i) => { const x = lerp(p.from.x, p.to.x, easeInOut(straighten)); const w = lerp(p.from.w, p.to.w, easeOut(straighten)); const tlt= lerp(p.from.tilt, p.to.tilt, easeInOut(straighten)); const isMiddle = i === 1; const op = isMiddle ? (1 - fade) : 1; const fill = `url(#${i === 1 ? 't34-rail' : 't34-rail'})`; // Render as a tilted rect via path const half = w / 2; const top = 40, bot = 380; const xt = x + Math.tan(tlt * Math.PI / 180) * (bot - top) / 2; const xb = x - Math.tan(tlt * Math.PI / 180) * (bot - top) / 2; const d = `M ${xt - half} ${top} L ${xt + half} ${top} L ${xb + half} ${bot} L ${xb - half} ${bot} Z`; // Color blends with straighten const c1 = straighten < 0.8 ? p.from.color : '#FFBA00'; return ( {/* Rail overlay strengthens as we straighten */} ); })} {/* Rungs draw across the outer two rails */} {RUNGS.map((label, i) => { // bottom-up draw order const order = RUNGS.length - 1 - i; const localT = clamp01(rungs * RUNGS.length - order); const y = 360 - i * 52; const x1 = 200, x2 = 600; const cx = (x1 + x2) / 2; const half = (x2 - x1) / 2; const draw1 = cx - half * easeOut(localT); const draw2 = cx + half * easeOut(localT); return ( {/* Rung tick numbers */} {String(i+1).padStart(2,'0')} {/* Rung labels — appear after rung is drawn */} {label} ); })} {/* Climbing dot */} {climb > 0.02 && ( )}
); } /* ============================================================ 04 → 05 — Ladder rungs detach and curl into the capability wheel ============================================================ */ function Trans04to05() { const ref = React.useRef(null); const t = useScrollProgress(ref); // Phase A: ladder visible const settle = clamp01(t / 0.20); // Phase B: rungs detach + curl outward into circle const curl = clamp01((t - 0.18) / 0.55); // Phase C: wheel hub + ring appears const hub = clamp01((t - 0.65) / 0.30); // 8 capability target angles + colors (from section 05). // Angles match window.CAPABILITIES.angle (every 45°, starting at 0 = top). const CAPS = [ { angle: 0, color: '#0D903A', short: 'Finance' }, { angle: 45, color: '#0F8E42', short: 'Business' }, { angle: 90, color: '#246BB2', short: 'Macro' }, { angle: 135, color: '#1A549F', short: 'Products' }, { angle: 180, color: '#164A97', short: 'Value' }, { angle: 225, color: '#19519C', short: 'Acquisition' }, { angle: 270, color: '#077934', short: 'Reactivation' }, { angle: 315, color: '#057430', short: 'Execution' }, ]; const cx = 400, cy = 210, R = 150; // Source ladder rung Y positions (matching Trans03to04: 360 - i*52, 6 rungs) // We have 8 capability targets. The 6 original rungs map to 6 capabilities; // the remaining 2 emerge from rail tips. const RUNG_Y = [360, 308, 256, 204, 152, 100]; const SOURCE = [ { x: 400, y: RUNG_Y[0], spawn: 0 }, // rung 1 { x: 400, y: RUNG_Y[1], spawn: 0 }, // rung 2 { x: 400, y: RUNG_Y[2], spawn: 0 }, // rung 3 { x: 400, y: RUNG_Y[3], spawn: 0 }, // rung 4 { x: 400, y: RUNG_Y[4], spawn: 0 }, // rung 5 { x: 400, y: RUNG_Y[5], spawn: 0 }, // rung 6 { x: 200, y: 60, spawn: 0.1 }, // emerges from left rail top { x: 600, y: 60, spawn: 0.1 }, // emerges from right rail top ]; return (
{/* Fading ladder rails */} {/* 6 rungs visible at start */} {RUNG_Y.map((y, i) => ( ))} {/* Faint guide circle for the wheel — appears with hub */} {hub > 0.05 && ( {/* 8 light spokes */} {CAPS.map((c, i) => { const a = (c.angle - 90) * Math.PI / 180; return ( ); })} )} {/* 8 morphing nodes — ladder rungs ➜ capability wedges */} {CAPS.map((c, i) => { const src = SOURCE[i]; const localT = clamp01((curl - src.spawn) / Math.max(0.001, 1 - src.spawn)); const a = (c.angle - 90) * Math.PI / 180; const tgtX = cx + Math.cos(a) * R; const tgtY = cy + Math.sin(a) * R; // Bezier curl: anchor pulls toward center first, so it arcs. const midX = lerp(src.x, tgtX, 0.5); const midY = lerp(src.y, tgtY, 0.5); // Bend toward center as t goes 0 → 1 (smooth arc, not straight) const bendX = lerp(midX, cx, 0.4) * (1 - Math.abs(localT - 0.5) * 2); const bendY = lerp(midY, cy, 0.4) * (1 - Math.abs(localT - 0.5) * 2); // Bezier point const u = easeInOut(localT); const px = (1 - u) * ((1 - u) * src.x + u * midX) + u * ((1 - u) * midX + u * tgtX); const py = (1 - u) * ((1 - u) * src.y + u * midY) + u * ((1 - u) * midY + u * tgtY); // Append arc curvature const _ = bendX + bendY; // silence unused const dotR = lerp(5, 11, localT); // Trailing arc — short segment from previous step const u0 = Math.max(0, u - 0.06); const sx = (1 - u0) * ((1 - u0) * src.x + u0 * midX) + u0 * ((1 - u0) * midX + u0 * tgtX); const sy = (1 - u0) * ((1 - u0) * src.y + u0 * midY) + u0 * ((1 - u0) * midY + u0 * tgtY); // At very end, draw a short capability wedge marker const wedgeOp = clamp01((localT - 0.85) / 0.15); const wa1 = a - (Math.PI / 8) * 0.7; const wa2 = a + (Math.PI / 8) * 0.7; const Rin = R - 10, Rou = R + 18; const wedgeD = ` M ${cx + Math.cos(wa1) * Rin} ${cy + Math.sin(wa1) * Rin} L ${cx + Math.cos(wa1) * Rou} ${cy + Math.sin(wa1) * Rou} A ${Rou} ${Rou} 0 0 1 ${cx + Math.cos(wa2) * Rou} ${cy + Math.sin(wa2) * Rou} L ${cx + Math.cos(wa2) * Rin} ${cy + Math.sin(wa2) * Rin} A ${Rin} ${Rin} 0 0 0 ${cx + Math.cos(wa1) * Rin} ${cy + Math.sin(wa1) * Rin} Z`; return ( {/* Trailing dotted arc */} {localT > 0.1 && localT < 0.95 && ( )} {/* Moving node */} {/* Final wedge bloom at endpoint */} {wedgeOp > 0 && ( )} ); })} {/* Center hub */} {hub > 0.02 && ( STANDARD )}
); } Object.assign(window, { Trans02to03, Trans03to04, Trans04to05 });