Files
qtalker---/frontend/src/three/GhostCanvas.tsx
Indiana 6edbbbbc2a feat: complete Quantumancy web app — full frontend + docs
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
2026-07-20 21:11:49 +00:00

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>
)
}