From 012368c52bfc56e40d3f72cdc42c9325df3d196e Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 3 Aug 2025 18:20:41 +0200 Subject: [PATCH] Revert Cached Blend Area --- js/CanvasLayers.js | 155 +++++++++++------------------------ js/CanvasState.js | 3 - src/CanvasLayers.ts | 194 +++++++++++++++----------------------------- src/CanvasState.ts | 3 - src/types.ts | 2 - 5 files changed, 114 insertions(+), 243 deletions(-) diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index dac95e6..84489bc 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -313,7 +313,6 @@ export class CanvasLayers { this.canvas.canvasSelection.selectedLayers.forEach((layer) => { layer.width *= scale; layer.height *= scale; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -323,7 +322,6 @@ export class CanvasLayers { return; this.canvas.canvasSelection.selectedLayers.forEach((layer) => { layer.rotation += angle; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -373,27 +371,64 @@ export class CanvasLayers { const blendArea = layer.blendArea ?? 0; const needsBlendAreaEffect = blendArea > 0; if (needsBlendAreaEffect) { - // Check if we have a valid cached blended image - if (layer.blendedImageCache && !layer.blendedImageDirty) { - // Use cached blended image for optimal performance - ctx.globalCompositeOperation = layer.blendMode || 'normal'; - ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; - ctx.drawImage(layer.blendedImageCache, -layer.width / 2, -layer.height / 2, layer.width, layer.height); + log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); + // --- BLEND AREA MASK: Use cropped region if cropBounds is set --- + 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 { - // Cache is invalid or doesn't exist, update it - this.updateLayerBlendEffect(layer); - // Use the newly created cache if available, otherwise fallback - if (layer.blendedImageCache) { + // 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 || 'normal'; 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 { // Fallback to normal drawing this._drawLayerImage(ctx, layer); } } + else { + // Fallback to normal drawing + this._drawLayerImage(ctx, layer); + } } else { // 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) ); } - /** - * 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) { // Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references) let cacheKey = imageOrCanvas; @@ -581,7 +530,6 @@ export class CanvasLayers { return; this.canvas.canvasSelection.selectedLayers.forEach((layer) => { layer.flipH = !layer.flipH; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -591,7 +539,6 @@ export class CanvasLayers { return; this.canvas.canvasSelection.selectedLayers.forEach((layer) => { layer.flipV = !layer.flipV; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -821,16 +768,10 @@ export class CanvasLayers { if (selectedLayer) { const newValue = parseInt(blendAreaSlider.value, 10); selectedLayer.blendArea = newValue; - // Invalidate cache when blend area changes - this.invalidateBlendCache(selectedLayer); this.canvas.render(); } }; blendAreaSlider.addEventListener('change', () => { - if (selectedLayer) { - // Update the blend effect cache when the slider value is finalized - this.updateLayerBlendEffect(selectedLayer); - } this.canvas.saveState(); }); blendAreaContainer.appendChild(blendAreaLabel); diff --git a/js/CanvasState.js b/js/CanvasState.js index dd907ef..0b7745b 100644 --- a/js/CanvasState.js +++ b/js/CanvasState.js @@ -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 newLayer = { ...layer, imageId: layer.imageId || '' }; 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.imageId) { newLayer.imageId = layer.imageId; diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index af34f3b..723e80c 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -359,7 +359,6 @@ export class CanvasLayers { this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => { layer.width *= scale; layer.height *= scale; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -370,7 +369,6 @@ export class CanvasLayers { this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => { layer.rotation += angle; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -434,25 +432,80 @@ export class CanvasLayers { const needsBlendAreaEffect = blendArea > 0; if (needsBlendAreaEffect) { - // Check if we have a valid cached blended image - 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); + log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); + + // --- BLEND AREA MASK: Use cropped region if cropBounds is set --- + 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 { - // Cache is invalid or doesn't exist, update it - this.updateLayerBlendEffect(layer); + // 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); - // Use the newly created cache if available, otherwise fallback - if (layer.blendedImageCache) { + 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.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 { // Fallback to normal drawing this._drawLayerImage(ctx, layer); } + } else { + // Fallback to normal drawing + this._drawLayerImage(ctx, layer); } } else { // 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 { // Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references) let cacheKey: any = imageOrCanvas; @@ -669,7 +615,6 @@ export class CanvasLayers { if (this.canvas.canvasSelection.selectedLayers.length === 0) return; this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => { layer.flipH = !layer.flipH; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -679,7 +624,6 @@ export class CanvasLayers { if (this.canvas.canvasSelection.selectedLayers.length === 0) return; this.canvas.canvasSelection.selectedLayers.forEach((layer: Layer) => { layer.flipV = !layer.flipV; - this.invalidateBlendCache(layer); }); this.canvas.render(); this.canvas.requestSaveState(); @@ -965,17 +909,11 @@ export class CanvasLayers { if (selectedLayer) { const newValue = parseInt(blendAreaSlider.value, 10); selectedLayer.blendArea = newValue; - // Invalidate cache when blend area changes - this.invalidateBlendCache(selectedLayer); this.canvas.render(); } }; blendAreaSlider.addEventListener('change', () => { - if (selectedLayer) { - // Update the blend effect cache when the slider value is finalized - this.updateLayerBlendEffect(selectedLayer); - } this.canvas.saveState(); }); diff --git a/src/CanvasState.ts b/src/CanvasState.ts index 3b40824..0aa89e0 100644 --- a/src/CanvasState.ts +++ b/src/CanvasState.ts @@ -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 newLayer: Omit & { imageId: string } = { ...layer, imageId: layer.imageId || '' }; 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.imageId) { diff --git a/src/types.ts b/src/types.ts index b86eb6b..b74d366 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,8 +28,6 @@ export interface Layer { width: number; // szerokość 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 {