"use client"; import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; type ScrollSequenceProps = { /** Builds a frame image URL for a 1-based index (1..frameCount). */ frameSrc: (index: number) => string; frameCount: number; /** Intrinsic frame dimensions (the canvas drawing buffer). */ width: number; height: number; /** Scroll length of the pinned scrub, in vh (default 300 = ~3 screens). */ heightVh?: number; /** Accessible description of what the sequence depicts. */ label: string; className?: string; children?: React.ReactNode; // optional overlay (caption, etc.) }; /** * Scroll-scrubbed image sequence. A canvas is pinned (sticky) while * the section scrolls past; the frame drawn tracks scroll progress. Frames are * preloaded. Reduced motion => a single representative frame, no scrubbing. * * Used for the homepage lens-reveal (49 frames, 2560×1440, 300vh). */ export function ScrollSequence({ frameSrc, frameCount, width, height, heightVh = 300, label, className, children, }: ScrollSequenceProps) { const sectionRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { const section = sectionRef.current; const canvas = canvasRef.current; if (!section || !canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const images: HTMLImageElement[] = new Array(frameCount); let ready = 0; let current = -1; const draw = (i: number) => { const im = images[i]; if (i === current || !im || !im.complete) return; ctx.clearRect(0, 0, width, height); ctx.drawImage(im, 0, 0, width, height); current = i; }; const frameIndex = () => { const rect = section.getBoundingClientRect(); const span = section.offsetHeight - window.innerHeight; const p = span > 0 ? Math.min(1, Math.max(0, -rect.top / span)) : 0; return Math.round(p * (frameCount - 1)); }; let raf = 0; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; draw(frameIndex()); }); }; for (let i = 0; i < frameCount; i++) { const im = new Image(); im.onload = () => { ready++; if (i === 0) draw(0); if (ready === frameCount && !reduce) draw(frameIndex()); }; im.src = frameSrc(i + 1); images[i] = im; } if (!reduce) { window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); } return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, [frameSrc, frameCount, width, height]); return (
{children}
); }