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>
This commit is contained in:
171
store/canvas-store.ts
Normal file
171
store/canvas-store.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { create } from 'zustand';
|
||||
import type { CanvasState, Selection, Point } from '@/types';
|
||||
|
||||
interface CanvasStore extends CanvasState {
|
||||
/** Selection state */
|
||||
selection: Selection;
|
||||
|
||||
/** Set canvas dimensions */
|
||||
setDimensions: (width: number, height: number) => void;
|
||||
/** Set zoom level */
|
||||
setZoom: (zoom: number) => void;
|
||||
/** Zoom in (1.2x) */
|
||||
zoomIn: () => void;
|
||||
/** Zoom out (0.8x) */
|
||||
zoomOut: () => void;
|
||||
/** Fit canvas to viewport */
|
||||
zoomToFit: (viewportWidth: number, viewportHeight: number) => void;
|
||||
/** Zoom to actual size (100%) */
|
||||
zoomToActual: () => void;
|
||||
/** Set pan offset */
|
||||
setPanOffset: (x: number, y: number) => void;
|
||||
/** Pan by delta */
|
||||
pan: (dx: number, dy: number) => void;
|
||||
/** Reset pan to center */
|
||||
resetPan: () => void;
|
||||
/** Toggle grid visibility */
|
||||
toggleGrid: () => void;
|
||||
/** Set grid size */
|
||||
setGridSize: (size: number) => void;
|
||||
/** Toggle rulers */
|
||||
toggleRulers: () => void;
|
||||
/** Toggle snap to grid */
|
||||
toggleSnapToGrid: () => void;
|
||||
/** Set background color */
|
||||
setBackgroundColor: (color: string) => void;
|
||||
/** Set selection */
|
||||
setSelection: (selection: Partial<Selection>) => void;
|
||||
/** Clear selection */
|
||||
clearSelection: () => void;
|
||||
/** Convert screen coordinates to canvas coordinates */
|
||||
screenToCanvas: (screenX: number, screenY: number) => Point;
|
||||
/** Convert canvas coordinates to screen coordinates */
|
||||
canvasToScreen: (canvasX: number, canvasY: number) => Point;
|
||||
}
|
||||
|
||||
const DEFAULT_CANVAS_WIDTH = 800;
|
||||
const DEFAULT_CANVAS_HEIGHT = 600;
|
||||
const MIN_ZOOM = 0.1;
|
||||
const MAX_ZOOM = 10;
|
||||
const ZOOM_STEP = 1.2;
|
||||
|
||||
export const useCanvasStore = create<CanvasStore>((set, get) => ({
|
||||
width: DEFAULT_CANVAS_WIDTH,
|
||||
height: DEFAULT_CANVAS_HEIGHT,
|
||||
zoom: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
backgroundColor: '#ffffff',
|
||||
showGrid: false,
|
||||
gridSize: 20,
|
||||
showRulers: true,
|
||||
snapToGrid: false,
|
||||
selection: {
|
||||
active: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
|
||||
setDimensions: (width, height) => {
|
||||
set({ width, height });
|
||||
},
|
||||
|
||||
setZoom: (zoom) => {
|
||||
const clampedZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
||||
set({ zoom: clampedZoom });
|
||||
},
|
||||
|
||||
zoomIn: () => {
|
||||
const { zoom } = get();
|
||||
get().setZoom(zoom * ZOOM_STEP);
|
||||
},
|
||||
|
||||
zoomOut: () => {
|
||||
const { zoom } = get();
|
||||
get().setZoom(zoom / ZOOM_STEP);
|
||||
},
|
||||
|
||||
zoomToFit: (viewportWidth, viewportHeight) => {
|
||||
const { width, height } = get();
|
||||
const padding = 40;
|
||||
const scaleX = (viewportWidth - padding * 2) / width;
|
||||
const scaleY = (viewportHeight - padding * 2) / height;
|
||||
const zoom = Math.min(scaleX, scaleY, 1);
|
||||
set({ zoom, offsetX: 0, offsetY: 0 });
|
||||
},
|
||||
|
||||
zoomToActual: () => {
|
||||
set({ zoom: 1 });
|
||||
},
|
||||
|
||||
setPanOffset: (x, y) => {
|
||||
set({ offsetX: x, offsetY: y });
|
||||
},
|
||||
|
||||
pan: (dx, dy) => {
|
||||
set((state) => ({
|
||||
offsetX: state.offsetX + dx,
|
||||
offsetY: state.offsetY + dy,
|
||||
}));
|
||||
},
|
||||
|
||||
resetPan: () => {
|
||||
set({ offsetX: 0, offsetY: 0 });
|
||||
},
|
||||
|
||||
toggleGrid: () => {
|
||||
set((state) => ({ showGrid: !state.showGrid }));
|
||||
},
|
||||
|
||||
setGridSize: (size) => {
|
||||
set({ gridSize: Math.max(1, size) });
|
||||
},
|
||||
|
||||
toggleRulers: () => {
|
||||
set((state) => ({ showRulers: !state.showRulers }));
|
||||
},
|
||||
|
||||
toggleSnapToGrid: () => {
|
||||
set((state) => ({ snapToGrid: !state.snapToGrid }));
|
||||
},
|
||||
|
||||
setBackgroundColor: (color) => {
|
||||
set({ backgroundColor: color });
|
||||
},
|
||||
|
||||
setSelection: (selection) => {
|
||||
set((state) => ({
|
||||
selection: { ...state.selection, ...selection },
|
||||
}));
|
||||
},
|
||||
|
||||
clearSelection: () => {
|
||||
set({
|
||||
selection: {
|
||||
active: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
screenToCanvas: (screenX, screenY) => {
|
||||
const { zoom, offsetX, offsetY } = get();
|
||||
return {
|
||||
x: (screenX - offsetX) / zoom,
|
||||
y: (screenY - offsetY) / zoom,
|
||||
};
|
||||
},
|
||||
|
||||
canvasToScreen: (canvasX, canvasY) => {
|
||||
const { zoom, offsetX, offsetY } = get();
|
||||
return {
|
||||
x: canvasX * zoom + offsetX,
|
||||
y: canvasY * zoom + offsetY,
|
||||
};
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user