d9404a31-1970-4e79-ac9a-f4f9db60350b
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 深空星空背景 Canvas
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface Star {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number; // 深度 0-1
|
||||
r: number;
|
||||
tw: number; // 闪烁相位
|
||||
hue: number;
|
||||
}
|
||||
|
||||
interface Dust {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
life: number;
|
||||
maxLife: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function StarfieldCanvas({ className }: { className?: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
let dpr = 1;
|
||||
let stars: Star[] = [];
|
||||
const dust: Dust[] = [];
|
||||
let raf = 0;
|
||||
let lastT = performance.now();
|
||||
|
||||
const palette = ["#34d399", "#fb7185", "#fbbf24", "#e879f9", "#a5f3fc"];
|
||||
|
||||
function resize() {
|
||||
dpr = Math.min(2, window.devicePixelRatio || 1);
|
||||
w = canvas!.clientWidth;
|
||||
h = canvas!.clientHeight;
|
||||
canvas!.width = Math.floor(w * dpr);
|
||||
canvas!.height = Math.floor(h * dpr);
|
||||
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
// 生成星星:密度随面积
|
||||
const count = Math.min(220, Math.floor((w * h) / 7000));
|
||||
stars = Array.from({ length: count }, () => ({
|
||||
x: Math.random() * w,
|
||||
y: Math.random() * h,
|
||||
z: Math.random(),
|
||||
r: Math.random() * 1.4 + 0.2,
|
||||
tw: Math.random() * Math.PI * 2,
|
||||
hue: Math.random() < 0.15 ? Math.floor(Math.random() * palette.length) : -1,
|
||||
}));
|
||||
}
|
||||
resize();
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(canvas);
|
||||
|
||||
function spawnDust() {
|
||||
if (dust.length > 60) return;
|
||||
const color = palette[Math.floor(Math.random() * palette.length)];
|
||||
dust.push({
|
||||
x: Math.random() * w,
|
||||
y: h + 10,
|
||||
vx: (Math.random() - 0.5) * 0.3,
|
||||
vy: -(Math.random() * 0.5 + 0.2),
|
||||
life: 0,
|
||||
maxLife: 600 + Math.random() * 800,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
function draw(t: number) {
|
||||
const dt = t - lastT;
|
||||
lastT = t;
|
||||
// 背景渐变
|
||||
const g = ctx!.createRadialGradient(w * 0.5, h * 0.4, 0, w * 0.5, h * 0.5, Math.max(w, h) * 0.8);
|
||||
g.addColorStop(0, "#1a0f2e");
|
||||
g.addColorStop(0.45, "#0d0a1f");
|
||||
g.addColorStop(1, "#050410");
|
||||
ctx!.fillStyle = g;
|
||||
ctx!.fillRect(0, 0, w, h);
|
||||
|
||||
// 星云团块
|
||||
ctx!.globalCompositeOperation = "screen";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cx = w * (0.2 + i * 0.3) + Math.sin(t / 9000 + i) * 40;
|
||||
const cy = h * (0.3 + (i % 2) * 0.4) + Math.cos(t / 11000 + i) * 30;
|
||||
const rad = Math.max(80, Math.min(w, h) * (0.25 + i * 0.05));
|
||||
const ng = ctx!.createRadialGradient(cx, cy, 0, cx, cy, rad);
|
||||
const col = i === 0 ? "rgba(232,121,249,0.10)" : i === 1 ? "rgba(52,211,153,0.08)" : "rgba(251,113,133,0.08)";
|
||||
ng.addColorStop(0, col);
|
||||
ng.addColorStop(1, "rgba(0,0,0,0)");
|
||||
ctx!.fillStyle = ng;
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(cx, cy, rad, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
}
|
||||
ctx!.globalCompositeOperation = "source-over";
|
||||
|
||||
// 星星
|
||||
for (const s of stars) {
|
||||
s.tw += 0.02 + s.z * 0.04;
|
||||
const alpha = 0.4 + Math.sin(s.tw) * 0.35 + s.z * 0.3;
|
||||
if (s.hue >= 0) {
|
||||
ctx!.fillStyle = palette[s.hue];
|
||||
ctx!.globalAlpha = Math.max(0, Math.min(1, alpha));
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(s.x, s.y, s.r * 1.6, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
// 光晕
|
||||
ctx!.globalAlpha = Math.max(0, Math.min(1, alpha * 0.25));
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(s.x, s.y, s.r * 4, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
} else {
|
||||
ctx!.fillStyle = `rgba(220,230,255,${Math.max(0, Math.min(1, alpha))})`;
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
}
|
||||
// 缓慢漂移
|
||||
s.y += 0.02 + s.z * 0.05;
|
||||
if (s.y > h + 2) {
|
||||
s.y = -2;
|
||||
s.x = Math.random() * w;
|
||||
}
|
||||
}
|
||||
ctx!.globalAlpha = 1;
|
||||
|
||||
// 偶尔生成星尘
|
||||
if (Math.random() < 0.04) spawnDust();
|
||||
// 星尘
|
||||
ctx!.globalCompositeOperation = "screen";
|
||||
for (let i = dust.length - 1; i >= 0; i--) {
|
||||
const d = dust[i];
|
||||
d.life += dt;
|
||||
d.x += d.vx * dt * 0.06;
|
||||
d.y += d.vy * dt * 0.06;
|
||||
if (d.life > d.maxLife || d.y < -10) {
|
||||
dust.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
const a = Math.sin((d.life / d.maxLife) * Math.PI) * 0.7;
|
||||
ctx!.fillStyle = d.color;
|
||||
ctx!.globalAlpha = a;
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(d.x, d.y, 1.6, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
ctx!.globalAlpha = a * 0.2;
|
||||
ctx!.beginPath();
|
||||
ctx!.arc(d.x, d.y, 5, 0, Math.PI * 2);
|
||||
ctx!.fill();
|
||||
}
|
||||
ctx!.globalAlpha = 1;
|
||||
ctx!.globalCompositeOperation = "source-over";
|
||||
|
||||
raf = requestAnimationFrame(draw);
|
||||
}
|
||||
raf = requestAnimationFrame(draw);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user