import { useEffect, useRef, useState } from 'react' import { X } from 'lucide-react' import { objectAnimationModeOf, useEditorStore, type GridConfig, type TileObject, type TileRegion, } from '../store/editorStore' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { playOrder } from '../lib/objectTransform' import { cn } from '@/lib/utils' import PropertyList from './PropertyList' import CollapsibleSection from './CollapsibleSection' const THUMB_MAX = 56 /** Cycles an object's frames (frame 0 = base region) at its configured timing. */ function AnimatedPreview({ image, grid, object, }: { image: HTMLImageElement | null grid: GridConfig object: TileObject }) { const ref = useRef(null) useEffect(() => { const canvas = ref.current if (!canvas) return const ctx = canvas.getContext('2d') if (!ctx) return const cells = playOrder( [{ col: object.col, row: object.row }, ...object.frames], objectAnimationModeOf(object.properties), ) const sw = object.cols * grid.tileWidth const sh = object.rows * grid.tileHeight const scale = Math.min(72 / sw, 72 / sh, 8) const dw = Math.max(1, Math.round(sw * scale)) const dh = Math.max(1, Math.round(sh * scale)) const dpr = window.devicePixelRatio || 1 canvas.width = Math.round(dw * dpr) canvas.height = Math.round(dh * dpr) canvas.style.width = `${dw}px` canvas.style.height = `${dh}px` ctx.setTransform(dpr, 0, 0, dpr, 0, 0) ctx.imageSmoothingEnabled = false const dur = Math.max(16, object.frameDurationMs) let idx = 0 let acc = 0 let prev = performance.now() let raf = 0 const render = () => { const cell = cells[idx] ctx.clearRect(0, 0, dw, dh) if (image) { ctx.drawImage( image, grid.offsetX + cell.col * grid.tileWidth, grid.offsetY + cell.row * grid.tileHeight, sw, sh, 0, 0, dw, dh, ) } } const loop = (now: number) => { acc += now - prev prev = now while (acc >= dur) { acc -= dur idx = (idx + 1) % cells.length render() } raf = requestAnimationFrame(loop) } render() raf = requestAnimationFrame(loop) return () => cancelAnimationFrame(raf) }, [image, grid, object]) return } /** Renders a region of the tileset image cropped + scaled into a small canvas. */ function ObjectThumb({ image, grid, region, }: { image: HTMLImageElement | null grid: GridConfig region: TileRegion }) { const ref = useRef(null) useEffect(() => { const canvas = ref.current if (!canvas) return const ctx = canvas.getContext('2d') if (!ctx) return const sw = region.cols * grid.tileWidth const sh = region.rows * grid.tileHeight const scale = Math.min(THUMB_MAX / sw, THUMB_MAX / sh, 8) const dw = Math.max(1, Math.round(sw * scale)) const dh = Math.max(1, Math.round(sh * scale)) const dpr = window.devicePixelRatio || 1 canvas.width = Math.round(dw * dpr) canvas.height = Math.round(dh * dpr) canvas.style.width = `${dw}px` canvas.style.height = `${dh}px` ctx.setTransform(dpr, 0, 0, dpr, 0, 0) ctx.clearRect(0, 0, dw, dh) if (!image) return ctx.imageSmoothingEnabled = false const sx = grid.offsetX + region.col * grid.tileWidth const sy = grid.offsetY + region.row * grid.tileHeight ctx.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh) }, [image, grid, region]) return } function NumInput({ label, value, min, max, onChange, }: { label: string value: number min?: number max?: number onChange: (v: number) => void }) { return ( ) } export default function ObjectPanel() { const image = useEditorStore((s) => s.image) const grid = useEditorStore((s) => s.grid) const objects = useEditorStore((s) => s.objects) const selection = useEditorStore((s) => s.selection) const activeObjectId = useEditorStore((s) => s.activeObjectId) const objectTool = useEditorStore((s) => s.objectTool) const addObject = useEditorStore((s) => s.addObject) const updateObject = useEditorStore((s) => s.updateObject) const removeObject = useEditorStore((s) => s.removeObject) const setActiveObject = useEditorStore((s) => s.setActiveObject) const setObjectTool = useEditorStore((s) => s.setObjectTool) const setObjectOrigin = useEditorStore((s) => s.setObjectOrigin) const updateCollisionBox = useEditorStore((s) => s.updateCollisionBox) const removeCollisionBox = useEditorStore((s) => s.removeCollisionBox) const removeFrame = useEditorStore((s) => s.removeFrame) const setFrameDuration = useEditorStore((s) => s.setFrameDuration) const setObjectPlayMode = useEditorStore((s) => s.setObjectPlayMode) const addProperty = useEditorStore((s) => s.addProperty) const updateProperty = useEditorStore((s) => s.updateProperty) const removeProperty = useEditorStore((s) => s.removeProperty) const [name, setName] = useState('') const active = objects.find((o) => o.id === activeObjectId) ?? null const w = active ? active.cols * grid.tileWidth : 0 const h = active ? active.rows * grid.tileHeight : 0 const create = () => { addObject(name) setName('') } return ( <> {selection ? (

Selected{' '} {selection.cols}×{selection.rows} {' '} tiles

setName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') create() }} />
) : (

Drag on the tileset to select tiles.

)}
    {objects.map((o) => { const isActive = o.id === activeObjectId return (
  • setActiveObject(o.id)} >
    e.stopPropagation()} onChange={(e) => updateObject(o.id, { name: e.target.value })} /> {o.cols}×{o.rows} · {o.collision.length} box {o.collision.length === 1 ? '' : 'es'}
  • ) })} {objects.length === 0 && (
  • No objects yet.
  • )}
{active ? (

Edit “{active.name}”{' '} {w}×{h}px

{/* origin */}
Origin
setObjectOrigin(active.id, { ...active.origin, x: v })} /> setObjectOrigin(active.id, { ...active.origin, y: v })} />
{/* collision boxes */}
Collision boxes
{active.collision.length === 0 && (

None. Click “Draw box”, then drag a rectangle on the sprite.

)}
    {active.collision.map((b, i) => (
  • updateCollisionBox(active.id, i, { x: v })} /> updateCollisionBox(active.id, i, { y: v })} /> updateCollisionBox(active.id, i, { w: v })} /> updateCollisionBox(active.id, i, { h: v })} />
  • ))}
{/* animation */}
Animation
{active.frames.length === 0 ? (

Static. Click “Add frame”, then click a same-size region in the tileset.

) : ( <>
Playback
  • 1 · base
  • {active.frames.map((f, i) => (
  • {i + 2} · @{f.col},{f.row}
  • ))}
)}
{/* custom properties */} addProperty(active.id)} onUpdate={(i, patch) => updateProperty(active.id, i, patch)} onRemove={(i) => removeProperty(active.id, i)} />
) : (

Select an object to edit it.

)}
) }