From a1e00ca06a46edb128c9efc1763d17ef31850c67 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sat, 28 Jun 2025 07:37:53 +0200 Subject: [PATCH] Add brush preview overlay to MaskTool Introduces a brush preview overlay using a separate preview canvas in MaskTool. Mouse event handlers in CanvasInteractions and MaskTool are updated to support passing both world and view coordinates, enabling accurate brush preview rendering. The preview is shown or hidden appropriately on mouse enter/leave and while drawing. --- js/Canvas.js | 14 +++++++++ js/CanvasInteractions.js | 37 +++++++++++++++------- js/MaskTool.js | 68 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/js/Canvas.js b/js/Canvas.js index 6b90e32..b2d77d6 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -247,6 +247,20 @@ export class Canvas { return {x: worldX, y: worldY}; } + getMouseViewCoordinates(e) { + const rect = this.canvas.getBoundingClientRect(); + const mouseX_DOM = e.clientX - rect.left; + const mouseY_DOM = e.clientY - rect.top; + + const scaleX = this.canvas.width / rect.width; + const scaleY = this.canvas.height / rect.height; + + const mouseX_Canvas = mouseX_DOM * scaleX; + const mouseY_Canvas = mouseY_DOM * scaleY; + + return { x: mouseX_Canvas, y: mouseY_Canvas }; + } + moveLayer(fromIndex, toIndex) { return this.canvasLayers.moveLayer(fromIndex, toIndex); diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 6132964..bd8b024 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -34,11 +34,13 @@ export class CanvasInteractions { this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this)); this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this)); - this.canvas.canvas.addEventListener('mouseenter', () => { + this.canvas.canvas.addEventListener('mouseenter', (e) => { this.canvas.isMouseOver = true; + this.handleMouseEnter(e); }); - this.canvas.canvas.addEventListener('mouseleave', () => { + this.canvas.canvas.addEventListener('mouseleave', (e) => { this.canvas.isMouseOver = false; + this.handleMouseLeave(e); }); } @@ -56,14 +58,14 @@ export class CanvasInteractions { handleMouseDown(e) { this.canvas.canvas.focus(); const worldCoords = this.canvas.getMouseWorldCoordinates(e); + const viewCoords = this.canvas.getMouseViewCoordinates(e); if (this.canvas.maskTool.isActive) { if (e.button === 1) { this.startPanning(e); - this.canvas.render(); - return; + } else { + this.canvas.maskTool.handleMouseDown(worldCoords, viewCoords); } - this.canvas.maskTool.handleMouseDown(worldCoords); this.canvas.render(); return; } @@ -110,6 +112,7 @@ export class CanvasInteractions { handleMouseMove(e) { const worldCoords = this.canvas.getMouseWorldCoordinates(e); + const viewCoords = this.canvas.getMouseViewCoordinates(e); this.canvas.lastMousePosition = worldCoords; if (this.canvas.maskTool.isActive) { @@ -117,8 +120,10 @@ export class CanvasInteractions { this.panViewport(e); return; } - this.canvas.maskTool.handleMouseMove(worldCoords); - if (this.canvas.maskTool.isDrawing) this.canvas.render(); + this.canvas.maskTool.handleMouseMove(worldCoords, viewCoords); + if (this.canvas.maskTool.isDrawing) { + this.canvas.render(); + } return; } @@ -148,13 +153,13 @@ export class CanvasInteractions { } handleMouseUp(e) { + const viewCoords = this.canvas.getMouseViewCoordinates(e); if (this.canvas.maskTool.isActive) { if (this.interaction.mode === 'panning') { this.resetInteractionState(); - this.canvas.render(); - return; + } else { + this.canvas.maskTool.handleMouseUp(viewCoords); } - this.canvas.maskTool.handleMouseUp(); this.canvas.render(); return; } @@ -176,8 +181,12 @@ export class CanvasInteractions { } handleMouseLeave(e) { + const viewCoords = this.canvas.getMouseViewCoordinates(e); if (this.canvas.maskTool.isActive) { - this.canvas.maskTool.handleMouseUp(); + this.canvas.maskTool.handleMouseLeave(); + if (this.canvas.maskTool.isDrawing) { + this.canvas.maskTool.handleMouseUp(viewCoords); + } this.canvas.render(); return; } @@ -187,6 +196,12 @@ export class CanvasInteractions { } } + handleMouseEnter(e) { + if (this.canvas.maskTool.isActive) { + this.canvas.maskTool.handleMouseEnter(); + } + } + handleWheel(e) { e.preventDefault(); if (this.canvas.maskTool.isActive) { diff --git a/js/MaskTool.js b/js/MaskTool.js index a9b56fd..92ad026 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -20,9 +20,28 @@ export class MaskTool { this.isDrawing = false; this.lastPosition = null; + this.previewCanvas = document.createElement('canvas'); + this.previewCtx = this.previewCanvas.getContext('2d'); + this.previewVisible = false; + this.previewCanvasInitialized = false; + this.initMaskCanvas(); } + initPreviewCanvas() { + if (this.previewCanvas.parentElement) { + this.previewCanvas.parentElement.removeChild(this.previewCanvas); + } + this.previewCanvas.width = this.canvasInstance.canvas.width; + this.previewCanvas.height = this.canvasInstance.canvas.height; + this.previewCanvas.style.position = 'absolute'; + this.previewCanvas.style.left = `${this.canvasInstance.canvas.offsetLeft}px`; + this.previewCanvas.style.top = `${this.canvasInstance.canvas.offsetTop}px`; + this.previewCanvas.style.pointerEvents = 'none'; + this.previewCanvas.style.zIndex = '10'; + this.canvasInstance.canvas.parentElement.appendChild(this.previewCanvas); + } + setBrushSoftness(softness) { this.brushSoftness = Math.max(0, Math.min(1, softness)); } @@ -42,7 +61,12 @@ export class MaskTool { } activate() { + if (!this.previewCanvasInitialized) { + this.initPreviewCanvas(); + this.previewCanvasInitialized = true; + } this.isActive = true; + this.previewCanvas.style.display = 'block'; this.canvasInstance.interaction.mode = 'drawingMask'; if (this.canvasInstance.canvasState && this.canvasInstance.canvasState.maskUndoStack.length === 0) { this.canvasInstance.canvasState.saveMaskState(); @@ -54,6 +78,7 @@ export class MaskTool { deactivate() { this.isActive = false; + this.previewCanvas.style.display = 'none'; this.canvasInstance.interaction.mode = 'none'; this.canvasInstance.updateHistoryButtons(); @@ -68,20 +93,33 @@ export class MaskTool { this.brushStrength = Math.max(0, Math.min(1, strength)); } - handleMouseDown(worldCoords) { + handleMouseDown(worldCoords, viewCoords) { if (!this.isActive) return; this.isDrawing = true; this.lastPosition = worldCoords; this.draw(worldCoords); + this.clearPreview(); } - handleMouseMove(worldCoords) { + handleMouseMove(worldCoords, viewCoords) { + if (this.isActive) { + this.drawBrushPreview(viewCoords); + } if (!this.isActive || !this.isDrawing) return; this.draw(worldCoords); this.lastPosition = worldCoords; } - handleMouseUp() { + handleMouseLeave() { + this.previewVisible = false; + this.clearPreview(); + } + + handleMouseEnter() { + this.previewVisible = true; + } + + handleMouseUp(viewCoords) { if (!this.isActive) return; if (this.isDrawing) { this.isDrawing = false; @@ -92,6 +130,7 @@ export class MaskTool { if (this.onStateChange) { this.onStateChange(); } + this.drawBrushPreview(viewCoords); } } @@ -143,6 +182,28 @@ export class MaskTool { } } + drawBrushPreview(viewCoords) { + if (!this.previewVisible || this.isDrawing) { + this.clearPreview(); + return; + } + + this.clearPreview(); + const zoom = this.canvasInstance.viewport.zoom; + const radius = (this.brushSize / 2) * zoom; + + this.previewCtx.beginPath(); + this.previewCtx.arc(viewCoords.x, viewCoords.y, radius, 0, 2 * Math.PI); + this.previewCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + this.previewCtx.lineWidth = 1; + this.previewCtx.setLineDash([2, 4]); + this.previewCtx.stroke(); + } + + clearPreview() { + this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); + } + clear() { this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); if (this.isActive && this.canvasInstance.canvasState) { @@ -176,6 +237,7 @@ export class MaskTool { } resize(width, height) { + this.initPreviewCanvas(); const oldMask = this.maskCanvas; const oldX = this.x; const oldY = this.y;