From 58f85ff88f2ad8eda4de8cba3b71586d71d35a31 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Tue, 9 Jun 2026 06:07:44 -0400 Subject: [PATCH] feat(RealityOverridePage): enhance star field with parallax effect and optimize star generation --- package.json | 2 + src/app/components/RealityOverridePage.tsx | 121 +++++++++++++++------ 2 files changed, 90 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 0c3087d..d75e300 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/components/RealityOverridePage.tsx b/src/app/components/RealityOverridePage.tsx index a5bd6d1..72f5727 100644 --- a/src/app/components/RealityOverridePage.tsx +++ b/src/app/components/RealityOverridePage.tsx @@ -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: 4–14 seconds at 60 fps (0.007–0.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(null); - const starCanvasRef = useRef(null); - const starsRef = useRef([]); + const containerRef = useRef(null); + const parallaxSceneRef = useRef(null); + const starL1Ref = useRef(null); + const starL2Ref = useRef(null); + const starL3Ref = useRef(null); + const starsL1 = useRef([]); + const starsL2 = useRef([]); + const starsL3 = useRef([]); const msgIdRef = useRef(0); const fadeTimer = useRef | 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 */} - + {/* Star field — 3 parallax depth layers, always visible beneath the vortex */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
{/* Three.js vortex — fades out on double-click/tap */}