Fix button crop icon display and update functionality

This commit is contained in:
Dariusz L
2025-08-03 02:19:52 +02:00
parent 4df89a793e
commit 3d6e3901d0
6 changed files with 354 additions and 179 deletions

View File

@@ -12,6 +12,7 @@ import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js";
const log = createModuleLogger('CanvasLayers');
export class CanvasLayers {
constructor(canvas) {
this._canvasMaskCache = new Map();
this.blendMenuElement = null;
this.blendMenuWorldX = 0;
this.blendMenuWorldY = 0;
@@ -368,13 +369,32 @@ export class CanvasLayers {
const needsBlendAreaEffect = blendArea > 0;
if (needsBlendAreaEffect) {
log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`);
// Get or create distance field mask
const maskCanvas = this.getDistanceFieldMaskSync(layer.image, 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 {
// 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) {
// This logic is now unified to handle both cropped and non-cropped images correctly.
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);
@@ -384,17 +404,14 @@ export class CanvasLayers {
const layerScaleY = layer.height / layer.originalHeight;
const dWidth = s.width * layerScaleX;
const dHeight = s.height * layerScaleY;
// The destination is the top-left of the temp canvas, plus the scaled offset of the crop area.
const dX = s.x * layerScaleX;
const dY = s.y * layerScaleY;
// We draw into a temp canvas of size layer.width x layer.height.
// The destination rect must be positioned correctly within this temp canvas.
// The dX/dY here are offsets from the top-left of the transform frame.
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);
}
// Apply the distance field mask using destination-in for transparency effect
tempCtx.globalCompositeOperation = 'destination-in';
tempCtx.drawImage(maskCanvas, 0, 0, layer.width, layer.height);
// Draw the result
ctx.globalCompositeOperation = layer.blendMode || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -440,30 +457,59 @@ export class CanvasLayers {
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
);
}
getDistanceFieldMaskSync(image, blendArea) {
// Check cache first
let imageCache = this.distanceFieldCache.get(image);
if (!imageCache) {
imageCache = new Map();
this.distanceFieldCache.set(image, imageCache);
}
let maskCanvas = imageCache.get(blendArea);
if (!maskCanvas) {
getDistanceFieldMaskSync(imageOrCanvas, blendArea) {
// Use a WeakMap for images, and a Map for canvases (since canvases are not always stable references)
let cacheKey = imageOrCanvas;
if (imageOrCanvas instanceof HTMLCanvasElement) {
// For canvases, use a Map on this instance (not WeakMap)
if (!this._canvasMaskCache)
this._canvasMaskCache = new Map();
let canvasCache = this._canvasMaskCache.get(imageOrCanvas);
if (!canvasCache) {
canvasCache = new Map();
this._canvasMaskCache.set(imageOrCanvas, canvasCache);
}
if (canvasCache.has(blendArea)) {
log.info(`Using cached distance field mask for blendArea: ${blendArea}% (canvas)`);
return canvasCache.get(blendArea) || null;
}
try {
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
maskCanvas = createDistanceFieldMaskSync(image, blendArea);
log.info(`Creating distance field mask for blendArea: ${blendArea}% (canvas)`);
const maskCanvas = createDistanceFieldMaskSync(imageOrCanvas, blendArea);
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
imageCache.set(blendArea, maskCanvas);
canvasCache.set(blendArea, maskCanvas);
return maskCanvas;
}
catch (error) {
log.error('Failed to create distance field mask:', error);
log.error('Failed to create distance field mask (canvas):', error);
return null;
}
}
else {
log.info(`Using cached distance field mask for blendArea: ${blendArea}%`);
// For images, use the original WeakMap cache
let imageCache = this.distanceFieldCache.get(imageOrCanvas);
if (!imageCache) {
imageCache = new Map();
this.distanceFieldCache.set(imageOrCanvas, imageCache);
}
let maskCanvas = imageCache.get(blendArea);
if (!maskCanvas) {
try {
log.info(`Creating distance field mask for blendArea: ${blendArea}%`);
maskCanvas = createDistanceFieldMaskSync(imageOrCanvas, blendArea);
log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`);
imageCache.set(blendArea, maskCanvas);
}
catch (error) {
log.error('Failed to create distance field mask:', error);
return null;
}
}
else {
log.info(`Using cached distance field mask for blendArea: ${blendArea}%`);
}
return maskCanvas;
}
return maskCanvas;
}
_drawLayers(ctx, layers, options = {}) {
const sortedLayers = [...layers].sort((a, b) => a.zIndex - b.zIndex);