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.
This commit is contained in:
Dariusz L
2025-06-28 07:37:53 +02:00
parent 940f027b40
commit a1e00ca06a
3 changed files with 105 additions and 14 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;