Frontend (React 18 + TS + Vite): - Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits - Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive), Ouija planchette board spelling utterances, transcript with TTS replay, entity dossier, direct contact streaming, passive/active listening - Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact - Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete - State: VeilSocket (reconnect/backoff), seance reducer, auth context - 72 vitest tests green; served by FastAPI at :7777 Docs: README + as-built plans 3-7
50 lines
1.4 KiB
TypeScript
50 lines
1.4 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { GhostScene } from './GhostScene'
|
|
import type { GhostMood, GhostVisualState } from './GhostScene'
|
|
|
|
export type GhostCanvasProps = {
|
|
visual: GhostVisualState | null
|
|
mood: GhostMood
|
|
/** Called each frame to get the speaking amplitude (0..1). */
|
|
getAmplitude?: () => number
|
|
className?: string
|
|
}
|
|
|
|
/** React wrapper: owns the canvas element and the GhostScene lifecycle. */
|
|
export function GhostCanvas({ visual, mood, getAmplitude, className }: GhostCanvasProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
const sceneRef = useRef<GhostScene | null>(null)
|
|
const ampRef = useRef(getAmplitude)
|
|
ampRef.current = getAmplitude
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const scene = new GhostScene(canvas)
|
|
sceneRef.current = scene
|
|
scene.start()
|
|
const ampTimer = window.setInterval(() => {
|
|
scene.setSpeakAmplitude(ampRef.current?.() ?? 0)
|
|
}, 50)
|
|
return () => {
|
|
window.clearInterval(ampTimer)
|
|
scene.dispose()
|
|
sceneRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
sceneRef.current?.setVisual(visual)
|
|
}, [visual])
|
|
|
|
useEffect(() => {
|
|
sceneRef.current?.setMood(mood)
|
|
}, [mood])
|
|
|
|
return (
|
|
<div className={className} style={{ position: 'absolute', inset: 0, overflow: 'hidden' }}>
|
|
<canvas ref={canvasRef} style={{ width: '100%', height: '100%', display: 'block' }} />
|
|
</div>
|
|
)
|
|
}
|