forked from 77media/video-flow
空页面动画展示
This commit is contained in:
parent
b6d608fd54
commit
a5a6da472d
920
components/common/EmptyStateAnimation.tsx
Normal file
920
components/common/EmptyStateAnimation.tsx
Normal file
@ -0,0 +1,920 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { ArrowUp } from 'lucide-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import gsap from 'gsap';
|
||||||
|
import { SplitText } from 'gsap/SplitText';
|
||||||
|
|
||||||
|
// 注册 SplitText 插件
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
gsap.registerPlugin(SplitText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward';
|
||||||
|
|
||||||
|
const AnimatedText = ({ text, onComplete, shouldStart }: { text: string; onComplete: () => void; shouldStart: boolean }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const titleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cursorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mouseRef = useRef<HTMLDivElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
const animationRef = useRef<any>(null);
|
||||||
|
const [displayText, setDisplayText] = useState('');
|
||||||
|
|
||||||
|
const demoText = "a cute capybara with an orange on its head";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current && typeof window !== 'undefined' && shouldStart) {
|
||||||
|
// 清理之前的动画
|
||||||
|
if (animationRef.current) {
|
||||||
|
animationRef.current.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
setDisplayText('');
|
||||||
|
if (containerRef.current) {
|
||||||
|
containerRef.current.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置初始状态
|
||||||
|
gsap.set([titleRef.current, inputContainerRef.current], {
|
||||||
|
opacity: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建主时间轴
|
||||||
|
const mainTl = gsap.timeline();
|
||||||
|
|
||||||
|
// 1. 显示标题和输入框
|
||||||
|
mainTl.fromTo(titleRef.current, {
|
||||||
|
y: -30,
|
||||||
|
opacity: 0
|
||||||
|
}, {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "power2.out"
|
||||||
|
})
|
||||||
|
.fromTo(inputContainerRef.current, {
|
||||||
|
scale: 0.9,
|
||||||
|
opacity: 0
|
||||||
|
}, {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "back.out(1.7)"
|
||||||
|
}, "-=0.3");
|
||||||
|
|
||||||
|
// 2. 显示鼠标指针并移动到输入框
|
||||||
|
mainTl.fromTo(mouseRef.current,
|
||||||
|
{ opacity: 0, x: -100, y: -50 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
x: -150,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "power2.out"
|
||||||
|
},
|
||||||
|
"+=0.5"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 鼠标移动到输入框中心
|
||||||
|
mainTl.to(mouseRef.current, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 输入框聚焦效果
|
||||||
|
mainTl.to(inputRef.current, {
|
||||||
|
scale: 1.05,
|
||||||
|
rotationY: 1,
|
||||||
|
rotationX: 15,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
boxShadow: `
|
||||||
|
inset 0 3px 0 rgba(255,255,255,0.35),
|
||||||
|
inset 0 -3px 0 rgba(0,0,0,0.2),
|
||||||
|
inset 3px 0 0 rgba(255,255,255,0.15),
|
||||||
|
inset -3px 0 0 rgba(0,0,0,0.08),
|
||||||
|
0 0 0 3px rgba(59, 130, 246, 0.4),
|
||||||
|
0 4px 8px rgba(0,0,0,0.15),
|
||||||
|
0 12px 24px rgba(0,0,0,0.2),
|
||||||
|
0 20px 40px rgba(0,0,0,0.15),
|
||||||
|
0 0 0 1px rgba(59, 130, 246, 0.6)
|
||||||
|
`,
|
||||||
|
borderColor: 'rgba(79, 70, 229, 0.8)',
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "back.out(1.7)"
|
||||||
|
}, "+=0.2");
|
||||||
|
|
||||||
|
// 5. 显示按钮
|
||||||
|
mainTl.fromTo(buttonRef.current,
|
||||||
|
{ scale: 0, opacity: 0 },
|
||||||
|
{ scale: 1, opacity: 1, duration: 0.2, ease: "back.out(1.7)" },
|
||||||
|
"+=0.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 隐藏鼠标指针
|
||||||
|
mainTl.to(mouseRef.current, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.2
|
||||||
|
}, "+=0.2");
|
||||||
|
|
||||||
|
// 6. 打字动画
|
||||||
|
const typingDuration = demoText.length * 0.03;
|
||||||
|
let currentChar = 0;
|
||||||
|
|
||||||
|
mainTl.to({}, {
|
||||||
|
duration: typingDuration,
|
||||||
|
ease: "none",
|
||||||
|
onUpdate: function() {
|
||||||
|
const progress = this.progress();
|
||||||
|
const targetChar = Math.floor(progress * demoText.length);
|
||||||
|
if (targetChar !== currentChar && targetChar <= demoText.length) {
|
||||||
|
currentChar = targetChar;
|
||||||
|
setDisplayText(demoText.slice(0, currentChar));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "+=0.3");
|
||||||
|
|
||||||
|
// 7. 隐藏光标
|
||||||
|
mainTl.to(cursorRef.current, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.2
|
||||||
|
}, "+=0.2");
|
||||||
|
|
||||||
|
// 重新显示鼠标指针并移动到按钮
|
||||||
|
mainTl.fromTo(mouseRef.current,
|
||||||
|
{ opacity: 0, x: 0, y: 0 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.out"
|
||||||
|
},
|
||||||
|
"-=0.3"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 鼠标移动到按钮
|
||||||
|
mainTl.to(mouseRef.current, {
|
||||||
|
x: 20,
|
||||||
|
y: 10,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
}, "+=0.5");
|
||||||
|
|
||||||
|
// 8. 输入框失焦效果
|
||||||
|
mainTl.to(inputRef.current, {
|
||||||
|
scale: 1,
|
||||||
|
rotationY: 0,
|
||||||
|
rotationX: 0,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
boxShadow: `
|
||||||
|
inset 0 2px 0 rgba(255,255,255,0.25),
|
||||||
|
inset 0 -2px 0 rgba(0,0,0,0.15),
|
||||||
|
inset 2px 0 0 rgba(255,255,255,0.1),
|
||||||
|
inset -2px 0 0 rgba(0,0,0,0.05),
|
||||||
|
0 2px 4px rgba(0,0,0,0.1),
|
||||||
|
0 8px 16px rgba(0,0,0,0.15),
|
||||||
|
0 16px 32px rgba(0,0,0,0.1),
|
||||||
|
0 0 0 1px rgba(255,255,255,0.1)
|
||||||
|
`,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.out"
|
||||||
|
}, "+=0.2");
|
||||||
|
|
||||||
|
// 点击效果
|
||||||
|
mainTl.to(mouseRef.current, {
|
||||||
|
scale: 0.8,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
yoyo: true,
|
||||||
|
repeat: 1
|
||||||
|
})
|
||||||
|
.to(buttonRef.current, {
|
||||||
|
scale: 0.95,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
yoyo: true,
|
||||||
|
repeat: 1
|
||||||
|
}, "-=0.2")
|
||||||
|
.to(buttonRef.current, {
|
||||||
|
boxShadow: "0 0 20px rgba(79, 70, 229, 0.6)",
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out"
|
||||||
|
}, "-=0.1");
|
||||||
|
|
||||||
|
// 停留展示时间
|
||||||
|
mainTl.to({}, { duration: 0.1 });
|
||||||
|
|
||||||
|
// 退场动画
|
||||||
|
mainTl.to(containerRef.current, {
|
||||||
|
opacity: 0,
|
||||||
|
y: -50,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "power2.in",
|
||||||
|
onComplete: () => {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
animationRef.current = mainTl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationRef.current) {
|
||||||
|
animationRef.current.kill();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [shouldStart, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="animated-text flex flex-col items-center gap-6 text-center">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div ref={titleRef} className="title-text">
|
||||||
|
<h2 className="text-xl font-medium text-white/70 mb-2">
|
||||||
|
You can input script to generate video
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输入框模拟 */}
|
||||||
|
<div ref={inputContainerRef} className="input-container relative" style={{ perspective: '1000px' }}>
|
||||||
|
<div
|
||||||
|
ref={inputRef}
|
||||||
|
className="relative w-[600px] h-[60px] bg-gradient-to-br from-white/12 to-white/4 backdrop-blur-md rounded-lg px-4 py-3 flex items-center transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(233,231,231,0.9)',
|
||||||
|
boxShadow: `
|
||||||
|
inset 0 2px 0 rgba(233,231,231,0.25),
|
||||||
|
inset 0 -2px 0 rgba(0,0,0,0.15),
|
||||||
|
inset 2px 0 0 rgba(233,231,231,0.1),
|
||||||
|
inset -2px 0 0 rgba(0,0,0,0.05),
|
||||||
|
0 2px 4px rgba(0,0,0,0.1),
|
||||||
|
0 8px 16px rgba(0,0,0,0.15),
|
||||||
|
0 16px 32px rgba(0,0,0,0.1),
|
||||||
|
0 0 0 1px rgba(233,231,231,0.1)
|
||||||
|
`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-[#000] text-base font-mono">
|
||||||
|
{displayText}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
ref={cursorRef}
|
||||||
|
className="w-0.5 h-5 bg-blue-400 ml-1 animate-pulse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create按钮 */}
|
||||||
|
<div
|
||||||
|
ref={buttonRef}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-md text-sm font-medium cursor-pointer transition-all duration-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowUp className="w-4 h-4" />
|
||||||
|
Create
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 鼠标指针 */}
|
||||||
|
<div
|
||||||
|
ref={mouseRef}
|
||||||
|
className="absolute right-8 top-1/2 -translate-y-1/2 opacity-0 pointer-events-none z-10"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
className="text-white drop-shadow-lg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 2L8 22L12 18L16 22L22 16L12 6L8 2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="rgba(0,0,0,0.2)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 阶段文字解释组件
|
||||||
|
const StageExplanation = ({
|
||||||
|
text,
|
||||||
|
stage,
|
||||||
|
shouldStart,
|
||||||
|
onComplete
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
stage: 'images' | 'replacing' | 'merging';
|
||||||
|
shouldStart: boolean;
|
||||||
|
onComplete: () => void;
|
||||||
|
}) => {
|
||||||
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
const splitRef = useRef<any>(null);
|
||||||
|
const animationRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current && typeof window !== 'undefined' && shouldStart) {
|
||||||
|
// 重置显示
|
||||||
|
textRef.current.style.opacity = '1';
|
||||||
|
|
||||||
|
// 清理之前的动画
|
||||||
|
if (splitRef.current) {
|
||||||
|
splitRef.current.revert();
|
||||||
|
}
|
||||||
|
if (animationRef.current) {
|
||||||
|
animationRef.current.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建分割文本
|
||||||
|
splitRef.current = new SplitText(textRef.current, { type: "words" });
|
||||||
|
|
||||||
|
let tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
// 延迟后开始退场动画
|
||||||
|
setTimeout(() => {
|
||||||
|
const exitTl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不同阶段的退场动画
|
||||||
|
switch (stage) {
|
||||||
|
case 'images':
|
||||||
|
exitTl.to(splitRef.current.words, {
|
||||||
|
y: -50,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.in",
|
||||||
|
stagger: 0.05
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'replacing':
|
||||||
|
exitTl.to(splitRef.current.words, {
|
||||||
|
scale: 0,
|
||||||
|
rotation: 360,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "back.in(2)",
|
||||||
|
stagger: 0.08
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'merging':
|
||||||
|
exitTl.to(splitRef.current.words, {
|
||||||
|
x: "random(-200, 200)",
|
||||||
|
y: "random(-100, -200)",
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "power3.in",
|
||||||
|
stagger: 0.06
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, 1000); // 显示1秒
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不同阶段的入场动画
|
||||||
|
switch (stage) {
|
||||||
|
case 'images':
|
||||||
|
// 从下方弹跳进入
|
||||||
|
tl.from(splitRef.current.words, {
|
||||||
|
y: 100,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "bounce.out",
|
||||||
|
stagger: 0.1
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'replacing':
|
||||||
|
// 旋转缩放进入
|
||||||
|
tl.from(splitRef.current.words, {
|
||||||
|
scale: 0,
|
||||||
|
rotation: -180,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "back.out(2)",
|
||||||
|
stagger: 0.08
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'merging':
|
||||||
|
// 从四周飞入
|
||||||
|
tl.from(splitRef.current.words, {
|
||||||
|
x: "random(-300, 300)",
|
||||||
|
y: "random(-200, 200)",
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "power3.out",
|
||||||
|
stagger: 0.06
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationRef.current = tl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationRef.current) {
|
||||||
|
animationRef.current.kill();
|
||||||
|
}
|
||||||
|
if (splitRef.current) {
|
||||||
|
splitRef.current.revert();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [shouldStart, stage, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={textRef}
|
||||||
|
className="fixed top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center z-10"
|
||||||
|
>
|
||||||
|
<p className="text-lg font-medium text-white/80 max-w-[600px]">{text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onComplete: () => void }) => {
|
||||||
|
const imagesRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [currentStage, setCurrentStage] = useState<'images' | 'replacing' | 'merging'>('images');
|
||||||
|
const [replacementIndex, setReplacementIndex] = useState(0);
|
||||||
|
const [showStageText, setShowStageText] = useState(false);
|
||||||
|
const finalVideoContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
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'
|
||||||
|
];
|
||||||
|
|
||||||
|
const videoUrls = [
|
||||||
|
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
|
||||||
|
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
||||||
|
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
|
||||||
|
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4'
|
||||||
|
];
|
||||||
|
|
||||||
|
const finalVideoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
|
||||||
|
|
||||||
|
// 阶段文字映射
|
||||||
|
const stageTexts = {
|
||||||
|
images: 'Then, these are the key frames split from the storyboard',
|
||||||
|
replacing: 'Then, these are the videos corresponding to the split storyboard',
|
||||||
|
merging: 'Finally, efficiently edit the perfect video'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置函数
|
||||||
|
const resetComponent = () => {
|
||||||
|
// 清理最终视频容器
|
||||||
|
if (finalVideoContainerRef.current) {
|
||||||
|
finalVideoContainerRef.current.remove();
|
||||||
|
finalVideoContainerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外清理:查找并移除所有可能的最终视频容器
|
||||||
|
const allFinalVideos = document.querySelectorAll('div[class*="fixed"][class*="top-1/2"][class*="w-[400px]"]');
|
||||||
|
allFinalVideos.forEach(container => {
|
||||||
|
if (container.parentNode === document.body) {
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置所有状态
|
||||||
|
setCurrentStage('images');
|
||||||
|
setReplacementIndex(0);
|
||||||
|
setShowStageText(false);
|
||||||
|
|
||||||
|
// 重置DOM结构
|
||||||
|
if (imagesRef.current) {
|
||||||
|
imagesRef.current.style.display = 'flex';
|
||||||
|
Array.from(imagesRef.current.children).forEach((container, index) => {
|
||||||
|
const htmlContainer = container as HTMLElement;
|
||||||
|
|
||||||
|
// 移除视频元素
|
||||||
|
const videos = htmlContainer.querySelectorAll('video');
|
||||||
|
videos.forEach(video => video.remove());
|
||||||
|
|
||||||
|
// 使用GSAP重置容器样式
|
||||||
|
gsap.set(htmlContainer, {
|
||||||
|
position: 'static',
|
||||||
|
x: 0,
|
||||||
|
y: index % 2 ? 20 : -20,
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
rotation: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保图片显示
|
||||||
|
const img = htmlContainer.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
img.style.opacity = '1';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当shouldStart改变时重置并开始新的动画
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldStart) {
|
||||||
|
resetComponent();
|
||||||
|
// 延迟显示阶段文字
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowStageText(true);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [shouldStart]);
|
||||||
|
|
||||||
|
// 阶段文字完成回调
|
||||||
|
const handleStageTextComplete = () => {
|
||||||
|
setShowStageText(false);
|
||||||
|
// 根据当前阶段决定下一步
|
||||||
|
if (currentStage === 'images') {
|
||||||
|
// 开始图片入场动画
|
||||||
|
startImagesAnimation();
|
||||||
|
} else if (currentStage === 'replacing') {
|
||||||
|
// 继续图片替换视频
|
||||||
|
// replacementIndex会自动处理
|
||||||
|
} else if (currentStage === 'merging') {
|
||||||
|
// 开始合并动画
|
||||||
|
startMergingAnimation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片入场动画
|
||||||
|
const startImagesAnimation = () => {
|
||||||
|
if (imagesRef.current) {
|
||||||
|
const images = imagesRef.current.children;
|
||||||
|
|
||||||
|
gsap.set(images, {
|
||||||
|
x: window.innerWidth,
|
||||||
|
opacity: 0,
|
||||||
|
rotation: 45
|
||||||
|
});
|
||||||
|
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
// 图片动画完成后,切换到替换阶段
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentStage('replacing');
|
||||||
|
setShowStageText(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tl.to(images, {
|
||||||
|
x: -100,
|
||||||
|
opacity: 1,
|
||||||
|
rotation: 0,
|
||||||
|
duration: 1,
|
||||||
|
ease: "elastic.out(1, 0.5)",
|
||||||
|
stagger: {
|
||||||
|
amount: 1,
|
||||||
|
from: "random"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始合并动画
|
||||||
|
const startMergingAnimation = () => {
|
||||||
|
if (imagesRef.current) {
|
||||||
|
const containers = Array.from(imagesRef.current.children) as HTMLElement[];
|
||||||
|
const videos = containers.map(container => container.querySelector('video')).filter(Boolean) as HTMLVideoElement[];
|
||||||
|
|
||||||
|
if (videos.length > 0) {
|
||||||
|
// 创建最终的大视频容器
|
||||||
|
const finalVideoContainer = document.createElement('div');
|
||||||
|
finalVideoContainer.className = 'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] rounded-lg overflow-hidden shadow-2xl opacity-0';
|
||||||
|
|
||||||
|
const finalVideo = document.createElement('video');
|
||||||
|
finalVideo.src = finalVideoUrl;
|
||||||
|
finalVideo.autoplay = true;
|
||||||
|
finalVideo.loop = true;
|
||||||
|
finalVideo.muted = true;
|
||||||
|
finalVideo.playsInline = true;
|
||||||
|
finalVideo.className = 'w-full h-full object-cover';
|
||||||
|
|
||||||
|
finalVideoContainer.appendChild(finalVideo);
|
||||||
|
document.body.appendChild(finalVideoContainer);
|
||||||
|
finalVideoContainerRef.current = finalVideoContainer;
|
||||||
|
|
||||||
|
// 等待最终视频准备就绪
|
||||||
|
const onFinalVideoReady = () => {
|
||||||
|
finalVideo.removeEventListener('canplay', onFinalVideoReady);
|
||||||
|
|
||||||
|
finalVideo.play().then(() => {
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
// 移除原有的小视频容器
|
||||||
|
if (imagesRef.current) {
|
||||||
|
imagesRef.current.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟3秒后调用完成回调,开始下一轮循环
|
||||||
|
setTimeout(() => {
|
||||||
|
onComplete();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行合并动画
|
||||||
|
tl.to(containers, {
|
||||||
|
rotation: "random(-720, 720)",
|
||||||
|
x: "random(-200, 200)",
|
||||||
|
y: "random(-200, 200)",
|
||||||
|
scale: 0.8,
|
||||||
|
duration: 0.7,
|
||||||
|
ease: "power2.out",
|
||||||
|
stagger: 0.1
|
||||||
|
})
|
||||||
|
.to(containers, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotation: 0,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
stagger: 0.05
|
||||||
|
})
|
||||||
|
.to(containers, {
|
||||||
|
scale: 1.2,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
})
|
||||||
|
.to(finalVideoContainer, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.out"
|
||||||
|
}, "-=0.2")
|
||||||
|
.to(containers, {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.8,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.in"
|
||||||
|
}, "-=0.5")
|
||||||
|
.to(finalVideoContainer, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "back.out(1.7)"
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('最终视频播放失败');
|
||||||
|
finalVideoContainer.remove();
|
||||||
|
onComplete();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
finalVideo.addEventListener('canplay', onFinalVideoReady);
|
||||||
|
finalVideo.addEventListener('error', () => {
|
||||||
|
console.error('最终视频加载失败');
|
||||||
|
finalVideoContainer.remove();
|
||||||
|
onComplete();
|
||||||
|
});
|
||||||
|
|
||||||
|
finalVideo.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片到视频的替换动画
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStage === 'replacing' && imagesRef.current && replacementIndex < imageUrls.length && shouldStart && !showStageText) {
|
||||||
|
const targetContainer = imagesRef.current.children[replacementIndex] as HTMLElement;
|
||||||
|
|
||||||
|
if (targetContainer) {
|
||||||
|
// 创建视频元素
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = videoUrls[replacementIndex];
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.className = 'absolute inset-0 w-full h-full object-cover opacity-0';
|
||||||
|
|
||||||
|
// 添加视频到容器中
|
||||||
|
targetContainer.style.position = 'relative';
|
||||||
|
targetContainer.appendChild(video);
|
||||||
|
|
||||||
|
// 等待视频准备好播放
|
||||||
|
const onCanPlay = () => {
|
||||||
|
video.removeEventListener('canplay', onCanPlay);
|
||||||
|
video.removeEventListener('error', onError);
|
||||||
|
|
||||||
|
// 开始播放
|
||||||
|
video.play().then(() => {
|
||||||
|
// 创建无缝替换动画
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => {
|
||||||
|
// 移除图片元素
|
||||||
|
const img = targetContainer.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
img.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除绝对定位,让视频正常占位
|
||||||
|
video.className = 'w-full h-full object-cover';
|
||||||
|
|
||||||
|
// 检查是否是最后一个视频
|
||||||
|
if (replacementIndex === imageUrls.length - 1) {
|
||||||
|
// 最后一个视频替换完成,切换到合并阶段
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentStage('merging');
|
||||||
|
setShowStageText(true);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// 延迟后替换下一个
|
||||||
|
setTimeout(() => {
|
||||||
|
setReplacementIndex(prev => prev + 1);
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同时淡入视频和淡出图片
|
||||||
|
tl.to(video, {
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
})
|
||||||
|
.to(targetContainer.querySelector('img'), {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
}, 0)
|
||||||
|
.to(targetContainer, {
|
||||||
|
scale: 1.02,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
yoyo: true,
|
||||||
|
repeat: 1
|
||||||
|
}, 0.1);
|
||||||
|
}).catch(onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
video.removeEventListener('canplay', onCanPlay);
|
||||||
|
video.removeEventListener('error', onError);
|
||||||
|
console.error('视频加载或播放失败:', videoUrls[replacementIndex]);
|
||||||
|
|
||||||
|
// 移除失败的视频元素
|
||||||
|
video.remove();
|
||||||
|
|
||||||
|
// 跳过到下一个
|
||||||
|
if (replacementIndex === imageUrls.length - 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentStage('merging');
|
||||||
|
setShowStageText(true);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
setReplacementIndex(prev => prev + 1);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('canplay', onCanPlay);
|
||||||
|
video.addEventListener('error', onError);
|
||||||
|
|
||||||
|
// 加载视频
|
||||||
|
video.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentStage, replacementIndex, shouldStart, showStageText]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showStageText && (
|
||||||
|
<StageExplanation
|
||||||
|
text={stageTexts[currentStage]}
|
||||||
|
stage={currentStage}
|
||||||
|
shouldStart={showStageText}
|
||||||
|
onComplete={handleStageTextComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div ref={imagesRef} className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex gap-4">
|
||||||
|
{imageUrls.map((url, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-[200px] h-[150px] rounded-lg overflow-hidden shadow-lg"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${index % 2 ? '20px' : '-20px'})`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={url}
|
||||||
|
alt={`reference-${index + 1}`}
|
||||||
|
width={200}
|
||||||
|
height={150}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主要的空状态动画组件
|
||||||
|
export const EmptyStateAnimation = () => {
|
||||||
|
const [showText, setShowText] = useState(false);
|
||||||
|
const [showImages, setShowImages] = useState(false);
|
||||||
|
const [animationCycle, setAnimationCycle] = useState(0);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
// 全局清理函数
|
||||||
|
const globalCleanup = () => {
|
||||||
|
// 清理所有可能的最终视频容器
|
||||||
|
const existingFinalVideos = document.querySelectorAll('div[class*="fixed"][class*="top-1/2"][class*="w-[400px]"]');
|
||||||
|
existingFinalVideos.forEach(container => {
|
||||||
|
if (container.parentNode === document.body) {
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理所有body下的视频相关元素
|
||||||
|
const bodyChildren = Array.from(document.body.children);
|
||||||
|
bodyChildren.forEach(child => {
|
||||||
|
if (child instanceof HTMLElement &&
|
||||||
|
(child.querySelector('video') || child.tagName === 'VIDEO')) {
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 循环控制
|
||||||
|
const startNextCycle = () => {
|
||||||
|
// 先进行全局清理
|
||||||
|
globalCleanup();
|
||||||
|
|
||||||
|
setAnimationCycle(prev => prev + 1);
|
||||||
|
setShowImages(false);
|
||||||
|
|
||||||
|
// 延迟一下再显示文字,确保清理完成
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowText(true);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextComplete = () => {
|
||||||
|
setShowText(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowImages(true);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImagesComplete = () => {
|
||||||
|
setShowImages(false);
|
||||||
|
// 延迟后开始新的循环
|
||||||
|
setTimeout(() => {
|
||||||
|
startNextCycle();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始第一轮动画
|
||||||
|
useEffect(() => {
|
||||||
|
if (isClient && animationCycle === 0) {
|
||||||
|
startNextCycle();
|
||||||
|
}
|
||||||
|
}, [isClient]);
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
globalCleanup();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isClient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col justify-center items-center'>
|
||||||
|
{showText && (
|
||||||
|
<AnimatedText
|
||||||
|
key={`text-${animationCycle}`}
|
||||||
|
text="Choose to input a script below or select a video to replicate. Before clicking the create button, you can choose the generation mode - automatic or manual intervention, as well as the resolution of the generated video. Once confirmed, you can click the create button."
|
||||||
|
onComplete={handleTextComplete}
|
||||||
|
shouldStart={showText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showImages && (
|
||||||
|
<ImageQueue
|
||||||
|
key={`images-${animationCycle}`}
|
||||||
|
shouldStart={showImages}
|
||||||
|
onComplete={handleImagesComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -20,6 +20,7 @@ import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums";
|
|||||||
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
|
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
|
||||||
import { getUploadTokenWithDomain, uploadToQiniu } from "@/api/common";
|
import { getUploadTokenWithDomain, uploadToQiniu } from "@/api/common";
|
||||||
import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow";
|
import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow";
|
||||||
|
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation';
|
||||||
|
|
||||||
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
|
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -27,6 +28,11 @@ const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
|
|||||||
|
|
||||||
// 导入Step类型
|
// 导入Step类型
|
||||||
import type { Step } from 'react-joyride';
|
import type { Step } from 'react-joyride';
|
||||||
|
// interface Step {
|
||||||
|
// target: string;
|
||||||
|
// content: string;
|
||||||
|
// placement?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
// }
|
||||||
|
|
||||||
// 添加自定义滚动条样式
|
// 添加自定义滚动条样式
|
||||||
const scrollbarStyles = `
|
const scrollbarStyles = `
|
||||||
@ -72,6 +78,8 @@ export function CreateToVideo2() {
|
|||||||
const [runTour, setRunTour] = useState(true);
|
const [runTour, setRunTour] = useState(true);
|
||||||
const [episodeId, setEpisodeId] = useState<number>(0);
|
const [episodeId, setEpisodeId] = useState<number>(0);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
|
||||||
|
const [projectName, setProjectName] = useState(localStorage.getItem('projectName') || '默认名称');
|
||||||
|
|
||||||
const handleUploadVideo = async () => {
|
const handleUploadVideo = async () => {
|
||||||
console.log('upload video');
|
console.log('upload video');
|
||||||
@ -121,7 +129,7 @@ export function CreateToVideo2() {
|
|||||||
alert(`创建剧集失败: ${episodeResponse.message}`);
|
alert(`创建剧集失败: ${episodeResponse.message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let episodeId = episodeResponse.data.id ;
|
let episodeId = episodeResponse.data.id;
|
||||||
|
|
||||||
if (videoUrl || script) {
|
if (videoUrl || script) {
|
||||||
try {
|
try {
|
||||||
@ -150,18 +158,18 @@ export function CreateToVideo2() {
|
|||||||
}
|
}
|
||||||
// 更新剧集
|
// 更新剧集
|
||||||
const updateEpisodeData: UpdateScriptEpisodeRequest = {
|
const updateEpisodeData: UpdateScriptEpisodeRequest = {
|
||||||
id: episodeId,
|
id: episodeId,
|
||||||
atmosphere: convertResponse.data.atmosphere,
|
atmosphere: convertResponse.data.atmosphere,
|
||||||
summary: convertResponse.data.summary,
|
summary: convertResponse.data.summary,
|
||||||
scene: convertResponse.data.scene,
|
scene: convertResponse.data.scene,
|
||||||
characters: convertResponse.data.characters,
|
characters: convertResponse.data.characters,
|
||||||
};
|
};
|
||||||
const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData);
|
const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData);
|
||||||
|
|
||||||
// 检查转换结果
|
// 检查转换结果
|
||||||
if (convertResponse.code === 0) {
|
if (convertResponse.code === 0) {
|
||||||
// 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType
|
// 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType
|
||||||
router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`);
|
router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`);
|
||||||
} else {
|
} else {
|
||||||
alert(`转换失败: ${convertResponse.message}`);
|
alert(`转换失败: ${convertResponse.message}`);
|
||||||
}
|
}
|
||||||
@ -356,7 +364,7 @@ export function CreateToVideo2() {
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="container mx-auto overflow-hidden custom-scrollbar"
|
className="container mx-auto overflow-hidden custom-scrollbar"
|
||||||
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
|
style={isExpanded ? { height: 'calc(100vh - 12rem)' } : { height: 'calc(100vh - 20rem)' }}
|
||||||
>
|
>
|
||||||
{isClient && (
|
{isClient && (
|
||||||
<JoyrideNoSSR
|
<JoyrideNoSSR
|
||||||
@ -373,28 +381,7 @@ export function CreateToVideo2() {
|
|||||||
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
textColor: '#F3F4F6',
|
textColor: '#F3F4F6',
|
||||||
arrowColor: '#1F2937',
|
arrowColor: '#1F2937',
|
||||||
},
|
}
|
||||||
tooltip: {
|
|
||||||
borderRadius: '1rem',
|
|
||||||
},
|
|
||||||
tooltipContainer: {
|
|
||||||
textAlign: 'left',
|
|
||||||
},
|
|
||||||
buttonNext: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonBack: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonSkip: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
buttonClose: {
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
spotlight: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
disableOverlayClose
|
disableOverlayClose
|
||||||
spotlightClicks
|
spotlightClicks
|
||||||
@ -409,27 +396,12 @@ export function CreateToVideo2() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className='scroll-load-box h-full overflow-y-scroll w-[calc(100%-16px)] mx-auto'>
|
<div className='min-h-[100%] flex flex-col justify-center items-center'>
|
||||||
<div className='min-h-[100%]'>
|
{/* 空状态 */}
|
||||||
<div className='flex flex-col items-center fixed top-1/2 left-1/2 -translate-x-1/2 translate-y-[calc(-50%-68px)]'>
|
<EmptyStateAnimation />
|
||||||
<Image
|
|
||||||
src='/assets/empty_video.png'
|
|
||||||
width={160}
|
|
||||||
height={160}
|
|
||||||
alt='empty_video'
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className='text-[16px] font-[400] leading-[24px]'>
|
|
||||||
<span className='opacity-60'>Generated videos will appear here. </span>
|
|
||||||
<span className='font-[700] border-0 border-solid border-white hover:border-b-[1px] cursor-pointer' onClick={() => handleStartCreating()}>Start creating!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<div className='video-tool-component relative w-[1080px]'>
|
<div className='video-tool-component relative w-[1080px]'>
|
||||||
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#0C0E11] backdrop-blur-[15px]'>
|
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#0C0E11] backdrop-blur-[15px]'>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
|
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
|
||||||
{/* 图标 展开按钮 */}
|
{/* 图标 展开按钮 */}
|
||||||
@ -447,12 +419,12 @@ export function CreateToVideo2() {
|
|||||||
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
|
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
|
||||||
<span className='text-lg opacity-60'>script</span>
|
<span className='text-lg opacity-60'>script</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
|
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
|
||||||
<span className='text-lg opacity-60'>clone</span>
|
<span className='text-lg opacity-60'>clone</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 gap-4 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
|
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 gap-4 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
|
||||||
<div className='video-creation-tool-container flex flex-col gap-4'>
|
<div className='video-creation-tool-container flex flex-col gap-4'>
|
||||||
{activeTab === 'clone' && (
|
{activeTab === 'clone' && (
|
||||||
<div className='relative flex items-center gap-4 h-[94px]'>
|
<div className='relative flex items-center gap-4 h-[94px]'>
|
||||||
@ -462,7 +434,7 @@ export function CreateToVideo2() {
|
|||||||
<Video className='w-4 h-4' />
|
<Video className='w-4 h-4' />
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
|
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
|
||||||
<span className='text-xs opacity-30 cursor-[inherit]'>Add Video</span>
|
<span className='text-xs opacity-30 cursor-[inherit]'>Add Video</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{videoUrl && (
|
{videoUrl && (
|
||||||
@ -476,92 +448,93 @@ export function CreateToVideo2() {
|
|||||||
)}
|
)}
|
||||||
{activeTab === 'script' && (
|
{activeTab === 'script' && (
|
||||||
<div className='relative flex items-center gap-4 h-[94px]'>
|
<div className='relative flex items-center gap-4 h-[94px]'>
|
||||||
<div className={`video-prompt-editor relative flex flex-1 self-stretch items-center w-0 rounded-[6px] ${isFocus ? 'focus' : ''}`}>
|
<div className={`video-prompt-editor relative flex flex-1 self-stretch items-center w-0 rounded-[6px] ${isFocus ? 'focus' : ''}`}>
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className='editor-content flex-1 w-0 max-h-[78px] min-h-[26px] h-auto gap-4 pl-[10px] rounded-[10px] leading-[26px] text-sm border-none overflow-y-auto cursor-text'
|
className='editor-content flex-1 w-0 max-h-[78px] min-h-[26px] h-auto gap-4 pl-[10px] rounded-[10px] leading-[26px] text-sm border-none overflow-y-auto cursor-text'
|
||||||
contentEditable
|
contentEditable
|
||||||
style={{ paddingRight: '10px' }}
|
style={{ paddingRight: '10px' }}
|
||||||
onFocus={handleEditorFocus}
|
onFocus={handleEditorFocus}
|
||||||
onBlur={() => setIsFocus(false)}
|
onBlur={() => setIsFocus(false)}
|
||||||
onInput={handleEditorChange}
|
onInput={handleEditorChange}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
>
|
|
||||||
{script}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[26px] text-white/[0.40] ${script ? 'opacity-0' : 'opacity-100'}`}
|
|
||||||
>
|
|
||||||
<span>Describe the content you want to create. Get an </span>
|
|
||||||
<b
|
|
||||||
className='idea-link inline-flex items-center gap-0.5 text-white/[0.50] font-normal cursor-pointer pointer-events-auto underline'
|
|
||||||
onClick={() => setInputText(ideaText)}
|
|
||||||
>
|
>
|
||||||
<Lightbulb className='w-4 h-4' />idea
|
{script}
|
||||||
</b>
|
</div>
|
||||||
|
<div
|
||||||
|
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[26px] text-white/[0.40] ${script ? 'opacity-0' : 'opacity-100'}`}
|
||||||
|
>
|
||||||
|
<span>Describe the content you want to create. Get an </span>
|
||||||
|
<b
|
||||||
|
className='idea-link inline-flex items-center gap-0.5 text-white/[0.50] font-normal cursor-pointer pointer-events-auto underline'
|
||||||
|
onClick={() => setInputText(ideaText)}
|
||||||
|
>
|
||||||
|
<Lightbulb className='w-4 h-4' />idea
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className='flex gap-3'>
|
<div className='flex gap-3'>
|
||||||
<div className='tool-scroll-box relative flex-1 w-0'>
|
<div className='tool-scroll-box relative flex-1 w-0'>
|
||||||
<div className='tool-scroll-box-content overflow-x-auto scrollbar-hide'>
|
<div className='tool-scroll-box-content overflow-x-auto scrollbar-hide'>
|
||||||
<div className='flex items-center flex-1 gap-3'>
|
<div className='flex items-center flex-1 gap-3'>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: modeItems,
|
items: modeItems,
|
||||||
onClick: handleModeSelect,
|
onClick: handleModeSelect,
|
||||||
selectedKeys: [selectedMode.toString()],
|
selectedKeys: [selectedMode.toString()],
|
||||||
}}
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlayClassName="mode-dropdown"
|
overlayClassName="mode-dropdown"
|
||||||
placement="bottomLeft"
|
placement="bottomLeft"
|
||||||
>
|
>
|
||||||
<div className='tool-operation-button ant-dropdown-trigger'>
|
<div className='tool-operation-button ant-dropdown-trigger'>
|
||||||
<Package className='w-4 h-4' />
|
<Package className='w-4 h-4' />
|
||||||
<span className='text-nowrap opacity-70'>
|
<span className='text-nowrap opacity-70'>
|
||||||
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
|
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
|
||||||
</span>
|
</span>
|
||||||
<Crown className='w-4 h-4 text-yellow-500' />
|
<Crown className='w-4 h-4 text-yellow-500' />
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: resolutionItems,
|
items: resolutionItems,
|
||||||
onClick: handleResolutionSelect,
|
onClick: handleResolutionSelect,
|
||||||
selectedKeys: [selectedResolution.toString()],
|
selectedKeys: [selectedResolution.toString()],
|
||||||
}}
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlayClassName="mode-dropdown"
|
overlayClassName="mode-dropdown"
|
||||||
placement="bottomLeft"
|
placement="bottomLeft"
|
||||||
>
|
>
|
||||||
<div className='tool-operation-button ant-dropdown-trigger'>
|
<div className='tool-operation-button ant-dropdown-trigger'>
|
||||||
<Video className='w-4 h-4' />
|
<Video className='w-4 h-4' />
|
||||||
<span className='text-nowrap opacity-70'>
|
<span className='text-nowrap opacity-70'>
|
||||||
{selectedResolution === ResolutionEnum.HD_720P ? '720P' :
|
{selectedResolution === ResolutionEnum.HD_720P ? '720P' :
|
||||||
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
|
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
|
||||||
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
|
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
|
||||||
</span>
|
</span>
|
||||||
<Crown className='w-4 h-4 text-yellow-500' />
|
<Crown className='w-4 h-4 text-yellow-500' />
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='flex items-center gap-3'>
|
||||||
<div className='flex items-center gap-3'>
|
<div className={`tool-submit-button ${videoUrl || script ? '' : 'disabled'} ${isCreating ? 'loading' : ''}`} onClick={isCreating ? undefined : handleCreateVideo}>
|
||||||
<div className={`tool-submit-button ${videoUrl || script ? '' : 'disabled'} ${isCreating ? 'loading' : ''}`} onClick={isCreating ? undefined : handleCreateVideo}>
|
{isCreating ? (
|
||||||
{isCreating ? (
|
<>
|
||||||
<>
|
<Loader2 className='w-4 h-4 animate-spin' />
|
||||||
<Loader2 className='w-4 h-4 animate-spin' />
|
Creating...
|
||||||
Creating...
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<ArrowUp className='w-4 h-4' />
|
||||||
<ArrowUp className='w-4 h-4' />
|
Create
|
||||||
Create
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -137,3 +137,13 @@
|
|||||||
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
|
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animated-text {
|
||||||
|
color: #dfdcff;
|
||||||
|
font-size: clamp(2rem, 12rem, 1vw);
|
||||||
|
line-height: 1.2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
perspective: 500px;
|
||||||
|
}
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -41,6 +41,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@types/gsap": "^1.20.2",
|
||||||
"@types/node": "20.6.2",
|
"@types/node": "20.6.2",
|
||||||
"@types/react": "18.2.22",
|
"@types/react": "18.2.22",
|
||||||
"@types/react-beautiful-dnd": "^13.1.8",
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
@ -57,6 +58,7 @@
|
|||||||
"eslint": "8.49.0",
|
"eslint": "8.49.0",
|
||||||
"eslint-config-next": "13.5.1",
|
"eslint-config-next": "13.5.1",
|
||||||
"framer-motion": "^12.19.1",
|
"framer-motion": "^12.19.1",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
@ -6524,6 +6526,12 @@
|
|||||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/gsap": {
|
||||||
|
"version": "1.20.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/gsap/-/gsap-1.20.2.tgz",
|
||||||
|
"integrity": "sha512-i9nUsnS32+VTgoX5IlaCYukJpCoB3c6h3bZvO67aIRdb3z8NFTWgeUDpQutLyb1ujowp6nw37qLQNvEowqq8yw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/hoist-non-react-statics": {
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
||||||
@ -9061,6 +9069,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
||||||
},
|
},
|
||||||
|
"node_modules/gsap": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/gsap/-/gsap-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==",
|
||||||
|
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||||
|
},
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@types/gsap": "^1.20.2",
|
||||||
"@types/node": "20.6.2",
|
"@types/node": "20.6.2",
|
||||||
"@types/react": "18.2.22",
|
"@types/react": "18.2.22",
|
||||||
"@types/react-beautiful-dnd": "^13.1.8",
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"eslint": "8.49.0",
|
"eslint": "8.49.0",
|
||||||
"eslint-config-next": "13.5.1",
|
"eslint-config-next": "13.5.1",
|
||||||
"framer-motion": "^12.19.1",
|
"framer-motion": "^12.19.1",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user