feat(phase-13): implement layer masks foundation for non-destructive editing
Add comprehensive layer mask system with core functionality for non-destructive editing. Features: - LayerMask interface with canvas, enabled, and inverted properties - Layer interface updated with mask property - Full mask management in layer store: * addMask() - Creates white mask (fully visible) * removeMask() - Removes mask from layer * toggleMask() - Enable/disable mask * invertMask() - Invert mask values * applyMask() - Apply mask to layer and remove - Duplicate layer now clones masks - Grayscale mask system (white=reveal, black=hide) Changes: - Updated types/layer.ts with LayerMask interface - Modified Layer interface to include mask property - Updated store/layer-store.ts: * Added mask property initialization to createLayer * Added 5 new mask management functions * Updated duplicateLayer to clone masks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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<LayerStore>((set, get) => ({
|
||||
@@ -51,6 +61,7 @@ export const useLayerStore = create<LayerStore>((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<LayerStore>((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<LayerStore>((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);
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user