feat(RealityOverridePage): enhance star field with parallax effect and optimize star generation

This commit is contained in:
Jaime Idolpx 2026-06-09 06:07:44 -04:00
parent e4c2aa0dbc
commit 58f85ff88f
2 changed files with 90 additions and 33 deletions

View File

@ -42,6 +42,7 @@
"@radix-ui/react-toggle": "1.1.2",
"@radix-ui/react-toggle-group": "1.1.2",
"@radix-ui/react-tooltip": "1.1.8",
"@types/parallax-js": "^3.1.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@uiw/react-codemirror": "^4.25.10",
"canvas-confetti": "1.9.4",
@ -54,6 +55,7 @@
"lucide-react": "0.487.0",
"motion": "12.23.24",
"next-themes": "0.4.6",
"parallax-js": "^3.1.0",
"react-day-picker": "8.10.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronLeft, Loader2, Radio, WifiOff } from 'lucide-react';
import { useWs } from '../ws';
import Parallax from 'parallax-js';
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
@ -13,15 +14,20 @@ 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],
};
function mkStars(count: number, W: number, H: number, szMax: number): Star[] {
return Array.from({ length: count }, () => {
const r = Math.random();
const sz = szMax <= 1 ? 1 : szMax === 2 ? (r < 0.65 ? 1 : 2) : (r < 0.75 ? 1 : r < 0.92 ? 2 : 3);
return {
x: Math.random() * W | 0,
y: Math.random() * H | 0,
sz,
ph: Math.random() * Math.PI * 2,
// Twinkling period: 414 seconds at 60 fps (0.0070.027 rad/frame)
spd: Math.random() * 0.020 + 0.007,
col: STAR_PALETTE[Math.random() * STAR_PALETTE.length | 0],
};
});
}
// ── Shaders (verbatim from public/vortex/script.js) ──────────────────────────
@ -95,9 +101,14 @@ const FRAG_TRAIL = `
// ── Component ─────────────────────────────────────────────────────────────────
export default function RealityOverridePage({ onBack }: { onBack: () => void }) {
const containerRef = useRef<HTMLDivElement>(null);
const starCanvasRef = useRef<HTMLCanvasElement>(null);
const starsRef = useRef<Star[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const parallaxSceneRef = useRef<HTMLUListElement>(null);
const starL1Ref = useRef<HTMLCanvasElement>(null);
const starL2Ref = useRef<HTMLCanvasElement>(null);
const starL3Ref = useRef<HTMLCanvasElement>(null);
const starsL1 = useRef<Star[]>([]);
const starsL2 = useRef<Star[]>([]);
const starsL3 = useRef<Star[]>([]);
const msgIdRef = useRef(0);
const fadeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const pausedRef = useRef(localStorage.getItem('ro-bg') === 'off');
@ -252,34 +263,48 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
};
}, []);
// ── Star field (always running, visible under Three.js when vortex is hidden)
// ── Star field — 3 parallax layers (far / mid / near) ─────────────────────
useEffect(() => {
const canvas = starCanvasRef.current;
if (!canvas) return;
const layers = [
{ ref: starL1Ref, stars: starsL1, count: 150, szMax: 1, bright: 0.65, fillBg: true },
{ ref: starL2Ref, stars: starsL2, count: 60, szMax: 2, bright: 0.85, fillBg: false },
{ ref: starL3Ref, stars: starsL3, count: 20, szMax: 3, bright: 1.0, fillBg: false },
];
let animId = 0;
let prevW = 0, prevH = 0;
const loop = () => {
const W = canvas.clientWidth, H = canvas.clientHeight;
const scene = parallaxSceneRef.current;
const W = scene?.clientWidth ?? 0, H = scene?.clientHeight ?? 0;
if (!W || !H) { animId = requestAnimationFrame(loop); return; }
const dpr = window.devicePixelRatio || 1;
if (W !== prevW || H !== prevH) {
canvas.width = W; canvas.height = H;
prevW = W; prevH = H;
starsRef.current = Array.from({ length: 200 }, () => mkStar(W, H));
for (const { ref, stars, count, szMax } of layers) {
const cv = ref.current; if (!cv) continue;
cv.width = Math.round(W * dpr);
cv.height = Math.round(H * dpr);
const cx = cv.getContext('2d')!;
cx.scale(dpr, dpr);
stars.current = mkStars(count, W, H, szMax);
}
}
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);
for (const { ref, stars, bright, fillBg } of layers) {
const canvas = ref.current; if (!canvas) continue;
const ctx = canvas.getContext('2d')!;
if (fillBg) { ctx.fillStyle = '#000010'; ctx.fillRect(0, 0, W, H); }
else ctx.clearRect(0, 0, W, H);
for (const st of stars.current) {
st.ph += st.spd;
const br = (Math.sin(st.ph) * 0.4 + 0.6) * bright;
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.75) {
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);
@ -289,6 +314,22 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
return () => cancelAnimationFrame(animId);
}, []);
// ── Parallax depth effect on star layers ────────────────────────────────────
useEffect(() => {
const scene = parallaxSceneRef.current;
if (!scene) return;
const parallax = new Parallax(scene, {
relativeInput: false,
limitX: 80,
limitY: 55,
scalarX: 10,
scalarY: 8,
frictionX: 0.06,
frictionY: 0.06,
});
return () => parallax.destroy();
}, []);
// ── WebSocket (shared connection via context) ───────────────────────────────
useEffect(() => {
return subscribe((data) => {
@ -307,8 +348,22 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
onPointerDown={handlePointerDown}
>
{/* Star field — always visible beneath the vortex */}
<canvas ref={starCanvasRef} className="absolute inset-0 w-full h-full" style={{ imageRendering: 'pixelated' }} />
{/* Star field — 3 parallax depth layers, always visible beneath the vortex */}
<ul
ref={parallaxSceneRef}
className="absolute inset-0 overflow-hidden pointer-events-none"
style={{ listStyle: 'none', margin: 0, padding: 0 }}
>
<li data-depth="0.06" className="absolute inset-0">
<canvas ref={starL1Ref} className="w-full h-full" />
</li>
<li data-depth="0.25" className="absolute inset-0">
<canvas ref={starL2Ref} className="w-full h-full" />
</li>
<li data-depth="0.55" className="absolute inset-0">
<canvas ref={starL3Ref} className="w-full h-full" />
</li>
</ul>
{/* 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'}`} />