2026-06-02 12:38:01 +08:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
|
|
|
|
|
|
interface WelcomeSplashProps {
|
|
|
|
|
onEnter: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MATRIX_CHARS =
|
|
|
|
|
"01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" +
|
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{};:?/\\|~`";
|
|
|
|
|
|
2026-06-02 17:37:51 +08:00
|
|
|
const prefersReducedMotion = typeof window !== "undefined"
|
|
|
|
|
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
|
|
|
: false;
|
|
|
|
|
|
2026-06-02 12:38:01 +08:00
|
|
|
export default function WelcomeSplash({ onEnter }: WelcomeSplashProps) {
|
|
|
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
|
const rafRef = useRef(0);
|
2026-06-03 02:01:21 +08:00
|
|
|
const [showWelcome, setShowWelcome] = useState(true);
|
2026-06-02 12:38:01 +08:00
|
|
|
const [exiting, setExiting] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleEnter = useCallback(() => {
|
|
|
|
|
setExiting(true);
|
2026-06-02 17:37:51 +08:00
|
|
|
setTimeout(onEnter, prefersReducedMotion ? 0 : 700);
|
2026-06-02 12:38:01 +08:00
|
|
|
}, [onEnter]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-02 17:37:51 +08:00
|
|
|
const timer = setTimeout(() => setShowWelcome(true), prefersReducedMotion ? 0 : 6000);
|
2026-06-02 12:38:01 +08:00
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-02 17:37:51 +08:00
|
|
|
if (prefersReducedMotion) {
|
|
|
|
|
const canvas = canvasRef.current;
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
canvas.width = window.innerWidth;
|
|
|
|
|
canvas.height = window.innerHeight;
|
|
|
|
|
ctx.fillStyle = "rgba(0, 0, 0, 0.85)";
|
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:38:01 +08:00
|
|
|
const canvas = canvasRef.current;
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
|
|
|
|
|
let width = window.innerWidth;
|
|
|
|
|
let height = window.innerHeight;
|
|
|
|
|
canvas.width = width;
|
|
|
|
|
canvas.height = height;
|
|
|
|
|
const fontSize = width > 1200 ? 20 : width > 768 ? 18 : 14;
|
|
|
|
|
const columns = Math.floor(width / fontSize);
|
|
|
|
|
const drops: number[] = [];
|
|
|
|
|
for (let i = 0; i < columns; i++) {
|
|
|
|
|
drops[i] = Math.random() * -(height / fontSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function draw() {
|
|
|
|
|
ctx!.fillStyle = "rgba(0, 0, 0, 0.07)";
|
|
|
|
|
ctx!.fillRect(0, 0, width, height);
|
|
|
|
|
ctx!.font = `${fontSize}px "Courier New", monospace`;
|
|
|
|
|
ctx!.textAlign = "center";
|
|
|
|
|
for (let i = 0; i < drops.length; i++) {
|
|
|
|
|
const char = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)];
|
|
|
|
|
const intensity = Math.min(0.6 + (drops[i] / (height / fontSize)) * 0.4, 1);
|
|
|
|
|
const g = Math.floor(100 + 155 * intensity);
|
|
|
|
|
ctx!.fillStyle = `rgba(40, ${g}, 60, 0.9)`;
|
|
|
|
|
ctx!.shadowBlur = 6;
|
|
|
|
|
ctx!.shadowColor = "#0f0";
|
|
|
|
|
const x = i * fontSize + fontSize / 2;
|
|
|
|
|
const y = drops[i] * fontSize;
|
|
|
|
|
ctx!.fillText(char, x, y);
|
|
|
|
|
ctx!.shadowBlur = 0;
|
|
|
|
|
drops[i] += 0.7 + Math.random() * 1.4;
|
|
|
|
|
if (drops[i] * fontSize > height && Math.random() > 0.96) {
|
|
|
|
|
drops[i] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function animate() {
|
|
|
|
|
draw();
|
|
|
|
|
rafRef.current = requestAnimationFrame(animate);
|
|
|
|
|
}
|
|
|
|
|
animate();
|
|
|
|
|
|
|
|
|
|
function onResize() {
|
|
|
|
|
width = window.innerWidth;
|
|
|
|
|
height = window.innerHeight;
|
|
|
|
|
canvas!.width = width;
|
|
|
|
|
canvas!.height = height;
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("resize", onResize);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelAnimationFrame(rafRef.current);
|
|
|
|
|
window.removeEventListener("resize", onResize);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`welcome-splash${exiting ? " is-exiting" : ""}`}>
|
|
|
|
|
<canvas ref={canvasRef} className="welcome-splash__canvas" />
|
|
|
|
|
<div className="welcome-splash__ambient" />
|
|
|
|
|
<div className="welcome-splash__hero">
|
|
|
|
|
<h1 className="welcome-splash__title">OmniAI</h1>
|
|
|
|
|
<p className="welcome-splash__subtitle">The future with OmniAI</p>
|
|
|
|
|
{showWelcome && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="welcome-splash__enter"
|
|
|
|
|
onClick={handleEnter}
|
|
|
|
|
>
|
|
|
|
|
欢迎进入未来
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|