GPU accelerated maskeditor rendering (#6767)

## GPU accelerated brush engine for the mask editor

- Full GPU acceleration using TypeGPU and type-safe shaders
- Catmull-Rom Spline Smoothing
- arc-length equidistant resampling
- much improved performance, even for huge images
- photoshop like opacity clamping for brush strokes
- much improved soft brushes
- fallback to CPU fully implemented, much improved CPU rendering
features as well

### Tested Browsers
- Chrome (fully supported)
- Safari 26 (fully supported, prev versions CPU fallback)
- Firefox (CPU fallback, flags needed for full support)



https://github.com/user-attachments/assets/b7b5cb8a-2290-4a95-ae7d-180e11fccdb0



https://github.com/user-attachments/assets/4297aaa5-f249-499a-9b74-869677f1c73b



https://github.com/user-attachments/assets/602b4783-3e2b-489e-bcb9-70534bcaac5e

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6767-GPU-accelerated-maskeditor-rendering-2b16d73d3650818cb294e1fca03f6169)
by [Unito](https://www.unito.io)
This commit is contained in:
Tristan Sommer
2025-11-22 15:07:16 +01:00
committed by GitHub
parent 1dbb3fc1b9
commit 4adcf09cca
24 changed files with 2945 additions and 409 deletions

View File

@@ -22,7 +22,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
size: 10,
opacity: 0.7,
hardness: 1,
smoothingPrecision: 10
stepSize: 10
})
const maskBlendMode = ref<MaskBlendMode>(MaskBlendMode.Black)
@@ -50,6 +50,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
const panOffset = ref<Offset>({ x: 0, y: 0 })
const cursorPoint = ref<Point>({ x: 0, y: 0 })
const resetZoomTrigger = ref<number>(0)
const clearTrigger = ref<number>(0)
const maskCanvas = ref<HTMLCanvasElement | null>(null)
const maskCtx = ref<CanvasRenderingContext2D | null>(null)
@@ -70,6 +71,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
const canvasHistory = useCanvasHistory(20)
const tgpuRoot = ref<any>(null)
watch(maskCanvas, (canvas) => {
if (canvas) {
maskCtx.value = canvas.getContext('2d', { willReadFrequently: true })
@@ -110,7 +113,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
})
function setBrushSize(size: number): void {
brushSettings.value.size = _.clamp(size, 1, 100)
brushSettings.value.size = _.clamp(size, 1, 500)
}
function setBrushOpacity(opacity: number): void {
@@ -121,8 +124,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
brushSettings.value.hardness = _.clamp(hardness, 0, 1)
}
function setBrushSmoothingPrecision(precision: number): void {
brushSettings.value.smoothingPrecision = _.clamp(precision, 1, 100)
function setBrushStepSize(step: number): void {
brushSettings.value.stepSize = _.clamp(step, 1, 100)
}
function resetBrushToDefault(): void {
@@ -130,7 +133,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
brushSettings.value.size = 20
brushSettings.value.opacity = 1
brushSettings.value.hardness = 1
brushSettings.value.smoothingPrecision = 60
brushSettings.value.stepSize = 5
}
function setPaintBucketTolerance(tolerance: number): void {
@@ -169,6 +172,10 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
resetZoomTrigger.value++
}
function triggerClear(): void {
clearTrigger.value++
}
function setMaskOpacity(opacity: number): void {
maskOpacity.value = _.clamp(opacity, 0, 1)
}
@@ -179,7 +186,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
size: 10,
opacity: 0.7,
hardness: 1,
smoothingPrecision: 10
stepSize: 5
}
maskBlendMode.value = MaskBlendMode.Black
activeLayer.value = 'mask'
@@ -243,10 +250,12 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
canvasHistory,
tgpuRoot,
setBrushSize,
setBrushOpacity,
setBrushHardness,
setBrushSmoothingPrecision,
setBrushStepSize,
resetBrushToDefault,
setPaintBucketTolerance,
setFillOpacity,
@@ -257,6 +266,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
setPanOffset,
setCursorPoint,
resetZoom,
triggerClear,
clearTrigger,
setMaskOpacity,
resetState
}