// Shared-element scroll morphs between sections. // Real elements (e.g. 6 bento cards in section 02) literally fly out and land // as the real destination elements (3 pillars in section 03). Each zone uses // a for scroll runway; is one fixed overlay that // renders twin elements interpolated from src bounding rects to dst rects. // // Zone 23 → 6 bento cards → 3 OS pillars // Zone 34 → 6 mini rungs → 6 ladder rungs // Zone 45 → 6 ladder rungs → 6 capability wedges (out of 8) // // CSS variables --p23 / --p34 / --p45 are driven continuously by scroll so the // surrounding sections can fade + blur their content in/out around the morph. function MorphSpacer({ id, vh = 90 }) { return ( ); } /* ---------- math ---------- */ const _clamp01 = v => Math.max(0, Math.min(1, v)); const _lerp = (a, b, t) => a + (b - a) * t; const _easeInOut = t => t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2; const _easeOut = t => 1 - Math.pow(1 - t, 3); const _easeIn = t => t * t; /* ---------- 02 → 03 mapping ---------- 6 bento cards consolidate into 3 slabs, paired by colour family. Each pair has a "lead" (takes on the slab clip + gradient at the end) and a "follower" (fades out mid-flight as it converges on the lead). */ const T23_CARDS = [ { lead: true, dst: 0, bg: 'linear-gradient(155deg, #0D903A 0%, #074c2a 100%)' }, { lead: true, dst: 1, bg: 'linear-gradient(135deg, #1a3552 0%, #06192e 100%)' }, { lead: false, dst: 0, bg: 'linear-gradient(155deg, #d8efde 0%, #b3e2c0 100%)' }, { lead: false, dst: 1, bg: 'linear-gradient(155deg, #dde9f7 0%, #b8cfe9 100%)' }, { lead: true, dst: 2, bg: 'linear-gradient(155deg, #ffe28a 0%, #ffbb40 100%)' }, { lead: false, dst: 2, bg: 'linear-gradient(135deg, #0f1620 0%, #1e2433 100%)' }, ]; const T23_SLAB_CLIPS = [ 'polygon(0% 0%, 100% 0%, 86% 100%, 0% 100%)', 'polygon(14% 0%, 100% 0%, 86% 100%, 0% 100%)', 'polygon(14% 0%, 100% 0%, 100% 100%, 0% 100%)', ]; const T23_SLAB_GRADS = [ ['#0D903A', '#074c2a'], ['#246BB2', '#164A97'], ['#FFBA00', '#8C6100'], ]; /* ---------- 03 → 04 mapping ---------- 6 mini rungs inside pillar 01 (labels: Case / Sim / Mimic / Shadow / Supervise / Independent) → 6 actual ladder rungs in section 04 by index. Pillars 02 + 03 fade out via CSS — they were context, the ladder is the act. */ /* ---------- 04 → 05 mapping ---------- 6 ladder rungs orbit through the wheel centre and settle as 6 of the 8 capability wedges. The 2 remaining wedges (Value, Reactivation) appear independently via the section's --p45 fade-in. */ const T45_CAPS = [ { idx: 0, color1: '#0D903A', color2: '#074c2a' }, // Finance (Case) { idx: 1, color1: '#0F8E42', color2: '#063e1f' }, // Business (Sim) { idx: 2, color1: '#246BB2', color2: '#164A97' }, // Macro (Mimic) { idx: 3, color1: '#1A549F', color2: '#0e3669' }, // Products (Shadow) { idx: 5, color1: '#19519C', color2: '#0e3669' }, // Acquisition (Supervise) { idx: 7, color1: '#057430', color2: '#03461e' }, // Execution (Independent) ]; function MorphLayer() { const [, setTick] = React.useState(0); const stateRef = React.useRef({ z23: { p: 0, src: [], dst: [] }, z34: { p: 0, src: [], dst: [] }, z45: { p: 0, src: [], dst: [] }, }); // useLayoutEffect → variables set before first paint so users landing // mid-page don't see a flash of mis-staged sections. React.useLayoutEffect(() => { let raf = 0; const zones = [ { key: 'z23', spacer: 'morph-23', cssVar: '--p23', srcSel: '.bento > .bento-card', dstSel: '.os-slabs .os-slab', srcN: 6, dstN: 3 }, { key: 'z34', spacer: 'morph-34', cssVar: '--p34', srcSel: '.os-slab--ladder .os-slab-rung', dstSel: '.ladder-3d .rung', srcN: 6, dstN: 6 }, { key: 'z45', spacer: 'morph-45', cssVar: '--p45', srcSel: '.ladder-3d .rung', dstSel: '.caprich-wedge', srcN: 6, dstN: 8 }, ]; function measure() { raf = 0; const viewH = window.innerHeight; const next = {}; for (const z of zones) { const spacer = document.getElementById(z.spacer); if (!spacer) { next[z.key] = { p: 0, src: [], dst: [] }; continue; } const sr = spacer.getBoundingClientRect(); const total = sr.height + viewH; const passed = viewH - sr.top; const p = _clamp01(passed / total); document.documentElement.style.setProperty(z.cssVar, p.toFixed(4)); const srcEls = document.querySelectorAll(z.srcSel); const dstEls = document.querySelectorAll(z.dstSel); if (srcEls.length !== z.srcN || dstEls.length !== z.dstN) { next[z.key] = { p, src: [], dst: [] }; continue; } const src = [], dst = []; for (let i = 0; i < srcEls.length; i++) src.push(srcEls[i].getBoundingClientRect()); for (let i = 0; i < dstEls.length; i++) dst.push(dstEls[i].getBoundingClientRect()); next[z.key] = { p, src, dst }; } stateRef.current = next; setTick(t => t + 1); } function schedule() { if (!raf) raf = requestAnimationFrame(measure); } measure(); window.addEventListener('scroll', schedule, { passive: true }); window.addEventListener('resize', schedule); return () => { window.removeEventListener('scroll', schedule); window.removeEventListener('resize', schedule); if (raf) cancelAnimationFrame(raf); }; }, []); const { z23, z34, z45 } = stateRef.current; return ( ); } function MorphZone({ show, p, children }) { if (!show || p <= 0.001 || p >= 0.999) return null; // Crossfade against real source/dest at the ends of the zone. // Wider in/out windows so twins overlap longer with the real elements // and there's no visible pop on either side. const fade = _easeInOut(_clamp01((p - 0.06) / 0.18)) * (1 - _easeInOut(_clamp01((p - 0.84) / 0.14))); return (
{children}
); } /* ============================================================ 02 → 03 — 6 bento cards consolidate into 3 pillars ============================================================ */ function T23Twins({ p, src, dst }) { const tLead = _easeInOut(p); const tFollower = _easeInOut(_clamp01(p / 0.65)); // Compute every lead's interpolated rect so followers chase the moving lead. const leadRects = {}; for (let i = 0; i < T23_CARDS.length; i++) { const c = T23_CARDS[i]; if (!c.lead) continue; const sr = src[i], dr = dst[c.dst]; leadRects[c.dst] = { x: _lerp(sr.left, dr.left, tLead), y: _lerp(sr.top, dr.top, tLead), w: _lerp(sr.width, dr.width, tLead), h: _lerp(sr.height,dr.height,tLead), }; } const ordered = T23_CARDS .map((c, i) => ({ ...c, i })) .sort((a, b) => (a.lead ? 1 : 0) - (b.lead ? 1 : 0)); return ordered.map((c) => { const i = c.i; const sr = src[i]; const dr = dst[c.dst]; const slabClip = T23_SLAB_CLIPS[c.dst]; const [g1, g2] = T23_SLAB_GRADS[c.dst]; let x, y, w, h, op, clip, bg, radius, shadow; if (c.lead) { x = _lerp(sr.left, dr.left, tLead); y = _lerp(sr.top, dr.top, tLead); w = _lerp(sr.width, dr.width, tLead); h = _lerp(sr.height,dr.height,tLead); const blend = _easeOut(_clamp01((tLead - 0.30) / 0.55)); bg = blend < 0.02 ? c.bg : `linear-gradient(155deg, color-mix(in oklab, ${g1} ${blend*100}%, ${_extractStop(c.bg, 0)}) 0%, color-mix(in oklab, ${g2} ${blend*100}%, ${_extractStop(c.bg, 1)}) 100%)`; const clipBlend = _clamp01((tLead - 0.55) / 0.40); clip = clipBlend < 0.02 ? 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)' : _interpolateClip( 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', slabClip, _easeInOut(clipBlend) ); radius = _lerp(24, 0, _easeOut(tLead)); shadow = `0 ${_lerp(20, 50, tLead)}px ${_lerp(40, 120, tLead)}px rgba(5,10,20,${_lerp(0.18, 0.55, tLead)})`; op = 1; } else { const leadR = leadRects[c.dst]; x = _lerp(sr.left, leadR.x, tFollower); y = _lerp(sr.top, leadR.y, tFollower); w = _lerp(sr.width, leadR.w, tFollower); h = _lerp(sr.height,leadR.h, tFollower); bg = c.bg; clip = 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)'; radius = _lerp(24, 18, tFollower); op = Math.max(0, 1 - _easeIn(tFollower) * 1.15); shadow = `0 ${_lerp(14, 30, tFollower)}px ${_lerp(28, 60, tFollower)}px rgba(5,10,20,${_lerp(0.14, 0.0, tFollower)})`; } return (
); }); } /* ============================================================ 03 → 04 — 6 mini rungs → 6 ladder rungs ============================================================ */ function T34Twins({ p, src, dst }) { const tBase = _easeInOut(p); return src.map((sr, i) => { const dr = dst[i]; // Subtle stagger — top mini rung leaves slightly later so the stack // unfurls "from the bottom up" into the ladder. const stagger = i * 0.05; const t = _easeInOut(_clamp01((p - stagger) / (1 - stagger * 1.2))); const x = _lerp(sr.left, dr.left, t); const y = _lerp(sr.top, dr.top, t); const w = _lerp(sr.width, dr.width, t); const h = _lerp(sr.height,dr.height,t); const radius = _lerp(8, 14, t); // Source mini-rung look (white tile over green slab) → // destination ladder plate (deep dark with gold trim). const blend = _easeOut(_clamp01((t - 0.20) / 0.55)); const bg = `linear-gradient(155deg, color-mix(in oklab, #0c1424 ${blend*100}%, rgba(255,255,255,0.10)) 0%, color-mix(in oklab, #1d2940 ${blend*100}%, rgba(255,255,255,0.04)) 100%)`; const borderC = `rgba(255,186,0, ${_lerp(0.0, 0.35, blend).toFixed(2)})`; const shadow = `0 ${_lerp(2, 20, t).toFixed(1)}px ${_lerp(8, 50, t).toFixed(1)}px rgba(5,10,20,${_lerp(0.10, 0.45, t).toFixed(2)}), inset 0 1px 0 rgba(255,255,255,${_lerp(0.10, 0.20, t).toFixed(2)})`; // Small accent line at the bottom — mirrors the ::after gold line on // the real ladder rungs — appears toward the end of the morph. const accentOp = _easeOut(_clamp01((t - 0.55) / 0.40)); return (
); }); } /* ============================================================ 04 → 05 — 6 ladder rungs orbit into 6 capability wedges ============================================================ */ function T45Twins({ p, src, dst }) { const t = _easeInOut(p); // Wheel centre, derived from the average of all wedge bboxes. let cx = 0, cy = 0, n = 0; for (const dr of dst) { cx += dr.left + dr.width / 2; cy += dr.top + dr.height / 2; n++; } cx /= n; cy /= n; return src.map((sr, i) => { const cap = T45_CAPS[i]; const dr = dst[cap.idx]; // Stagger so the bottom rung leads — feels like an unfurling vortex. const stagger = i * 0.04; const u = _easeInOut(_clamp01((p - stagger) / (1 - stagger * 1.2))); const startX = sr.left + sr.width / 2; const startY = sr.top + sr.height / 2; const endX = dr.left + dr.width / 2; const endY = dr.top + dr.height / 2; // Quadratic bezier with control point pulled toward wheel centre — // gives every rung a curved orbital trajectory. const ctrlX = _lerp((startX + endX) / 2, cx, 0.45); const ctrlY = _lerp((startY + endY) / 2, cy, 0.45); const px = (1-u)*(1-u)*startX + 2*(1-u)*u*ctrlX + u*u*endX; const py = (1-u)*(1-u)*startY + 2*(1-u)*u*ctrlY + u*u*endY; // Size lerp from rung dims (horizontal plate) → wedge dims (small square-ish). const w = _lerp(sr.width, Math.min(dr.width, dr.height), u); const h = _lerp(sr.height,Math.min(dr.width, dr.height), u); const x = px - w / 2; const y = py - h / 2; const radius = _lerp(14, 200, _easeOut(u)); // rectangle → almost-circle const blend = _easeOut(_clamp01((u - 0.25) / 0.55)); const bg = `linear-gradient(135deg, color-mix(in oklab, ${cap.color1} ${blend*100}%, #FFBA00) 0%, color-mix(in oklab, ${cap.color2} ${blend*100}%, #8C6100) 100%)`; // Slight rotation toward the wedge's outward direction const dx = endX - cx, dy = endY - cy; const angle = Math.atan2(dy, dx) * 180 / Math.PI + 90; const rot = _lerp(0, angle, u); return (
); }); } /* ---------- helpers ---------- */ // Extract a representative colour stop from a CSS gradient string. function _extractStop(grad, idx) { const m = grad.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)/g); if (!m || !m.length) return grad; return m[Math.min(idx, m.length - 1)]; } // Interpolate two polygon() clip-paths point-by-point. function _interpolateClip(a, b, t) { const parse = s => { const inner = s.match(/polygon\(([^)]+)\)/); if (!inner) return null; return inner[1].split(',').map(pt => { const [x, y] = pt.trim().split(/\s+/); return [parseFloat(x), parseFloat(y)]; }); }; const pa = parse(a), pb = parse(b); if (!pa || !pb || pa.length !== pb.length) return b; const pts = pa.map((p, i) => [ _lerp(p[0], pb[i][0], t), _lerp(p[1], pb[i][1], t), ]); return 'polygon(' + pts.map(p => `${p[0]}% ${p[1]}%`).join(', ') + ')'; } Object.assign(window, { MorphSpacer, MorphLayer });