mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-02 03:30:04 +00:00
## 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)
171 lines
4.2 KiB
TypeScript
171 lines
4.2 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
|
|
|
export function useCanvasHistory(maxStates = 20) {
|
|
const store = useMaskEditorStore()
|
|
|
|
const states = ref<
|
|
{ mask: ImageData | ImageBitmap; rgb: ImageData | ImageBitmap }[]
|
|
>([])
|
|
const currentStateIndex = ref(-1)
|
|
const initialized = ref(false)
|
|
|
|
const canUndo = computed(
|
|
() => states.value.length > 1 && currentStateIndex.value > 0
|
|
)
|
|
|
|
const canRedo = computed(() => {
|
|
return (
|
|
states.value.length > 1 &&
|
|
currentStateIndex.value < states.value.length - 1
|
|
)
|
|
})
|
|
|
|
const saveInitialState = () => {
|
|
const maskCtx = store.maskCtx
|
|
const rgbCtx = store.rgbCtx
|
|
const maskCanvas = store.maskCanvas
|
|
const rgbCanvas = store.rgbCanvas
|
|
|
|
if (!maskCtx || !rgbCtx || !maskCanvas || !rgbCanvas) {
|
|
requestAnimationFrame(saveInitialState)
|
|
return
|
|
}
|
|
|
|
if (!maskCanvas.width || !rgbCanvas.width) {
|
|
requestAnimationFrame(saveInitialState)
|
|
return
|
|
}
|
|
|
|
states.value = []
|
|
const maskState = maskCtx.getImageData(
|
|
0,
|
|
0,
|
|
maskCanvas.width,
|
|
maskCanvas.height
|
|
)
|
|
const rgbState = rgbCtx.getImageData(
|
|
0,
|
|
0,
|
|
rgbCanvas.width,
|
|
rgbCanvas.height
|
|
)
|
|
states.value.push({ mask: maskState, rgb: rgbState })
|
|
currentStateIndex.value = 0
|
|
initialized.value = true
|
|
}
|
|
|
|
const saveState = (
|
|
providedMaskData?: ImageData | ImageBitmap,
|
|
providedRgbData?: ImageData | ImageBitmap
|
|
) => {
|
|
const maskCtx = store.maskCtx
|
|
const rgbCtx = store.rgbCtx
|
|
const maskCanvas = store.maskCanvas
|
|
const rgbCanvas = store.rgbCanvas
|
|
|
|
if (!maskCtx || !rgbCtx || !maskCanvas || !rgbCanvas) return
|
|
|
|
if (!initialized.value || currentStateIndex.value === -1) {
|
|
saveInitialState()
|
|
return
|
|
}
|
|
|
|
states.value = states.value.slice(0, currentStateIndex.value + 1)
|
|
|
|
let maskState: ImageData | ImageBitmap
|
|
let rgbState: ImageData | ImageBitmap
|
|
|
|
if (providedMaskData && providedRgbData) {
|
|
maskState = providedMaskData
|
|
rgbState = providedRgbData
|
|
} else {
|
|
maskState = maskCtx.getImageData(
|
|
0,
|
|
0,
|
|
maskCanvas.width,
|
|
maskCanvas.height
|
|
)
|
|
rgbState = rgbCtx.getImageData(0, 0, rgbCanvas.width, rgbCanvas.height)
|
|
}
|
|
|
|
states.value.push({ mask: maskState, rgb: rgbState })
|
|
currentStateIndex.value++
|
|
|
|
if (states.value.length > maxStates) {
|
|
const removed = states.value.shift()
|
|
// Cleanup ImageBitmaps to avoid memory leaks
|
|
if (removed) {
|
|
if (removed.mask instanceof ImageBitmap) removed.mask.close()
|
|
if (removed.rgb instanceof ImageBitmap) removed.rgb.close()
|
|
}
|
|
currentStateIndex.value--
|
|
}
|
|
}
|
|
|
|
const undo = () => {
|
|
if (!canUndo.value) {
|
|
alert('No more undo states available')
|
|
return
|
|
}
|
|
|
|
currentStateIndex.value--
|
|
restoreState(states.value[currentStateIndex.value])
|
|
}
|
|
|
|
const redo = () => {
|
|
if (!canRedo.value) {
|
|
alert('No more redo states available')
|
|
return
|
|
}
|
|
|
|
currentStateIndex.value++
|
|
restoreState(states.value[currentStateIndex.value])
|
|
}
|
|
|
|
const restoreState = (state: {
|
|
mask: ImageData | ImageBitmap
|
|
rgb: ImageData | ImageBitmap
|
|
}) => {
|
|
const maskCtx = store.maskCtx
|
|
const rgbCtx = store.rgbCtx
|
|
if (!maskCtx || !rgbCtx) return
|
|
|
|
if (state.mask instanceof ImageBitmap) {
|
|
maskCtx.clearRect(0, 0, state.mask.width, state.mask.height)
|
|
maskCtx.drawImage(state.mask, 0, 0)
|
|
} else {
|
|
maskCtx.putImageData(state.mask, 0, 0)
|
|
}
|
|
|
|
if (state.rgb instanceof ImageBitmap) {
|
|
rgbCtx.clearRect(0, 0, state.rgb.width, state.rgb.height)
|
|
rgbCtx.drawImage(state.rgb, 0, 0)
|
|
} else {
|
|
rgbCtx.putImageData(state.rgb, 0, 0)
|
|
}
|
|
}
|
|
|
|
const clearStates = () => {
|
|
// Cleanup bitmaps
|
|
states.value.forEach((state) => {
|
|
if (state.mask instanceof ImageBitmap) state.mask.close()
|
|
if (state.rgb instanceof ImageBitmap) state.rgb.close()
|
|
})
|
|
states.value = []
|
|
currentStateIndex.value = -1
|
|
initialized.value = false
|
|
}
|
|
|
|
return {
|
|
canUndo,
|
|
canRedo,
|
|
currentStateIndex,
|
|
saveInitialState,
|
|
saveState,
|
|
undo,
|
|
redo,
|
|
clearStates
|
|
}
|
|
}
|