From dd2a81b6f24ec8019238b4278477f6bd8329bcb3 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 8 Aug 2025 14:20:55 +0200 Subject: [PATCH] add advanced brush cursor visualization Implemented dynamic brush cursor with visual feedback for size (circle radius), strength (opacity), and hardness (solid/dashed border with gradient). Added overlay canvas system for smooth cursor updates without affecting main rendering performance. --- js/Canvas.js | 9 +++ js/CanvasInteractions.js | 22 +++++- js/CanvasRenderer.js | 126 ++++++++++++++++++++++++++++++ js/MaskTool.js | 39 +++++----- src/Canvas.ts | 12 +++ src/CanvasInteractions.ts | 22 +++++- src/CanvasRenderer.ts | 156 ++++++++++++++++++++++++++++++++++++++ src/MaskTool.ts | 48 ++++++------ 8 files changed, 388 insertions(+), 46 deletions(-) diff --git a/js/Canvas.js b/js/Canvas.js index f893c5c..92b06c7 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -61,6 +61,15 @@ export class Canvas { }); this.offscreenCanvas = offscreenCanvas; this.offscreenCtx = offscreenCtx; + // Create overlay canvas for brush cursor and other lightweight overlays + const { canvas: overlayCanvas, ctx: overlayCtx } = createCanvas(0, 0, '2d', { + alpha: true, + willReadFrequently: false + }); + if (!overlayCtx) + throw new Error("Could not create overlay canvas context"); + this.overlayCanvas = overlayCanvas; + this.overlayCtx = overlayCtx; this.canvasContainer = null; this.dataInitialized = false; this.pendingDataCheck = null; diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 96f72b1..8248ebc 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -234,7 +234,10 @@ export class CanvasInteractions { switch (this.interaction.mode) { case 'drawingMask': this.canvas.maskTool.handleMouseMove(coords.world, coords.view); - this.canvas.render(); + // Only render if actually drawing, not just moving cursor + if (this.canvas.maskTool.isDrawing) { + this.canvas.render(); + } break; case 'panning': this.panViewport(e); @@ -256,6 +259,10 @@ export class CanvasInteractions { break; default: this.updateCursor(coords.world); + // Update brush cursor on overlay if mask tool is active + if (this.canvas.maskTool.isActive) { + this.canvas.canvasRenderer.drawMaskBrushCursor(coords.world); + } break; } // --- DYNAMICZNY PODGLĄD LINII CUSTOM SHAPE --- @@ -350,8 +357,17 @@ export class CanvasInteractions { this.performZoomOperation(coords.world, zoomFactor); } else { - // Layer transformation when layers are selected - this.handleLayerWheelTransformation(e); + // Check if mouse is over any selected layer + const isOverSelectedLayer = this.isPointInSelectedLayers(coords.world.x, coords.world.y); + if (isOverSelectedLayer) { + // Layer transformation when layers are selected and mouse is over selected layer + this.handleLayerWheelTransformation(e); + } + else { + // Zoom operation when mouse is not over selected layers + const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1; + this.performZoomOperation(coords.world, zoomFactor); + } } this.canvas.render(); if (!this.canvas.maskTool.isActive) { diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 874b57d..ce350a5 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -7,6 +7,8 @@ export class CanvasRenderer { this.lastRenderTime = 0; this.renderInterval = 1000 / 60; this.isDirty = false; + // Initialize overlay canvas + this.initOverlay(); } /** * Helper function to draw text with background at world coordinates @@ -158,6 +160,9 @@ export class CanvasRenderer { this.canvas.canvas.height = this.canvas.offscreenCanvas.height; } this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0); + // Ensure overlay canvas is in DOM and properly sized + this.addOverlayToDOM(); + this.updateOverlaySize(); // Update Batch Preview UI positions if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { this.canvas.batchPreviewManagers.forEach((manager) => { @@ -583,4 +588,125 @@ export class CanvasRenderer { padding: 8 }); } + /** + * Initialize overlay canvas for lightweight overlays like brush cursor + */ + initOverlay() { + // Setup overlay canvas to match main canvas + this.updateOverlaySize(); + // Position overlay canvas on top of main canvas + this.canvas.overlayCanvas.style.position = 'absolute'; + this.canvas.overlayCanvas.style.left = '0px'; + this.canvas.overlayCanvas.style.top = '0px'; + this.canvas.overlayCanvas.style.pointerEvents = 'none'; + this.canvas.overlayCanvas.style.zIndex = '20'; // Above other overlays + // Add overlay to DOM when main canvas is added + this.addOverlayToDOM(); + log.debug('Overlay canvas initialized'); + } + /** + * Add overlay canvas to DOM if main canvas has a parent + */ + addOverlayToDOM() { + if (this.canvas.canvas.parentElement && !this.canvas.overlayCanvas.parentElement) { + this.canvas.canvas.parentElement.appendChild(this.canvas.overlayCanvas); + log.debug('Overlay canvas added to DOM'); + } + } + /** + * Update overlay canvas size to match main canvas + */ + updateOverlaySize() { + if (this.canvas.overlayCanvas.width !== this.canvas.canvas.clientWidth || + this.canvas.overlayCanvas.height !== this.canvas.canvas.clientHeight) { + this.canvas.overlayCanvas.width = Math.max(1, this.canvas.canvas.clientWidth); + this.canvas.overlayCanvas.height = Math.max(1, this.canvas.canvas.clientHeight); + log.debug(`Overlay canvas resized to ${this.canvas.overlayCanvas.width}x${this.canvas.overlayCanvas.height}`); + } + } + /** + * Clear overlay canvas + */ + clearOverlay() { + this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height); + } + /** + * Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness + * @param worldPoint World coordinates of cursor + */ + drawMaskBrushCursor(worldPoint) { + if (!this.canvas.maskTool.isActive || !this.canvas.isMouseOver) { + this.clearOverlay(); + return; + } + // Update overlay size if needed + this.updateOverlaySize(); + // Clear previous cursor + this.clearOverlay(); + // Convert world coordinates to screen coordinates + const screenX = (worldPoint.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; + const screenY = (worldPoint.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; + // Get brush properties + const brushRadius = (this.canvas.maskTool.brushSize / 2) * this.canvas.viewport.zoom; + const brushStrength = this.canvas.maskTool.brushStrength; + const brushHardness = this.canvas.maskTool.brushHardness; + // Save context state + this.canvas.overlayCtx.save(); + // 1. Draw inner fill to visualize STRENGTH (opacity) + // Higher strength = more opaque fill + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${brushStrength * 0.2})`; // Max 20% opacity for visibility + this.canvas.overlayCtx.fill(); + // 2. Draw gradient edge to visualize HARDNESS + // Hard brush = sharp edge, Soft brush = gradient edge + if (brushHardness < 1) { + // Create radial gradient for soft brushes + const innerRadius = brushRadius * brushHardness; + const gradient = this.canvas.overlayCtx.createRadialGradient(screenX, screenY, innerRadius, screenX, screenY, brushRadius); + // Inner part is solid + gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + brushStrength * 0.3})`); + // Outer part fades based on hardness + gradient.addColorStop(1, `rgba(255, 255, 255, ${0.05})`); + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = gradient; + this.canvas.overlayCtx.fill(); + } + // 3. Draw outer circle (SIZE indicator) + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + // Make the stroke opacity also reflect strength slightly + const strokeOpacity = 0.4 + brushStrength * 0.4; // Range from 0.4 to 0.8 + this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`; + this.canvas.overlayCtx.lineWidth = 1.5; + // Use solid line for hard brushes, dashed for soft brushes + if (brushHardness > 0.8) { + // Hard brush - solid line + this.canvas.overlayCtx.setLineDash([]); + } + else { + // Soft brush - dashed line, dash length based on hardness + const dashLength = 2 + (1 - brushHardness) * 4; // Longer dashes for softer brushes + this.canvas.overlayCtx.setLineDash([dashLength, dashLength]); + } + this.canvas.overlayCtx.stroke(); + // 4. Optional: Draw center dot for very precise brushes + if (brushRadius < 5) { + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${strokeOpacity})`; + this.canvas.overlayCtx.fill(); + } + // Restore context state + this.canvas.overlayCtx.restore(); + } + /** + * Update overlay position when viewport changes + */ + updateOverlayPosition() { + // Overlay canvas is positioned absolutely, so it doesn't need repositioning + // Just ensure it's the right size + this.updateOverlaySize(); + } } diff --git a/js/MaskTool.js b/js/MaskTool.js index b4e67e2..7b99753 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -28,8 +28,8 @@ export class MaskTool { this.isOverlayVisible = true; this.isActive = false; this.brushSize = 20; - this.brushStrength = 0.5; - this.brushHardness = 0.5; + this._brushStrength = 0.5; + this._brushHardness = 0.5; this.isDrawing = false; this.lastPosition = null; const { canvas: previewCanvas, ctx: previewCtx } = createCanvas(1, 1, '2d', { willReadFrequently: true }); @@ -79,8 +79,15 @@ export class MaskTool { this.canvasInstance.canvas.parentElement.appendChild(this.previewCanvas); } } + // Getters for brush properties + get brushStrength() { + return this._brushStrength; + } + get brushHardness() { + return this._brushHardness; + } setBrushHardness(hardness) { - this.brushHardness = Math.max(0, Math.min(1, hardness)); + this._brushHardness = Math.max(0, Math.min(1, hardness)); } initMaskCanvas() { // Initialize chunked system @@ -671,7 +678,7 @@ export class MaskTool { this.brushSize = Math.max(1, size); } setBrushStrength(strength) { - this.brushStrength = Math.max(0, Math.min(1, strength)); + this._brushStrength = Math.max(0, Math.min(1, strength)); } handleMouseDown(worldCoords, viewCoords) { if (!this.isActive) @@ -697,6 +704,8 @@ export class MaskTool { handleMouseLeave() { this.previewVisible = false; this.clearPreview(); + // Clear overlay canvas when mouse leaves + this.canvasInstance.canvasRenderer.clearOverlay(); } handleMouseEnter() { this.previewVisible = true; @@ -767,13 +776,13 @@ export class MaskTool { chunk.ctx.moveTo(startLocal.x, startLocal.y); chunk.ctx.lineTo(endLocal.x, endLocal.y); const gradientRadius = this.brushSize / 2; - if (this.brushHardness === 1) { - chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`; + if (this._brushHardness === 1) { + chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this._brushStrength})`; } else { - const innerRadius = gradientRadius * this.brushHardness; + const innerRadius = gradientRadius * this._brushHardness; const gradient = chunk.ctx.createRadialGradient(endLocal.x, endLocal.y, innerRadius, endLocal.x, endLocal.y, gradientRadius); - gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`); + gradient.addColorStop(0, `rgba(255, 255, 255, ${this._brushStrength})`); gradient.addColorStop(1, `rgba(255, 255, 255, 0)`); chunk.ctx.strokeStyle = gradient; } @@ -903,18 +912,12 @@ export class MaskTool { } drawBrushPreview(viewCoords) { if (!this.previewVisible || this.isDrawing) { - this.clearPreview(); + this.canvasInstance.canvasRenderer.clearOverlay(); 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(); + // Use overlay canvas instead of preview canvas for brush cursor + const worldCoords = this.canvasInstance.lastMousePosition; + this.canvasInstance.canvasRenderer.drawMaskBrushCursor(worldCoords); } clearPreview() { this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); diff --git a/src/Canvas.ts b/src/Canvas.ts index 944623c..e01b573 100644 --- a/src/Canvas.ts +++ b/src/Canvas.ts @@ -84,6 +84,8 @@ export class Canvas { node: ComfyNode; offscreenCanvas: HTMLCanvasElement; offscreenCtx: CanvasRenderingContext2D | null; + overlayCanvas: HTMLCanvasElement; + overlayCtx: CanvasRenderingContext2D; onHistoryChange: ((historyInfo: { canUndo: boolean; canRedo: boolean; }) => void) | undefined; onViewportChange: (() => void) | null; onStateChange: (() => void) | undefined; @@ -122,6 +124,16 @@ export class Canvas { }); this.offscreenCanvas = offscreenCanvas; this.offscreenCtx = offscreenCtx; + + // Create overlay canvas for brush cursor and other lightweight overlays + const { canvas: overlayCanvas, ctx: overlayCtx } = createCanvas(0, 0, '2d', { + alpha: true, + willReadFrequently: false + }); + if (!overlayCtx) throw new Error("Could not create overlay canvas context"); + this.overlayCanvas = overlayCanvas; + this.overlayCtx = overlayCtx; + this.canvasContainer = null; this.dataInitialized = false; diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 8002c94..a60de62 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -327,7 +327,10 @@ export class CanvasInteractions { switch (this.interaction.mode) { case 'drawingMask': this.canvas.maskTool.handleMouseMove(coords.world, coords.view); - this.canvas.render(); + // Only render if actually drawing, not just moving cursor + if (this.canvas.maskTool.isDrawing) { + this.canvas.render(); + } break; case 'panning': this.panViewport(e); @@ -349,6 +352,10 @@ export class CanvasInteractions { break; default: this.updateCursor(coords.world); + // Update brush cursor on overlay if mask tool is active + if (this.canvas.maskTool.isActive) { + this.canvas.canvasRenderer.drawMaskBrushCursor(coords.world); + } break; } @@ -460,8 +467,17 @@ export class CanvasInteractions { const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1; this.performZoomOperation(coords.world, zoomFactor); } else { - // Layer transformation when layers are selected - this.handleLayerWheelTransformation(e); + // Check if mouse is over any selected layer + const isOverSelectedLayer = this.isPointInSelectedLayers(coords.world.x, coords.world.y); + + if (isOverSelectedLayer) { + // Layer transformation when layers are selected and mouse is over selected layer + this.handleLayerWheelTransformation(e); + } else { + // Zoom operation when mouse is not over selected layers + const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1; + this.performZoomOperation(coords.world, zoomFactor); + } } this.canvas.render(); diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts index beec414..6b66950 100644 --- a/src/CanvasRenderer.ts +++ b/src/CanvasRenderer.ts @@ -14,6 +14,9 @@ export class CanvasRenderer { this.lastRenderTime = 0; this.renderInterval = 1000 / 60; this.isDirty = false; + + // Initialize overlay canvas + this.initOverlay(); } /** @@ -205,6 +208,10 @@ export class CanvasRenderer { } this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0); + // Ensure overlay canvas is in DOM and properly sized + this.addOverlayToDOM(); + this.updateOverlaySize(); + // Update Batch Preview UI positions if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) { this.canvas.batchPreviewManagers.forEach((manager: any) => { @@ -710,4 +717,153 @@ export class CanvasRenderer { padding: 8 }); } + + /** + * Initialize overlay canvas for lightweight overlays like brush cursor + */ + initOverlay(): void { + // Setup overlay canvas to match main canvas + this.updateOverlaySize(); + + // Position overlay canvas on top of main canvas + this.canvas.overlayCanvas.style.position = 'absolute'; + this.canvas.overlayCanvas.style.left = '0px'; + this.canvas.overlayCanvas.style.top = '0px'; + this.canvas.overlayCanvas.style.pointerEvents = 'none'; + this.canvas.overlayCanvas.style.zIndex = '20'; // Above other overlays + + // Add overlay to DOM when main canvas is added + this.addOverlayToDOM(); + + log.debug('Overlay canvas initialized'); + } + + /** + * Add overlay canvas to DOM if main canvas has a parent + */ + addOverlayToDOM(): void { + if (this.canvas.canvas.parentElement && !this.canvas.overlayCanvas.parentElement) { + this.canvas.canvas.parentElement.appendChild(this.canvas.overlayCanvas); + log.debug('Overlay canvas added to DOM'); + } + } + + /** + * Update overlay canvas size to match main canvas + */ + updateOverlaySize(): void { + if (this.canvas.overlayCanvas.width !== this.canvas.canvas.clientWidth || + this.canvas.overlayCanvas.height !== this.canvas.canvas.clientHeight) { + + this.canvas.overlayCanvas.width = Math.max(1, this.canvas.canvas.clientWidth); + this.canvas.overlayCanvas.height = Math.max(1, this.canvas.canvas.clientHeight); + + log.debug(`Overlay canvas resized to ${this.canvas.overlayCanvas.width}x${this.canvas.overlayCanvas.height}`); + } + } + + /** + * Clear overlay canvas + */ + clearOverlay(): void { + this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height); + } + + /** + * Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness + * @param worldPoint World coordinates of cursor + */ + drawMaskBrushCursor(worldPoint: { x: number, y: number }): void { + if (!this.canvas.maskTool.isActive || !this.canvas.isMouseOver) { + this.clearOverlay(); + return; + } + + // Update overlay size if needed + this.updateOverlaySize(); + + // Clear previous cursor + this.clearOverlay(); + + // Convert world coordinates to screen coordinates + const screenX = (worldPoint.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; + const screenY = (worldPoint.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; + + // Get brush properties + const brushRadius = (this.canvas.maskTool.brushSize / 2) * this.canvas.viewport.zoom; + const brushStrength = this.canvas.maskTool.brushStrength; + const brushHardness = this.canvas.maskTool.brushHardness; + + // Save context state + this.canvas.overlayCtx.save(); + + // 1. Draw inner fill to visualize STRENGTH (opacity) + // Higher strength = more opaque fill + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${brushStrength * 0.2})`; // Max 20% opacity for visibility + this.canvas.overlayCtx.fill(); + + // 2. Draw gradient edge to visualize HARDNESS + // Hard brush = sharp edge, Soft brush = gradient edge + if (brushHardness < 1) { + // Create radial gradient for soft brushes + const innerRadius = brushRadius * brushHardness; + const gradient = this.canvas.overlayCtx.createRadialGradient( + screenX, screenY, innerRadius, + screenX, screenY, brushRadius + ); + + // Inner part is solid + gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + brushStrength * 0.3})`); + // Outer part fades based on hardness + gradient.addColorStop(1, `rgba(255, 255, 255, ${0.05})`); + + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = gradient; + this.canvas.overlayCtx.fill(); + } + + // 3. Draw outer circle (SIZE indicator) + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI); + + // Make the stroke opacity also reflect strength slightly + const strokeOpacity = 0.4 + brushStrength * 0.4; // Range from 0.4 to 0.8 + this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`; + this.canvas.overlayCtx.lineWidth = 1.5; + + // Use solid line for hard brushes, dashed for soft brushes + if (brushHardness > 0.8) { + // Hard brush - solid line + this.canvas.overlayCtx.setLineDash([]); + } else { + // Soft brush - dashed line, dash length based on hardness + const dashLength = 2 + (1 - brushHardness) * 4; // Longer dashes for softer brushes + this.canvas.overlayCtx.setLineDash([dashLength, dashLength]); + } + + this.canvas.overlayCtx.stroke(); + + // 4. Optional: Draw center dot for very precise brushes + if (brushRadius < 5) { + this.canvas.overlayCtx.beginPath(); + this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI); + this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${strokeOpacity})`; + this.canvas.overlayCtx.fill(); + } + + // Restore context state + this.canvas.overlayCtx.restore(); + } + + /** + * Update overlay position when viewport changes + */ + updateOverlayPosition(): void { + // Overlay canvas is positioned absolutely, so it doesn't need repositioning + // Just ensure it's the right size + this.updateOverlaySize(); + } } diff --git a/src/MaskTool.ts b/src/MaskTool.ts index 015d6d7..089ba71 100644 --- a/src/MaskTool.ts +++ b/src/MaskTool.ts @@ -21,9 +21,9 @@ interface MaskChunk { } export class MaskTool { - private brushHardness: number; - private brushSize: number; - private brushStrength: number; + private _brushHardness: number; + public brushSize: number; + private _brushStrength: number; private canvasInstance: Canvas & { canvasState: CanvasState, width: number, height: number }; public isActive: boolean; public isDrawing: boolean; @@ -96,8 +96,8 @@ export class MaskTool { this.isOverlayVisible = true; this.isActive = false; this.brushSize = 20; - this.brushStrength = 0.5; - this.brushHardness = 0.5; + this._brushStrength = 0.5; + this._brushHardness = 0.5; this.isDrawing = false; this.lastPosition = null; @@ -156,8 +156,17 @@ export class MaskTool { } } + // Getters for brush properties + get brushStrength(): number { + return this._brushStrength; + } + + get brushHardness(): number { + return this._brushHardness; + } + setBrushHardness(hardness: number): void { - this.brushHardness = Math.max(0, Math.min(1, hardness)); + this._brushHardness = Math.max(0, Math.min(1, hardness)); } initMaskCanvas(): void { @@ -867,7 +876,7 @@ export class MaskTool { } setBrushStrength(strength: number): void { - this.brushStrength = Math.max(0, Math.min(1, strength)); + this._brushStrength = Math.max(0, Math.min(1, strength)); } handleMouseDown(worldCoords: Point, viewCoords: Point): void { @@ -898,6 +907,8 @@ export class MaskTool { handleMouseLeave(): void { this.previewVisible = false; this.clearPreview(); + // Clear overlay canvas when mouse leaves + this.canvasInstance.canvasRenderer.clearOverlay(); } handleMouseEnter(): void { @@ -982,15 +993,15 @@ export class MaskTool { const gradientRadius = this.brushSize / 2; - if (this.brushHardness === 1) { - chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`; + if (this._brushHardness === 1) { + chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this._brushStrength})`; } else { - const innerRadius = gradientRadius * this.brushHardness; + const innerRadius = gradientRadius * this._brushHardness; const gradient = chunk.ctx.createRadialGradient( endLocal.x, endLocal.y, innerRadius, endLocal.x, endLocal.y, gradientRadius ); - gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`); + gradient.addColorStop(0, `rgba(255, 255, 255, ${this._brushStrength})`); gradient.addColorStop(1, `rgba(255, 255, 255, 0)`); chunk.ctx.strokeStyle = gradient; } @@ -1142,20 +1153,13 @@ export class MaskTool { drawBrushPreview(viewCoords: Point): void { if (!this.previewVisible || this.isDrawing) { - this.clearPreview(); + this.canvasInstance.canvasRenderer.clearOverlay(); 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(); + // Use overlay canvas instead of preview canvas for brush cursor + const worldCoords = this.canvasInstance.lastMousePosition; + this.canvasInstance.canvasRenderer.drawMaskBrushCursor(worldCoords); } clearPreview(): void {