253 lines
12 KiB
TypeScript
253 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { X, Download, Image as ImageIcon, Layers, Settings, Monitor, Smartphone, Maximize2 } from 'lucide-react';
|
|
import { cn } from '../lib/utils';
|
|
|
|
interface RenderModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onRender: (settings: RenderSettings) => void;
|
|
}
|
|
|
|
export interface RenderSettings {
|
|
width: number;
|
|
height: number;
|
|
includeAlpha: boolean;
|
|
format: 'png' | 'jpg';
|
|
name: string;
|
|
folder?: string;
|
|
}
|
|
|
|
const RESOLUTION_PRESETS = [
|
|
{ label: '7680 x 4800 (8K)', w: 7680, h: 4800 },
|
|
{ label: '3840 x 2400 (4K)', w: 3840, h: 2400 },
|
|
{ label: '2560 x 1600 (2K)', w: 2560, h: 1600 },
|
|
{ label: '1920 x 1200 (Full HD)', w: 1920, h: 1200 },
|
|
{ label: '1600 x 1000', w: 1600, h: 1000 },
|
|
{ label: '1280 x 800', w: 1280, h: 800 },
|
|
{ label: '1024 x 640', w: 1024, h: 640 },
|
|
{ label: '800 x 500', w: 800, h: 500 },
|
|
{ label: '640 x 400', w: 640, h: 400 },
|
|
];
|
|
|
|
export const RenderModal: React.FC<RenderModalProps> = ({ isOpen, onClose, onRender }) => {
|
|
const [settings, setSettings] = useState<RenderSettings>({
|
|
width: 1920,
|
|
height: 1200,
|
|
includeAlpha: true,
|
|
format: 'png',
|
|
name: 'Render_1',
|
|
folder: 'C:/Users/Render/Documents'
|
|
});
|
|
|
|
const [activeTab, setActiveTab] = useState('Output');
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
className="w-full max-w-4xl bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl overflow-hidden flex flex-col h-[600px]"
|
|
>
|
|
{/* Header */}
|
|
<div className="h-10 bg-zinc-800 flex items-center justify-between px-4 border-b border-zinc-700">
|
|
<span className="text-xs font-bold text-zinc-300">Render</span>
|
|
<button onClick={onClose} className="p-1 hover:bg-zinc-700 rounded transition-colors">
|
|
<X size={16} className="text-zinc-400" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Sidebar */}
|
|
<div className="w-48 bg-zinc-900 border-r border-zinc-800 flex flex-col">
|
|
{['Output', 'Options', 'Queue'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={cn(
|
|
"px-4 py-2 text-left text-xs font-medium transition-colors",
|
|
activeTab === tab ? "bg-zinc-800 text-blue-400" : "text-zinc-500 hover:bg-zinc-800/50 hover:text-zinc-300"
|
|
)}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 p-6 overflow-y-auto bg-zinc-900/50">
|
|
<div className="flex flex-col gap-8 max-w-2xl">
|
|
{/* Mode Tabs */}
|
|
<div className="flex p-1 bg-zinc-950 rounded-lg border border-zinc-800 self-start">
|
|
{['Still Image', 'Animation', 'Titan3d XR', 'Configurator', 'CMF'].map((mode) => (
|
|
<button
|
|
key={mode}
|
|
className={cn(
|
|
"px-4 py-1.5 text-[10px] font-bold rounded transition-all",
|
|
mode === 'Still Image' ? "bg-zinc-800 text-blue-400 shadow-lg" : "text-zinc-500 hover:text-zinc-300"
|
|
)}
|
|
>
|
|
{mode}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Name & Resolution */}
|
|
<div className="grid grid-cols-1 gap-6">
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">Name</label>
|
|
<input
|
|
type="text"
|
|
value={settings.name}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, name: e.target.value }))}
|
|
className="bg-zinc-950 border border-zinc-800 rounded px-3 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-blue-500/50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">Folder</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={settings.folder}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, folder: e.target.value }))}
|
|
className="flex-1 bg-zinc-950 border border-zinc-800 rounded px-3 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-blue-500/50"
|
|
/>
|
|
<button className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] font-bold transition-colors">
|
|
Browse...
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<label className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">Format & Quality</label>
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-zinc-400">Format</span>
|
|
<select
|
|
value={settings.format}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, format: e.target.value as any }))}
|
|
className="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300"
|
|
>
|
|
<option value="png">PNG</option>
|
|
<option value="jpg">JPG</option>
|
|
</select>
|
|
</div>
|
|
<label className="flex items-center gap-2 cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={settings.includeAlpha}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, includeAlpha: e.target.checked }))}
|
|
className="hidden"
|
|
/>
|
|
<div className={cn(
|
|
"w-4 h-4 rounded border flex items-center justify-center transition-all",
|
|
settings.includeAlpha ? "bg-blue-500 border-blue-500" : "border-zinc-700 bg-zinc-950"
|
|
)}>
|
|
{settings.includeAlpha && <div className="w-2 h-2 bg-white rounded-sm" />}
|
|
</div>
|
|
<span className="text-xs text-zinc-400 group-hover:text-zinc-200 transition-colors">Include Alpha (Transparency)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<label className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">Resolution</label>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-zinc-500">W:</span>
|
|
<input
|
|
type="number"
|
|
value={settings.width}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, width: parseInt(e.target.value) || 0 }))}
|
|
className="w-20 bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300"
|
|
/>
|
|
<span className="text-[10px] text-zinc-600">px</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-zinc-500">H:</span>
|
|
<input
|
|
type="number"
|
|
value={settings.height}
|
|
onChange={(e) => setSettings(prev => ({ ...prev, height: parseInt(e.target.value) || 0 }))}
|
|
className="w-20 bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300"
|
|
/>
|
|
<span className="text-[10px] text-zinc-600">px</span>
|
|
</div>
|
|
<select
|
|
onChange={(e) => {
|
|
const preset = RESOLUTION_PRESETS.find(p => p.label === e.target.value);
|
|
if (preset) {
|
|
setSettings(prev => ({ ...prev, width: preset.w, height: preset.h }));
|
|
}
|
|
}}
|
|
className="bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300"
|
|
>
|
|
<option value="">Presets</option>
|
|
{RESOLUTION_PRESETS.map(p => (
|
|
<option key={p.label} value={p.label}>{p.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Collapsible Sections */}
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between p-3 bg-zinc-800/30 rounded border border-zinc-800/50 cursor-pointer hover:bg-zinc-800/50 transition-colors">
|
|
<span className="text-xs font-bold text-zinc-400">Layers and Passes</span>
|
|
<ChevronDown size={14} className="text-zinc-600" />
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-zinc-800/30 rounded border border-zinc-800/50 cursor-pointer hover:bg-zinc-800/50 transition-colors">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 border border-zinc-700 rounded-sm" />
|
|
<span className="text-xs font-bold text-zinc-400">Region</span>
|
|
</div>
|
|
<ChevronDown size={14} className="text-zinc-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="h-16 bg-zinc-800/50 border-t border-zinc-800 flex items-center justify-end px-6 gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-6 py-2 bg-zinc-700 hover:bg-zinc-600 text-zinc-300 rounded text-xs font-bold transition-all"
|
|
>
|
|
Add to Queue
|
|
</button>
|
|
<button
|
|
onClick={() => onRender(settings)}
|
|
className="px-8 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs font-bold shadow-lg shadow-blue-900/20 transition-all"
|
|
>
|
|
Render
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</AnimatePresence>
|
|
);
|
|
};
|
|
|
|
const ChevronDown = ({ size, className }: { size: number, className?: string }) => (
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={className}
|
|
>
|
|
<path d="m6 9 6 6 6-6"/>
|
|
</svg>
|
|
);
|