| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>MotionCraft Studio - SVG Animation Tool</title> |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> |
| <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/svg.js@3.0/dist/svg.min.js"></script> |
| </head> |
| <body class="bg-gray-50 text-gray-900"> |
| <div id="root" class="h-screen flex flex-col"> |
| <div class="flex items-center justify-center h-full"> |
| <div class="text-center"> |
| <div class="animate-pulse"> |
| <i data-feather="loader" class="w-12 h-12 text-indigo-500"></i> |
| </div> |
| <p class="mt-4 text-gray-600">Loading MotionCraft Studio...</p> |
| </div> |
| </div> |
| </div> |
| <script type="text/babel"> |
| const { useState, useRef, useEffect } = React; |
| const { createRoot } = ReactDOM; |
| |
| |
| const SVG = window.SVG; |
| function App() { |
| const [activeTool, setActiveTool] = useState('select'); |
| const [layers, setLayers] = React.useState([]); |
| const [selectedLayer, setSelectedLayer] = React.useState(null); |
| const [timeline, setTimeline] = React.useState([]); |
| const [currentFrame, setCurrentFrame] = React.useState(0); |
| const [isPlaying, setIsPlaying] = React.useState(false); |
| const svgContainerRef = React.useRef(null); |
| const [svgInstance, setSvgInstance] = React.useState(null); |
| |
| React.useEffect(() => { |
| try { |
| if (svgContainerRef.current && !svgInstance && window.SVG) { |
| const svg = SVG(svgContainerRef.current).size('100%', '100%'); |
| if (!svg) throw new Error('SVG initialization failed'); |
| setSvgInstance(svg); |
| } |
| } catch (error) { |
| console.error('SVG.js Error:', error); |
| alert('Failed to initialize SVG editor. Please refresh the page.'); |
| } |
| }, [svgInstance]); |
| |
| const tools = React.useMemo(() => [ |
| { id: 'select', icon: 'mouse-pointer', name: 'Select' }, |
| { id: 'rectangle', icon: 'square', name: 'Rectangle' }, |
| { id: 'circle', icon: 'circle', name: 'Circle' }, |
| { id: 'path', icon: 'pen-tool', name: 'Path' }, |
| { id: 'text', icon: 'type', name: 'Text' }, |
| ], []); |
| |
| const addShape = (type) => { |
| if (!svgInstance) return; |
| |
| const id = Date.now().toString(); |
| let shape; |
| |
| switch(type) { |
| case 'rectangle': |
| shape = svgInstance.rect(100, 100).fill('#4f46e5').move(50, 50); |
| break; |
| case 'circle': |
| shape = svgInstance.circle(100).fill('#ec4899').move(50, 50); |
| break; |
| case 'text': |
| shape = svgInstance.text('Hello').font({ size: 24 }).fill('#000').move(50, 50); |
| break; |
| default: |
| shape = svgInstance.rect(100, 100).fill('#4f46e5').move(50, 50); |
| } |
| |
| const newLayer = { |
| id, |
| name: `${type}-${id.slice(-4)}`, |
| type, |
| element: shape, |
| properties: {} |
| }; |
| |
| setLayers([...layers, newLayer]); |
| setSelectedLayer(newLayer); |
| }; |
| |
| |
| const togglePlayback = React.useCallback(() => { |
| setIsPlaying(prev => !prev); |
| |
| }, []); |
| return ( |
| <div className="flex flex-col h-full" id="app-container"> |
| {/* Top Navigation */} |
| <header className="bg-white border-b border-gray-200 px-4 py-2 flex items-center justify-between"> |
| <div className="flex items-center space-x-4"> |
| <h1 className="text-xl font-bold text-indigo-600">MotionCraft Studio</h1> |
| <nav className="flex space-x-2"> |
| <button className="px-3 py-1 text-sm rounded hover:bg-gray-100">File</button> |
| <button className="px-3 py-1 text-sm rounded hover:bg-gray-100">Edit</button> |
| <button className="px-3 py-1 text-sm rounded hover:bg-gray-100">View</button> |
| <button className="px-3 py-1 text-sm rounded hover:bg-gray-100">Help</button> |
| </nav> |
| </div> |
| <div className="flex items-center space-x-2"> |
| <button className="px-4 py-1 bg-indigo-600 text-white rounded text-sm font-medium hover:bg-indigo-700"> |
| Export |
| </button> |
| </div> |
| </header> |
| |
| {/* Main Workspace */} |
| <div className="flex flex-1 overflow-hidden"> |
| {/* Tools Panel */} |
| <div className="w-16 bg-white border-r border-gray-200 flex flex-col items-center py-4 space-y-4"> |
| {tools.map(tool => ( |
| <button |
| key={tool.id} |
| onClick={() => tool.id !== 'select' ? addShape(tool.id) : setActiveTool(tool.id)} |
| className={`p-2 rounded-lg ${activeTool === tool.id ? 'bg-indigo-100 text-indigo-600' : 'hover:bg-gray-100'}`} |
| title={tool.name} |
| > |
| <i data-feather={tool.icon} className="w-5 h-5"></i> |
| </button> |
| ))} |
| </div> |
| |
| {/* Layers Panel */} |
| <div className="w-64 bg-white border-r border-gray-200 flex flex-col"> |
| <div className="px-4 py-2 border-b border-gray-200 flex justify-between items-center"> |
| <h2 className="font-medium">Layers</h2> |
| <button className="text-gray-500 hover:text-gray-700"> |
| <i data-feather="plus" className="w-4 h-4"></i> |
| </button> |
| </div> |
| <div className="flex-1 overflow-y-auto"> |
| {layers.map(layer => ( |
| <div |
| key={layer.id} |
| onClick={() => setSelectedLayer(layer)} |
| className={`px-4 py-2 flex items-center space-x-2 cursor-pointer ${selectedLayer?.id === layer.id ? 'bg-indigo-50 text-indigo-600' : 'hover:bg-gray-50'}`} |
| > |
| <i data-feather={layer.type === 'rectangle' ? 'square' : layer.type === 'circle' ? 'circle' : 'type'} className="w-4 h-4"></i> |
| <span className="text-sm truncate">{layer.name}</span> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Canvas Area */} |
| <div className="flex-1 bg-gray-100 relative overflow-hidden"> |
| <div |
| ref={svgContainerRef} |
| className="absolute inset-0 bg-white shadow-inner" |
| style={{ backgroundImage: 'linear-gradient(45deg, #f5f5f5 25%, transparent 25%, transparent 75%, #f5f5f5 75%, #f5f5f5), linear-gradient(45deg, #f5f5f5 25%, transparent 25%, transparent 75%, #f5f5f5 75%, #f5f5f5)', backgroundSize: '20px 20px', backgroundPosition: '0 0, 10px 10px' }} |
| ></div> |
| </div> |
| |
| {/* Properties Panel */} |
| <div className="w-64 bg-white border-l border-gray-200 flex flex-col"> |
| <div className="px-4 py-2 border-b border-gray-200"> |
| <h2 className="font-medium">Properties</h2> |
| </div> |
| <div className="flex-1 overflow-y-auto p-4"> |
| {selectedLayer && ( |
| <div className="space-y-4"> |
| <div> |
| <label className="block text-sm font-medium text-gray-700 mb-1">Name</label> |
| <input |
| type="text" |
| value={selectedLayer.name} |
| onChange={(e) => { |
| const updated = layers.map(l => |
| l.id === selectedLayer.id ? {...l, name: e.target.value} : l |
| ); |
| setLayers(updated); |
| setSelectedLayer({...selectedLayer, name: e.target.value}); |
| }} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" |
| /> |
| </div> |
| <div> |
| <label className="block text-sm font-medium text-gray-700 mb-1">Position</label> |
| <div className="grid grid-cols-2 gap-2"> |
| <div> |
| <label className="text-xs text-gray-500">X</label> |
| <input |
| type="number" |
| value={0} // Would be dynamic |
| className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" |
| /> |
| </div> |
| <div> |
| <label className="text-xs text-gray-500">Y</label> |
| <input |
| type="number" |
| value={0} // Would be dynamic |
| className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" |
| /> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| </div> |
| </div> |
| |
| {/* Timeline */} |
| <div className="h-24 bg-white border-t border-gray-200 flex flex-col"> |
| <div className="px-4 py-2 border-b border-gray-200 flex items-center justify-between"> |
| <div className="flex items-center space-x-4"> |
| <button onClick={togglePlayback} className="text-gray-700 hover:text-indigo-600"> |
| <i data-feather={isPlaying ? 'pause' : 'play'} className="w-4 h-4"></i> |
| </button> |
| <span className="text-sm font-mono">{currentFrame} / 60</span> |
| </div> |
| <div className="flex items-center space-x-2"> |
| <button className="text-gray-700 hover:text-indigo-600"> |
| <i data-feather="plus" className="w-4 h-4"></i> |
| </button> |
| <button className="text-gray-700 hover:text-indigo-600"> |
| <i data-feather="minus" className="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| <div className="flex-1 overflow-x-auto px-4 py-2"> |
| <div className="h-full w-full min-w-max bg-gray-50 rounded flex items-center"> |
| {/* Keyframes would render here */} |
| <div className="h-4 w-4 bg-indigo-600 rounded-full"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
| |
| class ErrorBoundary extends React.Component { |
| constructor(props) { |
| super(props); |
| this.state = { hasError: false }; |
| } |
| |
| static getDerivedStateFromError(error) { |
| return { hasError: true }; |
| } |
| |
| componentDidCatch(error, errorInfo) { |
| console.error('App Error:', error, errorInfo); |
| } |
| |
| render() { |
| if (this.state.hasError) { |
| return ( |
| <div className="p-8 text-center"> |
| <h2 className="text-xl font-bold text-red-600 mb-2">Application Error</h2> |
| <p className="text-gray-700 mb-4">Something went wrong. Please refresh the page.</p> |
| <button |
| onClick={() => window.location.reload()} |
| className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" |
| > |
| Refresh Page |
| </button> |
| </div> |
| ); |
| } |
| return this.props.children; |
| } |
| } |
| |
| |
| if (!window.React || !window.ReactDOM || !window.SVG) { |
| document.getElementById('root').innerHTML = ` |
| <div class="p-8 text-center"> |
| <h2 class="text-xl font-bold text-red-600 mb-2">Missing Dependencies</h2> |
| <p class="text-gray-700">Required libraries failed to load. Please check your internet connection.</p> |
| </div> |
| `; |
| } else { |
| const root = createRoot(document.getElementById('root')); |
| root.render( |
| <React.StrictMode> |
| <ErrorBoundary> |
| <App /> |
| </ErrorBoundary> |
| </React.StrictMode> |
| ); |
| } |
| |
| function initializeFeather() { |
| if (window.feather) { |
| try { |
| feather.replace(); |
| } catch (error) { |
| console.error('Feather Icons Error:', error); |
| setTimeout(initializeFeather, 500); |
| } |
| } else { |
| setTimeout(initializeFeather, 500); |
| } |
| } |
| document.addEventListener('DOMContentLoaded', initializeFeather); |
| </script> |
| </body> |
| </html> |
|
|