Revert Cached Blend Area

This commit is contained in:
Dariusz L
2025-08-03 18:20:41 +02:00
parent 82c42f99fe
commit 012368c52b
5 changed files with 114 additions and 243 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();
}); });

View File

@@ -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) {

View File

@@ -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 {