Files
paint-ui/components/layers/layers-panel.tsx
Sebastian Krüger 4b01e92b88 feat: implement Phase 2 - Core Canvas Engine with layer system
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>
2025-11-20 21:20:06 +01:00

83 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}