From e0efcefd21ac6015b2b12768057719beab1f32fc Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Thu, 24 Jul 2025 07:44:28 -0400 Subject: [PATCH 1/6] Fixed square brush with hardness <1; improved the effect of hardness, improved the effect of smoothing precision --- src/extensions/core/maskeditor.ts | 169 +++++++++++++++++++----------- 1 file changed, 110 insertions(+), 59 deletions(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index c07a3d84de..dffdc3df9e 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -2285,9 +2285,23 @@ class BrushTool { totalLength += Math.sqrt(dx * dx + dy * dy) } - const distanceBetweenPoints = - (this.brushSettings.size / this.brushSettings.smoothingPrecision) * 6 - const stepNr = Math.ceil(totalLength / distanceBetweenPoints) + const maxSteps = 30 + const minSteps = 2 + + // Convert 1-100 range to 0-1 range + const smoothing = Math.min( + Math.max(this.brushSettings.smoothingPrecision, 1), + 100 + ) // clamp 1-100 + const normalizedSmoothing = (smoothing - 1) / 99 // Convert to 0-1 range + + // Optionality to use exponential curve + const stepNr = Math.round( + minSteps + (maxSteps - minSteps) * Math.pow(normalizedSmoothing, 1) + ) + + // Calculate step distance capped by brush size + const distanceBetweenPoints = totalLength / stepNr let interpolatedPoints = points @@ -2435,69 +2449,105 @@ class BrushTool { const hardness = brushSettings.hardness const x = point.x const y = point.y - // Extend the gradient radius beyond the brush size - const extendedSize = size * (2 - hardness) + + // Keep brush size constant - only the gradient changes with hardness + const brushRadius = size const isErasing = maskCtx.globalCompositeOperation === 'destination-out' const currentTool = await this.messageBroker.pull('currentTool') - // handle paint pen + // Helper function to draw soft square brush + const drawSoftSquare = ( + ctx: CanvasRenderingContext2D, + color: string, + centerOpacity: number + ) => { + const steps = 10 + const hardRadius = brushRadius * hardness + + for (let i = 0; i < steps; i++) { + const progress = i / (steps - 1) + const currentRadius = hardRadius + (brushRadius - hardRadius) * progress + const currentOpacity = centerOpacity * (1 - progress) + + ctx.fillStyle = color.replace(/[\d.]+\)$/, `${currentOpacity})`) + ctx.beginPath() + ctx.rect( + x - currentRadius, + y - currentRadius, + currentRadius * 2, + currentRadius * 2 + ) + ctx.fill() + } + } + + // RGB brush logic if ( this.activeLayer === 'rgb' && (currentTool === Tools.Eraser || currentTool === Tools.PaintPen) ) { const rgbaColor = this.formatRgba(this.rgbColor, opacity) - let gradient = rgbCtx.createRadialGradient(x, y, 0, x, y, extendedSize) + + if (brushType === BrushShape.Rect && hardness < 1) { + drawSoftSquare(rgbCtx, rgbaColor, opacity) + return + } + + // Original logic for circles and hard squares + let gradient = rgbCtx.createRadialGradient(x, y, 0, x, y, brushRadius) + if (hardness === 1) { gradient.addColorStop(0, rgbaColor) - gradient.addColorStop( - 1, - this.formatRgba(this.rgbColor, brushSettingsSliderOpacity) - ) + gradient.addColorStop(1, rgbaColor) } else { gradient.addColorStop(0, rgbaColor) - gradient.addColorStop(hardness, rgbaColor) + gradient.addColorStop( + hardness, + this.formatRgba(this.rgbColor, opacity * 0.5) + ) gradient.addColorStop(1, this.formatRgba(this.rgbColor, 0)) } + rgbCtx.fillStyle = gradient rgbCtx.beginPath() if (brushType === BrushShape.Rect) { rgbCtx.rect( - x - extendedSize, - y - extendedSize, - extendedSize * 2, - extendedSize * 2 + x - brushRadius, + y - brushRadius, + brushRadius * 2, + brushRadius * 2 ) } else { - rgbCtx.arc(x, y, extendedSize, 0, Math.PI * 2, false) + rgbCtx.arc(x, y, brushRadius, 0, Math.PI * 2, false) } rgbCtx.fill() return } - let gradient = maskCtx.createRadialGradient(x, y, 0, x, y, extendedSize) + // Mask brush logic + if (brushType === BrushShape.Rect && hardness < 1) { + const baseColor = isErasing + ? `rgba(255, 255, 255, ${opacity})` + : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` + + drawSoftSquare(maskCtx, baseColor, opacity) + return + } + + // Original logic for circles and hard squares + let gradient = maskCtx.createRadialGradient(x, y, 0, x, y, brushRadius) + if (hardness === 1) { - gradient.addColorStop( - 0, - isErasing - ? `rgba(255, 255, 255, ${opacity})` - : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - ) - gradient.addColorStop( - 1, - isErasing - ? `rgba(255, 255, 255, ${opacity})` - : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - ) + const solidColor = isErasing + ? `rgba(255, 255, 255, ${opacity})` + : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` + gradient.addColorStop(0, solidColor) + gradient.addColorStop(1, solidColor) } else { - let softness = 1 - hardness - let innerStop = Math.max(0, hardness - softness) - let outerStop = size / extendedSize - if (isErasing) { gradient.addColorStop(0, `rgba(255, 255, 255, ${opacity})`) - gradient.addColorStop(innerStop, `rgba(255, 255, 255, ${opacity})`) - gradient.addColorStop(outerStop, `rgba(255, 255, 255, ${opacity / 2})`) + gradient.addColorStop(hardness, `rgba(255, 255, 255, ${opacity * 0.5})`) gradient.addColorStop(1, `rgba(255, 255, 255, 0)`) } else { gradient.addColorStop( @@ -2505,12 +2555,8 @@ class BrushTool { `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` ) gradient.addColorStop( - innerStop, - `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - ) - gradient.addColorStop( - outerStop, - `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity / 2})` + hardness, + `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity * 0.5})` ) gradient.addColorStop( 1, @@ -2523,13 +2569,13 @@ class BrushTool { maskCtx.beginPath() if (brushType === BrushShape.Rect) { maskCtx.rect( - x - extendedSize, - y - extendedSize, - extendedSize * 2, - extendedSize * 2 + x - brushRadius, + y - brushRadius, + brushRadius * 2, + brushRadius * 2 ) } else { - maskCtx.arc(x, y, extendedSize, 0, Math.PI * 2, false) + maskCtx.arc(x, y, brushRadius, 0, Math.PI * 2, false) } maskCtx.fill() } @@ -4179,30 +4225,35 @@ class UIManager { const centerY = cursorPoint.y + pan_offset.y const brush = this.brush const hardness = brushSettings.hardness - const extendedSize = brushSettings.size * (2 - hardness) * 2 * zoom_ratio + + // Now that brush size is constant, preview is simple + const brushRadius = brushSettings.size * zoom_ratio + const previewSize = brushRadius * 2 this.brushSizeSlider.value = String(brushSettings.size) this.brushHardnessSlider.value = String(hardness) - brush.style.width = extendedSize + 'px' - brush.style.height = extendedSize + 'px' - brush.style.left = centerX - extendedSize / 2 + 'px' - brush.style.top = centerY - extendedSize / 2 + 'px' + brush.style.width = previewSize + 'px' + brush.style.height = previewSize + 'px' + brush.style.left = centerX - brushRadius + 'px' + brush.style.top = centerY - brushRadius + 'px' if (hardness === 1) { this.brushPreviewGradient.style.background = 'rgba(255, 0, 0, 0.5)' return } - const opacityStop = hardness / 4 + 0.25 + // Simplified gradient - hardness controls where the fade starts + const midStop = hardness * 100 + const outerStop = 100 this.brushPreviewGradient.style.background = ` - radial-gradient( - circle, - rgba(255, 0, 0, 0.5) 0%, - rgba(255, 0, 0, ${opacityStop}) ${hardness * 100}%, - rgba(255, 0, 0, 0) 100% - ) + radial-gradient( + circle, + rgba(255, 0, 0, 0.5) 0%, + rgba(255, 0, 0, 0.25) ${midStop}%, + rgba(255, 0, 0, 0) ${outerStop}% + ) ` } From 78572a861b55b8b21bc8d0634e00f6c31f5493f1 Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Mon, 25 Aug 2025 16:16:03 -0400 Subject: [PATCH 2/6] Improved square hardness and code quality with performance optimizations --- src/extensions/core/maskeditor.ts | 85 ++++++++++++++++++++++--------- src/utils/colorUtil.ts | 53 +++++++++++++++++++ 2 files changed, 115 insertions(+), 23 deletions(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index dffdc3df9e..9bee87a211 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -9,6 +9,7 @@ import { ComfyApp } from '../../scripts/app' import { $el, ComfyDialog } from '../../scripts/ui' import { getStorageValue, setStorageValue } from '../../scripts/utils' import { hexToRgb } from '../../utils/colorUtil' +import { parseToRgb } from '../../utils/colorUtil' import { ClipspaceDialog } from './clipspace' import { imageLayerFilenamesByTimestamp, @@ -2052,6 +2053,10 @@ class BrushTool { brushStrokeCanvas: HTMLCanvasElement | null = null brushStrokeCtx: CanvasRenderingContext2D | null = null + private static readonly SMOOTHING_MAX_STEPS = 30 + private static readonly SMOOTHING_MIN_STEPS = 2 + //private static readonly SOFT_BRUSH_STEPS = 20 + //brush adjustment isBrushAdjusting: boolean = false brushPreviewGradient: HTMLElement | null = null @@ -2254,6 +2259,10 @@ class BrushTool { } } + private clampSmoothingPrecision(value: number): number { + return Math.min(Math.max(value, 1), 100) + } + private drawWithBetterSmoothing(point: Point) { // Add current point to the smoothing array if (!this.smoothingCordsArray) { @@ -2285,19 +2294,17 @@ class BrushTool { totalLength += Math.sqrt(dx * dx + dy * dy) } - const maxSteps = 30 - const minSteps = 2 + const maxSteps = BrushTool.SMOOTHING_MAX_STEPS + const minSteps = BrushTool.SMOOTHING_MIN_STEPS - // Convert 1-100 range to 0-1 range - const smoothing = Math.min( - Math.max(this.brushSettings.smoothingPrecision, 1), - 100 - ) // clamp 1-100 + const smoothing = this.clampSmoothingPrecision( + this.brushSettings.smoothingPrecision + ) const normalizedSmoothing = (smoothing - 1) / 99 // Convert to 0-1 range // Optionality to use exponential curve const stepNr = Math.round( - minSteps + (maxSteps - minSteps) * Math.pow(normalizedSmoothing, 1) + Math.round(minSteps + (maxSteps - minSteps) * normalizedSmoothing) ) // Calculate step distance capped by brush size @@ -2462,24 +2469,56 @@ class BrushTool { color: string, centerOpacity: number ) => { - const steps = 10 + const tempCanvas = document.createElement('canvas') + const tempCtx = tempCanvas.getContext('2d') + + if (!tempCtx) return + + const size = brushRadius * 2 + tempCanvas.width = size + tempCanvas.height = size + + const centerX = size / 2 + const centerY = size / 2 const hardRadius = brushRadius * hardness - for (let i = 0; i < steps; i++) { - const progress = i / (steps - 1) - const currentRadius = hardRadius + (brushRadius - hardRadius) * progress - const currentOpacity = centerOpacity * (1 - progress) - - ctx.fillStyle = color.replace(/[\d.]+\)$/, `${currentOpacity})`) - ctx.beginPath() - ctx.rect( - x - currentRadius, - y - currentRadius, - currentRadius * 2, - currentRadius * 2 - ) - ctx.fill() + // Create ImageData to manually set pixel opacities + const imageData = tempCtx.createImageData(size, size) + const data = imageData.data + + // Use parseToRgb from colorUtil + const { r, g, b } = parseToRgb(color) + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const index = (y * size + x) * 4 + + // Calculate distance from center to edge of square + const distFromCenterX = Math.abs(x - centerX) + const distFromCenterY = Math.abs(y - centerY) + const distFromEdge = Math.max(distFromCenterX, distFromCenterY) // Square distance + + let opacity = 0 + + if (distFromEdge <= hardRadius) { + // Inside hard area - full opacity + opacity = centerOpacity + } else if (distFromEdge <= brushRadius) { + // In soft area - fade out + const fadeProgress = + (distFromEdge - hardRadius) / (brushRadius - hardRadius) + opacity = centerOpacity * (1 - fadeProgress) + } + + data[index] = r // Red + data[index + 1] = g // Green + data[index + 2] = b // Blue + data[index + 3] = opacity * 255 // Alpha + } } + + tempCtx.putImageData(imageData, 0, 0) + ctx.drawImage(tempCanvas, x - brushRadius, y - brushRadius) } // RGB brush logic diff --git a/src/utils/colorUtil.ts b/src/utils/colorUtil.ts index c57bc14ad7..0d80ff0069 100644 --- a/src/utils/colorUtil.ts +++ b/src/utils/colorUtil.ts @@ -59,6 +59,59 @@ export function hexToRgb(hex: string): RGB { return { r, g, b } } +export function parseToRgb(color: string): RGB { + const format = identifyColorFormat(color) + if (!format) return { r: 0, g: 0, b: 0 } + + const hsla = parseToHSLA(color, format) + if (!isHSLA(hsla)) return { r: 0, g: 0, b: 0 } + + // Convert HSL to RGB + const h = hsla.h / 360 + const s = hsla.s / 100 + const l = hsla.l / 100 + + const c = (1 - Math.abs(2 * l - 1)) * s + const x = c * (1 - Math.abs(((h * 6) % 2) - 1)) + const m = l - c / 2 + + let r = 0, + g = 0, + b = 0 + + if (h < 1 / 6) { + r = c + g = x + b = 0 + } else if (h < 2 / 6) { + r = x + g = c + b = 0 + } else if (h < 3 / 6) { + r = 0 + g = c + b = x + } else if (h < 4 / 6) { + r = 0 + g = x + b = c + } else if (h < 5 / 6) { + r = x + g = 0 + b = c + } else { + r = c + g = 0 + b = x + } + + return { + r: Math.round((r + m) * 255), + g: Math.round((g + m) * 255), + b: Math.round((b + m) * 255) + } +} + const identifyColorFormat = (color: string): ColorFormat | null => { if (!color) return null if (color.startsWith('#') && (color.length === 4 || color.length === 7)) From 18ad6b316760ef89d780dc05ab918cb82feab31b Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Fri, 29 Aug 2025 20:02:55 -0400 Subject: [PATCH 3/6] Fix brush rendering anti-aliasing and optimized square brushes using texture caching --- src/extensions/core/maskeditor.ts | 187 +++++++++++++++++++----------- 1 file changed, 118 insertions(+), 69 deletions(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index b1c9b51e33..aed9cde0df 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -2050,6 +2050,8 @@ class BrushTool { rgbCtx: CanvasRenderingContext2D | null = null initialDraw: boolean = true + private static brushTextureCache = new Map() + brushStrokeCanvas: HTMLCanvasElement | null = null brushStrokeCtx: CanvasRenderingContext2D | null = null @@ -2457,68 +2459,77 @@ class BrushTool { const x = point.x const y = point.y - // Keep brush size constant - only the gradient changes with hardness const brushRadius = size - const isErasing = maskCtx.globalCompositeOperation === 'destination-out' const currentTool = await this.messageBroker.pull('currentTool') - // Helper function to draw soft square brush - const drawSoftSquare = ( - ctx: CanvasRenderingContext2D, + // Helper function to get or create cached brush texture + const getCachedBrushTexture = ( + radius: number, + hardness: number, color: string, - centerOpacity: number - ) => { - const tempCanvas = document.createElement('canvas') - const tempCtx = tempCanvas.getContext('2d') + opacity: number + ): HTMLCanvasElement => { + const cacheKey = `${radius}_${hardness}_${color}_${opacity}` - if (!tempCtx) return + if (BrushTool.brushTextureCache.has(cacheKey)) { + return BrushTool.brushTextureCache.get(cacheKey)! + } - const size = brushRadius * 2 + const tempCanvas = document.createElement('canvas') + const tempCtx = tempCanvas.getContext('2d')! + const size = radius * 2 tempCanvas.width = size tempCanvas.height = size const centerX = size / 2 const centerY = size / 2 - const hardRadius = brushRadius * hardness + const hardRadius = radius * hardness - // Create ImageData to manually set pixel opacities const imageData = tempCtx.createImageData(size, size) const data = imageData.data - - // Use parseToRgb from colorUtil const { r, g, b } = parseToRgb(color) + // Pre-calculate values to avoid repeated computations + const fadeRange = radius - hardRadius + for (let y = 0; y < size; y++) { + const dy = y - centerY for (let x = 0; x < size; x++) { + const dx = x - centerX const index = (y * size + x) * 4 - // Calculate distance from center to edge of square - const distFromCenterX = Math.abs(x - centerX) - const distFromCenterY = Math.abs(y - centerY) - const distFromEdge = Math.max(distFromCenterX, distFromCenterY) // Square distance - - let opacity = 0 + // Calculate square distance (Chebyshev distance) + const distFromEdge = Math.max(Math.abs(dx), Math.abs(dy)) + let pixelOpacity = 0 if (distFromEdge <= hardRadius) { - // Inside hard area - full opacity - opacity = centerOpacity - } else if (distFromEdge <= brushRadius) { - // In soft area - fade out - const fadeProgress = - (distFromEdge - hardRadius) / (brushRadius - hardRadius) - opacity = centerOpacity * (1 - fadeProgress) + pixelOpacity = opacity + } else if (distFromEdge <= radius) { + const fadeProgress = (distFromEdge - hardRadius) / fadeRange + pixelOpacity = opacity * (1 - fadeProgress) } - data[index] = r // Red - data[index + 1] = g // Green - data[index + 2] = b // Blue - data[index + 3] = opacity * 255 // Alpha + data[index] = r + data[index + 1] = g + data[index + 2] = b + data[index + 3] = pixelOpacity * 255 } } tempCtx.putImageData(imageData, 0, 0) - ctx.drawImage(tempCanvas, x - brushRadius, y - brushRadius) + + // Cache the texture (with reasonable cache size limit) + if (BrushTool.brushTextureCache.size > 50) { + // Remove oldest entries when cache gets too large + const firstKey = BrushTool.brushTextureCache.keys().next().value + if (firstKey) { + BrushTool.brushTextureCache.delete(firstKey) + } + } + BrushTool.brushTextureCache.set(cacheKey, tempCanvas) + + return tempCanvas } // RGB brush logic @@ -2529,25 +2540,43 @@ class BrushTool { const rgbaColor = this.formatRgba(this.rgbColor, opacity) if (brushType === BrushShape.Rect && hardness < 1) { - drawSoftSquare(rgbCtx, rgbaColor, opacity) + const brushTexture = getCachedBrushTexture( + brushRadius, + hardness, + rgbaColor, + opacity + ) + rgbCtx.drawImage(brushTexture, x - brushRadius, y - brushRadius) return } - // Original logic for circles and hard squares - let gradient = rgbCtx.createRadialGradient(x, y, 0, x, y, brushRadius) - + // For max hardness, use solid fill to avoid anti-aliasing if (hardness === 1) { - gradient.addColorStop(0, rgbaColor) - gradient.addColorStop(1, rgbaColor) - } else { - gradient.addColorStop(0, rgbaColor) - gradient.addColorStop( - hardness, - this.formatRgba(this.rgbColor, opacity * 0.5) - ) - gradient.addColorStop(1, this.formatRgba(this.rgbColor, 0)) + rgbCtx.fillStyle = rgbaColor + rgbCtx.beginPath() + if (brushType === BrushShape.Rect) { + rgbCtx.rect( + x - brushRadius, + y - brushRadius, + brushRadius * 2, + brushRadius * 2 + ) + } else { + rgbCtx.arc(x, y, brushRadius, 0, Math.PI * 2, false) + } + rgbCtx.fill() + return } + // For soft brushes, use gradient + let gradient = rgbCtx.createRadialGradient(x, y, 0, x, y, brushRadius) + gradient.addColorStop(0, rgbaColor) + gradient.addColorStop( + hardness, + this.formatRgba(this.rgbColor, opacity * 0.5) + ) + gradient.addColorStop(1, this.formatRgba(this.rgbColor, 0)) + rgbCtx.fillStyle = gradient rgbCtx.beginPath() if (brushType === BrushShape.Rect) { @@ -2570,38 +2599,58 @@ class BrushTool { ? `rgba(255, 255, 255, ${opacity})` : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - drawSoftSquare(maskCtx, baseColor, opacity) + const brushTexture = getCachedBrushTexture( + brushRadius, + hardness, + baseColor, + opacity + ) + maskCtx.drawImage(brushTexture, x - brushRadius, y - brushRadius) return } - // Original logic for circles and hard squares - let gradient = maskCtx.createRadialGradient(x, y, 0, x, y, brushRadius) - + // For max hardness, use solid fill to avoid anti-aliasing if (hardness === 1) { const solidColor = isErasing ? `rgba(255, 255, 255, ${opacity})` : `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - gradient.addColorStop(0, solidColor) - gradient.addColorStop(1, solidColor) - } else { - if (isErasing) { - gradient.addColorStop(0, `rgba(255, 255, 255, ${opacity})`) - gradient.addColorStop(hardness, `rgba(255, 255, 255, ${opacity * 0.5})`) - gradient.addColorStop(1, `rgba(255, 255, 255, 0)`) - } else { - gradient.addColorStop( - 0, - `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` - ) - gradient.addColorStop( - hardness, - `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity * 0.5})` - ) - gradient.addColorStop( - 1, - `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, 0)` + + maskCtx.fillStyle = solidColor + maskCtx.beginPath() + if (brushType === BrushShape.Rect) { + maskCtx.rect( + x - brushRadius, + y - brushRadius, + brushRadius * 2, + brushRadius * 2 ) + } else { + maskCtx.arc(x, y, brushRadius, 0, Math.PI * 2, false) } + maskCtx.fill() + return + } + + // For soft brushes, use gradient + let gradient = maskCtx.createRadialGradient(x, y, 0, x, y, brushRadius) + + if (isErasing) { + gradient.addColorStop(0, `rgba(255, 255, 255, ${opacity})`) + gradient.addColorStop(hardness, `rgba(255, 255, 255, ${opacity * 0.5})`) + gradient.addColorStop(1, `rgba(255, 255, 255, 0)`) + } else { + gradient.addColorStop( + 0, + `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity})` + ) + gradient.addColorStop( + hardness, + `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${opacity * 0.5})` + ) + gradient.addColorStop( + 1, + `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, 0)` + ) } maskCtx.fillStyle = gradient From 9aa7cd46892dfe716dd1a9031e2aaf9b6d40e01e Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Sun, 31 Aug 2025 17:26:10 -0400 Subject: [PATCH 4/6] Switched to QuickLRU for brush cache --- src/extensions/core/maskeditor.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index aed9cde0df..85b4a1f037 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -1,3 +1,4 @@ +import QuickLRU from '@alloc/quick-lru' import { debounce } from 'es-toolkit/compat' import _ from 'es-toolkit/compat' @@ -755,12 +756,12 @@ styleSheet.type = 'text/css' styleSheet.innerText = styles document.head.appendChild(styleSheet) -enum BrushShape { +export enum BrushShape { Arc = 'arc', Rect = 'rect' } -enum Tools { +export enum Tools { MaskPen = 'pen', PaintPen = 'rgbPaint', Eraser = 'eraser', @@ -2036,7 +2037,7 @@ class ColorSelectTool { } } -class BrushTool { +export class BrushTool { brushSettings: Brush //this saves the current brush settings maskBlendMode: MaskBlendMode @@ -2050,7 +2051,9 @@ class BrushTool { rgbCtx: CanvasRenderingContext2D | null = null initialDraw: boolean = true - private static brushTextureCache = new Map() + private static brushTextureCache = new QuickLRU({ + maxSize: 8 // Reasonable limit for brush texture variations? + }) brushStrokeCanvas: HTMLCanvasElement | null = null brushStrokeCtx: CanvasRenderingContext2D | null = null @@ -2519,14 +2522,7 @@ class BrushTool { tempCtx.putImageData(imageData, 0, 0) - // Cache the texture (with reasonable cache size limit) - if (BrushTool.brushTextureCache.size > 50) { - // Remove oldest entries when cache gets too large - const firstKey = BrushTool.brushTextureCache.keys().next().value - if (firstKey) { - BrushTool.brushTextureCache.delete(firstKey) - } - } + // Cache the texture BrushTool.brushTextureCache.set(cacheKey, tempCanvas) return tempCanvas From b0b0519c48070f29f2f8bab1c8e5e6a4cacb9a20 Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Sun, 31 Aug 2025 17:32:06 -0400 Subject: [PATCH 5/6] Cleaned up exports from testing --- src/extensions/core/maskeditor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index 85b4a1f037..c9a6611c5b 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -756,12 +756,12 @@ styleSheet.type = 'text/css' styleSheet.innerText = styles document.head.appendChild(styleSheet) -export enum BrushShape { +enum BrushShape { Arc = 'arc', Rect = 'rect' } -export enum Tools { +enum Tools { MaskPen = 'pen', PaintPen = 'rgbPaint', Eraser = 'eraser', @@ -813,7 +813,7 @@ interface Offset { y: number } -export interface Brush { +interface Brush { type: BrushShape size: number opacity: number @@ -2037,7 +2037,7 @@ class ColorSelectTool { } } -export class BrushTool { +class BrushTool { brushSettings: Brush //this saves the current brush settings maskBlendMode: MaskBlendMode From fff29632186c24de2b965ecbf81821acd4bc16ca Mon Sep 17 00:00:00 2001 From: brucew4yn3rp Date: Sun, 31 Aug 2025 18:04:04 -0400 Subject: [PATCH 6/6] Removed SOFT_BRUSH_STEPS unused variable --- src/extensions/core/maskeditor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extensions/core/maskeditor.ts b/src/extensions/core/maskeditor.ts index c9a6611c5b..ff9036723a 100644 --- a/src/extensions/core/maskeditor.ts +++ b/src/extensions/core/maskeditor.ts @@ -2060,7 +2060,6 @@ class BrushTool { private static readonly SMOOTHING_MAX_STEPS = 30 private static readonly SMOOTHING_MIN_STEPS = 2 - //private static readonly SOFT_BRUSH_STEPS = 20 //brush adjustment isBrushAdjusting: boolean = false