第一版

This commit is contained in:
北枳 2025-06-23 20:47:46 +08:00
parent 88133f643d
commit b7722b9ba9
31 changed files with 3360 additions and 814 deletions

9
app/create/layout.tsx Normal file
View File

@ -0,0 +1,9 @@
import { DashboardLayout } from '@/components/layout/dashboard-layout';
export default function CreateLayout({
children,
}: {
children: React.ReactNode;
}) {
return <DashboardLayout>{children}</DashboardLayout>;
}

View File

@ -1,10 +1,5 @@
import { DashboardLayout } from '@/components/layout/dashboard-layout';
import { CreateVideoWorkflow } from '@/components/pages/create-video-workflow';
import { redirect } from 'next/navigation';
export default function CreatePage() {
return (
<DashboardLayout>
<CreateVideoWorkflow />
</DashboardLayout>
);
redirect('/create/video-to-video');
}

View File

@ -0,0 +1,5 @@
import { ScriptToVideo } from '@/components/pages/script-to-video';
export default function ScriptToVideoPage() {
return <ScriptToVideo />;
}

View File

@ -0,0 +1,5 @@
import { VideoToVideo } from '@/components/pages/video-to-video';
export default function VideoToVideoPage() {
return <VideoToVideo />;
}

View File

@ -19,12 +19,12 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
@ -44,6 +44,7 @@
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
@ -72,6 +73,20 @@
}
}
body {
height: 100vh;
overflow: hidden;
background: radial-gradient(circle at 50% 0,
rgba(255, 0, 0, 0.3),
rgba(255, 0, 0, 0) 70.71%),
radial-gradient(circle at 6.7% 75%,
rgba(0, 0, 255, 0.3),
rgba(0, 0, 255, 0) 70.71%),
radial-gradient(circle at 93.3% 75%,
rgba(0, 0, 255, 0.3),
rgba(0, 0, 255, 0) 70.71%) beige !important;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
@ -80,7 +95,8 @@
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply text-foreground;
}
}
}

View File

