235 lines
7.4 KiB
TypeScript
235 lines
7.4 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import type { Theme } from '../context/ThemeContext';
|
|
|
|
interface Orb {
|
|
x: number;
|
|
y: number;
|
|
r: number;
|
|
vx: number;
|
|
vy: number;
|
|
color: string;
|
|
glowAlpha: number;
|
|
pulsePhase: number;
|
|
pulseSpeed: number;
|
|
}
|
|
|
|
export default function StarBackground({ theme = 'dark' }: { theme?: Theme }) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
};
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
// 生成星星
|
|
const stars = Array.from({ length: 220 }, () => ({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
r: Math.random() * 1.2 + 0.2,
|
|
opacity: Math.random() * 0.6 + 0.2,
|
|
speed: Math.random() * 0.015 + 0.005,
|
|
phase: Math.random() * Math.PI * 2,
|
|
}));
|
|
|
|
// 深空云雾粒子
|
|
const nebula = Array.from({ length: 6 }, (_, i) => ({
|
|
x: (i / 6) * canvas.width + Math.random() * 200 - 100,
|
|
y: Math.random() * canvas.height,
|
|
r: Math.random() * 180 + 120,
|
|
color: ['rgba(0,80,160,', 'rgba(40,0,120,', 'rgba(0,60,100,', 'rgba(20,0,80,', 'rgba(0,100,120,', 'rgba(60,0,100,'][i],
|
|
}));
|
|
|
|
// 大小球体 — 飞来飞去的光球
|
|
const darkOrbColors = [
|
|
'rgba(220,240,255,',
|
|
'rgba(200,230,255,',
|
|
'rgba(255,255,255,',
|
|
'rgba(230,240,255,',
|
|
'rgba(0,180,255,',
|
|
'rgba(120,100,255,',
|
|
'rgba(0,229,153,',
|
|
'rgba(0,212,255,',
|
|
];
|
|
const lightOrbColors = [
|
|
'rgba(160,185,230,',
|
|
'rgba(180,200,240,',
|
|
'rgba(200,215,245,',
|
|
'rgba(140,170,220,',
|
|
'rgba(100,140,210,',
|
|
'rgba(170,190,235,',
|
|
'rgba(120,160,220,',
|
|
'rgba(190,210,245,',
|
|
];
|
|
const orbColors = theme === 'dark' ? darkOrbColors : lightOrbColors;
|
|
const orbGlowBase = theme === 'dark' ? 0.025 : 0.07;
|
|
const orbs: Orb[] = Array.from({ length: 64 }, (_, i) => ({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
r: i < 24 ? (Math.random() * 30 + 35) : (Math.random() * 12 + 10),
|
|
vx: (Math.random() - 0.5) * 2.2,
|
|
vy: (Math.random() - 0.5) * 1.6,
|
|
color: orbColors[i % orbColors.length],
|
|
glowAlpha: Math.random() * orbGlowBase + orbGlowBase * 0.4,
|
|
pulsePhase: Math.random() * Math.PI * 2,
|
|
pulseSpeed: Math.random() * 0.012 + 0.006,
|
|
}));
|
|
|
|
let frame = 0;
|
|
let animId: number;
|
|
|
|
const draw = () => {
|
|
frame++;
|
|
const W = canvas.width;
|
|
const H = canvas.height;
|
|
ctx.clearRect(0, 0, W, H);
|
|
|
|
// 背景渐变
|
|
const grad = ctx.createRadialGradient(W * 0.5, H * 0.5, 0, W * 0.5, H * 0.5, W * 0.7);
|
|
if (theme === 'dark') {
|
|
grad.addColorStop(0, 'rgba(0,15,40,1)');
|
|
grad.addColorStop(0.5, 'rgba(0,8,25,1)');
|
|
grad.addColorStop(1, 'rgba(0,4,15,1)');
|
|
} else {
|
|
grad.addColorStop(0, 'rgba(248,250,255,1)');
|
|
grad.addColorStop(0.5, 'rgba(243,246,255,1)');
|
|
grad.addColorStop(1, 'rgba(236,241,255,1)');
|
|
}
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// 星云雾
|
|
nebula.forEach(n => {
|
|
const g = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r);
|
|
const nebulaAlpha = theme === 'dark' ? '0.06' : '0.04';
|
|
g.addColorStop(0, n.color + nebulaAlpha + ')');
|
|
g.addColorStop(1, n.color + '0)');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath();
|
|
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
|
|
// ★ 飞行球体
|
|
orbs.forEach(orb => {
|
|
// 更新位置
|
|
orb.x += orb.vx;
|
|
orb.y += orb.vy;
|
|
|
|
// 边界反弹(加上球体半径,让球不会从中心出界)
|
|
if (orb.x < -orb.r) orb.x = W + orb.r;
|
|
if (orb.x > W + orb.r) orb.x = -orb.r;
|
|
if (orb.y < -orb.r) orb.y = H + orb.r;
|
|
if (orb.y > H + orb.r) orb.y = -orb.r;
|
|
|
|
// 脉冲效果
|
|
const pulse = 0.7 + 0.3 * Math.sin(frame * orb.pulseSpeed + orb.pulsePhase);
|
|
const alpha = orb.glowAlpha * pulse;
|
|
|
|
// 外层大光晕
|
|
const outerR = orb.r * 1.5;
|
|
const outerGrad = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, outerR);
|
|
outerGrad.addColorStop(0, orb.color + (alpha * 0.9).toFixed(3) + ')');
|
|
outerGrad.addColorStop(0.5, orb.color + (alpha * 0.3).toFixed(3) + ')');
|
|
outerGrad.addColorStop(1, orb.color + '0)');
|
|
ctx.fillStyle = outerGrad;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, outerR, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 内层球体
|
|
const innerGrad = ctx.createRadialGradient(
|
|
orb.x - orb.r * 0.2, orb.y - orb.r * 0.2, 0,
|
|
orb.x, orb.y, orb.r
|
|
);
|
|
innerGrad.addColorStop(0, orb.color + (alpha * 2.0).toFixed(3) + ')');
|
|
innerGrad.addColorStop(0.6, orb.color + (alpha * 0.8).toFixed(3) + ')');
|
|
innerGrad.addColorStop(1, orb.color + '0)');
|
|
ctx.fillStyle = innerGrad;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x, orb.y, orb.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 高光点
|
|
const hlR = orb.r * 0.25;
|
|
const hlGrad = ctx.createRadialGradient(
|
|
orb.x - orb.r * 0.25, orb.y - orb.r * 0.25, 0,
|
|
orb.x - orb.r * 0.25, orb.y - orb.r * 0.25, hlR
|
|
);
|
|
hlGrad.addColorStop(0, 'rgba(255,255,255,' + (alpha * 4).toFixed(3) + ')');
|
|
hlGrad.addColorStop(1, 'rgba(255,255,255,0)');
|
|
ctx.fillStyle = hlGrad;
|
|
ctx.beginPath();
|
|
ctx.arc(orb.x - orb.r * 0.25, orb.y - orb.r * 0.25, hlR, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
|
|
// 星星
|
|
stars.forEach(s => {
|
|
const twinkle = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(frame * s.speed + s.phase));
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = theme === 'dark'
|
|
? `rgba(180,220,255,${s.opacity * twinkle})`
|
|
: `rgba(100,140,200,${s.opacity * twinkle * 0.5})`;
|
|
ctx.fill();
|
|
// 亮星加光晕
|
|
if (s.r > 1.0) {
|
|
const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 4);
|
|
halo.addColorStop(0, theme === 'dark'
|
|
? `rgba(100,200,255,${0.2 * twinkle})`
|
|
: `rgba(80,130,220,${0.08 * twinkle})`);
|
|
halo.addColorStop(1, 'rgba(0,0,0,0)');
|
|
ctx.fillStyle = halo;
|
|
ctx.beginPath();
|
|
ctx.arc(s.x, s.y, s.r * 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
});
|
|
|
|
// 水平光线
|
|
if (frame % 300 === 0) {
|
|
const y = Math.random() * H;
|
|
const lineGrad = ctx.createLinearGradient(0, y, W, y);
|
|
lineGrad.addColorStop(0, 'rgba(0,212,255,0)');
|
|
lineGrad.addColorStop(0.5, 'rgba(0,212,255,0.06)');
|
|
lineGrad.addColorStop(1, 'rgba(0,212,255,0)');
|
|
ctx.strokeStyle = lineGrad;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(W, y);
|
|
ctx.stroke();
|
|
}
|
|
|
|
animId = requestAnimationFrame(draw);
|
|
};
|
|
|
|
draw();
|
|
return () => {
|
|
cancelAnimationFrame(animId);
|
|
window.removeEventListener('resize', resize);
|
|
};
|
|
}, [theme]);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0, left: 0,
|
|
width: '100%', height: '100%',
|
|
zIndex: 0,
|
|
pointerEvents: 'none',
|
|
}}
|
|
/>
|
|
);
|
|
}
|