video-flow-b/components/common/EmptyStateAnimation.tsx

328 lines
9.3 KiB
TypeScript
Raw 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.

"use client";
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { ArrowUp } from 'lucide-react';
import gsap from 'gsap';
import { ImageWave } from '@/components/ui/ImageWave';
interface AnimationStageProps {
shouldStart: boolean;
onComplete: () => void;
}
// 动画1模拟输入和点击
const InputAnimation: React.FC<AnimationStageProps> = ({ shouldStart, onComplete }) => {
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
const cursorRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const mouseRef = useRef<HTMLDivElement>(null);
const [displayText, setDisplayText] = useState('');
const demoText = "a cute capybara with an orange on its head";
useEffect(() => {
if (!shouldStart || !containerRef.current) return;
// 重置状态
setDisplayText('');
const tl = gsap.timeline({
onComplete: () => {
setTimeout(onComplete, 500);
}
});
// 1. 显示输入框和鼠标
tl.fromTo([inputRef.current, mouseRef.current], {
scale: 0.9,
opacity: 0
}, {
scale: 1,
opacity: 1,
duration: 0.3,
ease: "back.out(1.7)",
stagger: 0.1
});
// 2. 鼠标移动到输入框中心
tl.to(mouseRef.current, {
x: 20,
y: 0,
duration: 0.3
});
// 3. 输入框聚焦效果
tl.to(inputRef.current, {
scale: 1.02,
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)',
duration: 0.2
});
// 4. 打字动画
const typingDuration = demoText.length * 0.05;
tl.to({}, {
duration: typingDuration,
onUpdate: function() {
const progress = this.progress();
const targetChar = Math.floor(progress * demoText.length);
setDisplayText(demoText.slice(0, targetChar));
}
});
// 6. 鼠标移动到按钮位置(调整移动时间和缓动函数)
tl.to(mouseRef.current, {
x: 650,
y: 0,
duration: 0.8,
ease: "power2.inOut"
});
// 7. 等待一小段时间
tl.to({}, { duration: 0.2 });
// 8. 点击效果
tl.to(mouseRef.current, {
scale: 0.8,
duration: 0.1,
yoyo: true,
repeat: 1
}).to(buttonRef.current, {
scale: 0.95,
duration: 0.1,
yoyo: true,
repeat: 1
}, "<");
// 9. 等待一小段时间
tl.to({}, { duration: 0.3 });
// 10. 整体淡出
tl.to(containerRef.current, {
opacity: 0,
y: -20,
duration: 0.3
});
}, [shouldStart, onComplete]);
return (
<div ref={containerRef} className="fixed top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="relative flex items-center gap-4">
<div
ref={inputRef}
className="relative w-[600px] h-[50px] bg-white/10 backdrop-blur-md rounded-lg px-4 py-3 flex items-center"
>
<span className="text-white/70 text-base font-mono">
{displayText}
</span>
<div
ref={cursorRef}
className="w-0.5 h-5 bg-blue-400 ml-1 animate-pulse"
/>
</div>
<button
ref={buttonRef}
className={`${displayText ? 'bg-indigo-600 opacity-100' : 'bg-[#5b5b5b] opacity-30'} hover:bg-indigo-700 text-white px-6 py-2 rounded-md text-sm font-medium flex items-center gap-2 min-w-[100px]`}
>
<ArrowUp className="w-4 h-4" />
Action
</button>
<div
ref={mouseRef}
className="absolute left-0 top-1/2 -translate-y-1/2 opacity-0 pointer-events-none z-10"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="white" className="drop-shadow-lg">
<path d="M8 2L8 22L12 18L16 22L22 16L12 6L8 2Z" />
</svg>
</div>
</div>
</div>
);
};
// 动画2ImageWave 动画展示
const WaveAnimation: React.FC<AnimationStageProps> = ({ shouldStart, onComplete }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [showWave, setShowWave] = useState(false);
const [autoAnimate, setAutoAnimate] = useState(false);
const imageUrls = [
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
];
useEffect(() => {
if (!shouldStart) {
setShowWave(false);
setAutoAnimate(false);
return;
}
// 显示 ImageWave
setShowWave(true);
// 延迟开始自动动画
const startTimeout = setTimeout(() => {
setAutoAnimate(true);
}, 300);
return () => {
clearTimeout(startTimeout);
};
}, [shouldStart]);
const handleAnimationComplete = () => {
// 动画完成后淡出并触发完成回调
gsap.to(containerRef.current, {
opacity: 0,
scale: 0.9,
duration: 0.3,
onComplete: () => {
setAutoAnimate(false);
setShowWave(false);
onComplete();
}
});
};
return (
<div
ref={containerRef}
className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-all duration-300
${showWave ? 'opacity-100 scale-100' : 'opacity-0 scale-90'}`}
>
<ImageWave
images={imageUrls}
containerWidth="90vw"
containerHeight="60vh"
itemWidth="calc(var(--index) * 5)"
itemHeight="calc(var(--index) * 12)"
gap="0.3rem"
autoAnimate={autoAnimate}
autoAnimateInterval={100}
onAnimationComplete={handleAnimationComplete}
/>
</div>
);
};
// 动画3图片墙打破显示视频
const FinalAnimation: React.FC<AnimationStageProps> = ({ shouldStart, onComplete }) => {
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [showVideo, setShowVideo] = useState(false);
const videoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
useEffect(() => {
if (!shouldStart || !containerRef.current) return;
const tl = gsap.timeline({
onComplete: () => {
setTimeout(() => {
// 淡出视频
gsap.to(containerRef.current, {
opacity: 0,
scale: 0.9,
duration: 0.3,
onComplete
});
}, 3000);
}
});
// 显示容器
tl.fromTo(containerRef.current,
{ opacity: 0, scale: 0.9 },
{ opacity: 1, scale: 1, duration: 0.3 }
);
// 显示视频
setShowVideo(true);
}, [shouldStart, onComplete]);
return (
<div
ref={containerRef}
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] rounded-lg overflow-hidden opacity-0"
>
{showVideo && (
<video
ref={videoRef}
src={videoUrl}
className="w-full h-full object-cover"
autoPlay
muted
loop
playsInline
/>
)}
</div>
);
};
// 主组件
export const EmptyStateAnimation = ({ className }: { className: string }) => {
const [currentStage, setCurrentStage] = useState<'input' | 'wave' | 'final'>('input');
const [animationCycle, setAnimationCycle] = useState(0);
const [isReady, setIsReady] = useState(true);
const handleStageComplete = useCallback(() => {
// 先将当前阶段标记为不可执行
setIsReady(false);
// 延迟切换到下一个阶段
setTimeout(() => {
switch (currentStage) {
case 'input':
setCurrentStage('wave');
break;
case 'wave':
setCurrentStage('final');
break;
case 'final':
setAnimationCycle(prev => prev + 1);
setCurrentStage('input');
break;
}
// 给下一个阶段一些准备时间
setTimeout(() => {
setIsReady(true);
}, 100);
}, 300);
}, [currentStage]);
return (
<div className={className}>
<InputAnimation
key={`input-${animationCycle}`}
shouldStart={currentStage === 'input' && isReady}
onComplete={handleStageComplete}
/>
<WaveAnimation
key={`wave-${animationCycle}`}
shouldStart={currentStage === 'wave' && isReady}
onComplete={handleStageComplete}
/>
<FinalAnimation
key={`final-${animationCycle}`}
shouldStart={currentStage === 'final' && isReady}
onComplete={handleStageComplete}
/>
</div>
);
};