import { create } from 'zustand'; import { v4 as uuidv4 } from 'uuid'; import type { Layer, LayerUpdate, CreateLayerParams, BlendMode } from '@/types'; interface LayerStore { /** All layers in the canvas */ layers: Layer[]; /** ID of the currently active layer */ activeLayerId: string | null; /** Create a new layer */ createLayer: (params: CreateLayerParams) => Layer; /** Delete a layer by ID */ deleteLayer: (id: string) => void; /** Update layer properties */ updateLayer: (id: string, updates: LayerUpdate) => void; /** Set active layer */ setActiveLayer: (id: string) => void; /** Duplicate a layer */ duplicateLayer: (id: string) => Layer | null; /** Reorder layers */ reorderLayer: (id: string, newOrder: number) => void; /** Merge layer with layer below */ mergeDown: (id: string) => void; /** Flatten all visible layers */ flattenLayers: () => Layer | null; /** Get layer by ID */ getLayer: (id: string) => Layer | undefined; /** Get active layer */ getActiveLayer: () => Layer | undefined; /** Clear all layers */ clearLayers: () => void; } export const useLayerStore = create((set, get) => ({ layers: [], activeLayerId: null, createLayer: (params) => { const now = Date.now(); const layer: Layer = { id: uuidv4(), name: params.name || `Layer ${get().layers.length + 1}`, canvas: null, visible: true, opacity: params.opacity ?? 1, blendMode: params.blendMode || 'normal', order: get().layers.length, locked: false, width: params.width, height: params.height, x: params.x ?? 0, y: params.y ?? 0, createdAt: now, updatedAt: now, }; // Create canvas element const canvas = document.createElement('canvas'); canvas.width = layer.width; canvas.height = layer.height; // Fill with color if provided if (params.fillColor) { const ctx = canvas.getContext('2d'); if (ctx) { ctx.fillStyle = params.fillColor; ctx.fillRect(0, 0, layer.width, layer.height); } } layer.canvas = canvas; set((state) => ({ layers: [...state.layers, layer], activeLayerId: layer.id, })); return layer; }, deleteLayer: (id) => { set((state) => { const newLayers = state.layers.filter((l) => l.id !== id); const newActiveId = state.activeLayerId === id ? newLayers[newLayers.length - 1]?.id || null : state.activeLayerId; return { layers: newLayers, activeLayerId: newActiveId, }; }); }, updateLayer: (id, updates) => { set((state) => ({ layers: state.layers.map((layer) => layer.id === id ? { ...layer, ...updates, updatedAt: Date.now() } : layer ), })); }, setActiveLayer: (id) => { set({ activeLayerId: id }); }, duplicateLayer: (id) => { const layer = get().getLayer(id); if (!layer) return null; const now = Date.now(); const newLayer: Layer = { ...layer, id: uuidv4(), name: `${layer.name} copy`, order: get().layers.length, createdAt: now, updatedAt: now, }; // Clone canvas if (layer.canvas) { const canvas = document.createElement('canvas'); canvas.width = layer.width; canvas.height = layer.height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(layer.canvas, 0, 0); } newLayer.canvas = canvas; } set((state) => ({ layers: [...state.layers, newLayer], activeLayerId: newLayer.id, })); return newLayer; }, reorderLayer: (id, newOrder) => { set((state) => { const layers = [...state.layers]; const layerIndex = layers.findIndex((l) => l.id === id); if (layerIndex === -1) return state; const [layer] = layers.splice(layerIndex, 1); layers.splice(newOrder, 0, layer); // Update order values return { layers: layers.map((l, index) => ({ ...l, order: index })), }; }); }, mergeDown: (id) => { const layers = get().layers; const layerIndex = layers.findIndex((l) => l.id === id); if (layerIndex === -1 || layerIndex === 0) return; const topLayer = layers[layerIndex]; const bottomLayer = layers[layerIndex - 1]; if (!topLayer.canvas || !bottomLayer.canvas) return; // Merge onto bottom layer const ctx = bottomLayer.canvas.getContext('2d'); if (!ctx) return; ctx.globalAlpha = topLayer.opacity; ctx.globalCompositeOperation = topLayer.blendMode as GlobalCompositeOperation; ctx.drawImage(topLayer.canvas, topLayer.x, topLayer.y); ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; // Delete top layer get().deleteLayer(id); }, flattenLayers: () => { const layers = get().layers.filter((l) => l.visible); if (layers.length === 0) return null; // Get canvas dimensions const width = Math.max(...layers.map((l) => l.width)); const height = Math.max(...layers.map((l) => l.height)); // Create flattened layer const flatLayer = get().createLayer({ name: 'Flattened', width, height, }); if (!flatLayer.canvas) return null; const ctx = flatLayer.canvas.getContext('2d'); if (!ctx) return null; // Composite all visible layers layers.forEach((layer) => { if (layer.canvas) { ctx.globalAlpha = layer.opacity; ctx.globalCompositeOperation = layer.blendMode as GlobalCompositeOperation; ctx.drawImage(layer.canvas, layer.x, layer.y); } }); ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; // Delete old layers layers.forEach((layer) => { if (layer.id !== flatLayer.id) { get().deleteLayer(layer.id); } }); return flatLayer; }, getLayer: (id) => { return get().layers.find((l) => l.id === id); }, getActiveLayer: () => { const { layers, activeLayerId } = get(); return layers.find((l) => l.id === activeLayerId); }, clearLayers: () => { set({ layers: [], activeLayerId: null }); }, }));