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;