// TODO: make `pages` optional and measure the div when unspecified, this will // allow more normal document flow and make it easier to do both mobile and // desktop. import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; //////////////////////////////////////////////////////////////////////////////// interface TStageProps { frame: number; length: number; children: React.ReactNode; DEBUG?: boolean; } interface TActorProps { type?: "progress" | "frame"; start: number; end?: number; persistent?: boolean; children: React.ReactNode; } interface TScrollStageProps { pages: number; fallbackFrame?: number; fallbackLength?: number; children: React.ReactNode; DEBUG?: boolean; } interface TFrame { isDefault?: boolean; frame: number; progress: number; length: number; } //////////////////////////////////////////////////////////////////////////////// let StageContext = createContext({ isDefault: true, frame: 0, progress: 0, length: 0, }); let ActorContext = createContext({ isDefault: true, frame: 0, progress: 0, length: 0, }); //////////////////////////////////////////////////////////////////////////////// export let Stage = ({ frame, length, DEBUG, children }: TStageProps) => { let progress = frame / length; let context = useMemo(() => { let context: TFrame = { frame, progress, length }; return context; }, [frame, progress, length]); if (DEBUG) console.log(context); return ; }; export let Actor = ({ type = "progress", start: startProp, end: endProp, persistent = false, children, }: TActorProps) => { let stage = useContext(StageContext); let actor = useActor(); let parent = actor.isDefault ? stage : actor; let start = type === "progress" ? startProp * parent.length : startProp; let end = endProp ? type === "progress" ? endProp * parent.length : endProp : parent.length; let length = end - start; let frame = parent.frame - start; let progress = Math.max(0, Math.min(frame / length, 1)); let context = useMemo(() => { let context: TFrame = { frame, progress, length }; return context; }, [frame, progress, length]); let onStage = persistent ? true : parent.frame >= start && (endProp ? parent.frame < end : true); return onStage ? ( ) : null; }; export let ScrollStage = ({ pages, fallbackFrame = 0, fallbackLength = 1080, DEBUG = false, children, }: TScrollStageProps) => { let ref = useRef(null); let relativeScroll = useRelativeWindowScroll(ref, fallbackFrame); // let getLength = () => document.documentElement.clientHeight * pages; let getLength = () => window.innerHeight * pages; let hydrated = useHydrated(); let [length, setLength] = useState(() => { return hydrated ? getLength() : fallbackLength; }); // set length after server render useEffect(() => setLength(getLength()), []); useOnResize(useCallback(() => setLength(getLength()), [pages])); return (
{children}
); }; //////////////////////////////////////////////////////////////////////////////// export function useActor(): TFrame { return useContext(ActorContext); } export function useStage(): TFrame { return useContext(StageContext); } let hydrated = false; function useHydrated() { useEffect(() => { hydrated = true; }); return hydrated; } export function useOnResize(fn: () => void) { useEffect(() => { window.addEventListener("resize", fn); return () => window.removeEventListener("resize", fn); }, [fn]); } export function useWindowScroll(fallback: number = 0): number { let [scroll, setScroll] = useState( typeof window === "undefined" ? fallback : window.scrollY ); let handleScroll = useCallback(() => { setScroll(window.scrollY); }, []); useEffect(() => { handleScroll(); window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []); useOnResize(handleScroll); return scroll; } export function useRelativeWindowScroll( ref: React.RefObject, fallback: number = 0 ): number { let windowScroll = useWindowScroll(fallback); if (!ref.current) return fallback; return windowScroll - ref.current.offsetTop + window.innerHeight; }