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.
This commit is contained in:
Dariusz L
2025-06-26 22:09:28 +02:00
parent f2998f0f08
commit daf3abeea7
4 changed files with 162 additions and 38 deletions

View File

@@ -96,11 +96,50 @@ export class CanvasIO {
maskCtx.putImageData(maskData, 0, 0); maskCtx.putImageData(maskData, 0, 0);
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) { if (toolMaskCanvas) {
// Create a temp canvas for processing the mask
const tempMaskCanvas = document.createElement('canvas'); const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height; tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d'); 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); const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) { for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3]; const alpha = tempMaskData.data[i + 3];
@@ -108,6 +147,8 @@ export class CanvasIO {
tempMaskData.data[i + 3] = alpha; tempMaskData.data[i + 3] = alpha;
} }
tempMaskCtx.putImageData(tempMaskData, 0, 0); tempMaskCtx.putImageData(tempMaskData, 0, 0);
// Draw the processed mask to the final mask canvas
maskCtx.globalCompositeOperation = 'source-over'; maskCtx.globalCompositeOperation = 'source-over';
maskCtx.drawImage(tempMaskCanvas, 0, 0); maskCtx.drawImage(tempMaskCanvas, 0, 0);
} }

View File

@@ -502,6 +502,10 @@ export class CanvasInteractions {
layer.x -= finalX; layer.x -= finalX;
layer.y -= finalY; layer.y -= finalY;
}); });
// Update mask position when moving canvas
this.canvas.maskTool.updatePosition(-finalX, -finalY);
this.canvas.viewport.x -= finalX; this.canvas.viewport.x -= finalX;
this.canvas.viewport.y -= finalY; this.canvas.viewport.y -= finalY;
} }
@@ -686,6 +690,9 @@ export class CanvasInteractions {
layer.x -= rectX; layer.x -= rectX;
layer.y -= rectY; layer.y -= rectY;
}); });
// Update mask position when resizing canvas
this.canvas.maskTool.updatePosition(-rectX, -rectY);
this.canvas.viewport.x -= rectX; this.canvas.viewport.x -= rectX;
this.canvas.viewport.y -= rectY; this.canvas.viewport.y -= rectY;

View File

@@ -82,16 +82,24 @@ export class CanvasRenderer {
this.drawCanvasOutline(ctx); this.drawCanvasOutline(ctx);
const maskImage = this.canvas.maskTool.getMask(); const maskImage = this.canvas.maskTool.getMask();
if (this.canvas.maskTool.isActive) { if (maskImage) {
ctx.globalCompositeOperation = 'source-over'; // Create a clipping region to only show mask content that overlaps with the output area
ctx.globalAlpha = 0.5; ctx.save();
ctx.drawImage(maskImage, 0, 0);
ctx.globalAlpha = 1.0; // Only show what's visible inside the output area
} else if (maskImage) { if (this.canvas.maskTool.isActive) {
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0; ctx.globalAlpha = 0.5;
ctx.drawImage(maskImage, 0, 0); } 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.globalAlpha = 1.0;
ctx.restore();
} }
this.renderInteractionElements(ctx); this.renderInteractionElements(ctx);

View File

@@ -8,6 +8,10 @@ export class MaskTool {
this.maskCanvas = document.createElement('canvas'); this.maskCanvas = document.createElement('canvas');
this.maskCtx = this.maskCanvas.getContext('2d'); this.maskCtx = this.maskCanvas.getContext('2d');
// Add position coordinates for the mask
this.x = 0;
this.y = 0;
this.isActive = false; this.isActive = false;
this.brushSize = 20; this.brushSize = 20;
this.brushStrength = 0.5; this.brushStrength = 0.5;
@@ -23,9 +27,18 @@ export class MaskTool {
} }
initMaskCanvas() { initMaskCanvas() {
this.maskCanvas.width = this.canvasInstance.width; // Create a larger mask canvas that can extend beyond the output area
this.maskCanvas.height = this.canvasInstance.height; 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); 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() { activate() {
@@ -84,29 +97,49 @@ export class MaskTool {
this.lastPosition = worldCoords; this.lastPosition = worldCoords;
} }
this.maskCtx.beginPath(); // Convert world coordinates to mask canvas coordinates
this.maskCtx.moveTo(this.lastPosition.x, this.lastPosition.y); // Account for the mask's position in world space
this.maskCtx.lineTo(worldCoords.x, worldCoords.y); const canvasLastX = this.lastPosition.x - this.x;
const gradientRadius = this.brushSize / 2; const canvasLastY = this.lastPosition.y - this.y;
const canvasX = worldCoords.x - this.x;
if (this.brushSoftness === 0) { const canvasY = worldCoords.y - this.y;
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;
}
this.maskCtx.lineWidth = this.brushSize; // Check if drawing is within the expanded canvas bounds
this.maskCtx.lineCap = 'round'; // Since our canvas is much larger now, this should rarely be an issue
this.maskCtx.lineJoin = 'round'; const canvasWidth = this.maskCanvas.width;
this.maskCtx.globalCompositeOperation = 'source-over'; const canvasHeight = this.maskCanvas.height;
this.maskCtx.stroke();
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() { clear() {
@@ -143,14 +176,49 @@ export class MaskTool {
resize(width, height) { resize(width, height) {
const oldMask = this.maskCanvas; 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 = 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'); this.maskCtx = this.maskCanvas.getContext('2d');
if (oldMask.width > 0 && oldMask.height > 0) { 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})`);
} }
} }