echoes/frontend/hooks/Background.tsx

122 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, memo } from "react";
import { useThemeMode } from "hooks/ThemeMode";
interface AnimatedBackgroundProps {
onError?: () => void;
}
export const AnimatedBackground = memo(
({ onError }: AnimatedBackgroundProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const { mode } = useThemeMode();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
onError?.();
return;
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
try {
const ctx = canvas.getContext("2d", {
alpha: true,
desynchronized: true,
});
if (!ctx) {
console.error("无法获取 canvas context");
onError?.();
return;
}
const context = ctx;
const getRandomHSLColor = () => {
const hue = Math.random() * 360;
const saturation = 90 + Math.random() * 10;
const lightness =
mode === "dark"
? 50 + Math.random() * 15 // 暗色模式50-65%
: 60 + Math.random() * 15; // 亮色模式60-75%
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
};
const ballColor = getRandomHSLColor();
let ballRadius = 100;
let x = canvas.width / 2;
let y = canvas.height - 200;
let dx = 0.2;
let dy = -0.2;
function drawBall() {
context.beginPath();
context.arc(x, y, ballRadius, 0, Math.PI * 2);
context.fillStyle = ballColor;
context.fill();
context.closePath();
}
const fps = 30;
const interval = 1000 / fps;
let then = Date.now();
const draw = () => {
const now = Date.now();
const delta = now - then;
if (delta > interval) {
then = now - (delta % interval);
context.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}
x += dx;
y += dy;
}
animationFrameId = requestAnimationFrame(draw);
};
let animationFrameId: number;
draw();
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
} catch (error) {
console.error("Canvas 初始化失败:", error);
onError?.();
return;
}
}, [mode, onError]);
return (
<div className="fixed inset-0 -z-10 overflow-hidden">
<canvas
ref={canvasRef}
className="w-full h-full opacity-70"
style={{
filter: "blur(100px)",
position: "absolute",
top: 0,
left: 0,
willChange: "transform",
}}
/>
</div>
);
},
);