import { useEffect, useMemo, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import { examples, groups } from './catalog' import './viewer.css' function Viewer() { const initial = new URLSearchParams(location.search).get('example') const [name, setName] = useState(examples.some(e => e.id === initial) ? initial! : examples[0].id) const [phone, setPhone] = useState(false) const [still, setStill] = useState(() => matchMedia('(prefers-reduced-motion: reduce)').matches) const motionOverride = useRef(false) const [playing, setPlaying] = useState(true) const [speed, setSpeed] = useState(1) const [revision, replay] = useState(0) const [elapsed, setElapsed] = useState(0) const [height, setHeight] = useState(460) const [ready, setReady] = useState(false) const [staticScene, setStaticScene] = useState(false) const [error, setError] = useState(null) const iframe = useRef(null) const visible = useRef(false) const lesson = useRef(new URLSearchParams(location.search).get('lesson') ?? 'label') const annotation = useRef(new URLSearchParams(location.search).get('annotation') ?? 'collect') const selected = examples.find(e => e.id === name)! const transportDisabled = !ready || still || staticScene const source = useMemo(() => `?embed=${name}&still=${still ? 1 : 0}${name === 'contrasts' ? `&lesson=${encodeURIComponent(lesson.current)}` : name === 'annotations' ? `&annotation=${encodeURIComponent(annotation.current)}` : ''}`, [name, revision, still]) const send = (data: object) => iframe.current?.contentWindow?.postMessage({ type: 'stageshow:control', ...data }, location.origin) useEffect(() => { const preference = matchMedia('(prefers-reduced-motion: reduce)') const change = () => { if (!motionOverride.current) setStill(preference.matches) } preference.addEventListener('change', change) return () => preference.removeEventListener('change', change) }, []) useEffect(() => { const receive = (event: MessageEvent) => { if (event.origin !== location.origin || event.source !== iframe.current?.contentWindow || event.data?.type !== 'stageshow:status') return if (typeof event.data.lesson === 'string') { lesson.current = event.data.lesson const url = new URL(location.href); url.searchParams.set('lesson', event.data.lesson); history.replaceState(null, '', url) } if (Number.isFinite(event.data.elapsed)) setElapsed(event.data.elapsed) if (typeof event.data.annotation === 'string') { annotation.current = event.data.annotation const url = new URL(location.href); url.searchParams.set('annotation', event.data.annotation); history.replaceState(null, '', url) } if (typeof event.data.staticScene === 'boolean') setStaticScene(event.data.staticScene) if (Number.isFinite(event.data.height)) setHeight(Math.max(1, event.data.height)) if (event.data.error) { setError(String(event.data.error)); setReady(false); setPlaying(false) } else if (event.data.ready) { setReady(true); send({ playing: playing && !still, speed, visible: visible.current }) } } window.addEventListener('message', receive) return () => window.removeEventListener('message', receive) }, [playing, speed, still]) useEffect(() => { send({ playing: playing && !still, speed }) }, [playing, speed, still]) useEffect(() => { const observer = new IntersectionObserver(([entry]) => { visible.current = entry.isIntersecting send({ visible: visible.current }) }) if (iframe.current) observer.observe(iframe.current) return () => observer.disconnect() }, [name, revision, still]) useEffect(() => { setReady(false); setStaticScene(false); setElapsed(0); setError(null) const url = new URL(location.href); url.searchParams.set('example', name); history.replaceState(null, '', url) }, [name, revision, still]) return
StageshowPlayground