diff --git a/src/components/Landing.tsx b/src/components/Landing.tsx index c0e7d5b..7a895d3 100644 --- a/src/components/Landing.tsx +++ b/src/components/Landing.tsx @@ -1389,7 +1389,6 @@ export function Landing() { storeLogout(); }; const videoRef = useRef(null); - const underTheHoodRef = useRef(null); const exploreScienceRef = useRef(null); // Hovering / focusing "Explore the science" brings the science backdrop into focus. const [scienceRevealed, setScienceRevealed] = useState(false); @@ -2782,7 +2781,6 @@ export function Landing() { {/* Matching intelligence — the science behind a single good match */}
@@ -2794,14 +2792,13 @@ export function Landing() { }} /> - {/* The science page's orbit animation, blurred, centred under the pointer; it sharpens and - settles on "Explore the science" while that button is hovered or focused. */} + {/* The science page's orbit animation, centred on "Explore the science": blurred, and it + comes into focus while that button is hovered or focused. */} {/* It spans the curved top cap and the bottom fade strip as well: --cap / --fade mirror their heights, and the mask follows the cap's ellipse and the strip's fade-out. */} {enableOrbitBackdrop && ( ; - /** Element the centre snaps onto while `revealed`. */ + /** Element the disc is centred on. */ anchor: RefObject; className?: string; /** Extra positioning / masking for the root (merged under the blur and opacity). */ @@ -80,66 +71,50 @@ export function ScienceOrbitBackdrop({ const { layout: hero, frame } = useScienceHeroDisc(); const disc = { cx: 0, cy: 0, tilt: hero.tilt, orbitScale: hero.orbitScale, logoR: hero.logoR }; - // Centre of the system, in section pixels. - const centerX = useMotionValue(0); - const centerY = useMotionValue(0); - const cx = useSpring(centerX, { stiffness: 110, damping: 22, mass: 0.7 }); - const cy = useSpring(centerY, { stiffness: 110, damping: 22, mass: 0.7 }); - // Parallax is replaced by following the pointer; the rings still take motion values. + // Centre of the system, in section pixels: always on the anchor. + const cx = useMotionValue(0); + const cy = useMotionValue(0); + // The rings take parallax motion values; here they stay still. const still = useMotionValue(0); - const home = useCallback(() => { - const r = rootRef.current?.getBoundingClientRect(); - if (!r) return; - centerX.set(r.width / 2); - centerY.set(r.height / 2); - }, [centerX, centerY]); - const pinToAnchor = useCallback(() => { - const root = rootRef.current?.getBoundingClientRect(); - const a = anchor.current?.getBoundingClientRect(); + const root = rootRef.current; + const a = anchor.current; if (!root || !a) return; - centerX.set(a.left + a.width / 2 - root.left); - centerY.set(a.top + a.height / 2 - root.top); - }, [anchor, centerX, centerY]); - - // Start in the middle without animating in from the corner. - useLayoutEffect(() => { - if (!size.w) return; - if (centerX.get() === 0 && centerY.get() === 0) { - cx.jump(size.w / 2); - cy.jump(size.h / 2); - home(); + // Layout offsets, not on-screen rects: the button's section fades up into place (a transform), + // and the disc should sit where the button settles, not where it is mid-animation. + const base = root.offsetParent; + let x = a.offsetWidth / 2; + let y = a.offsetHeight / 2; + let el: HTMLElement | null = a; + while (el && el !== base) { + x += el.offsetLeft; + y += el.offsetTop; + el = el.offsetParent as HTMLElement | null; } - }, [size.w, size.h, cx, cy, centerX, centerY, home]); - - // Follow the pointer; while revealed, stay pinned on the anchor instead. - useEffect(() => { - const target = pointerTarget.current; - if (!target || reduce) return; - const onMove = (e: PointerEvent) => { - if (revealed) return pinToAnchor(); - const r = rootRef.current?.getBoundingClientRect(); - if (!r) return; - centerX.set(e.clientX - r.left); - centerY.set(e.clientY - r.top); - }; - const onLeave = () => { - if (!revealed) home(); - }; - target.addEventListener('pointermove', onMove, { passive: true }); - target.addEventListener('pointerleave', onLeave); - return () => { - target.removeEventListener('pointermove', onMove); - target.removeEventListener('pointerleave', onLeave); - }; - }, [pointerTarget, reduce, revealed, pinToAnchor, home, centerX, centerY]); + if (el === base) { + cx.set(x - root.offsetLeft); + cy.set(y - root.offsetTop); + return; + } + const r = root.getBoundingClientRect(); + const ar = a.getBoundingClientRect(); + cx.set(ar.left + ar.width / 2 - r.left); + cy.set(ar.top + ar.height / 2 - r.top); + }, [anchor, cx, cy]); - // Snap onto the button the moment it is revealed (covers keyboard focus too), and back home after. + // Re-centre whenever the section or the button changes size (resize, fonts, layout). + useLayoutEffect(() => { + pinToAnchor(); + const a = anchor.current; + if (!a) return; + const ro = new ResizeObserver(pinToAnchor); + ro.observe(a); + return () => ro.disconnect(); + }, [size.w, size.h, anchor, pinToAnchor]); useEffect(() => { if (revealed) pinToAnchor(); - else if (reduce) home(); - }, [revealed, reduce, pinToAnchor, home]); + }, [revealed, pinToAnchor]); const onArrive = useCallback( (p: Pulse) => {