Skip to content

Instantly share code, notes, and snippets.

@ThimoDEV
Forked from ryanflorence/stage.tsx
Created December 22, 2023 20:36
Show Gist options
  • Save ThimoDEV/4a7000559ad72d515964cb10ed786e33 to your computer and use it in GitHub Desktop.
Save ThimoDEV/4a7000559ad72d515964cb10ed786e33 to your computer and use it in GitHub Desktop.

Revisions

  1. @ryanflorence ryanflorence renamed this gist Jun 19, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. @ryanflorence ryanflorence revised this gist Jun 19, 2022. 1 changed file with 12 additions and 8 deletions.
    20 changes: 12 additions & 8 deletions usage.tsx
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,15 @@
    <ScrollStage pages={2}>
    <Actor start={0} end={0.5}>
    <p>I'll be here from start to 50%</p>
    </Actor>
    <Actor start={0.5}>
    <p>I'll be here from 50% on.</p>
    </Actor>
    </ScrollStage>
    function App() {
    return (
    <ScrollStage pages={2}>
    <Actor start={0} end={0.5}>
    <p>I'll be here from start to 50%</p>
    </Actor>
    <Actor start={0.5}>
    <p>I'll be here from 50% on.</p>
    </Actor>
    </ScrollStage>
    )
    }

    // hooks access info about the progress
    // for an actor (relative to stage) or the
  3. @ryanflorence ryanflorence revised this gist Jun 19, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion usage.tsx
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@
    </Actor>
    <Actor start={0.5}>
    <p>I'll be here from 50% on.</p>
    <Actor>
    </Actor>
    </ScrollStage>

    // hooks access info about the progress
  4. @ryanflorence ryanflorence revised this gist Jun 17, 2022. No changes.
  5. @ryanflorence ryanflorence renamed this gist Jun 17, 2022. 1 changed file with 1 addition and 5 deletions.
    6 changes: 1 addition & 5 deletions stage.ts → stage.tsx
    Original file line number Diff line number Diff line change
    @@ -186,9 +186,5 @@ export function useRelativeWindowScroll(
    ): number {
    let windowScroll = useWindowScroll(fallback);
    if (!ref.current) return fallback;
    return (
    // windowScroll - ref.current.offsetTop + document.documentElement.clientHeight
    windowScroll - ref.current.offsetTop + window.innerHeight
    // windowScroll - ref.current.offsetTop
    );
    return windowScroll - ref.current.offsetTop + window.innerHeight;
    }
  6. @ryanflorence ryanflorence revised this gist Jun 17, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion usage.tsx
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@
    <Actor start={0.5}>
    <p>I'll be here from 50% on.</p>
    <Actor>
    <ScrollStage>
    </ScrollStage>

    // hooks access info about the progress
    // for an actor (relative to stage) or the
  7. @ryanflorence ryanflorence created this gist Jun 17, 2022.
    194 changes: 194 additions & 0 deletions stage.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,194 @@
    // 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<TFrame>({
    isDefault: true,
    frame: 0,
    progress: 0,
    length: 0,
    });

    let ActorContext = createContext<TFrame>({
    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 <StageContext.Provider value={context} children={children} />;
    };

    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 ? (
    <ActorContext.Provider value={context} children={children} />
    ) : null;
    };

    export let ScrollStage = ({
    pages,
    fallbackFrame = 0,
    fallbackLength = 1080,
    DEBUG = false,
    children,
    }: TScrollStageProps) => {
    let ref = useRef<HTMLDivElement>(null);
    let relativeScroll = useRelativeWindowScroll(ref, fallbackFrame);
    // let getLength = () => document.documentElement.clientHeight * pages;
    let getLength = () => window.innerHeight * pages;
    let hydrated = useHydrated();
    let [length, setLength] = useState<number>(() => {
    return hydrated ? getLength() : fallbackLength;
    });

    // set length after server render
    useEffect(() => setLength(getLength()), []);
    useOnResize(useCallback(() => setLength(getLength()), [pages]));

    return (
    <Stage
    frame={Math.max(0, Math.min(relativeScroll, length))}
    length={length}
    DEBUG={DEBUG}
    >
    <div ref={ref} style={{ height: `${pages * 100}vh` }}>
    {children}
    </div>
    </Stage>
    );
    };

    ////////////////////////////////////////////////////////////////////////////////
    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<number>(
    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<HTMLElement>,
    fallback: number = 0
    ): number {
    let windowScroll = useWindowScroll(fallback);
    if (!ref.current) return fallback;
    return (
    // windowScroll - ref.current.offsetTop + document.documentElement.clientHeight
    windowScroll - ref.current.offsetTop + window.innerHeight
    // windowScroll - ref.current.offsetTop
    );
    }
    15 changes: 15 additions & 0 deletions usage.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    <ScrollStage pages={2}>
    <Actor start={0} end={0.5}>
    <p>I'll be here from start to 50%</p>
    </Actor>
    <Actor start={0.5}>
    <p>I'll be here from 50% on.</p>
    <Actor>
    <ScrollStage>

    // hooks access info about the progress
    // for an actor (relative to stage) or the
    // the entire stage
    let actor = useActor()
    let stage = useStage()