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})`); } }