Refactor layer rendering into reusable methods

Moved layer drawing logic into CanvasLayers._drawLayer and _drawLayers methods, replacing repeated rendering code in CanvasIO and CanvasLayers. This improves maintainability and ensures consistent handling of layer properties such as blend mode, opacity, rotation, and flipping. Also, fixed layer serialization to only generate imageId when missing, and ensured new layers have flipH/flipV set when created from matted images.
This commit is contained in:
Dariusz L
2025-07-21 23:03:52 +02:00
parent 57bd1e1499
commit 20d52b632a
8 changed files with 117 additions and 262 deletions

View File

@@ -62,25 +62,9 @@ export class CanvasIO {
maskCtx.fillStyle = '#ffffff';
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
log.debug(`Canvas contexts created, starting layer rendering`);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
log.debug(`Processing ${sortedLayers.length} layers in order`);
sortedLayers.forEach((layer, index) => {
log.debug(`Processing layer ${index}: zIndex=${layer.zIndex}, size=${layer.width}x${layer.height}, pos=(${layer.x},${layer.y})`);
log.debug(`Layer ${index}: blendMode=${layer.blendMode || 'normal'}, opacity=${layer.opacity !== undefined ? layer.opacity : 1}`);
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
log.debug(`Layer ${index} rendered successfully`);
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
log.debug(`Finished rendering layers`);
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) {
@@ -232,21 +216,8 @@ export class CanvasIO {
throw new Error("Could not create temp context");
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => {
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore();
});
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) {

View File

@@ -303,6 +303,34 @@ export class CanvasLayers {
}
return null;
}
_drawLayer(ctx, layer, options = {}) {
if (!layer.image)
return;
const { offsetX = 0, offsetY = 0 } = options;
ctx.save();
ctx.globalCompositeOperation = layer.blendMode || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2 - offsetX;
const centerY = layer.y + layer.height / 2 - offsetY;
ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
ctx.scale(scaleH, scaleV);
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
ctx.restore();
}
_drawLayers(ctx, layers, options = {}) {
const sortedLayers = [...layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
}
drawLayersToContext(ctx, layers, options = {}) {
this._drawLayers(ctx, layers, options);
}
async mirrorHorizontal() {
if (this.canvas.canvasSelection.selectedLayers.length === 0)
return;
@@ -329,12 +357,14 @@ export class CanvasLayers {
throw new Error("Could not create canvas context");
tempCanvas.width = layer.width;
tempCanvas.height = layer.height;
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempCtx.save();
tempCtx.translate(layer.width / 2, layer.height / 2);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
// We need to draw the layer relative to the new canvas, so we "move" it to 0,0
// by creating a temporary layer object for drawing.
const layerToDraw = {
...layer,
x: 0,
y: 0,
};
this._drawLayer(tempCtx, layerToDraw);
const dataUrl = tempCanvas.toDataURL('image/png');
if (!dataUrl.startsWith('data:image/png;base64,')) {
throw new Error("Invalid image data format");
@@ -568,20 +598,7 @@ export class CanvasLayers {
reject(new Error("Could not create canvas context"));
return;
}
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
this._drawLayers(tempCtx, this.canvas.layers);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
const toolMaskCanvas = this.canvas.maskTool.getMask();
@@ -643,20 +660,7 @@ export class CanvasLayers {
reject(new Error("Could not create canvas context"));
return;
}
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
this._drawLayers(tempCtx, this.canvas.layers);
tempCanvas.toBlob((blob) => {
if (blob) {
resolve(blob);
@@ -714,25 +718,7 @@ export class CanvasLayers {
return;
}
tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
sortedSelection.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
tempCtx.scale(scaleH, scaleV);
}
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
tempCanvas.toBlob((blob) => {
resolve(blob);
}, 'image/png');
@@ -784,25 +770,7 @@ export class CanvasLayers {
if (!tempCtx)
throw new Error("Could not create canvas context");
tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
sortedSelection.forEach((layer) => {
if (!layer.image)
return;
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
tempCtx.translate(centerX, centerY);
tempCtx.rotate(layer.rotation * Math.PI / 180);
const scaleH = layer.flipH ? -1 : 1;
const scaleV = layer.flipV ? -1 : 1;
if (layer.flipH || layer.flipV) {
tempCtx.scale(scaleH, scaleV);
}
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore();
});
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
const fusedImage = new Image();
fusedImage.src = tempCanvas.toDataURL();
await new Promise((resolve, reject) => {
@@ -842,7 +810,7 @@ export class CanvasLayers {
this.canvas.canvasLayersPanel.onLayersChanged();
}
log.info("Layers fused successfully", {
originalLayerCount: sortedSelection.length,
originalLayerCount: this.canvas.canvasSelection.selectedLayers.length,
fusedDimensions: { width: fusedWidth, height: fusedHeight },
fusedPosition: { x: minX, y: minY }
});

View File

@@ -259,13 +259,15 @@ export class CanvasState {
const newLayer = { ...layer, imageId: layer.imageId || '' };
delete newLayer.image;
if (layer.image instanceof HTMLImageElement) {
log.debug(`Layer ${index}: Using imageId instead of serializing image.`);
if (!layer.imageId) {
if (layer.imageId) {
newLayer.imageId = layer.imageId;
}
else {
log.debug(`Layer ${index}: No imageId found, generating new one and saving image.`);
newLayer.imageId = generateUUID();
const imageBitmap = await createImageBitmap(layer.image);
await saveImage(newLayer.imageId, imageBitmap);
}
newLayer.imageId = layer.imageId;
}
else if (!layer.imageId) {
log.error(`Layer ${index}: No image or imageId found, skipping layer.`);

View File

@@ -330,7 +330,7 @@ async function createCanvasWidget(node, widget, app) {
const mattedImage = new Image();
mattedImage.src = result.matted_image;
await mattedImage.decode();
const newLayer = { ...selectedLayer, image: mattedImage };
const newLayer = { ...selectedLayer, image: mattedImage, flipH: false, flipV: false };
delete newLayer.imageId;
canvas.layers[selectedLayerIndex] = newLayer;
canvas.canvasSelection.updateSelection([newLayer]);