Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import { useRef, useEffect } from 'react' import type { AlignmentConfig } from './renderPhiExploreImage' export interface PhiExploreImageData { image: HTMLImageElement alignment: AlignmentConfig subjectId: string } interface AlignmentEntry { scale?: number rotation?: number offsetX?: number offsetY?: number light?: AlignmentConfig dark?: AlignmentConfig } type AlignmentJson = Record<string, AlignmentEntry> /** * Hook that loads phi-explore alignment data and preloads a random themed * image. Returns a ref containing the image + alignment, or null while loading. * * On theme change, picks a new random subject and preloads its themed image. */ export function usePhiExploreImage( resolvedTheme: string | undefined ): React.MutableRefObject<PhiExploreImageData | null> { const dataRef = useRef<PhiExploreImageData | null>(null) const alignmentCacheRef = useRef<AlignmentJson | null>(null) const loadIdRef = useRef(0) useEffect(() => { const theme = resolvedTheme === 'dark' ? 'dark' : 'light' const loadId = ++loadIdRef.current dataRef.current = null async function load() { // Fetch alignment data (cached after first load) let alignmentData = alignmentCacheRef.current if (!alignmentData) { try { const res = await fetch('/images/constants/phi-explore/alignment.json') if (!res.ok) return alignmentData = (await res.json()) as AlignmentJson alignmentCacheRef.current = alignmentData } catch { return } } if (loadId !== loadIdRef.current) return // Filter to subjects that have alignment for this theme const candidates = Object.entries(alignmentData).filter(([, entry]) => entry[theme] != null) if (candidates.length === 0) return // Pick a random subject const [subjectId, entry] = candidates[Math.floor(Math.random() * candidates.length)] const alignment = entry[theme]! // Preload the themed image const img = new Image() img.crossOrigin = 'anonymous' await new Promise<void>((resolve, reject) => { img.onload = () => resolve() img.onerror = () => reject() img.src = `/images/constants/phi-explore/${subjectId}-${theme}.png` }).catch(() => {}) if (loadId !== loadIdRef.current) return if (!img.naturalWidth) return // failed to load dataRef.current = { image: img, alignment, subjectId } } load() return () => { dataRef.current = null } }, [resolvedTheme]) return dataRef } |