import React, { useState, useEffect, useRef } from 'react'; import { ArrowLeft, Grid, ZoomIn, ZoomOut, Maximize, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Download, Archive, Copy, Check, Target, MousePointer2, Play, Pause, Undo2, Layers } from 'lucide-react'; import { Frame, SpriteSheetSettings } from '../types'; import { motion, AnimatePresence } from 'motion/react'; import { generateSpriteSheet } from '../utils'; interface SpriteAnalyzerProps { frames: Frame[]; settings: SpriteSheetSettings; focusedFrameId: string | null; onBack: () => void; onSettingsChange: (settings: SpriteSheetSettings) => void; onUpdateFrameOffset: (id: string, x: number, y: number) => void; onFocusFrame: (id: string) => void; onUndo: () => void; canUndo: boolean; } export function SpriteAnalyzer({ frames, settings, focusedFrameId, onBack, onSettingsChange, onUpdateFrameOffset, onFocusFrame, onUndo, canUndo }: SpriteAnalyzerProps) { const [copied, setCopied] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [currentIndex, setCurrentIndex] = useState(0); const [isDraggingGuide, setIsDraggingGuide] = useState(false); const timerRef = useRef(null); const containerRef = useRef(null); const focusedFrame = frames.find(f => f.id === focusedFrameId) || frames[0]; const focusedIndex = frames.findIndex(f => f.id === focusedFrameId); useEffect(() => { if (isPlaying && frames.length > 0) { const currentFrame = frames[currentIndex]; const duration = (1000 / settings.fps) * (currentFrame?.durationMultiplier || 1); timerRef.current = window.setTimeout(() => { setCurrentIndex((prev) => (prev >= frames.length - 1 ? 0 : prev + 1)); }, duration); } else { if (timerRef.current) clearTimeout(timerRef.current); } return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, [isPlaying, frames.length, settings.fps, currentIndex, frames]); const handleCopyCode = () => { const code = generateSpriteCode(); navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const generateSpriteCode = () => { const sheetWidth = settings.columns * (settings.frameSize.width + settings.padding * 2); const sheetHeight = Math.ceil(frames.length / settings.columns) * (settings.frameSize.height + settings.padding * 2); return `// Spritesheet: spritesheet.png // Size: ${sheetWidth}x${sheetHeight} // Frames: ${frames.length} // Frame Size: ${settings.frameSize.width}x${settings.frameSize.height} this.load.spritesheet('sprite_key', 'spritesheet.png', { frameWidth: ${settings.frameSize.width}, frameHeight: ${settings.frameSize.height}, margin: ${settings.padding}, spacing: ${settings.padding * 2} }); // Individual Frame Offsets: ${frames.map((f, i) => f.offset.x !== 0 || f.offset.y !== 0 ? `// Frame ${i}: offset { x: ${f.offset.x}, y: ${f.offset.y} }` : '').filter(Boolean).join('\n')}`; }; const navigateFrame = (delta: number) => { const nextIndex = (focusedIndex + delta + frames.length) % frames.length; onFocusFrame(frames[nextIndex].id); }; const previousFrame = focusedIndex > 0 ? frames[focusedIndex - 1] : null; const handleDownloadSheet = async () => { const dataUrl = await generateSpriteSheet(frames, settings); const link = document.createElement('a'); link.download = 'spritesheet.png'; link.href = dataUrl; link.click(); }; const toggleGridMode = () => { const modes: ('none' | 'grid' | 'guide')[] = ['none', 'grid', 'guide']; const currentIdx = modes.indexOf(settings.guideMode); const nextMode = modes[(currentIdx + 1) % modes.length]; onSettingsChange({ ...settings, guideMode: nextMode }); }; const handleGuideMouseDown = (e: React.MouseEvent) => { if (settings.guideMode === 'guide') { setIsDraggingGuide(true); } }; const handleMouseMove = (e: React.MouseEvent) => { if (isDraggingGuide && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); // Calculate percentage relative to the frame's actual bounding box on screen const relX = ((e.clientX - rect.left) / rect.width) * 100; const relY = ((e.clientY - rect.top) / rect.height) * 100; onSettingsChange({ ...settings, guidePosition: { // Allow guide to move outside the frame for better alignment reference x: Math.max(-50, Math.min(150, relX)), y: Math.max(-50, Math.min(150, relY)) } }); } }; const handleMouseUp = () => { setIsDraggingGuide(false); }; const currentPreviewFrame = isPlaying ? frames[currentIndex] : focusedFrame; return (
{/* Header */}
Sprite Analyzer Pro
{/* Controls moved to floating panel */}
{/* Floating Controls Panel */}
{settings.analyzerZoom}%
{settings.showOnionSkin && (
Opacity onSettingsChange({ ...settings, onionOpacity: parseInt(e.target.value) })} className="w-24 h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-blue-500" /> {settings.onionOpacity}%
)}
{/* Main Preview View (Focused Frame / Animation) */}
{/* Extended Grid Overlay (Covers more area) */} {settings.guideMode === 'grid' && (
{/* Main Frame Border */}
{/* Full Area Grid */}
{/* Center Crosshair for the frame */}
)} {/* Guide Overlay (Extended Range) */} {settings.guideMode === 'guide' && (
{/* Vertical Line */}
{/* Horizontal Line (Bottom of T) */}
{/* Center Handle */}
)} {/* Frame Image */}
{/* Onion Skin (Previous Frame) - Now 100% visible underneath */} {settings.showOnionSkin && previousFrame && !isPlaying && ( Onion Skin )} {/* Current Frame - Now with variable opacity */} Preview
{/* Playback Controls Overlay */}
{focusedIndex + 1} / {frames.length}
{/* Sidebar */}
{/* Thumbnails Grid (Sheet View moved here) */}
Spritesheet Grid
{settings.columns} cols
{frames.map((frame, i) => (
{ setIsPlaying(false); onFocusFrame(frame.id); }} className={` relative aspect-square rounded-lg border overflow-hidden cursor-pointer transition-all group ${focusedFrameId === frame.id ? 'ring-2 ring-purple-500 border-transparent' : 'border-zinc-800 hover:border-zinc-700'} `} > {`Frame
{i}
))}
{/* Sidebar Navigation Buttons */}
{/* Alignment Controls */}
Alignment
{focusedFrame?.offset.x} , {focusedFrame?.offset.y}
{/* Export & Code */}
Sprite Code
); }