Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/thatstupidcomputer / apps/web/src/animation/choreography/useWorkshopStory.ts |
| Date | Not specified in source |
| Repository | Janady13/thatstupidcomputer |
| Revision | 57555cde46910102dd25d6c1c1fb2c359e9655bd |
| Branch | main |
| Path or archive member | apps/web/src/animation/choreography/useWorkshopStory.ts |
| Original SHA-256 | 8c58a0912cbd2745261075edea19e10af6498f0e79bb9264ab7e6cb8f232f712 |
| Git blob SHA | 329ca0306d41358dcc90675789a664b2ce44b3d2 |
| Reading-copy SHA-256 | 8c58a0912cbd2745261075edea19e10af6498f0e79bb9264ab7e6cb8f232f712 |
Recovered source transcript
Download complete source text ↓55 lines · 3.4 KB
import { useCallback, useEffect, useRef, useState, type RefObject } from 'react';
import { useAnimation } from '../orchestration/AnimationProvider';
import { WORKSHOP_STORIES, type WorkshopBeat, type WorkshopStory } from '../performances/workshop';
/** A finite performance clock. The existing SVG controllers still own every joint. */
export function useWorkshopStory(root: RefObject<HTMLElement | null>, paused: boolean) {
const { send } = useAnimation();
const [selection, setSelection] = useState<{ story: WorkshopStory; index: number; run: number } | null>(null);
const [reduced, setReduced] = useState(true);
const [visible, setVisible] = useState(false);
const [hidden, setHidden] = useState(false);
const [holding, setHolding] = useState(false);
const run = useRef(0);
const remainder = useRef(0);
const story = selection ? WORKSHOP_STORIES[selection.story] : null;
const beat: WorkshopBeat | undefined = selection ? story?.beats[selection.index] : undefined;
const frozen = paused || reduced || hidden || !visible || holding;
const final = !!selection && selection.index === (story?.beats.length ?? 0) - 1;
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
const sync = () => { setReduced(media.matches); setHidden(document.hidden); };
sync();
media.addEventListener('change', sync);
document.addEventListener('visibilitychange', sync);
// A quick scroll can queue several changes for this one stage. Only the
// newest entry describes whether its clock should be running now.
const observer = new IntersectionObserver(entries => setVisible(entries.at(-1)?.isIntersecting ?? false), { threshold: 0.15 });
if (root.current) observer.observe(root.current);
return () => { media.removeEventListener('change', sync); document.removeEventListener('visibilitychange', sync); observer.disconnect(); };
}, [root]);
useEffect(() => {
if (!beat) return;
remainder.current = beat.seconds * 1000;
send({ type: 'ATTITUDE', enabled: false });
send({ type: 'PERFORM', action: beat.mobile });
send({ type: 'DESKTOP', state: beat.desktop });
}, [beat, selection?.run, send]);
useEffect(() => {
if (!selection || !beat || frozen || final) return;
const started = performance.now();
const timer = window.setTimeout(() => setSelection(current => current && current.run === selection.run && current.index === selection.index ? { ...current, index: current.index + 1 } : current), remainder.current);
return () => { window.clearTimeout(timer); remainder.current = Math.max(0, remainder.current - (performance.now() - started)); };
}, [selection, beat, frozen, final]);
const start = useCallback((name: WorkshopStory) => { setHolding(false); setSelection({ story: name, index: 0, run: ++run.current }); }, []);
const stop = useCallback(() => { setSelection(null); setHolding(false); send({ type: 'CUE', cue: 'scene.rest' }); }, [send]);
const move = useCallback((direction: number) => setSelection(current => current ? { ...current, index: Math.max(0, Math.min(WORKSHOP_STORIES[current.story].beats.length - 1, current.index + direction)) } : null), []);
return { active: selection !== null, selection, story, beat, final, frozen, reduced, holding, start, stop, next: () => move(1), previous: () => move(-1), toggle: () => setHolding(value => !value) };
}
export type WorkshopStoryControls = ReturnType<typeof useWorkshopStory>;