forked from 77media/video-flow
328 lines
9.3 KiB
TypeScript
328 lines
9.3 KiB
TypeScript
"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>
|
||
);
|
||
};
|
||
|
||
// 动画2:ImageWave 动画展示
|
||
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>
|
||
);
|
||
};
|