feat(RealityOverridePage): enhance star field with parallax effect and optimize star generation
This commit is contained in:
parent
e4c2aa0dbc
commit
58f85ff88f
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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: Math.random() < 0.8 ? 1 : Math.random() < 0.6 ? 2 : 3,
|
||||
sz,
|
||||
ph: Math.random() * Math.PI * 2,
|
||||
spd: Math.random() * 0.04 + 0.005,
|
||||
// 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) ──────────────────────────
|
||||
|
|
@ -96,8 +102,13 @@ const FRAG_TRAIL = `
|
|||
|
||||
export default function RealityOverridePage({ onBack }: { onBack: () => void }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const starCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const starsRef = useRef<Star[]>([]);
|
||||
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,36 +263,50 @@ 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);
|
||||
}
|
||||
}
|
||||
for (const { ref, stars, bright, fillBg } of layers) {
|
||||
const canvas = ref.current; if (!canvas) continue;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.fillStyle = '#000010';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
for (const st of starsRef.current) {
|
||||
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;
|
||||
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.85) {
|
||||
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'}`} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user