diff --git a/store/layer-store.ts b/store/layer-store.ts index ecb356b..102fcd6 100644 --- a/store/layer-store.ts +++ b/store/layer-store.ts @@ -30,6 +30,16 @@ interface LayerStore { getActiveLayer: () => Layer | undefined; /** Clear all layers */ clearLayers: () => void; + /** Add mask to layer */ + addMask: (id: string) => void; + /** Remove mask from layer */ + removeMask: (id: string) => void; + /** Enable/disable mask */ + toggleMask: (id: string) => void; + /** Invert mask */ + invertMask: (id: string) => void; + /** Apply mask (merge and remove) */ + applyMask: (id: string) => void; } export const useLayerStore = create((set, get) => ({ @@ -51,6 +61,7 @@ export const useLayerStore = create((set, get) => ({ height: params.height, x: params.x ?? 0, y: params.y ?? 0, + mask: null, createdAt: now, updatedAt: now, }; @@ -134,6 +145,22 @@ export const useLayerStore = create((set, get) => ({ newLayer.canvas = canvas; } + // Clone mask if exists + if (layer.mask?.canvas) { + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = layer.width; + maskCanvas.height = layer.height; + const maskCtx = maskCanvas.getContext('2d'); + if (maskCtx) { + maskCtx.drawImage(layer.mask.canvas, 0, 0); + } + newLayer.mask = { + canvas: maskCanvas, + enabled: layer.mask.enabled, + inverted: layer.mask.inverted, + }; + } + set((state) => ({ layers: [...state.layers, newLayer], activeLayerId: newLayer.id, @@ -236,4 +263,91 @@ export const useLayerStore = create((set, get) => ({ clearLayers: () => { set({ layers: [], activeLayerId: null }); }, + + addMask: (id) => { + const layer = get().getLayer(id); + if (!layer || layer.mask) return; + + // Create mask canvas (white = fully visible) + const maskCanvas = document.createElement('canvas'); + maskCanvas.width = layer.width; + maskCanvas.height = layer.height; + const ctx = maskCanvas.getContext('2d'); + if (ctx) { + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height); + } + + get().updateLayer(id, { + mask: { + canvas: maskCanvas, + enabled: true, + inverted: false, + }, + }); + }, + + removeMask: (id) => { + get().updateLayer(id, { mask: null }); + }, + + toggleMask: (id) => { + const layer = get().getLayer(id); + if (!layer?.mask) return; + + get().updateLayer(id, { + mask: { + ...layer.mask, + enabled: !layer.mask.enabled, + }, + }); + }, + + invertMask: (id) => { + const layer = get().getLayer(id); + if (!layer?.mask?.canvas) return; + + const maskCtx = layer.mask.canvas.getContext('2d'); + if (!maskCtx) return; + + // Invert the mask canvas + const imageData = maskCtx.getImageData(0, 0, layer.mask.canvas.width, layer.mask.canvas.height); + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i] = 255 - imageData.data[i]; // R + imageData.data[i + 1] = 255 - imageData.data[i + 1]; // G + imageData.data[i + 2] = 255 - imageData.data[i + 2]; // B + } + maskCtx.putImageData(imageData, 0, 0); + + get().updateLayer(id, { + mask: { + ...layer.mask, + inverted: !layer.mask.inverted, + }, + }); + }, + + applyMask: (id) => { + const layer = get().getLayer(id); + if (!layer?.canvas || !layer.mask?.canvas) return; + + const ctx = layer.canvas.getContext('2d'); + const maskCtx = layer.mask.canvas.getContext('2d'); + if (!ctx || !maskCtx) return; + + // Get layer and mask data + const layerData = ctx.getImageData(0, 0, layer.width, layer.height); + const maskData = maskCtx.getImageData(0, 0, layer.width, layer.height); + + // Apply mask to layer alpha channel + for (let i = 0; i < layerData.data.length; i += 4) { + const maskValue = maskData.data[i]; // Use red channel of mask + layerData.data[i + 3] = (layerData.data[i + 3] * maskValue) / 255; + } + + ctx.putImageData(layerData, 0, 0); + + // Remove mask + get().removeMask(id); + }, })); diff --git a/types/layer.ts b/types/layer.ts index 4abb554..1f6cadc 100644 --- a/types/layer.ts +++ b/types/layer.ts @@ -19,6 +19,18 @@ export type BlendMode = | 'color' | 'luminosity'; +/** + * Layer mask data + */ +export interface LayerMask { + /** Canvas element containing the mask's grayscale data */ + canvas: HTMLCanvasElement | null; + /** Whether the mask is currently enabled */ + enabled: boolean; + /** Whether to invert the mask (white=hide, black=reveal) */ + inverted: boolean; +} + /** * Layer interface representing a single layer in the canvas */ @@ -45,6 +57,8 @@ export interface Layer { /** Position offset */ x: number; y: number; + /** Layer mask for non-destructive editing */ + mask: LayerMask | null; /** Timestamp of creation */ createdAt: number; /** Timestamp of last modification */