"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; type ScrollFilmProps = { src: string; poster: string; children: ReactNode; }; function easeInOutCubic(value: number) { return value < 0.5 ? 4 * value * value * value : 1 - (-2 * value + 2) ** 3 / 2; } export function ScrollFilm({ src, poster, children }: ScrollFilmProps) { const orbitRef = useRef(null); const filmRef = useRef(null); const [loading, setLoading] = useState(true); useEffect(() => { const orbit = orbitRef.current; const film = filmRef.current; if (!orbit || !film) { return; } film.muted = true; film.defaultMuted = true; film.playsInline = true; film.preload = "auto"; film.pause(); let ready = false; let current = 0; let frame = 0; let blobUrl: string | null = null; let introStartedAt = 0; let introDone = false; const introMs = 2400; const markReady = () => { if (ready) { return; } ready = true; film.pause(); film.classList.add("is-ready"); setLoading(false); const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; introDone = reduce; introStartedAt = reduce ? 0 : performance.now(); }; fetch(src) .then((response) => response.blob()) .then((blob) => { blobUrl = URL.createObjectURL(blob); film.src = blobUrl; film.load(); }) .catch(() => { film.src = src; film.load(); }); const progressFromScroll = () => { const total = Math.max(orbit.offsetHeight - window.innerHeight, 1); const y = Math.min(Math.max(-orbit.getBoundingClientRect().top, 0), total); return y / total; }; const introSeconds = (now: number, duration: number) => { if (introDone || !introStartedAt) { return null; } if (progressFromScroll() > 0.02) { introDone = true; return null; } const elapsed = now - introStartedAt; const t = Math.min(elapsed / introMs, 1); const peak = Math.min(2, Math.max(duration * 0.22, 0.6)); const trip = t < 0.5 ? easeInOutCubic(t * 2) : 1 - easeInOutCubic((t - 0.5) * 2); if (t >= 1) { introDone = true; return 0; } return trip * peak; }; const tick = (now: number) => { const target = progressFromScroll(); current += (target - current) * 0.42; orbit.style.setProperty("--scrub", current.toFixed(4)); if (ready && film.duration && !film.seeking) { const intro = introSeconds(now, film.duration); const time = intro ?? current * (film.duration - 0.04); if (Math.abs(film.currentTime - time) >= 0.02) { film.currentTime = time; } } frame = requestAnimationFrame(tick); }; film.addEventListener("canplaythrough", markReady); const timeout = window.setTimeout(markReady, 10000); frame = requestAnimationFrame(tick); return () => { cancelAnimationFrame(frame); film.removeEventListener("canplaythrough", markReady); window.clearTimeout(timeout); if (blobUrl) { URL.revokeObjectURL(blobUrl); } }; }, [src]); return (
); }