399 lines
20 KiB
TypeScript
399 lines
20 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
MapPin,
|
|
Users,
|
|
MessageCircle,
|
|
Activity,
|
|
Eye,
|
|
Copy,
|
|
Check,
|
|
Sparkles,
|
|
Camera
|
|
} from 'lucide-react';
|
|
import { SCENES } from '../data/storyData';
|
|
import { soundManager } from '../utils/audio';
|
|
|
|
interface StoryboardViewProps {
|
|
isBedtimeMode: boolean;
|
|
selectedSceneId?: number;
|
|
}
|
|
|
|
export const StoryboardView: React.FC<StoryboardViewProps> = ({
|
|
isBedtimeMode,
|
|
selectedSceneId,
|
|
}) => {
|
|
const [filterAct, setFilterAct] = useState<'all' | 'beginning' | 'middle' | 'end'>('all');
|
|
const [activeSceneId, setActiveSceneId] = useState<number>(selectedSceneId || 1);
|
|
const [copiedText, setCopiedText] = useState<string | null>(null);
|
|
|
|
const filteredScenes = filterAct === 'all'
|
|
? SCENES
|
|
: SCENES.filter(s => s.act === filterAct);
|
|
|
|
const currentScene = SCENES.find(s => s.id === activeSceneId) || SCENES[0];
|
|
|
|
const handleCopyPrompt = (text: string) => {
|
|
soundManager.playPop();
|
|
navigator.clipboard.writeText(text);
|
|
setCopiedText(text);
|
|
setTimeout(() => setCopiedText(null), 2500);
|
|
};
|
|
|
|
return (
|
|
<div className={`min-h-[calc(100vh-7rem)] py-8 px-4 sm:px-6 lg:px-8 transition-colors duration-300 ${
|
|
isBedtimeMode
|
|
? 'bg-gradient-to-b from-indigo-950 via-slate-900 to-indigo-950 text-indigo-100'
|
|
: 'bg-[#faf6ee] text-stone-900'
|
|
}`}>
|
|
<div className="max-w-7xl mx-auto space-y-8">
|
|
{/* Header & Concept Pitch */}
|
|
<div className={`p-6 sm:p-8 rounded-3xl border shadow-sm ${
|
|
isBedtimeMode
|
|
? 'bg-slate-900/90 border-indigo-800'
|
|
: 'bg-white border-amber-200/90'
|
|
}`}>
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-300/40">
|
|
Visual Narrative Architecture
|
|
</span>
|
|
<span className="text-xs text-stone-500 dark:text-indigo-300">
|
|
5 Key Visual Beats (Act I, II, III)
|
|
</span>
|
|
</div>
|
|
<h2 className="text-2xl sm:text-3xl font-heading font-bold text-amber-950 dark:text-white">
|
|
Complete Storyboard & Scene Bible
|
|
</h2>
|
|
<p className="mt-1 text-sm sm:text-base text-stone-600 dark:text-indigo-200 max-w-3xl leading-relaxed">
|
|
Every frame pairs narrative intent with rigorous art direction: environmental settings, natural dialogue, physical actions, camera perspectives, and exact color palettes.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Act Filter Tabs */}
|
|
<div className="flex items-center gap-1.5 bg-stone-100 dark:bg-indigo-950 p-1.5 rounded-2xl border border-stone-200 dark:border-indigo-800 shrink-0">
|
|
<button
|
|
onClick={() => { soundManager.playPop(); setFilterAct('all'); }}
|
|
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
|
filterAct === 'all'
|
|
? 'bg-amber-500 text-white shadow-sm'
|
|
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
|
|
}`}
|
|
>
|
|
All (5)
|
|
</button>
|
|
<button
|
|
onClick={() => { soundManager.playPop(); setFilterAct('beginning'); }}
|
|
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
|
filterAct === 'beginning'
|
|
? 'bg-amber-500 text-white shadow-sm'
|
|
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
|
|
}`}
|
|
>
|
|
Act I: Beginning
|
|
</button>
|
|
<button
|
|
onClick={() => { soundManager.playPop(); setFilterAct('middle'); }}
|
|
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
|
filterAct === 'middle'
|
|
? 'bg-amber-500 text-white shadow-sm'
|
|
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
|
|
}`}
|
|
>
|
|
Act II: Middle
|
|
</button>
|
|
<button
|
|
onClick={() => { soundManager.playPop(); setFilterAct('end'); }}
|
|
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
|
filterAct === 'end'
|
|
? 'bg-amber-500 text-white shadow-sm'
|
|
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
|
|
}`}
|
|
>
|
|
Act III: End
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Storyboard Navigation Thumbnails */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-3">
|
|
{filteredScenes.map((sc) => {
|
|
const isSelected = activeSceneId === sc.id;
|
|
return (
|
|
<div
|
|
key={sc.id}
|
|
onClick={() => {
|
|
soundManager.playPop();
|
|
setActiveSceneId(sc.id);
|
|
}}
|
|
className={`p-3 rounded-2xl cursor-pointer border transition-all text-left group ${
|
|
isSelected
|
|
? 'bg-amber-100/90 dark:bg-indigo-900 border-amber-400 dark:border-indigo-600 shadow-md ring-2 ring-amber-400/40'
|
|
: 'bg-white dark:bg-slate-900/80 border-stone-200 dark:border-indigo-900 hover:bg-amber-50/60 dark:hover:bg-indigo-900/50'
|
|
}`}
|
|
>
|
|
<div className="relative aspect-[16/10] rounded-xl overflow-hidden mb-2.5">
|
|
<img
|
|
src={sc.image}
|
|
alt={sc.title}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
<span className="absolute top-1.5 left-1.5 bg-black/60 text-white text-[10px] font-bold px-2 py-0.5 rounded-md backdrop-blur-sm">
|
|
Scene {sc.id}
|
|
</span>
|
|
<span className={`absolute bottom-1.5 right-1.5 text-[9px] font-bold uppercase px-1.5 py-0.2 rounded-md ${
|
|
sc.act === 'beginning' ? 'bg-amber-500 text-white' : sc.act === 'middle' ? 'bg-sky-500 text-white' : 'bg-purple-500 text-white'
|
|
}`}>
|
|
{sc.act}
|
|
</span>
|
|
</div>
|
|
<h4 className="font-heading font-bold text-xs line-clamp-1 text-stone-900 dark:text-white">
|
|
{sc.title}
|
|
</h4>
|
|
<p className="text-[11px] text-stone-500 dark:text-indigo-300 line-clamp-1">
|
|
{sc.subtitle}
|
|
</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Detailed Scene Storyboard Inspector */}
|
|
<div className={`rounded-3xl border overflow-hidden shadow-xl ${
|
|
isBedtimeMode
|
|
? 'bg-slate-900 border-indigo-800'
|
|
: 'bg-white border-amber-200/90'
|
|
}`}>
|
|
{/* Top Bar with Scene Header */}
|
|
<div className="p-6 bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-transparent border-b border-stone-200 dark:border-indigo-800 flex flex-wrap items-center justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wider ${
|
|
currentScene.act === 'beginning'
|
|
? 'bg-amber-100 text-amber-800 border border-amber-300'
|
|
: currentScene.act === 'middle'
|
|
? 'bg-sky-100 text-sky-800 border border-sky-300'
|
|
: 'bg-purple-100 text-purple-800 border border-purple-300'
|
|
}`}>
|
|
Act: {currentScene.act.toUpperCase()}
|
|
</span>
|
|
<span className="text-xs font-semibold text-stone-500 dark:text-indigo-300">
|
|
Scene #{currentScene.id} of {SCENES.length}
|
|
</span>
|
|
</div>
|
|
<h3 className="text-2xl sm:text-3xl font-heading font-bold text-stone-900 dark:text-white">
|
|
{currentScene.title}
|
|
</h3>
|
|
<p className="text-sm font-semibold text-amber-700 dark:text-amber-400">
|
|
"{currentScene.subtitle}"
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-stone-500 dark:text-indigo-300">Key Palette:</span>
|
|
<div className="flex items-center gap-1.5">
|
|
{currentScene.artNotes.palette.map((color, idx) => (
|
|
<div
|
|
key={idx}
|
|
style={{ backgroundColor: color }}
|
|
className="w-6 h-6 rounded-full border-2 border-white dark:border-slate-800 shadow-sm"
|
|
title={color}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x divide-stone-200 dark:divide-indigo-800">
|
|
{/* Visual Frame & Illustration Prompt Column (5 cols) */}
|
|
<div className="lg:col-span-5 p-6 space-y-6">
|
|
{/* Scene Frame Rendering */}
|
|
<div className="relative rounded-2xl overflow-hidden shadow-lg border border-stone-200 dark:border-indigo-700 aspect-[4/3] group">
|
|
<img
|
|
src={currentScene.image}
|
|
alt={currentScene.imageAlt}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
|
|
<span className="absolute bottom-3 left-3 text-white text-xs font-bold drop-shadow-md">
|
|
Frame #{currentScene.id} Preview
|
|
</span>
|
|
</div>
|
|
|
|
{/* Illustration Prompt Box */}
|
|
<div className="p-4 rounded-2xl bg-amber-50/70 dark:bg-indigo-950/60 border border-amber-200 dark:border-indigo-800 space-y-2.5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-heading font-bold uppercase tracking-wider text-amber-800 dark:text-amber-300 flex items-center gap-1.5">
|
|
<Sparkles className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
|
|
Illustration Prompt (AI / Artist Bible)
|
|
</span>
|
|
<button
|
|
onClick={() => handleCopyPrompt(currentScene.illustrationPrompt)}
|
|
className="flex items-center gap-1 text-[11px] font-bold text-amber-700 dark:text-amber-300 hover:text-amber-900 bg-white/80 dark:bg-indigo-900/80 px-2 py-1 rounded-lg border border-amber-200 dark:border-indigo-700 transition-colors"
|
|
>
|
|
{copiedText === currentScene.illustrationPrompt ? (
|
|
<>
|
|
<Check className="w-3 h-3 text-emerald-600" />
|
|
<span>Copied!</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3 h-3" />
|
|
<span>Copy Prompt</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-reading leading-relaxed italic text-stone-700 dark:text-stone-300">
|
|
"{currentScene.illustrationPrompt}"
|
|
</p>
|
|
</div>
|
|
|
|
{/* Art Direction Specs */}
|
|
<div className="space-y-3">
|
|
<h5 className="font-heading font-bold text-xs uppercase tracking-wider text-stone-500 dark:text-indigo-400 flex items-center gap-1.5">
|
|
<Camera className="w-3.5 h-3.5" />
|
|
Visual Direction Parameters
|
|
</h5>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
|
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
|
|
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
|
|
Camera Perspective:
|
|
</span>
|
|
<p className="text-stone-700 dark:text-stone-300 leading-snug">
|
|
{currentScene.artNotes.cameraAngle}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
|
|
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
|
|
Lighting & Atmosphere:
|
|
</span>
|
|
<p className="text-stone-700 dark:text-stone-300 leading-snug">
|
|
{currentScene.artNotes.lighting}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
|
|
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
|
|
Focal Point:
|
|
</span>
|
|
<p className="text-stone-700 dark:text-stone-300 leading-snug">
|
|
{currentScene.artNotes.focalPoint}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
|
|
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
|
|
Character Expressions:
|
|
</span>
|
|
<p className="text-stone-700 dark:text-stone-300 leading-snug">
|
|
{currentScene.artNotes.expressions}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Script & Stage Direction Storyboard Column (7 cols) */}
|
|
<div className="lg:col-span-7 p-6 space-y-6">
|
|
{/* Setting */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
|
|
<MapPin className="w-4 h-4 text-rose-500" />
|
|
<span>Scene Setting & Environment</span>
|
|
</div>
|
|
<div className="p-4 rounded-2xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800 text-sm leading-relaxed text-stone-800 dark:text-stone-200 font-reading">
|
|
{currentScene.setting}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Characters Present */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
|
|
<Users className="w-4 h-4 text-sky-500" />
|
|
<span>Characters on Stage</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{currentScene.characters.map((charName) => (
|
|
<span
|
|
key={charName}
|
|
className="px-3 py-1 rounded-xl bg-amber-100 dark:bg-indigo-900 border border-amber-200 dark:border-indigo-700 text-xs font-bold text-amber-900 dark:text-amber-300 flex items-center gap-1.5"
|
|
>
|
|
<span>{charName === 'Pip' ? '🦔' : charName === 'Caelum' ? '☁️' : charName === 'Barnaby' ? '🐻' : '🐦'}</span>
|
|
<span>{charName}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Script & Dialogue */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
|
|
<MessageCircle className="w-4 h-4 text-amber-500" />
|
|
<span>Spoken Dialogue & Tone</span>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{currentScene.dialogue.map((dlg, i) => (
|
|
<div
|
|
key={i}
|
|
className="p-4 rounded-2xl bg-amber-50/50 dark:bg-indigo-950/60 border border-amber-200/60 dark:border-indigo-800 flex flex-col sm:flex-row sm:items-baseline justify-between gap-2"
|
|
>
|
|
<div className="space-y-1">
|
|
<span className="font-heading font-bold text-sm text-amber-900 dark:text-amber-300">
|
|
{dlg.speaker}:
|
|
</span>
|
|
<p className="text-base font-reading text-stone-900 dark:text-white italic">
|
|
"{dlg.text}"
|
|
</p>
|
|
</div>
|
|
<span className="shrink-0 text-xs font-semibold px-2.5 py-1 rounded-lg bg-white dark:bg-indigo-900 text-stone-600 dark:text-indigo-300 border border-stone-200 dark:border-indigo-700 self-start sm:self-auto">
|
|
🎭 Tone: {dlg.tone}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Key Physical Actions & Choreography */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
|
|
<Activity className="w-4 h-4 text-emerald-500" />
|
|
<span>Key Physical Actions (Beat by Beat)</span>
|
|
</div>
|
|
|
|
<div className="p-4 rounded-2xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800 space-y-2.5">
|
|
{currentScene.keyActions.map((action, i) => (
|
|
<div key={i} className="flex items-start gap-3 text-sm text-stone-800 dark:text-stone-200">
|
|
<span className="w-5 h-5 rounded-full bg-amber-200 dark:bg-indigo-800 text-amber-900 dark:text-amber-200 font-bold text-xs flex items-center justify-center shrink-0 mt-0.5">
|
|
{i + 1}
|
|
</span>
|
|
<p className="font-reading leading-relaxed">
|
|
{action}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Director's & Illustrator's Intent */}
|
|
<div className="p-4 rounded-2xl bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-indigo-500/10 border border-amber-300/60 dark:border-indigo-700 space-y-1.5">
|
|
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-amber-900 dark:text-amber-300">
|
|
<Eye className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
|
<span>Director's Core Intention & Emotional Beat</span>
|
|
</div>
|
|
<p className="text-xs sm:text-sm font-reading text-stone-800 dark:text-stone-200 leading-relaxed italic">
|
|
"{currentScene.directorNotes}"
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|