mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Refactor CanvasLayers.ts: unify & deduplicate logic
Refactored CanvasLayers.ts to eliminate code duplication by unifying five main areas into reusable functions, following the DRY principle. Improved code readability, maintainability, and flexibility with better naming, documentation, and parameterization.
This commit is contained in:
@@ -484,14 +484,19 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
_drawLayerImage(ctx, layer) {
|
/**
|
||||||
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
* Zunifikowana funkcja do rysowania obrazu warstwy z crop
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
* @param ctx Canvas context
|
||||||
|
* @param layer Warstwa do narysowania
|
||||||
|
* @param offsetX Przesunięcie X względem środka warstwy (domyślnie -width/2)
|
||||||
|
* @param offsetY Przesunięcie Y względem środka warstwy (domyślnie -height/2)
|
||||||
|
*/
|
||||||
|
drawLayerImageWithCrop(ctx, layer, offsetX = -layer.width / 2, offsetY = -layer.height / 2) {
|
||||||
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
// Fallback for older layers without original dimensions or if data is missing
|
// Fallback for older layers without original dimensions or if data is missing
|
||||||
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
ctx.drawImage(layer.image, offsetX, offsetY, layer.width, layer.height);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Calculate the on-screen scale of the layer's transform frame
|
// Calculate the on-screen scale of the layer's transform frame
|
||||||
@@ -500,23 +505,25 @@ export class CanvasLayers {
|
|||||||
// Calculate the on-screen size of the cropped portion
|
// Calculate the on-screen size of the cropped portion
|
||||||
const dWidth = s.width * layerScaleX;
|
const dWidth = s.width * layerScaleX;
|
||||||
const dHeight = s.height * layerScaleY;
|
const dHeight = s.height * layerScaleY;
|
||||||
// Calculate the on-screen position of the top-left of the cropped portion.
|
// Calculate the on-screen position of the top-left of the cropped portion
|
||||||
// This is relative to the layer's center (the context's 0,0).
|
const dX = offsetX + (s.x * layerScaleX);
|
||||||
const dX = (-layer.width / 2) + (s.x * layerScaleX);
|
const dY = offsetY + (s.y * layerScaleY);
|
||||||
const dY = (-layer.height / 2) + (s.y * layerScaleY);
|
|
||||||
ctx.drawImage(layer.image, s.x, s.y, s.width, s.height, // source rect (from original image)
|
ctx.drawImage(layer.image, s.x, s.y, s.width, s.height, // source rect (from original image)
|
||||||
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
|
dX, dY, dWidth, dHeight // destination rect (scaled and positioned)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_drawLayerImage(ctx, layer) {
|
||||||
|
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||||
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
this.drawLayerImageWithCrop(ctx, layer);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Draw layer with live blend area effect during user activity (original behavior)
|
* Zunifikowana funkcja do tworzenia maski blend area dla warstwy
|
||||||
|
* @param layer Warstwa dla której tworzymy maskę
|
||||||
|
* @returns Obiekt zawierający maskę i jej wymiary lub null
|
||||||
*/
|
*/
|
||||||
_drawLayerWithLiveBlendArea(ctx, layer) {
|
createBlendAreaMask(layer) {
|
||||||
const blendArea = layer.blendArea ?? 0;
|
const blendArea = layer.blendArea ?? 0;
|
||||||
// --- 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) {
|
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||||
// Create a cropped canvas
|
// Create a cropped canvas
|
||||||
const s = layer.cropBounds;
|
const s = layer.cropBounds;
|
||||||
@@ -524,48 +531,78 @@ export class CanvasLayers {
|
|||||||
if (cropCtx) {
|
if (cropCtx) {
|
||||||
cropCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, 0, 0, s.width, s.height);
|
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
|
// Generate distance field mask for the cropped region
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
||||||
maskWidth = s.width;
|
if (maskCanvas) {
|
||||||
maskHeight = s.height;
|
return {
|
||||||
|
maskCanvas,
|
||||||
|
maskWidth: s.width,
|
||||||
|
maskHeight: s.height
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// No crop, use full image
|
// No crop, use full image
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
maskWidth = layer.originalWidth || layer.width;
|
if (maskCanvas) {
|
||||||
maskHeight = layer.originalHeight || layer.height;
|
return {
|
||||||
|
maskCanvas,
|
||||||
|
maskWidth: layer.originalWidth || layer.width,
|
||||||
|
maskHeight: layer.originalHeight || layer.height
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (maskCanvas) {
|
return null;
|
||||||
// Create a temporary canvas for the masked layer
|
}
|
||||||
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
/**
|
||||||
if (tempCtx) {
|
* Zunifikowana funkcja do rysowania warstwy z blend area na canvas
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
* @param ctx Canvas context
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
* @param layer Warstwa do narysowania
|
||||||
tempCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
* @param offsetX Przesunięcie X (domyślnie -width/2)
|
||||||
}
|
* @param offsetY Przesunięcie Y (domyślnie -height/2)
|
||||||
else {
|
*/
|
||||||
const layerScaleX = layer.width / layer.originalWidth;
|
drawLayerWithBlendArea(ctx, layer, offsetX = -layer.width / 2, offsetY = -layer.height / 2) {
|
||||||
const layerScaleY = layer.height / layer.originalHeight;
|
const maskInfo = this.createBlendAreaMask(layer);
|
||||||
const dWidth = s.width * layerScaleX;
|
if (maskInfo) {
|
||||||
const dHeight = s.height * layerScaleY;
|
const { maskCanvas, maskWidth, maskHeight } = maskInfo;
|
||||||
const dX = s.x * layerScaleX;
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
const dY = s.y * layerScaleY;
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
tempCtx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight);
|
// Fallback - just draw the image normally
|
||||||
// --- Apply the distance field mask only to the visible (cropped) area ---
|
ctx.drawImage(layer.image, offsetX, offsetY, layer.width, layer.height);
|
||||||
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(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Fallback to normal drawing
|
const layerScaleX = layer.width / layer.originalWidth;
|
||||||
this._drawLayerImage(ctx, layer);
|
const layerScaleY = layer.height / layer.originalHeight;
|
||||||
|
const dWidth = s.width * layerScaleX;
|
||||||
|
const dHeight = s.height * layerScaleY;
|
||||||
|
const dX = offsetX + (s.x * layerScaleX);
|
||||||
|
const dY = offsetY + (s.y * layerScaleY);
|
||||||
|
// Draw the image
|
||||||
|
ctx.drawImage(layer.image, s.x, s.y, s.width, s.height, dX, dY, dWidth, dHeight);
|
||||||
|
// Apply the distance field mask
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(maskCanvas, 0, 0, maskWidth, maskHeight, dX, dY, dWidth, dHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Fallback - just draw the image normally
|
||||||
|
this.drawLayerImageWithCrop(ctx, layer, offsetX, offsetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Draw layer with live blend area effect during user activity (original behavior)
|
||||||
|
*/
|
||||||
|
_drawLayerWithLiveBlendArea(ctx, layer) {
|
||||||
|
// Create a temporary canvas for the masked layer
|
||||||
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
||||||
|
if (tempCtx) {
|
||||||
|
// Draw the layer with blend area to temp canvas
|
||||||
|
this.drawLayerWithBlendArea(tempCtx, layer, 0, 0);
|
||||||
|
// Draw the result with blend mode and opacity
|
||||||
|
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||||
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
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);
|
||||||
@@ -696,55 +733,12 @@ export class CanvasLayers {
|
|||||||
if (!processedCtx)
|
if (!processedCtx)
|
||||||
return null;
|
return null;
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
// --- BLEND AREA MASK: Use cropped region if cropBounds is set ---
|
// Use the unified blend area drawing function
|
||||||
let maskCanvas = null;
|
this.drawLayerWithBlendArea(processedCtx, layer, 0, 0);
|
||||||
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) {
|
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
processedCtx.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;
|
|
||||||
processedCtx.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 ---
|
|
||||||
processedCtx.globalCompositeOperation = 'destination-in';
|
|
||||||
// Scale the mask to match the drawn area
|
|
||||||
processedCtx.drawImage(maskCanvas, 0, 0, maskWidth, maskHeight, dX, dY, dWidth, dHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Fallback - just draw the image normally
|
|
||||||
this._drawLayerImageToCanvas(processedCtx, layer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Just apply crop effect without blend area
|
// Just apply crop effect without blend area
|
||||||
this._drawLayerImageToCanvas(processedCtx, layer);
|
this.drawLayerImageWithCrop(processedCtx, layer, 0, 0);
|
||||||
}
|
}
|
||||||
// Convert canvas to image
|
// Convert canvas to image
|
||||||
const processedImage = new Image();
|
const processedImage = new Image();
|
||||||
@@ -752,28 +746,11 @@ export class CanvasLayers {
|
|||||||
return processedImage;
|
return processedImage;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Helper method to draw layer image to a specific canvas context
|
* Helper method to draw layer image to a specific canvas context (position 0,0)
|
||||||
|
* Uses the unified drawLayerImageWithCrop function
|
||||||
*/
|
*/
|
||||||
_drawLayerImageToCanvas(ctx, layer) {
|
_drawLayerImageToCanvas(ctx, layer) {
|
||||||
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
this.drawLayerImageWithCrop(ctx, layer, 0, 0);
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
// Fallback for older layers without original dimensions or if data is missing
|
|
||||||
ctx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Calculate the on-screen scale of the layer's transform frame
|
|
||||||
const layerScaleX = layer.width / layer.originalWidth;
|
|
||||||
const layerScaleY = layer.height / layer.originalHeight;
|
|
||||||
// Calculate the on-screen size of the cropped portion
|
|
||||||
const dWidth = s.width * layerScaleX;
|
|
||||||
const dHeight = s.height * layerScaleY;
|
|
||||||
// Calculate the on-screen position of the top-left of the cropped portion.
|
|
||||||
const dX = s.x * layerScaleX;
|
|
||||||
const dY = s.y * layerScaleY;
|
|
||||||
ctx.drawImage(layer.image, s.x, s.y, s.width, s.height, // source rect (from original image)
|
|
||||||
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the canvas)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Invalidate processed image cache for a specific layer
|
* Invalidate processed image cache for a specific layer
|
||||||
@@ -809,34 +786,79 @@ export class CanvasLayers {
|
|||||||
this.processedImageDebounceTimers.clear();
|
this.processedImageDebounceTimers.clear();
|
||||||
log.info('Cleared all processed image cache and pending timers');
|
log.info('Cleared all processed image cache and pending timers');
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Zunifikowana funkcja do obsługi transformacji końcowych
|
||||||
|
* @param layer Warstwa do przetworzenia
|
||||||
|
* @param transformType Typ transformacji (crop, scale, wheel)
|
||||||
|
* @param delay Opóźnienie w ms (domyślnie 0)
|
||||||
|
*/
|
||||||
|
handleTransformEnd(layer, transformType, delay = 0) {
|
||||||
|
if (!layer.blendArea)
|
||||||
|
return;
|
||||||
|
const layerId = layer.id;
|
||||||
|
const cacheKey = this.getProcessedImageCacheKey(layer);
|
||||||
|
// Add to appropriate transforming set to continue live rendering
|
||||||
|
let transformingSet;
|
||||||
|
let transformName;
|
||||||
|
switch (transformType) {
|
||||||
|
case 'crop':
|
||||||
|
transformingSet = this.layersTransformingCropBounds;
|
||||||
|
transformName = 'crop bounds';
|
||||||
|
break;
|
||||||
|
case 'scale':
|
||||||
|
transformingSet = this.layersTransformingScale;
|
||||||
|
transformName = 'scale';
|
||||||
|
break;
|
||||||
|
case 'wheel':
|
||||||
|
transformingSet = this.layersWheelScaling;
|
||||||
|
transformName = 'wheel';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
transformingSet.add(layerId);
|
||||||
|
// Create processed image asynchronously with optional delay
|
||||||
|
const executeTransform = () => {
|
||||||
|
try {
|
||||||
|
const processedImage = this.createProcessedImage(layer);
|
||||||
|
if (processedImage) {
|
||||||
|
this.processedImageCache.set(cacheKey, processedImage);
|
||||||
|
log.debug(`Cached processed image for layer ${layerId} after ${transformName} transform`);
|
||||||
|
// Only now remove from live rendering set and trigger re-render
|
||||||
|
transformingSet.delete(layerId);
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
log.error(`Failed to create processed image after ${transformName} transform:`, error);
|
||||||
|
// Fallback: remove from live rendering even if cache creation failed
|
||||||
|
transformingSet.delete(layerId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (delay > 0) {
|
||||||
|
// For wheel scaling, use debounced approach
|
||||||
|
const timerKey = `${layerId}_${transformType}scaling`;
|
||||||
|
const existingTimer = this.processedImageDebounceTimers.get(timerKey);
|
||||||
|
if (existingTimer) {
|
||||||
|
clearTimeout(existingTimer);
|
||||||
|
}
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
log.debug(`Creating new cache for layer ${layerId} after ${transformName} scaling stopped`);
|
||||||
|
executeTransform();
|
||||||
|
this.processedImageDebounceTimers.delete(timerKey);
|
||||||
|
}, delay);
|
||||||
|
this.processedImageDebounceTimers.set(timerKey, timer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// For crop and scale, use immediate async approach
|
||||||
|
setTimeout(executeTransform, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Handle end of crop bounds transformation - create cache asynchronously but keep live rendering until ready
|
* Handle end of crop bounds transformation - create cache asynchronously but keep live rendering until ready
|
||||||
*/
|
*/
|
||||||
handleCropBoundsTransformEnd(layer) {
|
handleCropBoundsTransformEnd(layer) {
|
||||||
if (!layer.cropMode || !layer.blendArea)
|
if (!layer.cropMode || !layer.blendArea)
|
||||||
return;
|
return;
|
||||||
const layerId = layer.id;
|
this.handleTransformEnd(layer, 'crop', 0);
|
||||||
const cacheKey = this.getProcessedImageCacheKey(layer);
|
|
||||||
// Add to transforming set to continue live rendering
|
|
||||||
this.layersTransformingCropBounds.add(layerId);
|
|
||||||
// Create processed image asynchronously
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const processedImage = this.createProcessedImage(layer);
|
|
||||||
if (processedImage) {
|
|
||||||
this.processedImageCache.set(cacheKey, processedImage);
|
|
||||||
log.debug(`Cached processed image for layer ${layerId} after crop bounds transform`);
|
|
||||||
// Only now remove from live rendering set and trigger re-render
|
|
||||||
this.layersTransformingCropBounds.delete(layerId);
|
|
||||||
this.canvas.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
log.error('Failed to create processed image after crop bounds transform:', error);
|
|
||||||
// Fallback: remove from live rendering even if cache creation failed
|
|
||||||
this.layersTransformingCropBounds.delete(layerId);
|
|
||||||
}
|
|
||||||
}, 0); // Use setTimeout to make it asynchronous
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Handle end of scale transformation - create cache asynchronously but keep live rendering until ready
|
* Handle end of scale transformation - create cache asynchronously but keep live rendering until ready
|
||||||
@@ -844,28 +866,7 @@ export class CanvasLayers {
|
|||||||
handleScaleTransformEnd(layer) {
|
handleScaleTransformEnd(layer) {
|
||||||
if (!layer.blendArea)
|
if (!layer.blendArea)
|
||||||
return;
|
return;
|
||||||
const layerId = layer.id;
|
this.handleTransformEnd(layer, 'scale', 0);
|
||||||
const cacheKey = this.getProcessedImageCacheKey(layer);
|
|
||||||
// Add to transforming set to continue live rendering
|
|
||||||
this.layersTransformingScale.add(layerId);
|
|
||||||
// Create processed image asynchronously
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const processedImage = this.createProcessedImage(layer);
|
|
||||||
if (processedImage) {
|
|
||||||
this.processedImageCache.set(cacheKey, processedImage);
|
|
||||||
log.debug(`Cached processed image for layer ${layerId} after scale transform`);
|
|
||||||
// Only now remove from live rendering set and trigger re-render
|
|
||||||
this.layersTransformingScale.delete(layerId);
|
|
||||||
this.canvas.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
log.error('Failed to create processed image after scale transform:', error);
|
|
||||||
// Fallback: remove from live rendering even if cache creation failed
|
|
||||||
this.layersTransformingScale.delete(layerId);
|
|
||||||
}
|
|
||||||
}, 0); // Use setTimeout to make it asynchronous
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Handle end of wheel/button scaling - use debounced cache creation
|
* Handle end of wheel/button scaling - use debounced cache creation
|
||||||
@@ -873,26 +874,7 @@ export class CanvasLayers {
|
|||||||
handleWheelScalingEnd(layer) {
|
handleWheelScalingEnd(layer) {
|
||||||
if (!layer.blendArea)
|
if (!layer.blendArea)
|
||||||
return;
|
return;
|
||||||
const layerId = layer.id;
|
this.handleTransformEnd(layer, 'wheel', 500);
|
||||||
// Add to wheel scaling set to use cached image during scaling
|
|
||||||
this.layersWheelScaling.add(layerId);
|
|
||||||
log.debug(`Added layer ${layerId} to wheel scaling set for cached rendering`);
|
|
||||||
// Clear any existing wheel scaling timer
|
|
||||||
const existingTimer = this.processedImageDebounceTimers.get(`${layerId}_wheelscaling`);
|
|
||||||
if (existingTimer) {
|
|
||||||
clearTimeout(existingTimer);
|
|
||||||
}
|
|
||||||
// Schedule cache creation ONLY after scaling stops (debounced)
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
log.debug(`Creating new cache for layer ${layerId} after wheel scaling stopped`);
|
|
||||||
// Now create new cache after scaling has stopped
|
|
||||||
this.scheduleProcessedImageCreation(layer, this.getProcessedImageCacheKey(layer));
|
|
||||||
// Remove from wheel scaling set after cache creation is scheduled
|
|
||||||
this.layersWheelScaling.delete(layerId);
|
|
||||||
log.debug(`Removed layer ${layerId} from wheel scaling set after cache creation scheduled`);
|
|
||||||
this.processedImageDebounceTimers.delete(`${layerId}_wheelscaling`);
|
|
||||||
}, 500); // 500ms delay to ensure scaling has stopped
|
|
||||||
this.processedImageDebounceTimers.set(`${layerId}_wheelscaling`, timer);
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
@@ -561,16 +561,20 @@ export class CanvasLayers {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _drawLayerImage(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
/**
|
||||||
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
* Zunifikowana funkcja do rysowania obrazu warstwy z crop
|
||||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
* @param ctx Canvas context
|
||||||
|
* @param layer Warstwa do narysowania
|
||||||
|
* @param offsetX Przesunięcie X względem środka warstwy (domyślnie -width/2)
|
||||||
|
* @param offsetY Przesunięcie Y względem środka warstwy (domyślnie -height/2)
|
||||||
|
*/
|
||||||
|
private drawLayerImageWithCrop(ctx: CanvasRenderingContext2D, layer: Layer, offsetX = -layer.width / 2, offsetY = -layer.height / 2): void {
|
||||||
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
// Fallback for older layers without original dimensions or if data is missing
|
// Fallback for older layers without original dimensions or if data is missing
|
||||||
ctx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
ctx.drawImage(layer.image, offsetX, offsetY, layer.width, layer.height);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,29 +586,31 @@ export class CanvasLayers {
|
|||||||
const dWidth = s.width * layerScaleX;
|
const dWidth = s.width * layerScaleX;
|
||||||
const dHeight = s.height * layerScaleY;
|
const dHeight = s.height * layerScaleY;
|
||||||
|
|
||||||
// Calculate the on-screen position of the top-left of the cropped portion.
|
// Calculate the on-screen position of the top-left of the cropped portion
|
||||||
// This is relative to the layer's center (the context's 0,0).
|
const dX = offsetX + (s.x * layerScaleX);
|
||||||
const dX = (-layer.width / 2) + (s.x * layerScaleX);
|
const dY = offsetY + (s.y * layerScaleY);
|
||||||
const dY = (-layer.height / 2) + (s.y * layerScaleY);
|
|
||||||
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
layer.image,
|
layer.image,
|
||||||
s.x, s.y, s.width, s.height, // source rect (from original image)
|
s.x, s.y, s.width, s.height, // source rect (from original image)
|
||||||
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the transform frame)
|
dX, dY, dWidth, dHeight // destination rect (scaled and positioned)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _drawLayerImage(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
||||||
|
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
||||||
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
this.drawLayerImageWithCrop(ctx, layer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw layer with live blend area effect during user activity (original behavior)
|
* Zunifikowana funkcja do tworzenia maski blend area dla warstwy
|
||||||
|
* @param layer Warstwa dla której tworzymy maskę
|
||||||
|
* @returns Obiekt zawierający maskę i jej wymiary lub null
|
||||||
*/
|
*/
|
||||||
private _drawLayerWithLiveBlendArea(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
private createBlendAreaMask(layer: Layer): { maskCanvas: HTMLCanvasElement, maskWidth: number, maskHeight: number } | null {
|
||||||
const blendArea = layer.blendArea ?? 0;
|
const blendArea = layer.blendArea ?? 0;
|
||||||
|
|
||||||
// --- 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) {
|
if (layer.cropBounds && layer.originalWidth && layer.originalHeight) {
|
||||||
// Create a cropped canvas
|
// Create a cropped canvas
|
||||||
const s = layer.cropBounds;
|
const s = layer.cropBounds;
|
||||||
@@ -616,59 +622,92 @@ export class CanvasLayers {
|
|||||||
0, 0, s.width, s.height
|
0, 0, s.width, s.height
|
||||||
);
|
);
|
||||||
// Generate distance field mask for the cropped region
|
// Generate distance field mask for the cropped region
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(cropCanvas, blendArea);
|
||||||
maskWidth = s.width;
|
if (maskCanvas) {
|
||||||
maskHeight = s.height;
|
return {
|
||||||
|
maskCanvas,
|
||||||
|
maskWidth: s.width,
|
||||||
|
maskHeight: s.height
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No crop, use full image
|
// No crop, use full image
|
||||||
maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea);
|
||||||
maskWidth = layer.originalWidth || layer.width;
|
if (maskCanvas) {
|
||||||
maskHeight = layer.originalHeight || layer.height;
|
return {
|
||||||
}
|
maskCanvas,
|
||||||
|
maskWidth: layer.originalWidth || layer.width,
|
||||||
if (maskCanvas) {
|
maskHeight: layer.originalHeight || layer.height
|
||||||
// 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.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
|
||||||
ctx.drawImage(tempCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
|
||||||
} else {
|
|
||||||
// Fallback to normal drawing
|
|
||||||
this._drawLayerImage(ctx, layer);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zunifikowana funkcja do rysowania warstwy z blend area na canvas
|
||||||
|
* @param ctx Canvas context
|
||||||
|
* @param layer Warstwa do narysowania
|
||||||
|
* @param offsetX Przesunięcie X (domyślnie -width/2)
|
||||||
|
* @param offsetY Przesunięcie Y (domyślnie -height/2)
|
||||||
|
*/
|
||||||
|
private drawLayerWithBlendArea(ctx: CanvasRenderingContext2D, layer: Layer, offsetX = -layer.width / 2, offsetY = -layer.height / 2): void {
|
||||||
|
const maskInfo = this.createBlendAreaMask(layer);
|
||||||
|
|
||||||
|
if (maskInfo) {
|
||||||
|
const { maskCanvas, maskWidth, maskHeight } = maskInfo;
|
||||||
|
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
||||||
|
|
||||||
|
if (!layer.originalWidth || !layer.originalHeight) {
|
||||||
|
// Fallback - just draw the image normally
|
||||||
|
ctx.drawImage(layer.image, offsetX, offsetY, 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 = offsetX + (s.x * layerScaleX);
|
||||||
|
const dY = offsetY + (s.y * layerScaleY);
|
||||||
|
|
||||||
|
// Draw the image
|
||||||
|
ctx.drawImage(
|
||||||
|
layer.image,
|
||||||
|
s.x, s.y, s.width, s.height,
|
||||||
|
dX, dY, dWidth, dHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply the distance field mask
|
||||||
|
ctx.globalCompositeOperation = 'destination-in';
|
||||||
|
ctx.drawImage(
|
||||||
|
maskCanvas,
|
||||||
|
0, 0, maskWidth, maskHeight,
|
||||||
|
dX, dY, dWidth, dHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback - just draw the image normally
|
||||||
|
this.drawLayerImageWithCrop(ctx, layer, offsetX, offsetY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw layer with live blend area effect during user activity (original behavior)
|
||||||
|
*/
|
||||||
|
private _drawLayerWithLiveBlendArea(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
||||||
|
// Create a temporary canvas for the masked layer
|
||||||
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height);
|
||||||
|
|
||||||
|
if (tempCtx) {
|
||||||
|
// Draw the layer with blend area to temp canvas
|
||||||
|
this.drawLayerWithBlendArea(tempCtx, layer, 0, 0);
|
||||||
|
|
||||||
|
// Draw the result with blend mode and opacity
|
||||||
|
ctx.globalCompositeOperation = layer.blendMode as any || 'normal';
|
||||||
|
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
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);
|
||||||
@@ -816,69 +855,11 @@ export class CanvasLayers {
|
|||||||
if (!processedCtx) return null;
|
if (!processedCtx) return null;
|
||||||
|
|
||||||
if (needsBlendAreaEffect) {
|
if (needsBlendAreaEffect) {
|
||||||
// --- BLEND AREA MASK: Use cropped region if cropBounds is set ---
|
// Use the unified blend area drawing function
|
||||||
let maskCanvas: HTMLCanvasElement | null = null;
|
this.drawLayerWithBlendArea(processedCtx, layer, 0, 0);
|
||||||
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) {
|
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
processedCtx.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;
|
|
||||||
|
|
||||||
processedCtx.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 ---
|
|
||||||
processedCtx.globalCompositeOperation = 'destination-in';
|
|
||||||
// Scale the mask to match the drawn area
|
|
||||||
processedCtx.drawImage(
|
|
||||||
maskCanvas,
|
|
||||||
0, 0, maskWidth, maskHeight,
|
|
||||||
dX, dY, dWidth, dHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback - just draw the image normally
|
|
||||||
this._drawLayerImageToCanvas(processedCtx, layer);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Just apply crop effect without blend area
|
// Just apply crop effect without blend area
|
||||||
this._drawLayerImageToCanvas(processedCtx, layer);
|
this.drawLayerImageWithCrop(processedCtx, layer, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert canvas to image
|
// Convert canvas to image
|
||||||
@@ -888,35 +869,11 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to draw layer image to a specific canvas context
|
* Helper method to draw layer image to a specific canvas context (position 0,0)
|
||||||
|
* Uses the unified drawLayerImageWithCrop function
|
||||||
*/
|
*/
|
||||||
private _drawLayerImageToCanvas(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
private _drawLayerImageToCanvas(ctx: CanvasRenderingContext2D, layer: Layer): void {
|
||||||
// Use cropBounds if they exist, otherwise use the full image dimensions as the source
|
this.drawLayerImageWithCrop(ctx, layer, 0, 0);
|
||||||
const s = layer.cropBounds || { x: 0, y: 0, width: layer.originalWidth, height: layer.originalHeight };
|
|
||||||
|
|
||||||
if (!layer.originalWidth || !layer.originalHeight) {
|
|
||||||
// Fallback for older layers without original dimensions or if data is missing
|
|
||||||
ctx.drawImage(layer.image, 0, 0, layer.width, layer.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the on-screen scale of the layer's transform frame
|
|
||||||
const layerScaleX = layer.width / layer.originalWidth;
|
|
||||||
const layerScaleY = layer.height / layer.originalHeight;
|
|
||||||
|
|
||||||
// Calculate the on-screen size of the cropped portion
|
|
||||||
const dWidth = s.width * layerScaleX;
|
|
||||||
const dHeight = s.height * layerScaleY;
|
|
||||||
|
|
||||||
// Calculate the on-screen position of the top-left of the cropped portion.
|
|
||||||
const dX = s.x * layerScaleX;
|
|
||||||
const dY = s.y * layerScaleY;
|
|
||||||
|
|
||||||
ctx.drawImage(
|
|
||||||
layer.image,
|
|
||||||
s.x, s.y, s.width, s.height, // source rect (from original image)
|
|
||||||
dX, dY, dWidth, dHeight // destination rect (scaled and positioned within the canvas)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -959,35 +916,84 @@ export class CanvasLayers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle end of crop bounds transformation - create cache asynchronously but keep live rendering until ready
|
* Zunifikowana funkcja do obsługi transformacji końcowych
|
||||||
|
* @param layer Warstwa do przetworzenia
|
||||||
|
* @param transformType Typ transformacji (crop, scale, wheel)
|
||||||
|
* @param delay Opóźnienie w ms (domyślnie 0)
|
||||||
*/
|
*/
|
||||||
public handleCropBoundsTransformEnd(layer: Layer): void {
|
private handleTransformEnd(layer: Layer, transformType: 'crop' | 'scale' | 'wheel', delay = 0): void {
|
||||||
if (!layer.cropMode || !layer.blendArea) return;
|
if (!layer.blendArea) return;
|
||||||
|
|
||||||
const layerId = layer.id;
|
const layerId = layer.id;
|
||||||
const cacheKey = this.getProcessedImageCacheKey(layer);
|
const cacheKey = this.getProcessedImageCacheKey(layer);
|
||||||
|
|
||||||
// Add to transforming set to continue live rendering
|
// Add to appropriate transforming set to continue live rendering
|
||||||
this.layersTransformingCropBounds.add(layerId);
|
let transformingSet: Set<string>;
|
||||||
|
let transformName: string;
|
||||||
|
|
||||||
// Create processed image asynchronously
|
switch (transformType) {
|
||||||
setTimeout(() => {
|
case 'crop':
|
||||||
|
transformingSet = this.layersTransformingCropBounds;
|
||||||
|
transformName = 'crop bounds';
|
||||||
|
break;
|
||||||
|
case 'scale':
|
||||||
|
transformingSet = this.layersTransformingScale;
|
||||||
|
transformName = 'scale';
|
||||||
|
break;
|
||||||
|
case 'wheel':
|
||||||
|
transformingSet = this.layersWheelScaling;
|
||||||
|
transformName = 'wheel';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformingSet.add(layerId);
|
||||||
|
|
||||||
|
// Create processed image asynchronously with optional delay
|
||||||
|
const executeTransform = () => {
|
||||||
try {
|
try {
|
||||||
const processedImage = this.createProcessedImage(layer);
|
const processedImage = this.createProcessedImage(layer);
|
||||||
if (processedImage) {
|
if (processedImage) {
|
||||||
this.processedImageCache.set(cacheKey, processedImage);
|
this.processedImageCache.set(cacheKey, processedImage);
|
||||||
log.debug(`Cached processed image for layer ${layerId} after crop bounds transform`);
|
log.debug(`Cached processed image for layer ${layerId} after ${transformName} transform`);
|
||||||
|
|
||||||
// Only now remove from live rendering set and trigger re-render
|
// Only now remove from live rendering set and trigger re-render
|
||||||
this.layersTransformingCropBounds.delete(layerId);
|
transformingSet.delete(layerId);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to create processed image after crop bounds transform:', error);
|
log.error(`Failed to create processed image after ${transformName} transform:`, error);
|
||||||
// Fallback: remove from live rendering even if cache creation failed
|
// Fallback: remove from live rendering even if cache creation failed
|
||||||
this.layersTransformingCropBounds.delete(layerId);
|
transformingSet.delete(layerId);
|
||||||
}
|
}
|
||||||
}, 0); // Use setTimeout to make it asynchronous
|
};
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
// For wheel scaling, use debounced approach
|
||||||
|
const timerKey = `${layerId}_${transformType}scaling`;
|
||||||
|
const existingTimer = this.processedImageDebounceTimers.get(timerKey);
|
||||||
|
if (existingTimer) {
|
||||||
|
clearTimeout(existingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
log.debug(`Creating new cache for layer ${layerId} after ${transformName} scaling stopped`);
|
||||||
|
executeTransform();
|
||||||
|
this.processedImageDebounceTimers.delete(timerKey);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
this.processedImageDebounceTimers.set(timerKey, timer);
|
||||||
|
} else {
|
||||||
|
// For crop and scale, use immediate async approach
|
||||||
|
setTimeout(executeTransform, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle end of crop bounds transformation - create cache asynchronously but keep live rendering until ready
|
||||||
|
*/
|
||||||
|
public handleCropBoundsTransformEnd(layer: Layer): void {
|
||||||
|
if (!layer.cropMode || !layer.blendArea) return;
|
||||||
|
this.handleTransformEnd(layer, 'crop', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -995,31 +1001,7 @@ export class CanvasLayers {
|
|||||||
*/
|
*/
|
||||||
public handleScaleTransformEnd(layer: Layer): void {
|
public handleScaleTransformEnd(layer: Layer): void {
|
||||||
if (!layer.blendArea) return;
|
if (!layer.blendArea) return;
|
||||||
|
this.handleTransformEnd(layer, 'scale', 0);
|
||||||
const layerId = layer.id;
|
|
||||||
const cacheKey = this.getProcessedImageCacheKey(layer);
|
|
||||||
|
|
||||||
// Add to transforming set to continue live rendering
|
|
||||||
this.layersTransformingScale.add(layerId);
|
|
||||||
|
|
||||||
// Create processed image asynchronously
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const processedImage = this.createProcessedImage(layer);
|
|
||||||
if (processedImage) {
|
|
||||||
this.processedImageCache.set(cacheKey, processedImage);
|
|
||||||
log.debug(`Cached processed image for layer ${layerId} after scale transform`);
|
|
||||||
|
|
||||||
// Only now remove from live rendering set and trigger re-render
|
|
||||||
this.layersTransformingScale.delete(layerId);
|
|
||||||
this.canvas.render();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error('Failed to create processed image after scale transform:', error);
|
|
||||||
// Fallback: remove from live rendering even if cache creation failed
|
|
||||||
this.layersTransformingScale.delete(layerId);
|
|
||||||
}
|
|
||||||
}, 0); // Use setTimeout to make it asynchronous
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1027,33 +1009,7 @@ export class CanvasLayers {
|
|||||||
*/
|
*/
|
||||||
public handleWheelScalingEnd(layer: Layer): void {
|
public handleWheelScalingEnd(layer: Layer): void {
|
||||||
if (!layer.blendArea) return;
|
if (!layer.blendArea) return;
|
||||||
|
this.handleTransformEnd(layer, 'wheel', 500);
|
||||||
const layerId = layer.id;
|
|
||||||
|
|
||||||
// Add to wheel scaling set to use cached image during scaling
|
|
||||||
this.layersWheelScaling.add(layerId);
|
|
||||||
log.debug(`Added layer ${layerId} to wheel scaling set for cached rendering`);
|
|
||||||
|
|
||||||
// Clear any existing wheel scaling timer
|
|
||||||
const existingTimer = this.processedImageDebounceTimers.get(`${layerId}_wheelscaling`);
|
|
||||||
if (existingTimer) {
|
|
||||||
clearTimeout(existingTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule cache creation ONLY after scaling stops (debounced)
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
log.debug(`Creating new cache for layer ${layerId} after wheel scaling stopped`);
|
|
||||||
|
|
||||||
// Now create new cache after scaling has stopped
|
|
||||||
this.scheduleProcessedImageCreation(layer, this.getProcessedImageCacheKey(layer));
|
|
||||||
|
|
||||||
// Remove from wheel scaling set after cache creation is scheduled
|
|
||||||
this.layersWheelScaling.delete(layerId);
|
|
||||||
log.debug(`Removed layer ${layerId} from wheel scaling set after cache creation scheduled`);
|
|
||||||
this.processedImageDebounceTimers.delete(`${layerId}_wheelscaling`);
|
|
||||||
}, 500); // 500ms delay to ensure scaling has stopped
|
|
||||||
|
|
||||||
this.processedImageDebounceTimers.set(`${layerId}_wheelscaling`, timer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDistanceFieldMaskSync(imageOrCanvas: HTMLImageElement | HTMLCanvasElement, blendArea: number): HTMLCanvasElement | null {
|
private getDistanceFieldMaskSync(imageOrCanvas: HTMLImageElement | HTMLCanvasElement, blendArea: number): HTMLCanvasElement | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user