From daf3abeea7721a0b4bbdb2b03bed9f9bef862adf Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 26 Jun 2025 22:09:28 +0200 Subject: [PATCH] Add world-space positioning and resizing for mask tool Introduces x/y coordinates to MaskTool for world-space positioning, allowing the mask to extend beyond the output area. Updates mask drawing, export, and rendering logic to account for mask position. Ensures mask position is updated when moving or resizing the canvas, and preserves mask content during canvas resizing. Improves mask extraction and rendering accuracy. --- js/CanvasIO.js | 43 +++++++++++++- js/CanvasInteractions.js | 7 +++ js/CanvasRenderer.js | 26 +++++--- js/MaskTool.js | 124 ++++++++++++++++++++++++++++++--------- 4 files changed, 162 insertions(+), 38 deletions(-) diff --git a/js/CanvasIO.js b/js/CanvasIO.js index 55086cc..a2ff041 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -96,11 +96,50 @@ export class CanvasIO { maskCtx.putImageData(maskData, 0, 0); const toolMaskCanvas = this.canvas.maskTool.getMask(); if (toolMaskCanvas) { + // Create a temp canvas for processing the mask const tempMaskCanvas = document.createElement('canvas'); tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.height = this.canvas.height; const tempMaskCtx = tempMaskCanvas.getContext('2d'); - tempMaskCtx.drawImage(toolMaskCanvas, 0, 0); + + // Clear the canvas + tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); + + // Calculate the correct position to extract the mask + // The mask's position in world space + const maskX = this.canvas.maskTool.x; + const maskY = this.canvas.maskTool.y; + + log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`); + + // Calculate the source rectangle in the mask canvas that corresponds to the output area + const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading + const sourceY = Math.max(0, -maskY); + const destX = Math.max(0, maskX); // Where in the output canvas to start writing + const destY = Math.max(0, maskY); + + // Calculate the dimensions of the area to copy + const copyWidth = Math.min( + toolMaskCanvas.width - sourceX, // Available width in source + this.canvas.width - destX // Available width in destination + ); + const copyHeight = Math.min( + toolMaskCanvas.height - sourceY, // Available height in source + this.canvas.height - destY // Available height in destination + ); + + // Only draw if there's an actual intersection + if (copyWidth > 0 && copyHeight > 0) { + log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); + + tempMaskCtx.drawImage( + toolMaskCanvas, + sourceX, sourceY, copyWidth, copyHeight, // Source rectangle + destX, destY, copyWidth, copyHeight // Destination rectangle + ); + } + + // Convert to proper mask format const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); for (let i = 0; i < tempMaskData.data.length; i += 4) { const alpha = tempMaskData.data[i + 3]; @@ -108,6 +147,8 @@ export class CanvasIO { tempMaskData.data[i + 3] = alpha; } tempMaskCtx.putImageData(tempMaskData, 0, 0); + + // Draw the processed mask to the final mask canvas maskCtx.globalCompositeOperation = 'source-over'; maskCtx.drawImage(tempMaskCanvas, 0, 0); } diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 9246b0d..4429f81 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -502,6 +502,10 @@ export class CanvasInteractions { layer.x -= finalX; layer.y -= finalY; }); + + // Update mask position when moving canvas + this.canvas.maskTool.updatePosition(-finalX, -finalY); + this.canvas.viewport.x -= finalX; this.canvas.viewport.y -= finalY; } @@ -686,6 +690,9 @@ export class CanvasInteractions { layer.x -= rectX; layer.y -= rectY; }); + + // Update mask position when resizing canvas + this.canvas.maskTool.updatePosition(-rectX, -rectY); this.canvas.viewport.x -= rectX; this.canvas.viewport.y -= rectY; diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index fd1d5a0..f9bf667 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -82,16 +82,24 @@ export class CanvasRenderer { this.drawCanvasOutline(ctx); const maskImage = this.canvas.maskTool.getMask(); - if (this.canvas.maskTool.isActive) { - ctx.globalCompositeOperation = 'source-over'; - ctx.globalAlpha = 0.5; - ctx.drawImage(maskImage, 0, 0); - ctx.globalAlpha = 1.0; - } else if (maskImage) { - ctx.globalCompositeOperation = 'source-over'; - ctx.globalAlpha = 1.0; - ctx.drawImage(maskImage, 0, 0); + if (maskImage) { + // Create a clipping region to only show mask content that overlaps with the output area + ctx.save(); + + // Only show what's visible inside the output area + if (this.canvas.maskTool.isActive) { + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = 0.5; + } else { + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = 1.0; + } + + // Draw the mask at its world space position + ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y); + ctx.globalAlpha = 1.0; + ctx.restore(); } this.renderInteractionElements(ctx); diff --git a/js/MaskTool.js b/js/MaskTool.js index 309c382..2f2fdc3 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -8,6 +8,10 @@ export class MaskTool { this.maskCanvas = document.createElement('canvas'); this.maskCtx = this.maskCanvas.getContext('2d'); + // Add position coordinates for the mask + this.x = 0; + this.y = 0; + this.isActive = false; this.brushSize = 20; this.brushStrength = 0.5; @@ -23,9 +27,18 @@ export class MaskTool { } initMaskCanvas() { - this.maskCanvas.width = this.canvasInstance.width; - this.maskCanvas.height = this.canvasInstance.height; + // Create a larger mask canvas that can extend beyond the output area + const extraSpace = 2000; // Allow for a generous drawing area outside the output area + this.maskCanvas.width = this.canvasInstance.width + extraSpace; + this.maskCanvas.height = this.canvasInstance.height + extraSpace; + + // Position the mask's origin point in the center of the expanded canvas + // This allows drawing in any direction from the output area + this.x = -extraSpace / 2; + this.y = -extraSpace / 2; + this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); + log.info(`Initialized mask canvas with extended size: ${this.maskCanvas.width}x${this.maskCanvas.height}, origin at (${this.x}, ${this.y})`); } activate() { @@ -84,29 +97,49 @@ export class MaskTool { this.lastPosition = worldCoords; } - this.maskCtx.beginPath(); - this.maskCtx.moveTo(this.lastPosition.x, this.lastPosition.y); - this.maskCtx.lineTo(worldCoords.x, worldCoords.y); - const gradientRadius = this.brushSize / 2; - - if (this.brushSoftness === 0) { - this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`; - } else { - const innerRadius = gradientRadius * this.brushSoftness; - const gradient = this.maskCtx.createRadialGradient( - worldCoords.x, worldCoords.y, innerRadius, - worldCoords.x, worldCoords.y, gradientRadius - ); - gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`); - gradient.addColorStop(1, `rgba(255, 255, 255, 0)`); - this.maskCtx.strokeStyle = gradient; - } + // Convert world coordinates to mask canvas coordinates + // Account for the mask's position in world space + const canvasLastX = this.lastPosition.x - this.x; + const canvasLastY = this.lastPosition.y - this.y; + const canvasX = worldCoords.x - this.x; + const canvasY = worldCoords.y - this.y; - this.maskCtx.lineWidth = this.brushSize; - this.maskCtx.lineCap = 'round'; - this.maskCtx.lineJoin = 'round'; - this.maskCtx.globalCompositeOperation = 'source-over'; - this.maskCtx.stroke(); + // Check if drawing is within the expanded canvas bounds + // Since our canvas is much larger now, this should rarely be an issue + const canvasWidth = this.maskCanvas.width; + const canvasHeight = this.maskCanvas.height; + + if (canvasX >= 0 && canvasX < canvasWidth && + canvasY >= 0 && canvasY < canvasHeight && + canvasLastX >= 0 && canvasLastX < canvasWidth && + canvasLastY >= 0 && canvasLastY < canvasHeight) { + + this.maskCtx.beginPath(); + this.maskCtx.moveTo(canvasLastX, canvasLastY); + this.maskCtx.lineTo(canvasX, canvasY); + const gradientRadius = this.brushSize / 2; + + if (this.brushSoftness === 0) { + this.maskCtx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`; + } else { + const innerRadius = gradientRadius * this.brushSoftness; + const gradient = this.maskCtx.createRadialGradient( + canvasX, canvasY, innerRadius, + canvasX, canvasY, gradientRadius + ); + gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`); + gradient.addColorStop(1, `rgba(255, 255, 255, 0)`); + this.maskCtx.strokeStyle = gradient; + } + + this.maskCtx.lineWidth = this.brushSize; + this.maskCtx.lineCap = 'round'; + this.maskCtx.lineJoin = 'round'; + this.maskCtx.globalCompositeOperation = 'source-over'; + this.maskCtx.stroke(); + } else { + log.debug(`Drawing outside mask canvas bounds: (${canvasX}, ${canvasY})`); + } } clear() { @@ -143,14 +176,49 @@ export class MaskTool { resize(width, height) { const oldMask = this.maskCanvas; + const oldX = this.x; + const oldY = this.y; + const oldWidth = oldMask.width; + const oldHeight = oldMask.height; + + // Determine if we're increasing or decreasing the canvas size + const isIncreasingWidth = width > (this.canvasInstance.width); + const isIncreasingHeight = height > (this.canvasInstance.height); + + // Create a new mask canvas this.maskCanvas = document.createElement('canvas'); - this.maskCanvas.width = width; - this.maskCanvas.height = height; + + // Calculate the new size based on whether we're increasing or decreasing + const extraSpace = 2000; + + // If we're increasing the size, expand the mask canvas + // If we're decreasing, keep the current mask canvas size to preserve content + const newWidth = isIncreasingWidth ? width + extraSpace : Math.max(oldWidth, width + extraSpace); + const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace); + + this.maskCanvas.width = newWidth; + this.maskCanvas.height = newHeight; this.maskCtx = this.maskCanvas.getContext('2d'); + if (oldMask.width > 0 && oldMask.height > 0) { - this.maskCtx.drawImage(oldMask, 0, 0); + // Calculate offset to maintain the same world position of the mask content + const offsetX = this.x - oldX; + const offsetY = this.y - oldY; + + // Draw the old mask at the correct position to maintain world alignment + this.maskCtx.drawImage(oldMask, offsetX, offsetY); + + log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`); } - log.info(`Mask canvas resized to ${width}x${height}`); + log.info(`Mask canvas resized to ${this.maskCanvas.width}x${this.maskCanvas.height}, position (${this.x}, ${this.y})`); + log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`); + } + + // Add method to update mask position + updatePosition(dx, dy) { + this.x += dx; + this.y += dy; + log.info(`Mask position updated to (${this.x}, ${this.y})`); } }