video-shuoshan/web/src/components/AuroraCanvas.tsx
seaislee1209 e5273540e9
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m0s
feat: v0.9.0~v0.9.1 — 5层极光首页 + 登录弹窗 + 播放器修复
- 全新 Landing Page:Canvas 极光动画(5色光球 + additive blending + blur 融合)
- 暗角/胶片颗粒/渐变遮罩 4 层视觉架构
- 鼠标推动光球交互(lerp 缓动)+ 呼吸效果
- 登录弹窗(磨砂玻璃 backdrop-filter blur)替代独立登录页
- Air Spark 全屏毛玻璃弹窗 + 音乐彩蛋(SVG 音波 + BGM)
- 品牌名 AIRFLOW STUDIO / AI VISUAL NARRATIVE + Space Grotesk 字体
- 路由重构:/ → LandingPage, /login → 自动弹登录框
- 自适应视频播放器比例修复(读取 videoWidth/videoHeight)
- adaptive 英文显示改为中文「自适应」
- 首页音乐泄漏修复(组件卸载时 pause+reset)
- 登录弹窗添加「目前仅限受邀创作者体验」提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 05:40:41 +08:00

251 lines
8.0 KiB
TypeScript

import { useRef, useEffect, useCallback } from 'react';
/**
* Aurora background — 5 large diffuse orbs with additive blending.
* Orbs are deliberately offset from center to avoid uniform "blue blob" look.
* Mouse gently pushes nearby orbs (10-20px, very subtle).
*/
interface Orb {
cx: number; // base center X ratio (0-1)
cy: number; // base center Y ratio (0-1)
color: [number, number, number];
alpha: number; // peak alpha — varies per orb for brightness contrast
phase: number;
freqX: number;
freqY: number;
ampX: number;
ampY: number;
radius: number; // ratio of max(w,h)
// breathing
breathFreq: number;
breathAmp: number;
}
// Orbs deliberately NOT centered — some in corners, some offset,
// creating uneven light distribution with dark pockets.
const ORBS: Orb[] = [
// Cyan — upper-left area, large and bright
{ cx: 0.25, cy: 0.30, color: [126, 220, 200], alpha: 0.28,
phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10,
radius: 0.50, breathFreq: 0.0004, breathAmp: 0.06 },
// Purple — lower-right, medium
{ cx: 0.72, cy: 0.65, color: [108, 99, 255], alpha: 0.22,
phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12,
radius: 0.45, breathFreq: 0.0003, breathAmp: 0.08 },
// Blue — center-right, smaller and dimmer (fills gap)
{ cx: 0.58, cy: 0.38, color: [59, 130, 246], alpha: 0.18,
phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15,
radius: 0.38, breathFreq: 0.0005, breathAmp: 0.07 },
// Light purple — bottom-left corner, dim accent
{ cx: 0.20, cy: 0.70, color: [167, 139, 250], alpha: 0.15,
phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08,
radius: 0.40, breathFreq: 0.00035, breathAmp: 0.10 },
// Bright cyan — upper-right, small bright accent
{ cx: 0.78, cy: 0.25, color: [34, 211, 238], alpha: 0.20,
phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12,
radius: 0.35, breathFreq: 0.00045, breathAmp: 0.09 },
];
// Mouse influence radius (px) and max push distance (px)
const MOUSE_RADIUS = 400;
const MOUSE_PUSH = 28;
export function AuroraCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const grainRef = useRef<HTMLCanvasElement>(null);
const mouseRef = useRef({ x: -9999, y: -9999, active: false });
const handleMouseMove = useCallback((e: MouseEvent) => {
mouseRef.current.x = e.clientX;
mouseRef.current.y = e.clientY;
mouseRef.current.active = true;
}, []);
const handleMouseLeave = useCallback(() => {
mouseRef.current.active = false;
}, []);
// ── Aurora animation ──
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let w = window.innerWidth;
let h = window.innerHeight;
function resize() {
w = window.innerWidth;
h = window.innerHeight;
const dpr = w < 768 ? 0.5 : Math.min(window.devicePixelRatio, 1.5);
canvas!.width = w * dpr;
canvas!.height = h * dpr;
canvas!.style.width = w + 'px';
canvas!.style.height = h + 'px';
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
let animId: number;
const t0 = performance.now();
// Smoothed mouse position for gentle push
let smoothMx = -9999;
let smoothMy = -9999;
function draw(now: number) {
const t = now - t0;
ctx!.clearRect(0, 0, w, h);
ctx!.globalCompositeOperation = 'lighter';
// Smooth mouse tracking (lerp toward actual position)
const mouse = mouseRef.current;
if (mouse.active) {
smoothMx += (mouse.x - smoothMx) * 0.035;
smoothMy += (mouse.y - smoothMy) * 0.035;
} else {
// Slowly drift smoothed mouse away (return to no-influence)
smoothMx += (-9999 - smoothMx) * 0.01;
smoothMy += (-9999 - smoothMy) * 0.01;
}
for (const orb of ORBS) {
// Base position from slow sinusoidal movement
let x = w * (orb.cx + Math.sin(t * orb.freqX + orb.phase) * orb.ampX);
let y = h * (orb.cy + Math.cos(t * orb.freqY + orb.phase * 0.7) * orb.ampY);
// Mouse push — gently offset orb away from cursor
const dx = x - smoothMx;
const dy = y - smoothMy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_RADIUS && dist > 1) {
const strength = (1 - dist / MOUSE_RADIUS) * MOUSE_PUSH;
x += (dx / dist) * strength;
y += (dy / dist) * strength;
}
// Breathing: radius and alpha pulse slowly
const breathT = Math.sin(t * orb.breathFreq + orb.phase * 1.3);
const r = Math.max(w, h) * orb.radius * (1 + breathT * orb.breathAmp);
const a = orb.alpha * (1 + breathT * 0.15);
const [cr, cg, cb] = orb.color;
const grad = ctx!.createRadialGradient(x, y, 0, x, y, r);
grad.addColorStop(0, `rgba(${cr},${cg},${cb},${(a).toFixed(3)})`);
grad.addColorStop(0.25, `rgba(${cr},${cg},${cb},${(a * 0.5).toFixed(3)})`);
grad.addColorStop(0.5, `rgba(${cr},${cg},${cb},${(a * 0.15).toFixed(3)})`);
grad.addColorStop(0.8, `rgba(${cr},${cg},${cb},${(a * 0.03).toFixed(3)})`);
grad.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
ctx!.fillStyle = grad;
ctx!.beginPath();
ctx!.arc(x, y, r, 0, Math.PI * 2);
ctx!.fill();
}
ctx!.globalCompositeOperation = 'source-over';
animId = requestAnimationFrame(draw);
}
animId = requestAnimationFrame(draw);
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove, { passive: true });
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
cancelAnimationFrame(animId);
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
};
}, [handleMouseMove, handleMouseLeave]);
// ── Film grain — 4 FPS low-noise ──
useEffect(() => {
const canvas = grainRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = 256;
canvas.height = 256;
let animId: number;
let lastFrame = 0;
function drawGrain(now: number) {
if (now - lastFrame > 250) {
lastFrame = now;
const img = ctx!.createImageData(256, 256);
const d = img.data;
for (let i = 0; i < d.length; i += 4) {
const v = Math.random() * 255;
d[i] = d[i + 1] = d[i + 2] = v;
d[i + 3] = 255;
}
ctx!.putImageData(img, 0, 0);
}
animId = requestAnimationFrame(drawGrain);
}
animId = requestAnimationFrame(drawGrain);
return () => cancelAnimationFrame(animId);
}, []);
return (
<>
{/* Layer 1: Vignette — radial darkening, heavy at edges */}
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
background: 'radial-gradient(ellipse 65% 55% at 50% 50%, transparent 0%, rgba(0,0,0,0.8) 100%)',
pointerEvents: 'none',
}}
/>
{/* Layer 2: Film grain */}
<canvas
ref={grainRef}
style={{
position: 'absolute',
inset: 0,
zIndex: 2,
width: '100%',
height: '100%',
opacity: 0.035,
pointerEvents: 'none',
mixBlendMode: 'overlay',
}}
/>
{/* Layer 3: Aurora — blur merges orbs into organic glow */}
<canvas
ref={canvasRef}
style={{
position: 'absolute',
inset: 0,
zIndex: 3,
filter: 'blur(50px)',
pointerEvents: 'none',
}}
/>
{/* Layer 4: Top/bottom gradient mask */}
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 4,
pointerEvents: 'none',
background:
'linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 18%, transparent 82%, rgba(0,0,0,0.5) 100%)',
}}
/>
</>
);
}