import { useEffect, useRef, useState } from 'react'; import { ChevronLeft, Loader2, Wifi, WifiOff } from 'lucide-react'; import * as THREE from 'three'; import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; interface Msg { text: string; id: number; } interface Star { x: number; y: number; sz: number; ph: number; spd: number; col: [number, number, number]; } const STAR_PALETTE: [number, number, number][] = [ [255, 255, 255], [255, 240, 210], [210, 220, 255], [255, 210, 210], [200, 255, 230], ]; function mkStar(W: number, H: number): Star { return { x: Math.random() * W | 0, y: Math.random() * H | 0, sz: Math.random() < 0.8 ? 1 : Math.random() < 0.6 ? 2 : 3, ph: Math.random() * Math.PI * 2, spd: Math.random() * 0.04 + 0.005, col: STAR_PALETTE[Math.random() * STAR_PALETTE.length | 0], }; } // ── Shaders (verbatim from public/vortex/script.js) ────────────────────────── const VERT_SHELL = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const FRAG_SHELL = ` uniform float uTime; uniform vec2 uGridSize; uniform vec3 uColor; uniform float uPulseSpeed; uniform float uGap; uniform float uOpacity; varying vec2 vUv; float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); } void main() { vec2 gridUV = vUv * uGridSize; vec2 cellID = floor(gridUV); vec2 cellUV = fract(gridUV); float padding = uGap; float square = step(padding, cellUV.x) * step(padding, cellUV.y) * step(cellUV.x, 1.0 - padding) * step(cellUV.y, 1.0 - padding); float rnd = random(cellID); float pulse = 0.5 + 0.5 * sin(uTime * uPulseSpeed + rnd * 6.28); float alpha = square * (0.05 + 0.95 * pulse) * uOpacity; gl_FragColor = vec4(uColor, alpha); } `; const VERT_TRAIL = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const FRAG_TRAIL = ` uniform float uTime; uniform vec3 uColor; uniform float uSpeed; uniform float uSpeedMult; uniform float uLength; uniform float uLengthMult; uniform float uOffset; uniform float uDirection; varying vec2 vUv; void main() { float finalSpeed = uSpeed * uSpeedMult; float progress = fract(vUv.x * 2.0 - (uTime * finalSpeed * uDirection) + uOffset); float finalLength = clamp(uLength * uLengthMult, 0.01, 0.99); float alpha = 0.0; if (uDirection > 0.0) { alpha = smoothstep(1.0 - finalLength, 1.0, progress); } else { alpha = 1.0 - smoothstep(0.0, finalLength, progress); } alpha = pow(alpha, 3.0); gl_FragColor = vec4(uColor, alpha); } `; // ── Component ───────────────────────────────────────────────────────────────── export default function RealityOverridePage({ onBack }: { onBack: () => void }) { const containerRef = useRef(null); const starCanvasRef = useRef(null); const starsRef = useRef([]); const wsRef = useRef(null); const msgIdRef = useRef(0); const fadeTimer = useRef | undefined>(undefined); const pausedRef = useRef(localStorage.getItem('ro-bg') === 'off'); const lastTapRef = useRef(0); const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting'); const [currentCmd, setCurrentCmd] = useState(null); const [history, setHistory] = useState([]); const [bgVisible, setBgVisible] = useState(() => localStorage.getItem('ro-bg') !== 'off'); const toggleBg = () => { pausedRef.current = !pausedRef.current; const next = !pausedRef.current; setBgVisible(next); localStorage.setItem('ro-bg', next ? 'on' : 'off'); }; const handleTouchEnd = () => { const now = Date.now(); if (now - lastTapRef.current < 300) { toggleBg(); lastTapRef.current = 0; } else { lastTapRef.current = now; } }; // ── Three.js vortex ───────────────────────────────────────────────────────── useEffect(() => { const container = containerRef.current; if (!container) return; const W = container.clientWidth || window.innerWidth; const H = container.clientHeight || window.innerHeight; // Scene const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x000000, 0.12); // Camera const camera = new THREE.PerspectiveCamera(90, W / H, 0.01, 100); // Renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(W, H); renderer.setPixelRatio(window.devicePixelRatio); renderer.toneMapping = THREE.NoToneMapping; container.appendChild(renderer.domElement); // Post-processing const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bloomPass = new UnrealBloomPass(new THREE.Vector2(W, H), 1.5, 0.4, 0.85); bloomPass.threshold = 0.1; bloomPass.strength = 0.5; bloomPass.radius = 0.6; composer.addPass(bloomPass); // Tokamak group const tokamakGroup = new THREE.Group(); scene.add(tokamakGroup); // Grid shell const shellMaterial = new THREE.ShaderMaterial({ vertexShader: VERT_SHELL, fragmentShader: FRAG_SHELL, uniforms: { uTime: { value: 0 }, uGridSize: { value: new THREE.Vector2(600, 300) }, uColor: { value: new THREE.Color('#3388ff') }, uPulseSpeed: { value: 4.89 }, uGap: { value: 0.23 }, uOpacity: { value: 1.0 }, }, transparent: true, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending, }); tokamakGroup.add(new THREE.Mesh(new THREE.TorusGeometry(10, 3.0, 60, 200), shellMaterial)); // Trail lines const trailsGroup = new THREE.Group(); tokamakGroup.add(trailsGroup); const trailMaterials: THREE.ShaderMaterial[] = []; for (let i = 0; i < 80; i++) { const laneRadius = 7.5 + Math.random() * 5.0; const mat = new THREE.ShaderMaterial({ vertexShader: VERT_TRAIL, fragmentShader: FRAG_TRAIL, uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color('#38cdff') }, uSpeed: { value: 0.1 + Math.random() * 0.3 }, uSpeedMult: { value: 1.0 }, uLength: { value: 0.1 + Math.random() * 0.2 }, uLengthMult: { value: 0.47 }, uOffset: { value: Math.random() * 100.0 }, uDirection: { value: 1.0 }, }, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, }); const mesh = new THREE.Mesh(new THREE.TorusGeometry(laneRadius, 0.005, 6, 120), mat); mesh.rotation.x = (Math.random() - 0.5) * 0.1; mesh.position.z = (Math.random() - 0.5) * 0.5; trailsGroup.add(mesh); trailMaterials.push(mat); } // Animation loop — automated camera flight (no OrbitControls needed) const clock = new THREE.Clock(); let cameraAngle = 0; let animId = 0; const animate = () => { animId = requestAnimationFrame(animate); const delta = clock.getDelta(); const elapsed = clock.getElapsedTime(); cameraAngle += 0.3 * delta; const r = 10; camera.position.set(Math.cos(cameraAngle) * r, Math.sin(cameraAngle) * r, 0.5); const la = cameraAngle + 0.1; camera.lookAt(Math.cos(la) * r, Math.sin(la) * r, 0); if (pausedRef.current) return; shellMaterial.uniforms.uTime.value = elapsed; trailMaterials.forEach(m => { m.uniforms.uTime.value = elapsed; }); composer.render(); }; animate(); // Resize observer keeps canvas filling its container const ro = new ResizeObserver(() => { const w = container.clientWidth; const h = container.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); composer.setSize(w, h); }); ro.observe(container); return () => { cancelAnimationFrame(animId); ro.disconnect(); renderer.dispose(); if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement); }; }, []); // ── Star field (always running, visible under Three.js when vortex is hidden) ─ useEffect(() => { const canvas = starCanvasRef.current; if (!canvas) return; let animId = 0; let prevW = 0, prevH = 0; const loop = () => { const W = canvas.clientWidth, H = canvas.clientHeight; if (!W || !H) { animId = requestAnimationFrame(loop); return; } if (W !== prevW || H !== prevH) { canvas.width = W; canvas.height = H; prevW = W; prevH = H; starsRef.current = Array.from({ length: 200 }, () => mkStar(W, H)); } const ctx = canvas.getContext('2d')!; ctx.fillStyle = '#000010'; ctx.fillRect(0, 0, W, H); for (const st of starsRef.current) { st.ph += st.spd; const br = Math.sin(st.ph) * 0.4 + 0.6; const [r, g, b] = st.col; ctx.fillStyle = `rgba(${r},${g},${b},${br})`; ctx.fillRect(st.x, st.y, st.sz, st.sz); if (st.sz >= 3 && br > 0.85) { ctx.fillStyle = `rgba(${r},${g},${b},${br * 0.35})`; ctx.fillRect(st.x - 2, st.y + 1, st.sz + 4, 1); ctx.fillRect(st.x + 1, st.y - 2, 1, st.sz + 4); } } animId = requestAnimationFrame(loop); }; animId = requestAnimationFrame(loop); return () => cancelAnimationFrame(animId); }, []); // ── WebSocket ─────────────────────────────────────────────────────────────── useEffect(() => { let cancelled = false; let ws: WebSocket | null = null; const connect = () => { if (cancelled) return; ws = new WebSocket(`ws://${window.location.hostname}/ws`); wsRef.current = ws; setWsStatus('connecting'); ws.onopen = () => { if (!cancelled) setWsStatus('connected'); }; ws.onmessage = (e) => { if (cancelled) return; const id = msgIdRef.current++; const msg: Msg = { text: String(e.data), id }; setCurrentCmd(msg); setHistory(prev => [...prev.slice(-9), msg]); clearTimeout(fadeTimer.current); fadeTimer.current = setTimeout(() => setCurrentCmd(null), 5000); }; ws.onclose = () => { if (!cancelled) { setWsStatus('disconnected'); setTimeout(connect, 3000); } }; ws.onerror = () => ws?.close(); }; connect(); return () => { cancelled = true; ws?.close(); }; }, []); return (
{/* Star field — always visible beneath the vortex */} {/* Three.js vortex — fades out on double-click/tap */}
{/* CSS keyframe for command flash */} {/* Back */} {/* WS status */}
{wsStatus === 'connecting' && <>Connecting…} {wsStatus === 'connected' && <>Connected} {wsStatus === 'disconnected' && <>Reconnecting…}
{/* Idle title — shown only before first message */} {history.length === 0 && (
Reality Override
Awaiting Commands_
)} {/* Centered current command */} {currentCmd && (
{currentCmd.text}
)} {/* History log */} {history.length > 0 && (
{history.map(m => (
>{m.text}
))}
)}
); }