-
-
Save ThimoDEV/4a7000559ad72d515964cb10ed786e33 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 + window.innerHeight; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
// the entire stage | |
let actor = useActor() | |
let stage = useStage() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment