mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 14:25:44 -03:00
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:
@@ -62,25 +62,9 @@ export class CanvasIO {
|
|||||||
maskCtx.fillStyle = '#ffffff';
|
maskCtx.fillStyle = '#ffffff';
|
||||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
log.debug(`Canvas contexts created, starting layer rendering`);
|
log.debug(`Canvas contexts created, starting layer rendering`);
|
||||||
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
log.debug(`Processing ${sortedLayers.length} layers in order`);
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
sortedLayers.forEach((layer, index) => {
|
log.debug(`Finished rendering layers`);
|
||||||
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();
|
|
||||||
});
|
|
||||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const maskData = maskCtx.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) {
|
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");
|
throw new Error("Could not create temp context");
|
||||||
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
|
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
|
||||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
sortedLayers.forEach((layer) => {
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
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();
|
|
||||||
});
|
|
||||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const maskData = maskCtx.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) {
|
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||||
|
|||||||
@@ -303,6 +303,34 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
return null;
|
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() {
|
async mirrorHorizontal() {
|
||||||
if (this.canvas.canvasSelection.selectedLayers.length === 0)
|
if (this.canvas.canvasSelection.selectedLayers.length === 0)
|
||||||
return;
|
return;
|
||||||
@@ -329,12 +357,14 @@ export class CanvasLayers {
|
|||||||
throw new Error("Could not create canvas context");
|
throw new Error("Could not create canvas context");
|
||||||
tempCanvas.width = layer.width;
|
tempCanvas.width = layer.width;
|
||||||
tempCanvas.height = layer.height;
|
tempCanvas.height = layer.height;
|
||||||
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
// We need to draw the layer relative to the new canvas, so we "move" it to 0,0
|
||||||
tempCtx.save();
|
// by creating a temporary layer object for drawing.
|
||||||
tempCtx.translate(layer.width / 2, layer.height / 2);
|
const layerToDraw = {
|
||||||
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
...layer,
|
||||||
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
x: 0,
|
||||||
tempCtx.restore();
|
y: 0,
|
||||||
|
};
|
||||||
|
this._drawLayer(tempCtx, layerToDraw);
|
||||||
const dataUrl = tempCanvas.toDataURL('image/png');
|
const dataUrl = tempCanvas.toDataURL('image/png');
|
||||||
if (!dataUrl.startsWith('data:image/png;base64,')) {
|
if (!dataUrl.startsWith('data:image/png;base64,')) {
|
||||||
throw new Error("Invalid image data format");
|
throw new Error("Invalid image data format");
|
||||||
@@ -568,20 +598,7 @@ export class CanvasLayers {
|
|||||||
reject(new Error("Could not create canvas context"));
|
reject(new Error("Could not create canvas context"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.layers);
|
||||||
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();
|
|
||||||
});
|
|
||||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
const toolMaskCanvas = this.canvas.maskTool.getMask();
|
||||||
@@ -643,20 +660,7 @@ export class CanvasLayers {
|
|||||||
reject(new Error("Could not create canvas context"));
|
reject(new Error("Could not create canvas context"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.layers);
|
||||||
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();
|
|
||||||
});
|
|
||||||
tempCanvas.toBlob((blob) => {
|
tempCanvas.toBlob((blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
@@ -714,25 +718,7 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
|
||||||
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();
|
|
||||||
});
|
|
||||||
tempCanvas.toBlob((blob) => {
|
tempCanvas.toBlob((blob) => {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
@@ -784,25 +770,7 @@ export class CanvasLayers {
|
|||||||
if (!tempCtx)
|
if (!tempCtx)
|
||||||
throw new Error("Could not create canvas context");
|
throw new Error("Could not create canvas context");
|
||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
|
||||||
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();
|
|
||||||
});
|
|
||||||
const fusedImage = new Image();
|
const fusedImage = new Image();
|
||||||
fusedImage.src = tempCanvas.toDataURL();
|
fusedImage.src = tempCanvas.toDataURL();
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -842,7 +810,7 @@ export class CanvasLayers {
|
|||||||
this.canvas.canvasLayersPanel.onLayersChanged();
|
this.canvas.canvasLayersPanel.onLayersChanged();
|
||||||
}
|
}
|
||||||
log.info("Layers fused successfully", {
|
log.info("Layers fused successfully", {
|
||||||
originalLayerCount: sortedSelection.length,
|
originalLayerCount: this.canvas.canvasSelection.selectedLayers.length,
|
||||||
fusedDimensions: { width: fusedWidth, height: fusedHeight },
|
fusedDimensions: { width: fusedWidth, height: fusedHeight },
|
||||||
fusedPosition: { x: minX, y: minY }
|
fusedPosition: { x: minX, y: minY }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -259,13 +259,15 @@ export class CanvasState {
|
|||||||
const newLayer = { ...layer, imageId: layer.imageId || '' };
|
const newLayer = { ...layer, imageId: layer.imageId || '' };
|
||||||
delete newLayer.image;
|
delete newLayer.image;
|
||||||
if (layer.image instanceof HTMLImageElement) {
|
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();
|
newLayer.imageId = generateUUID();
|
||||||
const imageBitmap = await createImageBitmap(layer.image);
|
const imageBitmap = await createImageBitmap(layer.image);
|
||||||
await saveImage(newLayer.imageId, imageBitmap);
|
await saveImage(newLayer.imageId, imageBitmap);
|
||||||
}
|
}
|
||||||
newLayer.imageId = layer.imageId;
|
|
||||||
}
|
}
|
||||||
else if (!layer.imageId) {
|
else if (!layer.imageId) {
|
||||||
log.error(`Layer ${index}: No image or imageId found, skipping layer.`);
|
log.error(`Layer ${index}: No image or imageId found, skipping layer.`);
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
const mattedImage = new Image();
|
const mattedImage = new Image();
|
||||||
mattedImage.src = result.matted_image;
|
mattedImage.src = result.matted_image;
|
||||||
await mattedImage.decode();
|
await mattedImage.decode();
|
||||||
const newLayer = { ...selectedLayer, image: mattedImage };
|
const newLayer = { ...selectedLayer, image: mattedImage, flipH: false, flipV: false };
|
||||||
delete newLayer.imageId;
|
delete newLayer.imageId;
|
||||||
canvas.layers[selectedLayerIndex] = newLayer;
|
canvas.layers[selectedLayerIndex] = newLayer;
|
||||||
canvas.canvasSelection.updateSelection([newLayer]);
|
canvas.canvasSelection.updateSelection([newLayer]);
|
||||||
|
|||||||
@@ -72,27 +72,11 @@ export class CanvasIO {
|
|||||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
log.debug(`Canvas contexts created, starting layer rendering`);
|
log.debug(`Canvas contexts created, starting layer rendering`);
|
||||||
const sortedLayers = this.canvas.layers.sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
|
||||||
log.debug(`Processing ${sortedLayers.length} layers in order`);
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
sortedLayers.forEach((layer: Layer, index: number) => {
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
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}`);
|
log.debug(`Finished rendering layers`);
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const maskData = maskCtx.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) {
|
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
||||||
@@ -259,23 +243,8 @@ export class CanvasIO {
|
|||||||
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
|
maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
|
||||||
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
const sortedLayers = this.canvas.layers.sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
||||||
sortedLayers.forEach((layer: Layer) => {
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|||||||
@@ -349,6 +349,46 @@ export class CanvasLayers {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _drawLayer(ctx: CanvasRenderingContext2D, layer: Layer, options: { offsetX?: number, offsetY?: number } = {}): void {
|
||||||
|
if (!layer.image) return;
|
||||||
|
|
||||||
|
const { offsetX = 0, offsetY = 0 } = options;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalCompositeOperation = layer.blendMode as any || '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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _drawLayers(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
|
||||||
|
const sortedLayers = [...layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
||||||
|
sortedLayers.forEach(layer => this._drawLayer(ctx, layer, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public drawLayersToContext(ctx: CanvasRenderingContext2D, layers: Layer[], options: { offsetX?: number, offsetY?: number } = {}): void {
|
||||||
|
this._drawLayers(ctx, layers, options);
|
||||||
|
}
|
||||||
|
|
||||||
async mirrorHorizontal(): Promise<void> {
|
async mirrorHorizontal(): Promise<void> {
|
||||||
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
if (this.canvas.canvasSelection.selectedLayers.length === 0) return;
|
||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
||||||
@@ -376,19 +416,15 @@ export class CanvasLayers {
|
|||||||
tempCanvas.width = layer.width;
|
tempCanvas.width = layer.width;
|
||||||
tempCanvas.height = layer.height;
|
tempCanvas.height = layer.height;
|
||||||
|
|
||||||
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
tempCtx.save();
|
this._drawLayer(tempCtx, layerToDraw);
|
||||||
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();
|
|
||||||
|
|
||||||
const dataUrl = tempCanvas.toDataURL('image/png');
|
const dataUrl = tempCanvas.toDataURL('image/png');
|
||||||
if (!dataUrl.startsWith('data:image/png;base64,')) {
|
if (!dataUrl.startsWith('data:image/png;base64,')) {
|
||||||
@@ -657,27 +693,7 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedLayers = [...this.canvas.layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.layers);
|
||||||
|
|
||||||
sortedLayers.forEach((layer: Layer) => {
|
|
||||||
if (!layer.image) return;
|
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
@@ -754,27 +770,7 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedLayers = [...this.canvas.layers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.layers);
|
||||||
|
|
||||||
sortedLayers.forEach((layer: Layer) => {
|
|
||||||
if (!layer.image) return;
|
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
|
|
||||||
tempCanvas.toBlob((blob) => {
|
tempCanvas.toBlob((blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
@@ -843,33 +839,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
|
|
||||||
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
|
||||||
|
|
||||||
sortedSelection.forEach((layer: Layer) => {
|
|
||||||
if (!layer.image) return;
|
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
|
|
||||||
tempCanvas.toBlob((blob) => {
|
tempCanvas.toBlob((blob) => {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
@@ -933,33 +903,7 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
tempCtx.translate(-minX, -minY);
|
tempCtx.translate(-minX, -minY);
|
||||||
|
|
||||||
const sortedSelection = [...this.canvas.canvasSelection.selectedLayers].sort((a: Layer, b: Layer) => a.zIndex - b.zIndex);
|
this._drawLayers(tempCtx, this.canvas.canvasSelection.selectedLayers);
|
||||||
|
|
||||||
sortedSelection.forEach((layer: Layer) => {
|
|
||||||
if (!layer.image) return;
|
|
||||||
|
|
||||||
tempCtx.save();
|
|
||||||
tempCtx.globalCompositeOperation = layer.blendMode as any || '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();
|
|
||||||
});
|
|
||||||
|
|
||||||
const fusedImage = new Image();
|
const fusedImage = new Image();
|
||||||
fusedImage.src = tempCanvas.toDataURL();
|
fusedImage.src = tempCanvas.toDataURL();
|
||||||
@@ -1006,7 +950,7 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info("Layers fused successfully", {
|
log.info("Layers fused successfully", {
|
||||||
originalLayerCount: sortedSelection.length,
|
originalLayerCount: this.canvas.canvasSelection.selectedLayers.length,
|
||||||
fusedDimensions: { width: fusedWidth, height: fusedHeight },
|
fusedDimensions: { width: fusedWidth, height: fusedHeight },
|
||||||
fusedPosition: { x: minX, y: minY }
|
fusedPosition: { x: minX, y: minY }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -297,13 +297,14 @@ export class CanvasState {
|
|||||||
delete (newLayer as any).image;
|
delete (newLayer as any).image;
|
||||||
|
|
||||||
if (layer.image instanceof HTMLImageElement) {
|
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();
|
newLayer.imageId = generateUUID();
|
||||||
const imageBitmap = await createImageBitmap(layer.image);
|
const imageBitmap = await createImageBitmap(layer.image);
|
||||||
await saveImage(newLayer.imageId, imageBitmap);
|
await saveImage(newLayer.imageId, imageBitmap);
|
||||||
}
|
}
|
||||||
newLayer.imageId = layer.imageId;
|
|
||||||
} else if (!layer.imageId) {
|
} else if (!layer.imageId) {
|
||||||
log.error(`Layer ${index}: No image or imageId found, skipping layer.`);
|
log.error(`Layer ${index}: No image or imageId found, skipping layer.`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
|
|||||||
const mattedImage = new Image();
|
const mattedImage = new Image();
|
||||||
mattedImage.src = result.matted_image;
|
mattedImage.src = result.matted_image;
|
||||||
await mattedImage.decode();
|
await mattedImage.decode();
|
||||||
const newLayer = {...selectedLayer, image: mattedImage} as Layer;
|
const newLayer = {...selectedLayer, image: mattedImage, flipH: false, flipV: false} as Layer;
|
||||||
delete (newLayer as any).imageId;
|
delete (newLayer as any).imageId;
|
||||||
canvas.layers[selectedLayerIndex] = newLayer;
|
canvas.layers[selectedLayerIndex] = newLayer;
|
||||||
canvas.canvasSelection.updateSelection([newLayer]);
|
canvas.canvasSelection.updateSelection([newLayer]);
|
||||||
|
|||||||
Reference in New Issue
Block a user