// Spine — a single continuous golden thread that runs down the right edge of // the page from section 02 through 05, morphing shape as you scroll: // 02 chaos squiggle (market pressure) // 03 three parallel (pillars) // 04 ladder rungs (performance) // 05 perfect circle (capability wheel) // // One fixed SVG, 4 path states, crossfaded by scroll progress. function Spine() { const [progress, setProgress] = React.useState(0); // 0..4 (which section we're in) const [stops, setStops] = React.useState(null); // { y0, y02, y03, y04, y05, y05end } const [headY, setHeadY] = React.useState(0.5); // 0..1 within viewport // Measure section tops on mount + resize React.useEffect(() => { function measure() { const s02 = document.getElementById('pressure'); const s03 = document.getElementById('system'); const s04 = document.getElementById('ladder'); const s05 = document.getElementById('capabilities'); if (!s02 || !s03 || !s04 || !s05) return; const off = (el) => el.getBoundingClientRect().top + window.scrollY; const next = { y02: off(s02), y03: off(s03), y04: off(s04), y05: off(s05), y05end: off(s05) + s05.offsetHeight, }; setStops(next); } measure(); window.addEventListener('resize', measure); // Re-measure once images / layout settle. const t1 = setTimeout(measure, 500); const t2 = setTimeout(measure, 1500); return () => { window.removeEventListener('resize', measure); clearTimeout(t1); clearTimeout(t2); }; }, []); // Track scroll → progress React.useEffect(() => { if (!stops) return; function onScroll() { const y = window.scrollY + window.innerHeight * 0.55; // Piecewise: 02 → 03 → 04 → 05 → end. Each segment maps to 1.0 of progress. let p; if (y < stops.y02) p = 0; else if (y < stops.y03) p = (y - stops.y02) / (stops.y03 - stops.y02); else if (y < stops.y04) p = 1 + (y - stops.y03) / (stops.y04 - stops.y03); else if (y < stops.y05) p = 2 + (y - stops.y04) / (stops.y05 - stops.y04); else if (y < stops.y05end) p = 3 + (y - stops.y05) / (stops.y05end - stops.y05); else p = 4; setProgress(p); // The head dot tracks scroll position within the viewport (very subtle). setHeadY(0.5); } onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, [stops]); // Show/hide envelope. Hide before 02 and after 05. const visible = progress > 0 && progress < 4; // Crossfade weights for each of the 4 states (squiggle / pillars / rungs / circle) function w(stateIdx) { // Triangle peak at stateIdx, zero at ±1 away const d = Math.abs(progress - 0.5 - stateIdx); return Math.max(0, 1 - d); } return ( ); } window.Spine = Spine;