Download source/src/useScrollStory.ts from CladeTeam/ouroboros: direct link, hf CLI and curl.
- Browser
- Download file 3.07 kB
-
https://huggingface.co/spaces/CladeTeam/ouroboros/resolve/main/source/src/useScrollStory.ts
- Command line
-
hf download hf://spaces/CladeTeam/ouroboros/source/src/useScrollStory.ts
-
curl -L -o useScrollStory.ts https://huggingface.co/spaces/CladeTeam/ouroboros/resolve/main/source/src/useScrollStory.ts
3.07 kB
| import {useCallback, useEffect, useRef, useState} from 'react'; | |
| import type {RefObject} from 'react'; | |
| import {clamp} from './dualStory'; | |
| export function useScrollStory(section: RefObject<HTMLDivElement | null>, stage: RefObject<HTMLDivElement | null>, enabled: boolean, flow: boolean) { | |
| const [progress, setProgress] = useState(0); | |
| const current = useRef(0); | |
| const commit = useCallback((p: number) => { current.current = p; setProgress(p); }, []); | |
| const position = useCallback(() => { | |
| if (!section.current || !stage.current) return 0; | |
| const top = parseFloat(getComputedStyle(stage.current).top) || 0; | |
| const span = section.current.offsetHeight - stage.current.offsetHeight; | |
| return span > 0 ? clamp((top - section.current.getBoundingClientRect().top) / span) : 0; | |
| }, [section, stage]); | |
| useEffect(() => { | |
| if (flow || !enabled) return; | |
| let frame = 0, last = 0, visible = false, disposed = false; | |
| let target = current.current; | |
| const stop = () => { cancelAnimationFrame(frame); frame = 0; last = 0; }; | |
| const tick = (now: number) => { | |
| frame = 0; | |
| if (!visible || document.hidden || disposed) return; | |
| const dt = last ? Math.min(64, now - last) : 16; | |
| last = now; | |
| const next = current.current + (target - current.current) * (1 - Math.exp(-dt / 85)); | |
| if (Math.abs(target - next) < .00008) { commit(target); last = 0; } | |
| else { commit(next); frame = requestAnimationFrame(tick); } | |
| }; | |
| const update = () => { | |
| if (!visible || document.hidden || disposed) return; | |
| target = position(); | |
| if (!frame && target !== current.current) frame = requestAnimationFrame(tick); | |
| }; | |
| const visibility = () => { if (document.hidden) stop(); else update(); }; | |
| const observer = new IntersectionObserver(([entry]) => { | |
| visible = entry.isIntersecting; | |
| if (visible) update(); else stop(); | |
| }); | |
| if (stage.current) observer.observe(stage.current); | |
| const resize = new ResizeObserver(update); | |
| if (section.current) resize.observe(section.current); | |
| resize.observe(document.body); | |
| window.addEventListener('scroll', update, {passive:true}); | |
| window.addEventListener('resize', update); | |
| document.addEventListener('visibilitychange', visibility); | |
| document.fonts.ready.then(update); | |
| return () => { disposed = true; stop(); observer.disconnect(); resize.disconnect(); window.removeEventListener('scroll', update); window.removeEventListener('resize', update); document.removeEventListener('visibilitychange', visibility); }; | |
| }, [enabled, flow, position, commit, section, stage]); | |
| const seek = useCallback((p: number) => { | |
| if (!section.current || !stage.current || flow) return; | |
| const offset = parseFloat(getComputedStyle(stage.current).top) || 0; | |
| const span = section.current.offsetHeight - stage.current.offsetHeight; | |
| // Explicit controls remain usable while paused; scrolling never advances a paused scene. | |
| commit(clamp(p)); | |
| window.scrollTo({top:section.current.getBoundingClientRect().top + scrollY - offset + span * clamp(p), behavior:'instant'}); | |
| }, [section, stage, flow, commit]); | |
| return {progress, seek}; | |
| } | |