import React from 'react';
import {
AbsoluteFill,
useCurrentFrame,
interpolate,
spring,
useVideoConfig,
Video,
staticFile,
Easing,
} from 'remotion';
import { SceneData } from './types';
import { designTokens, getTextShadow, getGlitchShadow } from './styles/design-tokens';
import { CyberGrid } from './components/CyberGrid';
import {
EleganceScene,
AuthorityScene,
LuxuryScene,
MinimalScene,
CinematicScene,
} from './premium-scenes';
interface SceneRendererProps {
scene: SceneData;
}
// Helper: Text reveal animation (逐字显示) - PROFESSIONAL GRADE
const TextReveal: React.FC<{
text: string;
frame: number;
delay?: number;
stagger?: number;
}> = ({
text,
frame,
delay = 8, // 整体延迟增加到 8 帧,让粒子先"预热"
stagger = 3, // 每个字符间隔 3 帧(0.1 秒),更易感知
}) => {
const chars = text.split('');
return (
<>
{chars.map((char, index) => {
// 阶段 1: 预期(anticipation)- 轻微下沉
const anticipationY = interpolate(
frame,
[index * stagger + delay - 3, index * stagger + delay],
[0, 5],
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
}
);
// 阶段 2: 动作(action)- 快速上升 + overshoot
const charTranslateY = interpolate(
frame,
[
index * stagger + delay,
index * stagger + delay + 12,
index * stagger + delay + 18,
],
[30, -8, 0], // 从下方弹起,过冲后回到位置
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.bezier(0.34, 1.56, 0.64, 1), // Custom easeOutBack
}
);
// 透明度:快速淡入
const charOpacity = interpolate(
frame,
[index * stagger + delay, index * stagger + delay + 8],
[0, 1],
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}
);
// 阶段 3: 跟随(follow-through)- 轻微缩放脉冲
const charScale = interpolate(
frame,
[
index * stagger + delay + 12,
index * stagger + delay + 18,
],
[1.15, 1], // 轻微放大后收缩
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
}
);
// 动态光效:字符出现时发光
const glowIntensity = interpolate(
frame,
[
index * stagger + delay,
index * stagger + delay + 10,
index * stagger + delay + 20,
],
[0, 1, 0.3], // 出现时强光,然后减弱到30%
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}
);
return (
{char === ' ' ? '\u00A0' : char}
);
})}
>
);
};
// Helper: Particle explosion effect (粒子爆炸) - PROFESSIONAL GRADE
interface Particle {
angle: number;
size: number;
color: string;
velocity: number;
depth: number;
rotationSpeed: number;
}
const ParticleExplosion: React.FC<{ frame: number; particleCount?: number }> = ({
frame,
particleCount = 50, // 增加到 50 个粒子
}) => {
// 使用 useMemo 确保粒子参数在组件生命周期内保持一致
const particles = React.useMemo(() => {
return Array.from({ length: particleCount }, (_, i) => {
// 添加随机性,但保持可预测(使用 index 作为种子)
const seed = i * 0.618033988749; // 黄金比例,产生均匀分布
const randomAngle = (seed % 1) * 0.2 - 0.1; // ±10度随机偏移
return {
angle: (i / particleCount) * Math.PI * 2 + randomAngle,
size: 4 + (seed % 1) * 8, // 4-12px 随机大小
color: [
'#00FFFF', // Cyan
'#FF00FF', // Magenta
'#FFFF00', // Yellow
'#00FF88', // Green
'#FF0080', // Pink
][i % 5],
velocity: 0.8 + (seed % 1) * 0.4, // 0.8-1.2 速度变化
depth: seed % 1, // 0-1 景深
rotationSpeed: (seed % 1) * 360 - 180, // -180 到 180 度/秒
} as Particle;
});
}, [particleCount]);
return (
<>
{particles.map((particle, i) => {
// 阶段 1: 爆炸(0-20 帧)
const explosionDistance = interpolate(
frame,
[0, 20],
[0, 200 * particle.velocity],
{
extrapolateRight: 'clamp',
easing: Easing.bezier(0.25, 0.46, 0.45, 0.94), // easeOutQuad - 减速
}
);
// 阶段 2: 重力下落(15-40 帧)
const gravityY = interpolate(
frame,
[15, 40],
[0, 80],
{
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad), // 加速下落
}
);
// 阶段 3: 淡出(30-45 帧)
const opacity = interpolate(
frame,
[0, 10, 30, 45],
[0, 1, 1, 0],
{
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.ease),
}
);
// 旋转动画
const rotation = interpolate(
frame,
[0, 60],
[0, particle.rotationSpeed * 2], // 2 秒旋转周期
{
extrapolateRight: 'extend',
}
);
// 景深缩放(远处的粒子更小)
const depthScale = 0.5 + particle.depth * 0.5; // 0.5-1.0 倍
// 计算最终位置
const x = Math.cos(particle.angle) * explosionDistance * depthScale;
const y = Math.sin(particle.angle) * explosionDistance * depthScale + gravityY;
// 动态光晕强度
const glowSize = interpolate(
frame,
[0, 10, 20],
[0, 20, 10],
{
extrapolateRight: 'clamp',
}
);
// 景深模糊(远处的粒子更模糊)
const blurAmount = particle.depth * 3;
return (
);
})}
>
);
};
// Helper: 3D rotating text - P1 ENHANCED
const Text3DEffect: React.FC<{ text: string; frame: number }> = ({ text, frame }) => {
// 三段式旋转:快速旋转 -> 停顿展示 -> 慢速旋转
const rotationY = interpolate(
frame,
[0, 30, 60, 90],
[0, Math.PI * 1.5, Math.PI * 1.5, Math.PI * 2], // 停顿在 270 度
{
extrapolateRight: 'clamp',
easing: Easing.bezier(0.65, 0, 0.35, 1), // easeInOutCubic
}
);
const rotateY = (rotationY * 180) / Math.PI;
// 动态景深调整
const perspective = interpolate(
frame,
[0, 30],
[1500, 800], // 逐渐拉近
{
extrapolateRight: 'clamp',
easing: Easing.out(Easing.quad),
}
);
// 动态缩放:旋转时放大
const scale = interpolate(
frame,
[0, 30, 60],
[0.8, 1.2, 1],
{
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
}
);
// 动态光效:正面时最亮
const glowIntensity = interpolate(
rotationY,
[Math.PI * 0.5, Math.PI, Math.PI * 1.5],
[0.3, 0.8, 1],
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}
);
return (
);
};
// Main animation functions (existing + enhanced)
const getTitleAnimation = (frame: number, fps: number) => {
const progress = frame / fps;
const glitchAmount = Math.sin(frame * 0.5) * 3;
return {
scale: spring({
frame,
fps,
from: 0,
to: 1,
config: designTokens.animation.spring.bouncy,
}),
translateY: 0,
opacity: interpolate(frame, [0, 10], [0, 1], { extrapolateRight: 'clamp' }),
glitch: getGlitchShadow(glitchAmount),
};
};
const getEmphasisAnimation = (frame: number, fps: number) => {
return {
scale: spring({
frame,
fps,
from: 0.5,
to: 1.1,
config: designTokens.animation.spring.snappy,
}),
translateY: 0,
opacity: 1,
glitch: '',
};
};
const getPainAnimation = (frame: number, fps: number) => {
const shake = Math.sin(frame * 0.8) * 2;
return {
scale: 1,
translateY: shake,
opacity: 1,
glitch: '',
};
};
const getCircleAnimation = (frame: number, fps: number) => {
return {
scale: 1,
translateY: 0,
opacity: interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}),
glitch: '',
};
};
const getEndAnimation = (frame: number, fps: number) => {
return {
scale: 1,
translateY: interpolate(frame, [0, 30], [0, -50], {
extrapolateRight: 'clamp',
easing: Easing.bezier(0.55, 0.055, 0.675, 0.19), // easeInCubic - 专业级加速
}),
opacity: interpolate(frame, [20, 30], [1, 0], {
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
}),
glitch: '',
};
};
const getContentAnimation = (frame: number, fps: number) => {
return {
scale: interpolate(frame, [0, 20], [0.95, 1], {
extrapolateRight: 'clamp',
easing: Easing.bezier(0.215, 0.61, 0.355, 1), // easeOutCubic - 专业级减速
}),
translateY: 0,
opacity: interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}),
glitch: '',
};
};
// Helper: Light sweep effect (扫光特效) - P1 NEW
const LightSweep: React.FC<{ frame: number; duration?: number }> = ({
frame,
duration = 60, // 2 秒扫过
}) => {
// 光带位置:从左至右扫过
const sweepX = interpolate(
frame,
[0, duration],
[-100, 200], // 从 -100% 到 200%(超出屏幕)
{
extrapolateRight: 'clamp',
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // easeInOutCubic
}
);
// 光带透明度:中间最亮
const opacity = interpolate(
frame,
[0, duration * 0.3, duration * 0.7, duration],
[0, 0.8, 0.8, 0],
{
extrapolateRight: 'clamp',
}
);
return (
);
};
// Highlight text helper
const highlightText = (text: string, highlight?: string) => {
if (!highlight) return <>{text}>;
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return (
<>
{parts.map((part, i) =>
part.toLowerCase() === highlight.toLowerCase() ? (
{part}
) : (
{part}
)
)}
>
);
};
// Helper: Camera movement effect (镜头运动) - P1 NEW
const getCameraEffect = (frame: number, sceneType: string) => {
switch (sceneType) {
case 'title':
// 推进镜头:从远到近
return {
scale: interpolate(
frame,
[0, 40],
[1.2, 1],
{
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}
),
translateY: interpolate(
frame,
[0, 40],
[-30, 0],
{
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}
),
translateX: 0,
};
case 'emphasis':
// 震动效果
const shake = Math.sin(frame * 0.8) * Math.max(0, 1 - frame / 20);
return {
scale: 1,
translateY: shake * 3,
translateX: shake * 2,
};
case 'end':
// 拉远镜头
return {
scale: interpolate(
frame,
[0, 30],
[1, 0.9],
{
extrapolateRight: 'clamp',
easing: Easing.in(Easing.quad),
}
),
translateY: 0,
translateX: 0,
};
default:
return { scale: 1, translateY: 0, translateX: 0 };
}
};
export const SceneRenderer: React.FC = ({ scene }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Check if using premium style theme
if (scene.styleTheme === 'premium') {
switch (scene.type) {
case 'elegance':
return ;
case 'authority':
return ;
case 'luxury':
return ;
case 'minimal':
return ;
case 'cinematic':
return ;
default:
// Fallback to elegance for unknown premium types
return ;
}
}
// Continue with existing cyber style for non-premium scenes
// Animation logic based on scene type
const getAnimation = () => {
switch (scene.type) {
case 'title':
return getTitleAnimation(frame, fps);
case 'emphasis':
return getEmphasisAnimation(frame, fps);
case 'pain':
return getPainAnimation(frame, fps);
case 'circle':
return getCircleAnimation(frame, fps);
case 'end':
return getEndAnimation(frame, fps);
default:
return getContentAnimation(frame, fps);
}
};
const animation = getAnimation();
// Determine if we should use enhanced effects
const useTextReveal = scene.type === 'title' || scene.type === 'emphasis';
const useParticles = scene.type === 'title';
const use3DText = scene.type === 'emphasis';
const useLightSweep = scene.type === 'emphasis' || scene.type === 'title';
// P1: Camera movement effect
const cameraEffect = getCameraEffect(frame, scene.type);
return (
{/* P1: Camera movement wrapper */}
{/* P2: Cyber grid background */}
{scene.type === 'title' &&
}
{/* P1: Light sweep effect */}
{useLightSweep &&
}
{/* Scene-specific background video */}
{scene.bgVideo && (
<>
>
)}
{/* Main title with background bar */}
{/* Particle explosion for title */}
{useParticles &&
}
{use3DText ? (
) : useTextReveal ? (
) : (
highlightText(scene.title, scene.highlight)
)}
{/* Subtitle */}
{scene.subtitle && (
{highlightText(scene.subtitle, scene.highlight)}
)}
{/* Number highlight */}
{scene.number && (
{scene.number}
)}
{/* End P1: Camera movement wrapper */}
);
};