mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Revert Cached Blend Area
This commit is contained in:
@@ -313,7 +313,6 @@ export class CanvasLayers {
|
|||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
||||||
layer.width *= scale;
|
layer.width *= scale;
|
||||||
layer.height *= scale;
|
layer.height *= scale;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -323,7 +322,6 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
||||||
layer.rotation += angle;
|
layer.rotation += angle;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -373,27 +371,64 @@ export class CanvasLayers {
|
|||||||
const blendArea = layer.blendArea ?? 0;
|
const blendArea = layer.blendArea ?? 0;
|
||||||
const needsBlendAreaEffect = blendArea > 0;
|
const needsBlendAreaEffect = blendArea > 0;
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
// Check if we have a valid cached blended image
|
log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`);
|
||||||
if (layer.blendedImageCache && !layer.blendedImageDirty) {
|
// --- BLEND AREA MASK: Use cropped region if cropBounds is set ---
|
||||||
// Use cached blended image for optimal performance
|
let maskCanvas = null;
|
||||||
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
let maskWidth = layer.width;
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
let maskHeight = layer.height;
|
||||||
ctx.drawImage(layer.blendedImageCache, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||||
|
// Create a cropped canvas
|
||||||
|
const s = layer.cropBounds;
|
||||||
|
const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height);
|
||||||
|
if (cropCtx) {
|
||||||
|
cropCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, 0, 0, s.width, s.height);
|
||||||
|
// Generate distance field mask for the cropped region
|
||||||
|
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
||||||
|
maskWidth = s.width;
|
||||||
|
maskHeight = s.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Cache is invalid or doesn't exist, update it
|
// No crop, use full image
|
||||||
this.updateLayerBlendEffect(layer);
|
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
// Use the newly created cache if available, otherwise fallback
|
maskWidth = layer.originalWidth || layer.width;
|
||||||
if (layer.blendedImageCache) {
|
maskHeight = layer.originalHeight || layer.height;
|
||||||
|
}
|
||||||
|
if (maskCanvas) {
|
||||||
|
// Create a temporary canvas for the masked layer
|
||||||
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
||||||
|
if (tempCtx) {
|
||||||
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
|
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const layerScaleX = layer.width / layer.originalWidth;
|
||||||
|
const layerScaleY = layer.height / layer.originalHeight;
|
||||||
|
const dWidth = s.width * layerScaleX;
|
||||||
|
const dHeight = s.height * layerScaleY;
|
||||||
|
const dX = s.x * layerScaleX;
|
||||||
|
const dY = s.y * layerScaleY;
|
||||||
|
tempCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight);
|
||||||
|
// --- Apply the distance field mask only to the visible (cropped) area ---
|
||||||
|
tempCtx.globalCompositeOperation = 'destination-in';
|
||||||
|
// Scale the mask to match the drawn area
|
||||||
|
tempCtx.drawImage(maskCanvas, 0, 0, maskWidth, maskHeight, dX, dY, dWidth, dHeight);
|
||||||
|
}
|
||||||
|
// Draw the result
|
||||||
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
ctx.drawImage(layer.blendedImageCache, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
ctx.drawImage(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Fallback to normal drawing
|
// Fallback to normal drawing
|
||||||
this._drawLayerImage(ctx, layer);
|
this._drawLayerImage(ctx, layer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Fallback to normal drawing
|
||||||
|
this._drawLayerImage(ctx, layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Normal drawing without blend area effect
|
// Normal drawing without blend area effect
|
||||||
@@ -425,92 +460,6 @@ export class CanvasLayers {
|
|||||||
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
|
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Invalidates the blended image cache for a layer
|
|
||||||
*/
|
|
||||||
invalidateBlendCache(layer) {
|
|
||||||
layer.blendedImageDirty = true;
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Updates the blended image cache for a layer with blendArea effect
|
|
||||||
*/
|
|
||||||
updateLayerBlendEffect(layer) {
|
|
||||||
const blendArea = layer.blendArea ?? 0;
|
|
||||||
if (blendArea <= 0) {
|
|
||||||
// No blend effect needed, clear cache
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
log.debug(`Updating blend effect cache for layer ${layer.id}, blendArea: ${blendArea}%`);
|
|
||||||
// Create the blended image using the same logic as _drawLayer
|
|
||||||
let maskCanvas = null;
|
|
||||||
let maskWidth = layer.width;
|
|
||||||
let maskHeight = layer.height;
|
|
||||||
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
|
||||||
// Create a cropped canvas
|
|
||||||
const s = layer.cropBounds;
|
|
||||||
const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height);
|
|
||||||
if (cropCtx) {
|
|
||||||
cropCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, 0, 0, s.width, s.height);
|
|
||||||
// Generate distance field mask for the cropped region
|
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
|
||||||
maskWidth = s.width;
|
|
||||||
maskHeight = s.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// No crop, use full image
|
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
|
||||||
maskWidth = layer.originalWidth || layer.width;
|
|
||||||
maskHeight = layer.originalHeight || layer.height;
|
|
||||||
}
|
|
||||||
if (maskCanvas) {
|
|
||||||
// Create the final blended canvas
|
|
||||||
const { canvas: blendedCanvas, ctx: blendedCtx } = createCanvas(layer.width, layer.height);
|
|
||||||
if (blendedCtx) {
|
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
blendedCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const layerScaleX = layer.width / layer.originalWidth;
|
|
||||||
const layerScaleY = layer.height / layer.originalHeight;
|
|
||||||
const dWidth = s.width * layerScaleX;
|
|
||||||
const dHeight = s.height * layerScaleY;
|
|
||||||
const dX = s.x * layerScaleX;
|
|
||||||
const dY = s.y * layerScaleY;
|
|
||||||
blendedCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight);
|
|
||||||
// Apply the distance field mask only to the visible (cropped) area
|
|
||||||
blendedCtx.globalCompositeOperation = 'destination-in';
|
|
||||||
// Scale the mask to match the drawn area
|
|
||||||
blendedCtx.drawImage(maskCanvas, 0, 0, maskWidth, maskHeight, dX, dY, dWidth, dHeight);
|
|
||||||
}
|
|
||||||
// Store the blended result in cache
|
|
||||||
layer.blendedImageCache = blendedCanvas;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
log.debug(`Blend effect cache updated for layer ${layer.id}`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.warn(`Failed to create blended canvas context for layer ${layer.id}`);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.warn(`Failed to create distance field mask for layer ${layer.id}`);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
log.error(`Error updating blend effect for layer ${layer.id}:`, error);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getDistanceFieldMaskSync(imageOrCanvas, blendArea) {
|
getDistanceFieldMaskSync(imageOrCanvas, blendArea) {
|
||||||
// Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references)
|
// Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references)
|
||||||
let cacheKey = imageOrCanvas;
|
let cacheKey = imageOrCanvas;
|
||||||
@@ -581,7 +530,6 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
||||||
layer.flipH = !layer.flipH;
|
layer.flipH = !layer.flipH;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -591,7 +539,6 @@ export class CanvasLayers {
|
|||||||
return;
|
return;
|
||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer) => {
|
||||||
layer.flipV = !layer.flipV;
|
layer.flipV = !layer.flipV;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -821,16 +768,10 @@ export class CanvasLayers {
|
|||||||
if (selectedLayer) {
|
if (selectedLayer) {
|
||||||
const newValue = parseInt(blendAreaSlider.value, 10);
|
const newValue = parseInt(blendAreaSlider.value, 10);
|
||||||
selectedLayer.blendArea = newValue;
|
selectedLayer.blendArea = newValue;
|
||||||
// Invalidate cache when blend area changes
|
|
||||||
this.invalidateBlendCache(selectedLayer);
|
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
blendAreaSlider.addEventListener('change', () => {
|
blendAreaSlider.addEventListener('change', () => {
|
||||||
if (selectedLayer) {
|
|
||||||
// Update the blend effect cache when the slider value is finalized
|
|
||||||
this.updateLayerBlendEffect(selectedLayer);
|
|
||||||
}
|
|
||||||
this.canvas.saveState();
|
this.canvas.saveState();
|
||||||
});
|
});
|
||||||
blendAreaContainer.appendChild(blendAreaLabel);
|
blendAreaContainer.appendChild(blendAreaLabel);
|
||||||
|
|||||||
@@ -286,9 +286,6 @@ If you see dark images or masks in the output, make sure node_id is set to ${cor
|
|||||||
const preparedLayers = await Promise.all(this.canvas.layers.map(async (layer, index) => {
|
const preparedLayers = await Promise.all(this.canvas.layers.map(async (layer, index) => {
|
||||||
const newLayer = { ...layer, imageId: layer.imageId || '' };
|
const newLayer = { ...layer, imageId: layer.imageId || '' };
|
||||||
delete newLayer.image;
|
delete newLayer.image;
|
||||||
// Remove cache properties that cannot be serialized for the worker
|
|
||||||
delete newLayer.blendedImageCache;
|
|
||||||
delete newLayer.blendedImageDirty;
|
|
||||||
if (layer.image instanceof HTMLImageElement) {
|
if (layer.image instanceof HTMLImageElement) {
|
||||||
if (layer.imageId) {
|
if (layer.imageId) {
|
||||||
newLayer.imageId = layer.imageId;
|
newLayer.imageId = layer.imageId;
|
||||||
|
|||||||
@@ -359,7 +359,6 @@ export class CanvasLayers {
|
|||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
||||||
layer.width *= scale;
|
layer.width *= scale;
|
||||||
layer.height *= scale;
|
layer.height *= scale;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -370,7 +369,6 @@ export class CanvasLayers {
|
|||||||
|
|
||||||
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => {
|
||||||
layer.rotation += angle;
|
layer.rotation += angle;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -434,25 +432,80 @@ export class CanvasLayers {
|
|||||||
const needsBlendAreaEffect = blendArea > 0;
|
const needsBlendAreaEffect = blendArea > 0;
|
||||||
|
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
// Check if we have a valid cached blended image
|
log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`);
|
||||||
if (layer.blendedImageCache && !layer.blendedImageDirty) {
|
|
||||||
// Use cached blended image for optimal performance
|
|
||||||
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
|
||||||
ctx.drawImage(layer.blendedImageCache, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
|
||||||
} else {
|
|
||||||
// Cache is invalid or doesn't exist, update it
|
|
||||||
this.updateLayerBlendEffect(layer);
|
|
||||||
|
|
||||||
// Use the newly created cache if available, otherwise fallback
|
// --- BLEND AREA MASK: Use cropped region if cropBounds is set ---
|
||||||
if (layer.blendedImageCache) {
|
let maskCanvas: HTMLCanvasElement | null = null;
|
||||||
|
let maskWidth = layer.width;
|
||||||
|
let maskHeight = layer.height;
|
||||||
|
|
||||||
|
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||||
|
// Create a cropped canvas
|
||||||
|
const s = layer.cropBounds;
|
||||||
|
const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height);
|
||||||
|
if (cropCtx) {
|
||||||
|
cropCtx.drawImage(
|
||||||
|
layer.image,
|
||||||
|
s.x, s.y, s.width, s.height,
|
||||||
|
0, 0, s.width, s.height
|
||||||
|
);
|
||||||
|
// Generate distance field mask for the cropped region
|
||||||
|
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
||||||
|
maskWidth = s.width;
|
||||||
|
maskHeight = s.height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No crop, use full image
|
||||||
|
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
|
maskWidth = layer.originalWidth || layer.width;
|
||||||
|
maskHeight = layer.originalHeight || layer.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maskCanvas) {
|
||||||
|
// Create a temporary canvas for the masked layer
|
||||||
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
||||||
|
|
||||||
|
if (tempCtx) {
|
||||||
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
|
|
||||||
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
|
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
||||||
|
} else {
|
||||||
|
const layerScaleX = layer.width / layer.originalWidth;
|
||||||
|
const layerScaleY = layer.height / layer.originalHeight;
|
||||||
|
|
||||||
|
const dWidth = s.width * layerScaleX;
|
||||||
|
const dHeight = s.height * layerScaleY;
|
||||||
|
const dX = s.x * layerScaleX;
|
||||||
|
const dY = s.y * layerScaleY;
|
||||||
|
|
||||||
|
tempCtx.drawImage(
|
||||||
|
layer.image,
|
||||||
|
s.x, s.y, s.width, s.height,
|
||||||
|
dX, dY, dWidth, dHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Apply the distance field mask only to the visible (cropped) area ---
|
||||||
|
tempCtx.globalCompositeOperation = 'destination-in';
|
||||||
|
// Scale the mask to match the drawn area
|
||||||
|
tempCtx.drawImage(
|
||||||
|
maskCanvas,
|
||||||
|
0, 0, maskWidth, maskHeight,
|
||||||
|
dX, dY, dWidth, dHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the result
|
||||||
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
ctx.drawImage(layer.blendedImageCache, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
ctx.drawImage(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to normal drawing
|
// Fallback to normal drawing
|
||||||
this._drawLayerImage(ctx, layer);
|
this._drawLayerImage(ctx, layer);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to normal drawing
|
||||||
|
this._drawLayerImage(ctx, layer);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal drawing without blend area effect
|
// Normal drawing without blend area effect
|
||||||
@@ -495,113 +548,6 @@ export class CanvasLayers {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates the blended image cache for a layer
|
|
||||||
*/
|
|
||||||
public invalidateBlendCache(layer: Layer): void {
|
|
||||||
layer.blendedImageDirty = true;
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the blended image cache for a layer with blendArea effect
|
|
||||||
*/
|
|
||||||
public updateLayerBlendEffect(layer: Layer): void {
|
|
||||||
const blendArea = layer.blendArea ?? 0;
|
|
||||||
|
|
||||||
if (blendArea <= 0) {
|
|
||||||
// No blend effect needed, clear cache
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
log.debug(`Updating blend effect cache for layer ${layer.id}, blendArea: ${blendArea}%`);
|
|
||||||
|
|
||||||
// Create the blended image using the same logic as _drawLayer
|
|
||||||
let maskCanvas: HTMLCanvasElement | null = null;
|
|
||||||
let maskWidth = layer.width;
|
|
||||||
let maskHeight = layer.height;
|
|
||||||
|
|
||||||
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
|
||||||
// Create a cropped canvas
|
|
||||||
const s = layer.cropBounds;
|
|
||||||
const { canvas: cropCanvas, ctx: cropCtx } = createCanvas(s.width, s.height);
|
|
||||||
if (cropCtx) {
|
|
||||||
cropCtx.drawImage(
|
|
||||||
layer.image,
|
|
||||||
s.x, s.y, s.width, s.height,
|
|
||||||
0, 0, s.width, s.height
|
|
||||||
);
|
|
||||||
// Generate distance field mask for the cropped region
|
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
|
||||||
maskWidth = s.width;
|
|
||||||
maskHeight = s.height;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No crop, use full image
|
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
|
||||||
maskWidth = layer.originalWidth || layer.width;
|
|
||||||
maskHeight = layer.originalHeight || layer.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maskCanvas) {
|
|
||||||
// Create the final blended canvas
|
|
||||||
const { canvas: blendedCanvas, ctx: blendedCtx } = createCanvas(layer.width, layer.height);
|
|
||||||
|
|
||||||
if (blendedCtx) {
|
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
blendedCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
|
||||||
} else {
|
|
||||||
const layerScaleX = layer.width / layer.originalWidth;
|
|
||||||
const layerScaleY = layer.height / layer.originalHeight;
|
|
||||||
|
|
||||||
const dWidth = s.width * layerScaleX;
|
|
||||||
const dHeight = s.height * layerScaleY;
|
|
||||||
const dX = s.x * layerScaleX;
|
|
||||||
const dY = s.y * layerScaleY;
|
|
||||||
|
|
||||||
blendedCtx.drawImage(
|
|
||||||
layer.image,
|
|
||||||
s.x, s.y, s.width, s.height,
|
|
||||||
dX, dY, dWidth, dHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
// Apply the distance field mask only to the visible (cropped) area
|
|
||||||
blendedCtx.globalCompositeOperation = 'destination-in';
|
|
||||||
// Scale the mask to match the drawn area
|
|
||||||
blendedCtx.drawImage(
|
|
||||||
maskCanvas,
|
|
||||||
0, 0, maskWidth, maskHeight,
|
|
||||||
dX, dY, dWidth, dHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the blended result in cache
|
|
||||||
layer.blendedImageCache = blendedCanvas;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
|
|
||||||
log.debug(`Blend effect cache updated for layer ${layer.id}`);
|
|
||||||
} else {
|
|
||||||
log.warn(`Failed to create blended canvas context for layer ${layer.id}`);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn(`Failed to create distance field mask for layer ${layer.id}`);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(`Error updating blend effect for layer ${layer.id}:`, error);
|
|
||||||
layer.blendedImageCache = undefined;
|
|
||||||
layer.blendedImageDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDistanceFieldMaskSync(imageOrCanvas: HTMLImageElement | HTMLCanvasElement, blendArea: number): HTMLCanvasElement | null {
|
private getDistanceFieldMaskSync(imageOrCanvas: HTMLImageElement | HTMLCanvasElement, blendArea: number): HTMLCanvasElement | null {
|
||||||
// Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references)
|
// Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references)
|
||||||
let cacheKey: any = imageOrCanvas;
|
let cacheKey: any = imageOrCanvas;
|
||||||
@@ -669,7 +615,6 @@ export class CanvasLayers {
|
|||||||
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) => {
|
||||||
layer.flipH = !layer.flipH;
|
layer.flipH = !layer.flipH;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -679,7 +624,6 @@ export class CanvasLayers {
|
|||||||
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) => {
|
||||||
layer.flipV = !layer.flipV;
|
layer.flipV = !layer.flipV;
|
||||||
this.invalidateBlendCache(layer);
|
|
||||||
});
|
});
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.requestSaveState();
|
this.canvas.requestSaveState();
|
||||||
@@ -965,17 +909,11 @@ export class CanvasLayers {
|
|||||||
if (selectedLayer) {
|
if (selectedLayer) {
|
||||||
const newValue = parseInt(blendAreaSlider.value, 10);
|
const newValue = parseInt(blendAreaSlider.value, 10);
|
||||||
selectedLayer.blendArea = newValue;
|
selectedLayer.blendArea = newValue;
|
||||||
// Invalidate cache when blend area changes
|
|
||||||
this.invalidateBlendCache(selectedLayer);
|
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
blendAreaSlider.addEventListener('change', () => {
|
blendAreaSlider.addEventListener('change', () => {
|
||||||
if (selectedLayer) {
|
|
||||||
// Update the blend effect cache when the slider value is finalized
|
|
||||||
this.updateLayerBlendEffect(selectedLayer);
|
|
||||||
}
|
|
||||||
this.canvas.saveState();
|
this.canvas.saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -326,9 +326,6 @@ If you see dark images or masks in the output, make sure node_id is set to ${cor
|
|||||||
const preparedLayers = await Promise.all(this.canvas.layers.map(async (layer: Layer, index: number) => {
|
const preparedLayers = await Promise.all(this.canvas.layers.map(async (layer: Layer, index: number) => {
|
||||||
const newLayer: Omit<Layer, 'image'> & { imageId: string } = { ...layer, imageId: layer.imageId || '' };
|
const newLayer: Omit<Layer, 'image'> & { imageId: string } = { ...layer, imageId: layer.imageId || '' };
|
||||||
delete (newLayer as any).image;
|
delete (newLayer as any).image;
|
||||||
// Remove cache properties that cannot be serialized for the worker
|
|
||||||
delete (newLayer as any).blendedImageCache;
|
|
||||||
delete (newLayer as any).blendedImageDirty;
|
|
||||||
|
|
||||||
if (layer.image instanceof HTMLImageElement) {
|
if (layer.image instanceof HTMLImageElement) {
|
||||||
if (layer.imageId) {
|
if (layer.imageId) {
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export interface Layer {
|
|||||||
width: number; // szerokość widocznego obszaru
|
width: number; // szerokość widocznego obszaru
|
||||||
height: number; // wysokość widocznego obszaru
|
height: number; // wysokość widocznego obszaru
|
||||||
};
|
};
|
||||||
blendedImageCache?: HTMLCanvasElement; // Cache for the pre-rendered blendArea effect
|
|
||||||
blendedImageDirty?: boolean; // Flag to invalidate the cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComfyNode {
|
export interface ComfyNode {
|
||||||
|
|||||||
Reference in New Issue
Block a user