Complete canvas rendering infrastructure and state management: **Type System (types/)** - Layer interface with blend modes, opacity, visibility - Canvas state with zoom, pan, grid, rulers - Tool types and settings interfaces - Selection and pointer state types **State Management (store/)** - Layer store: CRUD operations, reordering, merging, flattening - Canvas store: zoom (0.1x-10x), pan, grid, rulers, coordinate conversion - Tool store: active tool, brush settings (size, opacity, hardness, flow) - Full Zustand integration with selectors **Utilities (lib/)** - Canvas utils: create, clone, resize, load images, draw grid/checkerboard - General utils: cn, clamp, lerp, distance, snap to grid, debounce, throttle - Image data handling with error safety **Components** - CanvasWrapper: Multi-layer rendering with transformations - Checkerboard transparency background - Layer compositing with blend modes and opacity - Grid overlay support - Selection visualization - Mouse wheel zoom (Ctrl+scroll) - Middle-click or Shift+click panning - LayersPanel: Interactive layer management - Visibility toggle with eye icon - Active layer selection - Opacity display - Delete with confirmation - Sorted by z-order - EditorLayout: Full editor interface - Top toolbar with zoom controls (±, fit to screen, percentage) - Canvas area with full viewport - Right sidebar for layers panel - "New Layer" button with auto-naming **Features Implemented** ✓ Multi-layer canvas with proper z-ordering ✓ Layer visibility, opacity, blend modes ✓ Zoom: 10%-1000% with Ctrl+wheel ✓ Pan: Middle-click or Shift+drag ✓ Grid overlay (toggleable) ✓ Selection rendering ✓ Background color support ✓ Create/delete/duplicate layers ✓ Layer merging and flattening **Performance** - Dev server: 451ms startup - Efficient canvas rendering with transformations - Debounced/throttled event handlers ready - Memory-safe image data operations Ready for Phase 3: History & Undo System 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
'use client';
|
||
|
||
import { useLayerStore } from '@/store';
|
||
import { Eye, EyeOff, Trash2 } from 'lucide-react';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
export function LayersPanel() {
|
||
const { layers, activeLayerId, setActiveLayer, updateLayer, deleteLayer } = useLayerStore();
|
||
|
||
// Sort layers by order (highest first)
|
||
const sortedLayers = [...layers].sort((a, b) => b.order - a.order);
|
||
|
||
return (
|
||
<div className="flex h-full flex-col bg-card">
|
||
<div className="border-b border-border p-3">
|
||
<h2 className="text-sm font-semibold text-card-foreground">Layers</h2>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
||
{sortedLayers.length === 0 ? (
|
||
<div className="flex h-full items-center justify-center">
|
||
<p className="text-sm text-muted-foreground">No layers</p>
|
||
</div>
|
||
) : (
|
||
sortedLayers.map((layer) => (
|
||
<div
|
||
key={layer.id}
|
||
className={cn(
|
||
'group flex items-center gap-2 rounded-md border p-2 transition-colors cursor-pointer',
|
||
activeLayerId === layer.id
|
||
? 'border-primary bg-primary/10'
|
||
: 'border-border hover:border-primary/50 hover:bg-accent/50'
|
||
)}
|
||
onClick={() => setActiveLayer(layer.id)}
|
||
>
|
||
<button
|
||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
updateLayer(layer.id, { visible: !layer.visible });
|
||
}}
|
||
>
|
||
{layer.visible ? (
|
||
<Eye className="h-4 w-4" />
|
||
) : (
|
||
<EyeOff className="h-4 w-4" />
|
||
)}
|
||
</button>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-card-foreground truncate">
|
||
{layer.name}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{layer.width} × {layer.height}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button
|
||
className="shrink-0 text-muted-foreground hover:text-destructive"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
if (confirm('Delete this layer?')) {
|
||
deleteLayer(layer.id);
|
||
}
|
||
}}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="shrink-0 text-xs text-muted-foreground">
|
||
{Math.round(layer.opacity * 100)}%
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|