@ -7,7 +7,7 @@ import { Toaster } from '@/components/ui/sonner';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'AI Video Studio - Create Amazing Videos with AI',
title: 'AI Movie Flow - Create Amazing Videos with AI',
description: 'Professional AI-powered video creation platform with advanced editing tools',
};
@ -21,7 +21,7 @@ export default function RootLayout({
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>

View File

@ -1,10 +1,22 @@
import { DashboardLayout } from '@/components/layout/dashboard-layout';
import { HomePage } from '@/components/pages/home-page';
// import { HomePage } from '@/components/pages/home-page';
import { HomePage2 } from '@/components/pages/home-page2';
export default function Home() {
return (
<DashboardLayout>
<HomePage />
<HomePage2 />
</DashboardLayout>
);
}
}
// import LiquidGlass from '@/plugins/liquid-glass'
// export default function Home() {
// return (
// // <LiquidGlass />
// <div>
// <h1>Hello World</h1>
// </div>
// );
// }

View File

@ -9,14 +9,14 @@ interface DashboardLayoutProps {
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
return (
<div className="min-h-screen bg-background">
<div className="min-h-screen">
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
<div className={`transition-all duration-300 ${sidebarCollapsed ? 'ml-16' : 'ml-64'}`}>
<TopBar />
<main className="p-6">
<TopBar collapsed={sidebarCollapsed} />
<main className="p-6 mt-16">
{children}
</main>
</div>

View File

@ -53,18 +53,18 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
return (
<div
className={cn(
'fixed left-0 top-0 z-50 h-full bg-card border-r border-border transition-all duration-300',
'fixed left-0 top-0 z-50 h-full transition-all duration-300',
collapsed ? 'w-16' : 'w-64'
)}
>
<div className="flex h-full flex-col">
{/* Logo */}
<div className="flex h-16 items-center justify-between px-4 border-b border-border">
<div className="flex h-16 items-center justify-between px-4 border-border">
{!collapsed && (
<div className="flex items-center space-x-2">
<Video className="h-8 w-8 text-primary" />
<span className="text-xl font-bold bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent">
AI Studio
Movie Flow
</span>
</div>
)}

View File

@ -18,14 +18,14 @@ import {
Bell,
} from 'lucide-react';
export function TopBar() {
export function TopBar({ collapsed }: { collapsed: boolean }) {
const { theme, setTheme } = useTheme();
return (
<div className="h-16 border-b border-border bg-card/50 backdrop-blur supports-[backdrop-filter]:bg-card/50">
<div className={`fixed right-0 top-0 transition-all duration-300 h-16 border-border backdrop-blur ${collapsed ? 'left-16' : 'left-64'}`}>
<div className="h-full flex items-center justify-between px-6">
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-semibold">AI Video Studio</h1>
<div className={`flex items-center space-x-4`}>
<h1 className={`text-2xl font-bold bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent ${collapsed ? '' : 'hidden'}`}>Movie Flow</h1>
</div>
<div className="flex items-center space-x-4">

View File

@ -70,74 +70,8 @@ export function CreateVideoWorkflow() {
}
};
const progress = ((currentStep - 1) / (steps.length - 1)) * 100;
return (
<div className="max-w-6xl mx-auto space-y-8">
{/* Header */}
<div className="space-y-4">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Button>
</div>
<div className="space-y-2">
<h1 className="text-3xl font-bold">Create AI Video</h1>
<p className="text-muted-foreground">
Follow these steps to create your AI-powered video. You can navigate back and forth to make changes.
</p>
</div>
</div>
{/* Steps Navigation */}
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div
className={`flex items-center space-x-2 cursor-pointer p-2 rounded-lg transition-colors ${
step.id === currentStep
? 'bg-primary/10 text-primary'
: completedSteps.includes(step.id)
? 'text-green-600 hover:bg-green-50 dark:hover:bg-green-950'
: step.id < currentStep
? 'text-muted-foreground hover:bg-muted'
: 'text-muted-foreground/50'
}`}
onClick={() => handleStepClick(step.id)}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
step.id === currentStep
? 'border-primary bg-primary text-primary-foreground'
: completedSteps.includes(step.id)
? 'border-green-600 bg-green-600 text-white'
: step.id < currentStep
? 'border-muted-foreground bg-background'
: 'border-muted-foreground/30 bg-background'
}`}>
{completedSteps.includes(step.id) ? (
<CheckCircle className="h-4 w-4" />
) : (
<step.icon className="h-4 w-4" />
)}
</div>
<div className="hidden md:block">
<div className="font-medium text-sm">{step.name}</div>
<div className="text-xs text-muted-foreground">{step.description}</div>
</div>
</div>
{index < steps.length - 1 && (
<Separator orientation="horizontal" className="w-8 mx-2" />
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Step Content */}
<div>
{renderStepContent()}

View File

@ -0,0 +1,46 @@
"use client";
import { useState, useRef } from "react";
import { ArrowRight } from "lucide-react";
import "./style/home-page2.css";
import { useRouter } from "next/navigation";
export function HomePage2() {
const router = useRouter();
return (
<div className="min-h-[100%]">
<div className="px-[16px] pb-[16px] min-w-[1100px] select-none overflow-hidden relative flex flex-col gap-8">
<div className="flex flex-row gap-[16px]">
<div className="tab-item" onClick={() => {
router.push("/create/video-to-video");
}}>
<div className="content-wrapper">
<div className="tab-title">Video To Video</div>
<div className="tab-description">Transform your video into a new style</div>
<div className="flex flex-row mt-[30px]">
<div className="tab-btn hover-btn cursor-pointer">
{/* 图标 右箭头 */}
<ArrowRight className="w-5 h-5 font-bold icon" />
<div className="btn-text">Create</div>
</div>
</div>
</div>
</div>
<div className="tab-item">
<div className="content-wrapper">
<div className="tab-title">Script To Video</div>
<div className="tab-description">Transform your script into a video</div>
<div className="flex flex-row mt-[30px]">
<div className="tab-btn hover-btn cursor-pointer">
{/* 图标 右箭头 */}
<ArrowRight className="w-4 h-4 icon" />
<div className="btn-text">Create</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
export function ScriptToVideo() {
return (
<div>
<h1>Script To Video</h1>
</div>
);
}

View File

@ -0,0 +1,75 @@
.tab-item:first-child {
position: relative;
}
.tab-item {
position: relative;
cursor: pointer;
--tw-bg-opacity: .2;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
--tw-backdrop-blur: blur(15px);
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
flex: 1 1 0%;
border-radius: 16px;
}
.tab-item .content-wrapper {
position: relative;
height: 220px;
background-repeat: no-repeat;
background-size: 100% 100%;
overflow: hidden;
border-radius: 16px;
padding: 2rem;
background-position: left center, right center;
}
.tab-item .tab-title {
font-size: 36px;
font-weight: 600;
line-height: 64px;
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.tab-item .tab-description {
font-size: 16px;
font-weight: 300;
line-height: 22px;
color: rgba(255, 255, 255, 0.8);
}
.tab-item .tab-btn {
display: flex;
height: 40px;
min-width: 168px;
flex-direction: row;
align-items: center;
justify-content: center;
--tw-bg-opacity: -0.5;
background-color: rgb(89 0 255 / var(--tw-bg-opacity));
padding-left: 16px;
padding-right: 16px;
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
--tw-backdrop-blur: blur(10px);
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
gap: 6px;
overflow: hidden;
border-radius: 10px;
}
.tab-item .tab-btn .icon {
/* --tw-text-opacity: 1;
color: rgb(29 33 41 / var(--tw-text-opacity)); */
transform: translate(-100px);
transition-property: transform;
transition-duration: 0.3s;
}
.tab-item .tab-btn .btn-text {
transform: translate(-14px);
transition-property: transform;
transition-duration: 0.3s;
font-weight: 600;
}
.tab-item:hover .hover-btn {
--tw-bg-opacity: 0.5;
}
.tab-item:hover .hover-btn .btn-text, .tab-item:hover .hover-btn .icon {
transform: translate(0px);
}

View File

@ -0,0 +1,29 @@
.video-tool-component {
position: fixed;
left: 50%;
bottom: 1rem;
z-index: 99;
--tw-translate-x: calc(-50% + 34.5px);
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.video-storyboard-tools .tool-submit-button {
display: flex;
height: 36px;
width: 120px;
cursor: pointer;
align-items: center;
justify-content: center;
gap: 2px;
border-radius: 10px;
font-size: .875rem;
line-height: 1.25rem;
font-weight: 600;
--tw-text-opacity: 1;
color: rgb(29 33 41 / var(--tw-text-opacity));
background-color: #fff;
}
.video-storyboard-tools .tool-submit-button.disabled {
background-color: #fff;
opacity: .3;
cursor: not-allowed;
}

View File

@ -0,0 +1,783 @@
"use client";
import { useState, useEffect, useRef } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import './style/video-to-video.css';
// 添加自定义滚动条样式
const scrollbarStyles = `
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
`;
interface SceneVideo {
id: number;
video_url: string;
script: any;
}
export function VideoToVideo() {
const router = useRouter();
const [isUploading, setIsUploading] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [loadingText, setLoadingText] = useState('Generating...');
const [generateObj, setGenerateObj] = useState<any>({
scripts: null,
frame_urls: null,
video_info: null,
scene_videos: null,
cut_video_url: null,
audio_video_url: null,
final_video_url: null
});
const [showAllFrames, setShowAllFrames] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [showScrollNav, setShowScrollNav] = useState(false);
const [selectedVideoIndex, setSelectedVideoIndex] = useState<number | null>(null);
const videosContainerRef = useRef<HTMLDivElement>(null);
const scriptsContainerRef = useRef<HTMLDivElement>(null);
// 监听内容变化,自动滚动到底部
useEffect(() => {
if (containerRef.current && (generateObj.scripts || generateObj.frame_urls || generateObj.video_info)) {
setTimeout(() => {
containerRef.current?.scrollTo({
top: containerRef.current.scrollHeight,
behavior: 'smooth'
});
}, 100); // 给一个小延迟,确保内容已经渲染
}
}, [generateObj.scripts, generateObj.frame_urls, generateObj.video_info, generateObj.scene_videos, generateObj.cut_video_url, generateObj.audio_video_url, generateObj.final_video_url]);
// 计算每行可以显示的图片数量基于图片高度100px和容器宽度
const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9间距8px
// 计算三行可以显示的最大图片数量
const maxVisibleImages = imagesPerRow * 3;
const handleUploadVideo = () => {
console.log('upload video');
// 打开文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
setVideoUrl(URL.createObjectURL(file));
}
}
input.click();
}
const generateSences = async () => {
try {
generateObj.scene_videos = [];
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',
'https://cdn.qikongjian.com/videos/1750303377_8c3c4ca6-c4ea-4376-8583-de3afa5681d8_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
];
setGenerateObj({...generateObj, scene_videos: []});
// 使用 Promise.all 和 Array.map 来处理异步操作
const promises = generateObj.scripts.map((element: any, index: number) => {
return new Promise<void>((resolveVideo) => {
setTimeout(() => {
generateObj.scene_videos.push({
id: index,
video_url: videoUrls[index],
script: element
});
setGenerateObj({...generateObj});
setLoadingText(`生成第 ${index + 1} 个分镜视频...`);
resolveVideo();
}, index * 2000); // 每个视频间隔2秒
});
});
// 等待所有视频生成完成
await Promise.all(promises);
} catch (error) {
console.error('生成分镜视频失败:', error);
throw error;
}
}
const handleCreateVideo = async () => {
try {
// 清空所有数据
setGenerateObj({
scripts: null,
frame_urls: null,
video_info: null,
scene_videos: null,
cut_video_url: null,
audio_video_url: null,
final_video_url: null
});
console.log('create video');
setIsLoading(true);
setIsExpanded(true);
// 提取帧
await new Promise(resolve => setTimeout(resolve, 1000));
setLoadingText('提取帧...');
// 生成帧
await new Promise(resolve => setTimeout(resolve, 1000));
generateObj.frame_urls = [
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000001.jpg/1750507511_tmphfb431oc_000001.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000002.jpg/1750507511_tmphfb431oc_000002.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000003.jpg/1750507511_tmphfb431oc_000003.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000004.jpg/1750507512_tmphfb431oc_000004.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000005.jpg/1750507512_tmphfb431oc_000005.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000006.jpg/1750507513_tmphfb431oc_000006.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000007.jpg/1750507514_tmphfb431oc_000007.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000008.jpg/1750507515_tmphfb431oc_000008.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000009.jpg/1750507515_tmphfb431oc_000009.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000010.jpg/1750507516_tmphfb431oc_000010.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000011.jpg/1750507516_tmphfb431oc_000011.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000012.jpg/1750507516_tmphfb431oc_000012.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000013.jpg/1750507517_tmphfb431oc_000013.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000014.jpg/1750507517_tmphfb431oc_000014.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000015.jpg/1750507517_tmphfb431oc_000015.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000016.jpg/1750507517_tmphfb431oc_000016.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000017.jpg/1750507517_tmphfb431oc_000017.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000018.jpg/1750507518_tmphfb431oc_000018.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000019.jpg/1750507520_tmphfb431oc_000019.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000020.jpg/1750507521_tmphfb431oc_000020.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000021.jpg/1750507523_tmphfb431oc_000021.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000022.jpg/1750507523_tmphfb431oc_000022.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000023.jpg/1750507524_tmphfb431oc_000023.jpg",
];
setGenerateObj({...generateObj});
setLoadingText('分析视频...');
// 生成视频信息
await new Promise(resolve => setTimeout(resolve, 2000));
generateObj.video_info = {
roles: [
{
name: '雪 (YUKI)',
core_identity: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。',
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
},{
name: '春 (HARU)',
core_identity: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。',
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
}
],
sence: '叙事在两个不同的时间段展开。现在时空设定在一个安静的乡下小镇,冬季时被大雪覆盖。记忆则设定在春夏两季,一个日本高中的校园内外,天气温暖而晴朗。',
style: '电影感照片般逼真带有柔和、梦幻般的质感。其美学风格让人联想到日本的浪漫剧情片。现在时空的场景使用冷色、蓝色调的调色板而记忆序列则沐浴在温暖的黄金时刻光晕中。大量使用浅景深、微妙的镜头光晕以及平滑、富有情感的节奏。8K分辨率。'
};
setGenerateObj({...generateObj});
setLoadingText('提取分镜脚本...');
// 生成分镜脚本
await new Promise(resolve => setTimeout(resolve, 2000));
generateObj.scripts = [
{
shot: '面部特写,拉远至广角镜头。',
frame: '序列以雪YUKI面部的特写开场她闭着眼睛躺在一片纯净的雪地里。柔和的雪花轻轻飘落在她的黑发和苍白的皮肤上。摄像机缓慢拉远形成一幅令人惊叹的广角画面揭示出在黄昏时分她是在一片广阔、寂静、白雪覆盖的景观中的一个渺小、孤独的身影。',
atmosphere: '忧郁、宁静、寂静且寒冷。'
}, {
shot: '切至室内中景,随后是一个透过窗户的主观视角镜头。',
frame: '我们切到雪YUKI在一个舒适、光线温暖的卧室里醒来。她穿着一件舒适的毛衣从床上下来走向一扇窗户。她的呼吸在冰冷的玻璃上凝成雾气。她的主观视角镜头揭示了外面的雪景远处有一座红色的房子以及一封信被放入邮箱的记忆从而触发了一段闪回。',
atmosphere: '怀旧、温暖、内省。'
}, {
shot: '跟踪镜头,随后是一系列切出镜头和特写。',
frame: '记忆开始。一个跟踪镜头跟随着一群学生包括雪YUKI他们在飘落的樱花花瓣构成的华盖下走路上学。场景切到一个阳光普照的教室。雪YUKI穿着校服坐在她的课桌前害羞地瞥了一眼坐在前几排的春HARU。他仿佛感觉到她的凝视巧妙地转过头来。',
atmosphere: '充满青春气息、怀旧、温暖,带有一种萌芽的、未言明的浪漫感。'
}, {
shot: '静态中景,切到另一个中景,营造出共享空间的感觉。',
frame: '记忆转移到学校图书馆充满了金色的光束。一个中景镜头显示春HARU靠在一个书架上全神贯注地读一本书。然后摄像机切到雪YUKI她坐在附近的一张桌子旁专注于画架上的一幅小画当她感觉到他的存在时嘴唇上泛起一丝淡淡的、秘密的微笑。',
atmosphere: '平和、亲密、书卷气、温暖。'
}, {
shot: '雪YUKI的中景过渡到通过相机镜头的特写主观视角。',
frame: '在一个阳光明媚的运动日YUKI站在运动场边缘拿着一台老式相机。她举起相机镜头推进到她透过取景器看的眼睛的特写。我们切到她的主观视角除了站在运动场上、表情专注而坚定的春HARU之外整个世界都是失焦的。',
atmosphere: '充满活力、专注,一种投入的观察和遥远的钦佩感。'
},
];
setGenerateObj({...generateObj}); // 使用展开运算符创建新对象,确保触发更新
setLoadingText('生成分镜视频...');
// 生成分镜视频
await new Promise(resolve => setTimeout(resolve, 2000));
await generateSences();
setLoadingText('分镜剪辑中...');
// 生成剪辑后的视频
await new Promise(resolve => setTimeout(resolve, 2000));
generateObj.cut_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('口型同步中...');
// 口型同步后生成视频
await new Promise(resolve => setTimeout(resolve, 2000));
generateObj.audio_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('一致化处理中...');
// 最终完成
await new Promise(resolve => setTimeout(resolve, 2000));
generateObj.final_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('完成');
setIsLoading(false);
} catch (error) {
console.error('视频生成过程出错:', error);
setLoadingText('生成失败,请重试');
setIsLoading(false);
}
}
// 处理视频选中
const handleVideoSelect = (index: number) => {
setSelectedVideoIndex(index);
// 滚动脚本到对应位置
const scriptElement = document.getElementById(`script-${index}`);
scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
};
// 处理脚本选中
const handleScriptSelect = (index: number) => {
setSelectedVideoIndex(index);
// 滚动视频到对应位置
const videoElement = document.getElementById(`video-${index}`);
videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
};
return (
<div
ref={containerRef}
className="container mx-auto overflow-auto custom-scrollbar"
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
>
{/* 展示创作详细过程 */}
{generateObj && (
<div className='video-creation-process-container mb-6'>
{generateObj.frame_urls && (
<div id='step-frame_urls' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='relative'>
<div className={`video-creation-process-item-content flex flex-wrap gap-2 ${!showAllFrames && 'max-h-[324px]'} overflow-hidden transition-all duration-300`}>
{generateObj.frame_urls.map((frame: string, index: number) => (
<div key={index} className="relative group">
<img
src={frame}
alt={`frame ${index + 1}`}
className='h-[100px] rounded-md object-cover transition-transform duration-200 group-hover:scale-105'
/>
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-md flex items-center justify-center">
<span className="text-white/90 text-sm">Frame {index + 1}</span>
</div>
</div>
))}
</div>
{generateObj.frame_urls.length > maxVisibleImages && (
<div
className='absolute bottom-0 left-0 right-0 h-12 flex items-center justify-center bg-gradient-to-t from-black/20 to-transparent cursor-pointer'
onClick={() => setShowAllFrames(!showAllFrames)}
>
<div className='flex items-center gap-1 px-3 py-1 rounded-full bg-white/10 hover:bg-white/20 transition-colors'>
{showAllFrames ? (
<>
<ChevronUp className="w-4 h-4" />
<span className='text-sm'></span>
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
<span className='text-sm'> ({generateObj.frame_urls.length} )</span>
</>
)}
</div>
</div>
)}
</div>
</div>
)}
{/* 视频信息 */}
{generateObj.video_info && (
<div id='step-video_info' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
{/* 展示:角色档案卡(头像、姓名、核心身份);场景;风格 */}
<div className='video-creation-process-item-content flex flex-col gap-6'>
{/* 角色档案卡 */}
<div className='space-y-4'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<div className='flex flex-wrap gap-4'>
{generateObj.video_info.roles.map((role: any, index: number) => (
<div
key={index}
className='flex items-start gap-3 p-3 rounded-lg bg-white/[0.03] hover:bg-white/[0.05] transition-colors duration-200 min-w-[300px] max-w-[400px] group'
>
<div className='flex-shrink-0'>
<div className='w-[48px] h-[48px] rounded-full overflow-hidden border-2 border-white/10 group-hover:border-white/20 transition-colors duration-200'>
<img
src={role.avatar}
alt={role.name}
className='w-full h-full object-cover'
/>
</div>
</div>
<div className='flex-1 min-w-0'>
<div className='text-base font-medium mb-1 text-white/90'>{role.name}</div>
<div className='text-sm text-white/60 line-clamp-2'>{role.core_identity}</div>
</div>
</div>
))}
</div>
</div>
{/* 场景和风格 */}
<div className='space-y-4'>
<div className='video-creation-process-item-content-item-scene'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed pl-1'>{generateObj.video_info.sence}</p>
</div>
<div className='video-creation-process-item-content-item-style'>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed pl-1'>{generateObj.video_info.style}</p>
</div>
</div>
</div>
</div>
)}
{/* 分镜脚本 */}
{generateObj.scripts && (
<div id='step-scripts' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
{generateObj.scripts.map((script: any, index: number) => (
<div
key={index}
className='flex-shrink-0 w-[360px] h-[400px] bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'
>
{/* 序号 */}
<div className='flex items-center justify-between mb-3 pb-2 border-b border-white/10'>
<span className='text-lg font-medium text-blue-400/90'>Scene {index + 1}</span>
<div className='px-2 py-1 rounded-full bg-white/10 text-xs text-white/60'>
#{String(index + 1).padStart(2, '0')}
</div>
</div>
{/* 滚动内容区域 */}
<div className='h-[calc(100%-40px)] overflow-y-auto pr-2 space-y-4 custom-scrollbar'>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.shot}</p>
</div>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.frame}</p>
</div>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{script.atmosphere}</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 分镜视频 */}
{generateObj.scene_videos && (
<div id='step-scene_videos' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-sm font-medium'></span>
</div>
<div className='video-creation-process-item-content space-y-6'>
{/* 视频展示区 */}
<div
ref={videosContainerRef}
className='flex gap-4 overflow-x-auto pb-4 custom-scrollbar'
>
{generateObj.scripts.map((script: any, index: number) => {
const video = generateObj.scene_videos.find((v: any) => v.id === index);
const isSelected = selectedVideoIndex === index;
return (
<div
key={index}
id={`video-${index}`}
className={`flex-shrink-0 w-[320px] aspect-video rounded-lg overflow-hidden relative cursor-pointer transition-all duration-300
${isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-[#191B1E]' : 'hover:ring-2 hover:ring-white/20'}`}
onClick={() => handleVideoSelect(index)}
>
{video ? (
<>
<video
src={video.video_url}
className="w-full h-full object-cover"
controls={isSelected}
/>
{!isSelected && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<Play className="w-8 h-8 text-white/90" />
</div>
)}
</>
) : (
<div className="w-full h-full bg-white/5 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-white/40 animate-spin" />
</div>
)}
<div className="absolute top-2 right-2 px-2 py-1 rounded-full bg-black/60 text-xs text-white/80">
Scene {index + 1}
</div>
</div>
);
})}
</div>
{/* 脚本展示区 */}
<div
ref={scriptsContainerRef}
className='flex gap-4 overflow-x-auto pb-4 custom-scrollbar'
>
{generateObj.scripts.map((script: any, index: number) => {
const isSelected = selectedVideoIndex === index;
return (
<div
key={index}
id={`script-${index}`}
className={`flex-shrink-0 w-[320px] p-4 rounded-lg cursor-pointer transition-all duration-300
${isSelected ? 'bg-blue-400/10 border border-blue-400/30' : 'bg-white/5 hover:bg-white/10'}`}
onClick={() => handleScriptSelect(index)}
>
<div className="text-xs text-white/40 mb-2">Scene {index + 1}</div>
<div className="text-sm text-white/80 line-clamp-4">{script.frame}</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* 剪辑后的视频 */}
{generateObj.cut_video_url && (
<div id='step-cut_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.cut_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
{/* 口型同步后的视频 */}
{generateObj.audio_video_url && (
<div id='step-audio_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.audio_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
{/* 最终视频 */}
{generateObj.final_video_url && (
<div id='step-final_video' className='video-creation-process-item bg-white/5 rounded-lg p-4'>
<div className='video-creation-process-item-title mb-3'>
<span className='text-base font-medium'></span>
</div>
<div className='video-creation-process-item-content'>
<div className='flex flex-wrap gap-4'>
<div className='flex-shrink-0 w-full bg-white/[0.03] rounded-lg p-4 hover:bg-white/[0.05] transition-all duration-200 group'>
<video src={generateObj.final_video_url} className="w-full h-full object-cover" controls />
</div>
</div>
</div>
</div>
)}
</div>
)}
{/* 回滚条 */}
<div
className="fixed right-8 top-1/2 -translate-y-1/2 z-50"
onMouseEnter={() => setShowScrollNav(true)}
onMouseLeave={() => setShowScrollNav(false)}
>
{/* 悬浮按钮 */}
<button
className={`flex items-center justify-center w-12 h-12 rounded-full bg-gradient-to-br from-blue-400/20 to-blue-600/20 backdrop-blur-lg hover:from-blue-400/30 hover:to-blue-600/30 transition-all duration-300 group
${showScrollNav ? 'opacity-0 scale-90' : 'opacity-100 scale-100'}`}
>
<ListOrdered className="w-5 h-5 text-white/70 group-hover:text-white/90 transition-colors" />
</button>
{/* 展开的回滚导航 */}
<div className={`absolute right-0 top-1/2 -translate-y-1/2 transition-all duration-300 ease-out
${showScrollNav ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-4 pointer-events-none'}`}
>
<div className="flex items-center gap-4 bg-gradient-to-b from-black/30 to-black/10 backdrop-blur-lg rounded-l-2xl pl-6 pr-6 py-6">
{generateObj && (
<>
{/* 进度条背景 */}
<div className="absolute w-[3px] h-[280px] bg-gradient-to-b from-white/5 to-white/0 rounded-full left-8" />
{/* 动态进度条 */}
<div className="absolute w-[3px] rounded-full transition-all duration-500 ease-out left-8 overflow-hidden"
style={{
height: generateObj.final_video_url ? '280px' :
generateObj.audio_video_url ? '240px' :
generateObj.cut_video_url ? '200px' :
generateObj.scene_videos ? '160px' :
generateObj.scripts ? '120px' :
generateObj.video_info ? '80px' :
generateObj.frame_urls ? '40px' : '0px',
top: '24px',
background: 'linear-gradient(180deg, rgba(96,165,250,0.7) 0%, rgba(96,165,250,0.3) 100%)',
boxShadow: '0 0 20px rgba(96,165,250,0.3)'
}}
/>
{/* 步骤按钮 */}
<div className="relative flex flex-col justify-between h-[280px] py-2">
{generateObj.frame_urls && (
<button
onClick={() => {
const element = document.getElementById('step-frame_urls');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.video_info && (
<button
onClick={() => {
const element = document.getElementById('step-video_info');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.scripts && (
<button
onClick={() => {
const element = document.getElementById('step-scripts');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.scene_videos && (
<button
onClick={() => {
const element = document.getElementById('step-scene_videos');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.cut_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-cut_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.audio_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-audio_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
{generateObj.final_video_url && (
<button
onClick={() => {
const element = document.getElementById('step-final_video');
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
className="group flex items-center gap-3 -ml-1"
>
<span className="text-xs text-white/60 group-hover:text-white/90 transition-colors duration-200 min-w-[5rem] text-right"></span>
<div className="relative w-3 h-3">
<div className="absolute inset-0 bg-blue-400/80 rounded-full group-hover:scale-150 group-hover:bg-blue-400 transition-all duration-300" />
<div className="absolute inset-0 bg-blue-400/20 rounded-full animate-ping group-hover:animate-none" />
</div>
</button>
)}
</div>
</>
)}
</div>
</div>
</div>
{/* 工具栏 */}
<div className='video-tool-component relative w-[1080px]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#fff3] backdrop-blur-[15px]'>
{isExpanded ? (
<div className='absolute top-0 bottom-0 left-0 right-0 z-[9] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
{/* 图标 展开按钮 */}
<ChevronUp className='w-4 h-4' />
<span className='text-sm'>Click to create</span>
</div>
) : (
<div className='absolute top-[-8px] left-[50%] z-[10] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
{/* 图标 折叠按钮 */}
<ChevronDown className='w-4 h-4' />
</div>
)}
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
<div className='video-creation-tool-container flex flex-col gap-4'>
<div className='relative flex items-center gap-4'>
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
{/* 图标 添加视频 */}
<Video className='w-4 h-4' />
</div>
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
<span className='text-xs cursor-[inherit]'>Add Video</span>
</div>
</div>
{videoUrl && (
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
<video src={videoUrl} className='w-full h-full object-cover' />
</div>
</div>
)}
</div>
</div>
<div className='flex gap-3 justify-end'>
<div className='flex items-center gap-3'>
<div className='disabled tool-submit-button'>Stop</div>
<div className={`tool-submit-button ${videoUrl ? '' : 'disabled'}`} onClick={handleCreateVideo}>Create</div>
</div>
</div>
</div>
</div>
</div>
{/* Loading动画 */}
{isLoading && (
<div className='mt-8 flex justify-center'>
<div className='relative'>
{/* 外圈动画 */}
<div className='w-[40px] h-[40px] rounded-full bg-white/[0.05] flex items-center justify-center animate-bounce'>
{/* 中圈动画 */}
<div className='w-[20px] h-[20px] rounded-full bg-white/[0.05] flex items-center justify-center animate-pulse'>
{/* 内圈动画 */}
<div className='w-[10px] h-[10px] rounded-full bg-white/[0.05] flex items-center justify-center animate-ping'>
</div>
</div>
</div>
{/* Loading文字 */}
<div className='absolute top-[50px] left-1/2 -translate-x-1/2 whitespace-nowrap'>
<span className='text-white/70 text-sm animate-pulse inline-block'>{loadingText}</span>
<span className='inline-block ml-1 animate-bounce'>
<span className='inline-block animate-bounce delay-100'>.</span>
<span className='inline-block animate-bounce delay-200'>.</span>
<span className='inline-block animate-bounce delay-300'>.</span>
</span>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -5,5 +5,5 @@ import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
return <NextThemesProvider defaultTheme="light" {...props}>{children}</NextThemesProvider>
}

View File

@ -116,11 +116,7 @@ export function GenerateChaptersStep({ onNext, onPrevious }: GenerateChaptersSte
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground mb-6">
AI has automatically split your script into chapters and suggested actors for each section.
You can edit the content and assign multiple actors as needed.
</p>
<CardContent className="h-[calc(100vh-18rem)] overflow-y-auto hide-scrollbar">
<div className="space-y-6">
{chapters.map((chapter, index) => (

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@ -9,41 +9,372 @@ import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { ArrowRight, Sparkles, Users, FileText } from 'lucide-react';
import { ArrowRight, Sparkles, Users, FileText, Play, Pause, RefreshCw, Palette, Volume2 } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { motion } from 'framer-motion';
interface InputScriptStepProps {
onNext: () => void;
}
interface Character {
id: string;
name: string;
description: string;
personality: string;
appearance: string;
voice: string;
avatar: string;
fullBodyImage: string;
audioSample: string;
styles: string[];
currentStyle: number;
}
const aiModels = [
{ id: 'gpt-4', name: 'GPT-4 Turbo', description: 'Most advanced model with superior creativity' },
{ id: 'gpt-3.5', name: 'GPT-3.5 Turbo', description: 'Fast and efficient for most tasks' },
{ id: 'claude-3', name: 'Claude 3 Opus', description: 'Excellent for narrative and storytelling' },
];
const loadingSteps = [
{ text: "分析脚本内容...", progress: 20 },
{ text: "提取角色信息...", progress: 40 },
{ text: "生成角色形象...", progress: 60 },
{ text: "匹配音色特征...", progress: 80 },
{ text: "完成角色创建...", progress: 100 },
];
const mockCharacters: Character[] = [
{
id: "1",
name: "凤青楗",
description: "重生的凤凰,拥有强大的意志力,决心改变自己的命运",
personality: "坚强、勇敢、充满希望",
appearance: "优雅的凤凰形象,金色羽毛,炯炯有神的眼睛",
voice: "温暖而坚定的女声",
avatar: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
fullBodyImage: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=400",
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
styles: [
"https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300"
],
currentStyle: 0
},
{
id: "2",
name: "星光使者",
description: "掌控星辰力量的神秘角色,与凤青楗一同战斗",
personality: "智慧、冷静、神秘",
appearance: "星光环绕的身影,深邃的蓝色长袍",
voice: "低沉磁性的男声",
avatar: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
fullBodyImage: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=400",
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
styles: [
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300"
],
currentStyle: 0
}
];
// 新的Loading组件
const CharacterLoading = ({ step }: { step: string }) => {
return (
<div className="flex flex-col items-center justify-center min-h-[300px] bg-gradient-to-b from-gray-900 to-black text-white relative overflow-hidden rounded-xl">
{/* 旋转粒子环 */}
<motion.div
className="absolute w-40 h-40 border-2 border-cyan-400 rounded-full opacity-30"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 3, ease: 'linear' }}
/>
{/* 中心波动光圈 */}
<motion.div
className="absolute w-20 h-20 bg-cyan-500/10 rounded-full blur-xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.6, 0.3]
}}
transition={{ repeat: Infinity, duration: 2 }}
/>
{/* 扫光线条 */}
<motion.div
className="absolute bottom-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-400 to-transparent blur"
animate={{ y: [-30, 300] }}
transition={{ repeat: Infinity, duration: 2, ease: 'easeInOut' }}
/>
{/* 核心文本 */}
<motion.div
className="relative z-10 mt-12 text-lg font-semibold text-cyan-300"
animate={{ opacity: [1, 0.4, 1] }}
transition={{ repeat: Infinity, duration: 1.5 }}
>
{step}
</motion.div>
</div>
);
};
// 角色卡片组件
const CharacterCard = ({
character,
onStyleChange,
onPlayAudio,
isPlaying
}: {
character: Character;
onStyleChange: (id: string, styleIndex: number) => void;
onPlayAudio: (id: string) => void;
isPlaying: string | null;
}) => (
<Card className="bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 overflow-hidden group hover:shadow-2xl transition-all duration-300 hover:scale-105">
<CardContent className="p-0">
{/* 角色头像区域 */}
<div className="relative">
<div className="aspect-[3/4] overflow-hidden bg-gradient-to-b from-blue-500/20 to-purple-500/20">
<img
src={character.fullBodyImage}
alt={character.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
{/* 渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* 角色名字 */}
<div className="absolute bottom-4 left-4 right-4">
<h3 className="text-white text-xl font-bold mb-2">{character.name}</h3>
<Badge variant="secondary" className="bg-blue-600/80 text-white">
{character.voice}
</Badge>
</div>
{/* 音频播放按钮 */}
<Button
size="sm"
variant="ghost"
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white"
onClick={() => onPlayAudio(character.id)}
>
{isPlaying === character.id ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
<Volume2 className="h-4 w-4 ml-1" />
</Button>
</div>
{/* 详细信息 */}
<div className="p-4 space-y-3">
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.description}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.personality}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.appearance}</p>
</div>
{/* 样式切换 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-300"></Label>
<Button
size="sm"
variant="ghost"
className="text-blue-400 hover:text-blue-300"
onClick={() => onStyleChange(character.id, (character.currentStyle + 1) % character.styles.length)}
>
<Palette className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="flex space-x-2">
{character.styles.map((style, index) => (
<button
key={index}
className={`w-12 h-12 rounded-lg overflow-hidden border-2 transition-all ${
character.currentStyle === index
? 'border-blue-500 shadow-lg'
: 'border-gray-600 hover:border-gray-500'
}`}
onClick={() => onStyleChange(character.id, index)}
>
<img src={style} alt={`Style ${index + 1}`} className="w-full h-full object-cover" />
</button>
))}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
// 角色生成结果组件
const CharacterGenerationResult = ({
characters,
onStyleChange,
onPlayAudio,
isPlaying,
onContinue
}: {
characters: Character[];
onStyleChange: (id: string, styleIndex: number) => void;
onPlayAudio: (id: string) => void;
isPlaying: string | null;
onContinue: () => void;
}) => (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-purple-900 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* 标题区域 */}
<div className="text-center space-y-4">
<div className="inline-flex items-center space-x-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-3 rounded-full">
<Users className="h-5 w-5" />
<span className="font-medium"></span>
<Sparkles className="h-5 w-5" />
</div>
<h1 className="text-3xl font-bold text-white"></h1>
<p className="text-gray-300 max-w-2xl mx-auto">
AI已经根据您的脚本生成了{characters.length}
</p>
</div>
{/* 角色网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{characters.map((character) => (
<div key={character.id} className="transform transition-all duration-500 hover:-translate-y-2">
<CharacterCard
character={character}
onStyleChange={onStyleChange}
onPlayAudio={onPlayAudio}
isPlaying={isPlaying}
/>
</div>
))}
</div>
{/* 操作按钮 */}
<div className="flex justify-center space-x-4 pt-8">
<Button
variant="outline"
size="lg"
className="border-gray-600 text-gray-300 hover:bg-gray-800"
>
<RefreshCw className="mr-2 h-5 w-5" />
</Button>
<Button
size="lg"
onClick={onContinue}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</div>
</div>
</div>
);
export function InputScriptStep({ onNext }: InputScriptStepProps) {
const [script, setScript] = useState('');
const [chapters, setChapters] = useState('4');
const [shots, setShots] = useState('8');
const [showActorsPanel, setShowActorsPanel] = useState(false);
const [isGenerating, setIsGenerating] = useState(true);
const [showCharacters, setShowCharacters] = useState(false);
const [loadingStep, setLoadingStep] = useState(0);
const [characters, setCharacters] = useState<Character[]>(mockCharacters);
const [playingAudio, setPlayingAudio] = useState<string | null>(null);
// 模拟生成过程
useEffect(() => {
if (isGenerating) {
const timer = setInterval(() => {
setLoadingStep((prev) => {
if (prev >= loadingSteps.length - 1) {
clearInterval(timer);
setTimeout(() => {
setIsGenerating(false);
setShowCharacters(true);
}, 500);
return prev;
}
return prev + 1;
});
}, 1500);
return () => clearInterval(timer);
}
}, [isGenerating]);
const handleSubmit = () => {
if (script.trim() && chapters) {
onNext();
setIsGenerating(true);
setLoadingStep(0);
}
};
const handleStyleChange = (characterId: string, styleIndex: number) => {
setCharacters(prev =>
prev.map(char =>
char.id === characterId
? { ...char, currentStyle: styleIndex, fullBodyImage: char.styles[styleIndex] }
: char
)
);
};
const handlePlayAudio = (characterId: string) => {
if (playingAudio === characterId) {
setPlayingAudio(null);
} else {
setPlayingAudio(characterId);
// 模拟音频播放3秒后自动停止
setTimeout(() => setPlayingAudio(null), 3000);
}
};
const handleContinue = () => {
setShowCharacters(false);
onNext();
};
// 显示角色生成结果
if (showCharacters) {
return (
<CharacterGenerationResult
characters={characters}
onStyleChange={handleStyleChange}
onPlayAudio={handlePlayAudio}
isPlaying={playingAudio}
onContinue={handleContinue}
/>
);
}
// 原始的脚本输入界面
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FileText className="h-5 w-5" />
<span>Script Input & Configuration</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<Separator />
{/* Script Input */}
<div className="space-y-3">
<Label htmlFor="script" className="text-base font-medium">
@ -58,59 +389,25 @@ export function InputScriptStep({ onNext }: InputScriptStepProps) {
/>
<div className="flex justify-between text-sm text-muted-foreground">
<span>{script.length} characters</span>
<span>Estimated reading time: {Math.ceil(script.split(' ').length / 200)} min</span>
</div>
</div>
<Separator />
{/* Chapters Configuration */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<Label htmlFor="chapters" className="text-base font-medium">
Number of Chapters
</Label>
<Input
id="chapters"
type="number"
min="1"
max="10"
value={chapters}
onChange={(e) => setChapters(e.target.value)}
placeholder="4"
/>
<p className="text-sm text-muted-foreground">
AI will automatically split your script into this many chapters
</p>
</div>
<div className="space-y-3">
<Label className="text-base font-medium">AI Twins Reference</Label>
<Button
variant="outline"
onClick={() => setShowActorsPanel(true)}
className="w-full justify-start"
>
<Users className="mr-2 h-4 w-4" />
Select Reference Actors
</Button>
<p className="text-sm text-muted-foreground">
Choose existing actors or create new ones for character reference
</p>
</div>
</div>
</CardContent>
</Card>
{/* Loading Animation - 显示在输入框下方 */}
{isGenerating && (
<CharacterLoading step={loadingSteps[loadingStep].text} />
)}
{/* Action Buttons */}
<div className="flex justify-end">
<Button
onClick={handleSubmit}
disabled={!script.trim() || !chapters}
disabled={!script.trim() || isGenerating}
size="lg"
>
<Sparkles className="mr-2 h-4 w-4" />
Generate Chapters
Generate
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>

70
package-lock.json generated
View File

@ -48,8 +48,10 @@
"embla-carousel-react": "^8.3.0",
"eslint": "8.49.0",
"eslint-config-next": "13.5.1",
"framer-motion": "^12.18.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.446.0",
"motion": "^12.18.1",
"next": "13.5.1",
"next-themes": "^0.3.0",
"postcss": "8.4.30",
@ -4117,6 +4119,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.18.1",
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.18.1.tgz",
"integrity": "sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.18.1",
"motion-utils": "^12.18.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -5096,6 +5125,47 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion": {
"version": "12.18.1",
"resolved": "https://registry.npmmirror.com/motion/-/motion-12.18.1.tgz",
"integrity": "sha512-w1ns2hWQ4COhOvnZf4rg4mW0Pl36mzcShpgt0fSfI6qJxKUbi3kHho/HSKeJFRoY0TO1m5/7C8lG1+Li0uC9Fw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.18.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.18.1",
"resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.18.1.tgz",
"integrity": "sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.18.1"
}
},
"node_modules/motion-utils": {
"version": "12.18.1",
"resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.18.1.tgz",
"integrity": "sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -49,8 +49,10 @@
"embla-carousel-react": "^8.3.0",
"eslint": "8.49.0",
"eslint-config-next": "13.5.1",
"framer-motion": "^12.18.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.446.0",
"motion": "^12.18.1",
"next": "13.5.1",
"next-themes": "^0.3.0",
"postcss": "8.4.30",

27
plugins/liquid-glass/index.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
interface LiquidGlassProps {
children: React.ReactNode;
displacementScale?: number;
blurAmount?: number;
saturation?: number;
aberrationIntensity?: number;
elasticity?: number;
cornerRadius?: number;
globalMousePos?: {
x: number;
y: number;
};
mouseOffset?: {
x: number;
y: number;
};
mouseContainer?: React.RefObject<HTMLElement | null> | null;
className?: string;
padding?: string;
style?: React.CSSProperties;
overLight?: boolean;
mode?: "standard" | "polar" | "prominent" | "shader";
onClick?: () => void;
}
export default function LiquidGlass({ children, displacementScale, blurAmount, saturation, aberrationIntensity, elasticity, cornerRadius, globalMousePos: externalGlobalMousePos, mouseOffset: externalMouseOffset, mouseContainer, className, padding, overLight, style, mode, onClick, }: LiquidGlassProps): import("react/jsx-runtime").JSX.Element;
export {};
//# sourceMappingURL=index.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAwPA,UAAU,gBAAgB;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzC,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,QAAQ,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,QAAQ,EACR,iBAAsB,EACtB,UAAmB,EACnB,UAAgB,EAChB,mBAAuB,EACvB,UAAiB,EACjB,YAAkB,EAClB,cAAc,EAAE,sBAAsB,EACtC,WAAW,EAAE,mBAAmB,EAChC,cAAqB,EACrB,SAAc,EACd,OAAqB,EACrB,SAAiB,EACjB,KAAU,EACV,IAAiB,EACjB,OAAO,GACR,EAAE,gBAAgB,2CAuUlB"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25
plugins/liquid-glass/shader-utils.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
export interface Vec2 {
x: number;
y: number;
}
export interface ShaderOptions {
width: number;
height: number;
fragment: (uv: Vec2, mouse?: Vec2) => Vec2;
mousePosition?: Vec2;
}
export declare const fragmentShaders: {
liquidGlass: (uv: Vec2) => Vec2;
};
export type FragmentShaderType = keyof typeof fragmentShaders;
export declare class ShaderDisplacementGenerator {
private options;
private canvas;
private context;
private canvasDPI;
constructor(options: ShaderOptions);
updateShader(mousePosition?: Vec2): string;
destroy(): void;
getScale(): number;
}
//# sourceMappingURL=shader-utils.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"shader-utils.d.ts","sourceRoot":"","sources":["../src/shader-utils.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,KAAK,IAAI,CAAA;IAC1C,aAAa,CAAC,EAAE,IAAI,CAAA;CACrB;AAsBD,eAAO,MAAM,eAAe;sBACR,IAAI,KAAG,IAAI;CAQ9B,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,eAAe,CAAA;AAE7D,qBAAa,2BAA2B;IAK1B,OAAO,CAAC,OAAO;IAJ3B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,SAAS,CAAI;gBAED,OAAO,EAAE,aAAa;IAa1C,YAAY,CAAC,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM;IA6D1C,OAAO,IAAI,IAAI;IAIf,QAAQ,IAAI,MAAM;CAGnB"}

4
plugins/liquid-glass/utils.d.ts vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,40LAC+yL,CAAA;AAE30L,eAAO,MAAM,oBAAoB,4pLAC0nL,CAAA;AAE3pL,eAAO,MAAM,wBAAwB,uj5BACih5B,CAAA"}

74
tailwind.config.js Normal file
View File

@ -0,0 +1,74 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
transitionDelay: {
'100': '100ms',
'200': '200ms',
'300': '300ms',
}
},
},
plugins: [require("tailwindcss-animate")],
}