forked from 77media/video-flow
第一版
This commit is contained in:
parent
88133f643d
commit
b7722b9ba9
9
app/create/layout.tsx
Normal file
9
app/create/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { DashboardLayout } from '@/components/layout/dashboard-layout';
|
||||
|
||||
export default function CreateLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <DashboardLayout>{children}</DashboardLayout>;
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
5
app/create/script-to-video/page.tsx
Normal file
5
app/create/script-to-video/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { ScriptToVideo } from '@/components/pages/script-to-video';
|
||||
|
||||
export default function ScriptToVideoPage() {
|
||||
return <ScriptToVideo />;
|
||||
}
|
||||
5
app/create/video-to-video/page.tsx
Normal file
5
app/create/video-to-video/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { VideoToVideo } from '@/components/pages/video-to-video';
|
||||
|
||||
export default function VideoToVideoPage() {
|
||||
return <VideoToVideo />;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
>
|
||||
|
||||
18
app/page.tsx
18
app/page.tsx
@ -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>
|
||||
// );
|
||||
// }
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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()}
|
||||
|
||||
46
components/pages/home-page2.tsx
Normal file
46
components/pages/home-page2.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
components/pages/script-to-video.tsx
Normal file
7
components/pages/script-to-video.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export function ScriptToVideo() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Script To Video</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
components/pages/style/home-page2.css
Normal file
75
components/pages/style/home-page2.css
Normal 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);
|
||||
}
|
||||
29
components/pages/style/video-to-video.css
Normal file
29
components/pages/style/video-to-video.css
Normal 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;
|
||||
}
|
||||
783
components/pages/video-to-video.tsx
Normal file
783
components/pages/video-to-video.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
@ -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
@ -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
70
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
27
plugins/liquid-glass/index.d.ts
vendored
Normal 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
|
||||
1
plugins/liquid-glass/index.d.ts.map
Normal file
1
plugins/liquid-glass/index.d.ts.map
Normal 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"}
|
||||
586
plugins/liquid-glass/index.esm.js
Normal file
586
plugins/liquid-glass/index.esm.js
Normal file
File diff suppressed because one or more lines are too long
607
plugins/liquid-glass/index.js
Normal file
607
plugins/liquid-glass/index.js
Normal file
File diff suppressed because one or more lines are too long
25
plugins/liquid-glass/shader-utils.d.ts
vendored
Normal file
25
plugins/liquid-glass/shader-utils.d.ts
vendored
Normal 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
|
||||
1
plugins/liquid-glass/shader-utils.d.ts.map
Normal file
1
plugins/liquid-glass/shader-utils.d.ts.map
Normal 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
4
plugins/liquid-glass/utils.d.ts
vendored
Normal file
File diff suppressed because one or more lines are too long
1
plugins/liquid-glass/utils.d.ts.map
Normal file
1
plugins/liquid-glass/utils.d.ts.map
Normal 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
74
tailwind.config.js
Normal 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")],
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user