feat(RealityOverridePage): implement star field animation beneath Three.js vortex

This commit is contained in:
Jaime Idolpx 2026-06-08 18:54:21 -04:00
parent e87aeb6726
commit 53ed27e250

View File

@ -6,6 +6,22 @@ import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
interface Msg { text: string; id: number; } 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) ────────────────────────── // ── Shaders (verbatim from public/vortex/script.js) ──────────────────────────
@ -79,6 +95,8 @@ const FRAG_TRAIL = `
export default function RealityOverridePage({ onBack }: { onBack: () => void }) { export default function RealityOverridePage({ onBack }: { onBack: () => void }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const starCanvasRef = useRef<HTMLCanvasElement>(null);
const starsRef = useRef<Star[]>([]);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const msgIdRef = useRef(0); const msgIdRef = useRef(0);
const fadeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const fadeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
@ -231,6 +249,43 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
}; };
}, []); }, []);
// ── 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 ─────────────────────────────────────────────────────────────── // ── WebSocket ───────────────────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -266,7 +321,10 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
{/* Three.js canvas mount */} {/* Star field — always visible beneath the vortex */}
<canvas ref={starCanvasRef} className="absolute inset-0 w-full h-full" style={{ imageRendering: 'pixelated' }} />
{/* Three.js vortex — fades out on double-click/tap */}
<div ref={containerRef} className={`absolute inset-0 transition-opacity duration-500 ${bgVisible ? 'opacity-100' : 'opacity-0'}`} /> <div ref={containerRef} className={`absolute inset-0 transition-opacity duration-500 ${bgVisible ? 'opacity-100' : 'opacity-0'}`} />
{/* CSS keyframe for command flash */} {/* CSS keyframe for command flash */}