summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-01 13:57:32 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-01 13:57:32 +0200
commite579ae120290329fd697b50ea9138ffccb65a805 (patch)
tree12a63ee60b00e5004a1806ddf3ebd70c012e3e28
parentb048b739bd598f9d9ff97166f29bcb6e37c40607 (diff)
downloadopencut-e579ae120290329fd697b50ea9138ffccb65a805.tar.gz
feat: migrate GPU renderer from WebGL to wgpu/WASM
-rw-r--r--Cargo.lock597
-rw-r--r--Cargo.toml2
-rw-r--r--apps/web/next.config.ts8
-rw-r--r--apps/web/package.json1
-rw-r--r--apps/web/src/components/providers/editor-provider.tsx268
-rw-r--r--apps/web/src/lib/effects/definitions/blur.frag.glsl25
-rw-r--r--apps/web/src/lib/effects/definitions/blur.ts18
-rw-r--r--apps/web/src/lib/effects/effect.vert.glsl7
-rw-r--r--apps/web/src/lib/effects/index.ts6
-rw-r--r--apps/web/src/lib/effects/types.ts83
-rw-r--r--apps/web/src/lib/masks/shaders/jfa-distance.frag.glsl36
-rw-r--r--apps/web/src/lib/shaders/jfa-init.frag.glsl25
-rw-r--r--apps/web/src/lib/shaders/jfa-step.frag.glsl60
-rw-r--r--apps/web/src/services/renderer/effect-preview.ts53
-rw-r--r--apps/web/src/services/renderer/gpu-renderer.ts118
-rw-r--r--apps/web/src/services/renderer/mask-feather.ts74
-rw-r--r--apps/web/src/services/renderer/nodes/blur-background-node.ts4
-rw-r--r--apps/web/src/services/renderer/nodes/effect-layer-node.ts162
-rw-r--r--apps/web/src/services/renderer/nodes/text-node.ts4
-rw-r--r--apps/web/src/services/renderer/nodes/visual-node.ts4
-rw-r--r--apps/web/src/services/renderer/webgl/jfa.ts194
-rw-r--r--apps/web/src/services/renderer/webgl/webgl-context.ts49
-rw-r--r--apps/web/src/services/renderer/webgl/webgl-effect-renderer.ts35
-rw-r--r--apps/web/src/services/renderer/webgl/webgl-utils.ts298
-rw-r--r--apps/web/src/types/glsl.d.ts4
-rw-r--r--bun.lock173
-rw-r--r--docs/effects-renderer.md40
-rw-r--r--rust/crates/gpu/Cargo.toml9
-rw-r--r--rust/crates/gpu/src/context.rs250
-rw-r--r--rust/crates/gpu/src/effect_pipeline.rs194
-rw-r--r--rust/crates/gpu/src/lib.rs40
-rw-r--r--rust/crates/gpu/src/mask_feather.rs259
-rw-r--r--rust/crates/gpu/src/sdf_pipeline.rs307
-rw-r--r--rust/crates/gpu/src/shader_registry.rs183
-rw-r--r--rust/crates/gpu/src/shaders/blit.wgsl12
-rw-r--r--rust/crates/gpu/src/shaders/fullscreen.wgsl12
-rw-r--r--rust/crates/gpu/src/shaders/gaussian_blur.wgsl34
-rw-r--r--rust/crates/gpu/src/shaders/jfa_distance.wgsl50
-rw-r--r--rust/crates/gpu/src/shaders/jfa_init.wgsl35
-rw-r--r--rust/crates/gpu/src/shaders/jfa_step.wgsl75
-rw-r--r--rust/wasm/Cargo.toml7
-rw-r--r--rust/wasm/src/gpu_bridge.rs256
-rw-r--r--rust/wasm/src/wasm.rs5
-rw-r--r--turbo.json2
44 files changed, 2745 insertions, 1333 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 091717b1..8a34d0ea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,6 +61,21 @@ dependencies = [
]
[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -136,7 +151,7 @@ checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82"
dependencies = [
"ash",
"raw-window-handle",
- "raw-window-metal",
+ "raw-window-metal 0.4.0",
]
[[package]]
@@ -502,7 +517,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
- "bit-vec",
+ "bit-vec 0.8.0",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
+dependencies = [
+ "bit-vec 0.9.1",
]
[[package]]
@@ -512,6 +536,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+
+[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -548,8 +578,8 @@ dependencies = [
"ash-window",
"bitflags 2.11.0",
"bytemuck",
- "codespan-reporting",
- "glow",
+ "codespan-reporting 0.12.0",
+ "glow 0.16.0",
"gpu-alloc",
"gpu-alloc-ash",
"hidden-trait",
@@ -558,7 +588,7 @@ dependencies = [
"libloading",
"log",
"mint",
- "naga",
+ "naga 25.0.1",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
@@ -899,6 +929,17 @@ dependencies = [
]
[[package]]
+name = "codespan-reporting"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
+dependencies = [
+ "serde",
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1333,6 +1374,15 @@ dependencies = [
]
[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1666,6 +1716,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
name = "font-types"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1955,6 +2011,17 @@ dependencies = [
]
[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2011,6 +2078,36 @@ dependencies = [
]
[[package]]
+name = "glow"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gpu"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+ "thiserror 2.0.18",
+ "wgpu",
+]
+
+[[package]]
name = "gpu-alloc"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2041,6 +2138,40 @@ dependencies = [
]
[[package]]
+name = "gpu-allocator"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
+dependencies = [
+ "ash",
+ "hashbrown 0.16.1",
+ "log",
+ "presser",
+ "thiserror 2.0.18",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "gpu-descriptor"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
+dependencies = [
+ "bitflags 2.11.0",
+ "gpu-descriptor-types",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "gpu-descriptor-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
name = "gpui"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2091,7 +2222,7 @@ dependencies = [
"log",
"lyon",
"metal",
- "naga",
+ "naga 25.0.1",
"num_cpus",
"objc",
"oo7",
@@ -2126,7 +2257,7 @@ dependencies = [
"wayland-protocols-plasma",
"windows 0.61.3",
"windows-core 0.61.2",
- "windows-numerics",
+ "windows-numerics 0.2.0",
"windows-registry 0.5.3",
"x11-clipboard",
"x11rb",
@@ -2348,7 +2479,7 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
- "foldhash",
+ "foldhash 0.1.5",
]
[[package]]
@@ -2356,6 +2487,11 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
[[package]]
name = "heck"
@@ -2790,6 +2926,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2801,9 +2965,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.92"
+version = "0.3.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
+checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d"
dependencies = [
"cfg-if",
"futures-util",
@@ -2819,9 +2983,16 @@ checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
dependencies = [
"libc",
"libloading",
+ "pkg-config",
]
[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
name = "kurbo"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2940,6 +3111,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3165,10 +3342,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
- "bit-set",
+ "bit-set 0.8.0",
"bitflags 2.11.0",
"cfg_aliases",
- "codespan-reporting",
+ "codespan-reporting 0.12.0",
"half",
"hashbrown 0.15.5",
"hexf-parse",
@@ -3177,13 +3354,39 @@ dependencies = [
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
- "spirv",
+ "spirv 0.3.0+sdk-1.3.268.0",
"strum 0.26.3",
"thiserror 2.0.18",
"unicode-ident",
]
[[package]]
+name = "naga"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647"
+dependencies = [
+ "arrayvec",
+ "bit-set 0.9.1",
+ "bitflags 2.11.0",
+ "cfg-if",
+ "cfg_aliases",
+ "codespan-reporting 0.13.1",
+ "half",
+ "hashbrown 0.16.1",
+ "hexf-parse",
+ "indexmap",
+ "libm",
+ "log",
+ "num-traits",
+ "once_cell",
+ "rustc-hash 1.1.0",
+ "spirv 0.4.0+sdk-1.4.341.0",
+ "thiserror 2.0.18",
+ "unicode-ident",
+]
+
+[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3193,6 +3396,15 @@ dependencies = [
]
[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3567,7 +3779,14 @@ dependencies = [
name = "opencut-wasm"
version = "0.1.2"
dependencies = [
+ "gpu",
+ "js-sys",
+ "serde",
+ "serde-wasm-bindgen",
"time",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
]
[[package]]
@@ -3583,6 +3802,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3782,6 +4010,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
name = "postage"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3817,6 +4060,12 @@ dependencies = [
]
[[package]]
+name = "presser"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+
+[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4070,6 +4319,12 @@ dependencies = [
]
[[package]]
+name = "range-alloc"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+
+[[package]]
name = "rangemap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4144,6 +4399,18 @@ dependencies = [
]
[[package]]
+name = "raw-window-metal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
+dependencies = [
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4261,6 +4528,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
+name = "renderdoc-sys"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+
+[[package]]
name = "resvg"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4883,6 +5156,15 @@ dependencies = [
]
[[package]]
+name = "spirv"
+version = "0.4.0+sdk-1.4.341.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
+dependencies = [
+ "bitflags 2.11.0",
+]
+
+[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5927,9 +6209,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.115"
+version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
+checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631"
dependencies = [
"cfg-if",
"once_cell",
@@ -5940,9 +6222,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.65"
+version = "0.4.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0"
+checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5950,9 +6232,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.115"
+version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
+checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -5960,9 +6242,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.115"
+version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
+checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -5973,9 +6255,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.115"
+version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
+checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633"
dependencies = [
"unicode-ident",
]
@@ -6126,9 +6408,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.92"
+version = "0.3.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
+checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6151,6 +6433,173 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
+name = "wgpu"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.11.0",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
+ "js-sys",
+ "log",
+ "naga 29.0.1",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "static_assertions",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e"
+dependencies = [
+ "arrayvec",
+ "bit-set 0.9.1",
+ "bit-vec 0.9.1",
+ "bitflags 2.11.0",
+ "bytemuck",
+ "cfg_aliases",
+ "document-features",
+ "hashbrown 0.16.1",
+ "indexmap",
+ "log",
+ "naga 29.0.1",
+ "once_cell",
+ "parking_lot",
+ "portable-atomic",
+ "profiling",
+ "raw-window-handle",
+ "rustc-hash 1.1.0",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wgpu-core-deps-apple",
+ "wgpu-core-deps-emscripten",
+ "wgpu-core-deps-windows-linux-android",
+ "wgpu-hal",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core-deps-apple"
+version = "29.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-emscripten"
+version = "29.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-core-deps-windows-linux-android"
+version = "29.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28"
+dependencies = [
+ "wgpu-hal",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e"
+dependencies = [
+ "android_system_properties",
+ "arrayvec",
+ "ash",
+ "bit-set 0.9.1",
+ "bitflags 2.11.0",
+ "block2",
+ "bytemuck",
+ "cfg-if",
+ "cfg_aliases",
+ "glow 0.17.0",
+ "glutin_wgl_sys",
+ "gpu-allocator",
+ "gpu-descriptor",
+ "hashbrown 0.16.1",
+ "js-sys",
+ "khronos-egl",
+ "libc",
+ "libloading",
+ "log",
+ "naga 29.0.1",
+ "ndk-sys",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-metal",
+ "objc2-quartz-core",
+ "once_cell",
+ "ordered-float",
+ "parking_lot",
+ "portable-atomic",
+ "portable-atomic-util",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "raw-window-metal 1.1.0",
+ "renderdoc-sys",
+ "smallvec",
+ "thiserror 2.0.18",
+ "wasm-bindgen",
+ "wayland-sys",
+ "web-sys",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+ "windows 0.62.2",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "wgpu-naga-bridge"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51"
+dependencies = [
+ "naga 29.0.1",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "29.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6"
+dependencies = [
+ "bitflags 2.11.0",
+ "bytemuck",
+ "js-sys",
+ "log",
+ "raw-window-handle",
+ "web-sys",
+]
+
+[[package]]
name = "which"
version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6209,11 +6658,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
- "windows-collections",
+ "windows-collections 0.2.0",
"windows-core 0.61.2",
- "windows-future",
+ "windows-future 0.2.1",
"windows-link 0.1.3",
- "windows-numerics",
+ "windows-numerics 0.2.0",
+]
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections 0.3.2",
+ "windows-core 0.62.2",
+ "windows-future 0.3.2",
+ "windows-numerics 0.3.1",
]
[[package]]
@@ -6226,7 +6687,7 @@ dependencies = [
"rayon",
"thiserror 2.0.18",
"windows 0.61.3",
- "windows-future",
+ "windows-future 0.2.1",
]
[[package]]
@@ -6239,6 +6700,15 @@ dependencies = [
]
[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core 0.62.2",
+]
+
+[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6264,6 +6734,19 @@ dependencies = [
]
[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6271,7 +6754,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
- "windows-threading",
+ "windows-threading 0.1.0",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core 0.62.2",
+ "windows-link 0.2.1",
+ "windows-threading 0.2.1",
]
[[package]]
@@ -6341,6 +6835,16 @@ dependencies = [
]
[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core 0.62.2",
+ "windows-link 0.2.1",
+]
+
+[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6381,6 +6885,15 @@ dependencies = [
]
[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6399,6 +6912,15 @@ dependencies = [
]
[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6501,6 +7023,15 @@ dependencies = [
]
[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6879,6 +7410,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
+name = "xml-rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+
+[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index b037bbed..2b63476b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,5 +4,5 @@ members = [
"apps/desktop",
"rust/crates/time",
"rust/crates/bridge",
- "rust/wasm",
+ "rust/wasm", "rust/crates/gpu",
]
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index 08db3ead..71d96c23 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -3,14 +3,6 @@ import { withBotId } from "botid/next/config";
import { withContentCollections } from "@content-collections/next";
const nextConfig: NextConfig = {
- turbopack: {
- rules: {
- "*.glsl": {
- loaders: [require.resolve("raw-loader")],
- as: "*.js",
- },
- },
- },
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
diff --git a/apps/web/package.json b/apps/web/package.json
index 0c3a1f39..c7d55572 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -94,7 +94,6 @@
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.4",
"postcss": "^8",
- "raw-loader": "^4.0.2",
"sharp": "^0.34.5",
"tailwindcss": "^4.2.1",
"typescript": "^5.8.3",
diff --git a/apps/web/src/components/providers/editor-provider.tsx b/apps/web/src/components/providers/editor-provider.tsx
index 79f1809a..20476a13 100644
--- a/apps/web/src/components/providers/editor-provider.tsx
+++ b/apps/web/src/components/providers/editor-provider.tsx
@@ -1,133 +1,135 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { useRouter } from "next/navigation";
-import { Loader2 } from "lucide-react";
-import { EditorCore } from "@/core";
-import { useEditor } from "@/hooks/use-editor";
-import { useKeybindingsListener } from "@/hooks/use-keybindings";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { useEditorActions } from "@/hooks/actions/use-editor-actions";
-import { loadFontAtlas } from "@/lib/fonts/google-fonts";
-
-interface EditorProviderProps {
- projectId: string;
- children: React.ReactNode;
-}
-
-export function EditorProvider({ projectId, children }: EditorProviderProps) {
- const activeProject = useEditor((e) => e.project.getActiveOrNull());
- const router = useRouter();
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const { setLoadingProject } = useKeybindingsStore();
-
- useEffect(() => {
- setLoadingProject(isLoading);
- }, [isLoading, setLoadingProject]);
-
- useEffect(() => {
- let cancelled = false;
- const editor = EditorCore.getInstance();
-
- const loadProject = async () => {
- try {
- setIsLoading(true);
- await editor.project.loadProject({ id: projectId });
-
- if (cancelled) return;
-
- setIsLoading(false);
- loadFontAtlas();
- } catch (err) {
- if (cancelled) return;
-
- const isNotFound =
- err instanceof Error &&
- (err.message.includes("not found") ||
- err.message.includes("does not exist"));
-
- if (isNotFound) {
- try {
- const newProjectId = await editor.project.createNewProject({
- name: "Untitled Project",
- });
- router.replace(`/editor/${newProjectId}`);
- } catch (_createErr) {
- setError("Failed to create project");
- setIsLoading(false);
- }
- } else {
- setError(
- err instanceof Error ? err.message : "Failed to load project",
- );
- setIsLoading(false);
- }
- }
- };
-
- loadProject();
-
- return () => {
- cancelled = true;
- };
- }, [projectId, router]);
-
- if (error) {
- return (
- <div className="bg-background flex h-screen w-screen items-center justify-center">
- <div className="flex flex-col items-center gap-4">
- <p className="text-destructive text-sm">{error}</p>
- </div>
- </div>
- );
- }
-
- if (isLoading) {
- return (
- <div className="bg-background flex h-screen w-screen items-center justify-center">
- <div className="flex flex-col items-center gap-4">
- <Loader2 className="text-muted-foreground size-8 animate-spin" />
- <p className="text-muted-foreground text-sm">Loading project...</p>
- </div>
- </div>
- );
- }
-
- if (!activeProject) {
- return (
- <div className="bg-background flex h-screen w-screen items-center justify-center">
- <div className="flex flex-col items-center gap-4">
- <Loader2 className="text-muted-foreground size-8 animate-spin" />
- <p className="text-muted-foreground text-sm">Exiting project...</p>
- </div>
- </div>
- );
- }
-
- return (
- <>
- <EditorRuntimeBindings />
- {children}
- </>
- );
-}
-
-function EditorRuntimeBindings() {
- const editor = useEditor();
-
- useEffect(() => {
- const handleBeforeUnload = (event: BeforeUnloadEvent) => {
- if (!editor.save.getIsDirty()) return;
- event.preventDefault();
- (event as unknown as { returnValue: string }).returnValue = "";
- };
-
- window.addEventListener("beforeunload", handleBeforeUnload);
- return () => window.removeEventListener("beforeunload", handleBeforeUnload);
- }, [editor]);
-
- useEditorActions();
- useKeybindingsListener();
- return null;
-}
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Loader2 } from "lucide-react";
+import { EditorCore } from "@/core";
+import { useEditor } from "@/hooks/use-editor";
+import { useKeybindingsListener } from "@/hooks/use-keybindings";
+import { useKeybindingsStore } from "@/stores/keybindings-store";
+import { useEditorActions } from "@/hooks/actions/use-editor-actions";
+import { loadFontAtlas } from "@/lib/fonts/google-fonts";
+import { initializeGpuRenderer } from "@/services/renderer/gpu-renderer";
+
+interface EditorProviderProps {
+ projectId: string;
+ children: React.ReactNode;
+}
+
+export function EditorProvider({ projectId, children }: EditorProviderProps) {
+ const activeProject = useEditor((e) => e.project.getActiveOrNull());
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const { setLoadingProject } = useKeybindingsStore();
+
+ useEffect(() => {
+ setLoadingProject(isLoading);
+ }, [isLoading, setLoadingProject]);
+
+ useEffect(() => {
+ let cancelled = false;
+ const editor = EditorCore.getInstance();
+
+ const loadProject = async () => {
+ try {
+ setIsLoading(true);
+ await initializeGpuRenderer();
+ await editor.project.loadProject({ id: projectId });
+
+ if (cancelled) return;
+
+ setIsLoading(false);
+ loadFontAtlas();
+ } catch (err) {
+ if (cancelled) return;
+
+ const isNotFound =
+ err instanceof Error &&
+ (err.message.includes("not found") ||
+ err.message.includes("does not exist"));
+
+ if (isNotFound) {
+ try {
+ const newProjectId = await editor.project.createNewProject({
+ name: "Untitled Project",
+ });
+ router.replace(`/editor/${newProjectId}`);
+ } catch (_createErr) {
+ setError("Failed to create project");
+ setIsLoading(false);
+ }
+ } else {
+ setError(
+ err instanceof Error ? err.message : "Failed to load project",
+ );
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadProject();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [projectId, router]);
+
+ if (error) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <p className="text-destructive text-sm">{error}</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (isLoading) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ <p className="text-muted-foreground text-sm">Loading project...</p>
+ </div>
+ </div>
+ );
+ }
+
+ if (!activeProject) {
+ return (
+ <div className="bg-background flex h-screen w-screen items-center justify-center">
+ <div className="flex flex-col items-center gap-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ <p className="text-muted-foreground text-sm">Exiting project...</p>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <>
+ <EditorRuntimeBindings />
+ {children}
+ </>
+ );
+}
+
+function EditorRuntimeBindings() {
+ const editor = useEditor();
+
+ useEffect(() => {
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
+ if (!editor.save.getIsDirty()) return;
+ event.preventDefault();
+ (event as unknown as { returnValue: string }).returnValue = "";
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
+ }, [editor]);
+
+ useEditorActions();
+ useKeybindingsListener();
+ return null;
+}
diff --git a/apps/web/src/lib/effects/definitions/blur.frag.glsl b/apps/web/src/lib/effects/definitions/blur.frag.glsl
deleted file mode 100644
index c9ad2eae..00000000
--- a/apps/web/src/lib/effects/definitions/blur.frag.glsl
+++ /dev/null
@@ -1,25 +0,0 @@
-precision mediump float;
-
-uniform sampler2D u_texture;
-uniform vec2 u_resolution;
-uniform float u_sigma;
-uniform float u_step;
-uniform vec2 u_direction;
-
-varying vec2 v_texCoord;
-
-void main() {
- vec2 texelSize = 1.0 / u_resolution;
-
- vec4 color = vec4(0.0);
- float totalWeight = 0.0;
-
- for (int i = -30; i <= 30; i++) {
- float pos = float(i) * u_step;
- float weight = exp(-(pos * pos) / (2.0 * u_sigma * u_sigma));
- color += texture2D(u_texture, v_texCoord + texelSize * u_direction * pos) * weight;
- totalWeight += weight;
- }
-
- gl_FragColor = color / totalWeight;
-} \ No newline at end of file
diff --git a/apps/web/src/lib/effects/definitions/blur.ts b/apps/web/src/lib/effects/definitions/blur.ts
index 54934402..3bfd5e3c 100644
--- a/apps/web/src/lib/effects/definitions/blur.ts
+++ b/apps/web/src/lib/effects/definitions/blur.ts
@@ -1,5 +1,6 @@
-import type { EffectDefinition, ResolvedEffectPass } from "@/lib/effects/types";
-import blurFragmentShader from "./blur.frag.glsl";
+import type { EffectDefinition, EffectPass } from "@/lib/effects/types";
+
+export const GAUSSIAN_BLUR_SHADER = "gaussian-blur";
const MAX_SINGLE_PASS_SIGMA = 10;
const MAX_STEP = 4;
@@ -17,7 +18,7 @@ export function buildGaussianBlurPasses({
}: {
sigmaX: number;
sigmaY: number;
-}): ResolvedEffectPass[] {
+}): EffectPass[] {
const maxSigma = Math.max(sigmaX, sigmaY);
if (maxSigma < 0.001) return [];
@@ -36,10 +37,10 @@ export function buildGaussianBlurPasses({
const stepX = Math.max(1, perPassSigmaX / MAX_SINGLE_PASS_SIGMA);
const stepY = Math.max(1, perPassSigmaY / MAX_SINGLE_PASS_SIGMA);
- const passes: ResolvedEffectPass[] = [];
+ const passes: EffectPass[] = [];
for (let i = 0; i < iterations; i++) {
passes.push({
- fragmentShader: blurFragmentShader,
+ shader: GAUSSIAN_BLUR_SHADER,
uniforms: {
u_sigma: perPassSigmaX,
u_step: stepX,
@@ -47,7 +48,7 @@ export function buildGaussianBlurPasses({
},
});
passes.push({
- fragmentShader: blurFragmentShader,
+ shader: GAUSSIAN_BLUR_SHADER,
uniforms: {
u_sigma: perPassSigmaY,
u_step: stepY,
@@ -83,10 +84,9 @@ export const blurEffectDefinition: EffectDefinition = {
},
],
renderer: {
- type: "webgl",
passes: [
{
- fragmentShader: blurFragmentShader,
+ shader: GAUSSIAN_BLUR_SHADER,
uniforms: ({ effectParams, width }) => ({
u_sigma: Math.max(intensityToSigma(parseIntensity(effectParams), width, 1920), 0.001),
u_step: 1,
@@ -94,7 +94,7 @@ export const blurEffectDefinition: EffectDefinition = {
}),
},
{
- fragmentShader: blurFragmentShader,
+ shader: GAUSSIAN_BLUR_SHADER,
uniforms: ({ effectParams, height }) => ({
u_sigma: Math.max(intensityToSigma(parseIntensity(effectParams), height, 1080), 0.001),
u_step: 1,
diff --git a/apps/web/src/lib/effects/effect.vert.glsl b/apps/web/src/lib/effects/effect.vert.glsl
deleted file mode 100644
index 770faf0c..00000000
--- a/apps/web/src/lib/effects/effect.vert.glsl
+++ /dev/null
@@ -1,7 +0,0 @@
-attribute vec2 a_position;
-varying vec2 v_texCoord;
-
-void main() {
- v_texCoord = a_position * 0.5 + 0.5;
- gl_Position = vec4(a_position, 0.0, 1.0);
-}
diff --git a/apps/web/src/lib/effects/index.ts b/apps/web/src/lib/effects/index.ts
index 549811f5..92484ee2 100644
--- a/apps/web/src/lib/effects/index.ts
+++ b/apps/web/src/lib/effects/index.ts
@@ -2,7 +2,7 @@ import { generateUUID } from "@/utils/id";
import { buildDefaultParamValues } from "@/lib/registry";
import { effectsRegistry } from "./registry";
import type { ParamValues } from "@/lib/params";
-import type { Effect, EffectDefinition, ResolvedEffectPass } from "@/lib/effects/types";
+import type { Effect, EffectDefinition, EffectPass } from "@/lib/effects/types";
import { VISUAL_ELEMENT_TYPES } from "@/lib/timeline";
export { effectsRegistry } from "./registry";
@@ -18,12 +18,12 @@ export function resolveEffectPasses({
effectParams: ParamValues;
width: number;
height: number;
-}): ResolvedEffectPass[] {
+}): EffectPass[] {
if (definition.renderer.buildPasses) {
return definition.renderer.buildPasses({ effectParams, width, height });
}
return definition.renderer.passes.map((pass) => ({
- fragmentShader: pass.fragmentShader,
+ shader: pass.shader,
uniforms: pass.uniforms({ effectParams, width, height }),
}));
}
diff --git a/apps/web/src/lib/effects/types.ts b/apps/web/src/lib/effects/types.ts
index 5573ef12..1862b440 100644
--- a/apps/web/src/lib/effects/types.ts
+++ b/apps/web/src/lib/effects/types.ts
@@ -1,42 +1,41 @@
-import type { ParamDefinition, ParamValues } from "@/lib/params";
-
-export interface Effect {
- id: string;
- type: string;
- params: ParamValues;
- enabled: boolean;
-}
-
-export interface ResolvedEffectPass {
- fragmentShader: string;
- uniforms: Record<string, number | number[]>;
-}
-
-export interface WebGLEffectPass {
- fragmentShader: string;
- uniforms(params: {
- effectParams: ParamValues;
- width: number;
- height: number;
- }): Record<string, number | number[]>;
-}
-
-export interface WebGLEffectRenderer {
- type: "webgl";
- passes: WebGLEffectPass[];
- buildPasses?: (params: {
- effectParams: ParamValues;
- width: number;
- height: number;
- }) => ResolvedEffectPass[];
-}
-
-export type EffectRenderer = WebGLEffectRenderer;
-
-export interface EffectDefinition {
- type: string;
- name: string;
- keywords: string[];
- params: ParamDefinition[];
- renderer: EffectRenderer;
-}
+import type { ParamDefinition, ParamValues } from "@/lib/params";
+
+export interface Effect {
+ id: string;
+ type: string;
+ params: ParamValues;
+ enabled: boolean;
+}
+
+export type EffectUniformValue = number | number[];
+
+export interface EffectPass {
+ shader: string;
+ uniforms: Record<string, EffectUniformValue>;
+}
+
+export interface EffectPassTemplate {
+ shader: string;
+ uniforms(params: {
+ effectParams: ParamValues;
+ width: number;
+ height: number;
+ }): Record<string, EffectUniformValue>;
+}
+
+export interface EffectRendererConfig {
+ passes: EffectPassTemplate[];
+ buildPasses?: (params: {
+ effectParams: ParamValues;
+ width: number;
+ height: number;
+ }) => EffectPass[];
+}
+
+export interface EffectDefinition {
+ type: string;
+ name: string;
+ keywords: string[];
+ params: ParamDefinition[];
+ renderer: EffectRendererConfig;
+}
diff --git a/apps/web/src/lib/masks/shaders/jfa-distance.frag.glsl b/apps/web/src/lib/masks/shaders/jfa-distance.frag.glsl
deleted file mode 100644
index 9d9a8773..00000000
--- a/apps/web/src/lib/masks/shaders/jfa-distance.frag.glsl
+++ /dev/null
@@ -1,36 +0,0 @@
-precision mediump float;
-
-uniform sampler2D u_texture;
-uniform sampler2D u_jfa_outside;
-uniform vec2 u_resolution;
-uniform float u_feather_half;
-
-varying vec2 v_texCoord;
-
-vec2 decodeSeed(vec4 encoded) {
- float x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5);
- float y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5);
- return vec2(x, y);
-}
-
-bool isNoSeed(vec4 encoded) {
- return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99;
-}
-
-void main() {
- vec2 pixelCoord = floor(v_texCoord * u_resolution);
-
- vec4 insideEncoded = texture2D(u_texture, v_texCoord);
- vec4 outsideEncoded = texture2D(u_jfa_outside, v_texCoord);
-
- bool hasInside = !isNoSeed(insideEncoded);
- bool hasOutside = !isNoSeed(outsideEncoded);
-
- float distToInside = hasInside ? distance(pixelCoord, decodeSeed(insideEncoded)) : 1e5;
- float distToOutside = hasOutside ? distance(pixelCoord, decodeSeed(outsideEncoded)) : 1e5;
-
- float signedDist = distToOutside - distToInside;
- float alpha = smoothstep(-u_feather_half, u_feather_half, signedDist);
-
- gl_FragColor = vec4(alpha, alpha, alpha, alpha);
-}
diff --git a/apps/web/src/lib/shaders/jfa-init.frag.glsl b/apps/web/src/lib/shaders/jfa-init.frag.glsl
deleted file mode 100644
index e465752c..00000000
--- a/apps/web/src/lib/shaders/jfa-init.frag.glsl
+++ /dev/null
@@ -1,25 +0,0 @@
-precision mediump float;
-
-uniform sampler2D u_texture;
-uniform vec2 u_resolution;
-uniform float u_invert;
-
-varying vec2 v_texCoord;
-
-void main() {
- float mask = texture2D(u_texture, v_texCoord).r;
- bool isSeed = u_invert > 0.5 ? mask < 0.5 : mask > 0.5;
-
- if (isSeed) {
- vec2 pixelCoord = floor(v_texCoord * u_resolution);
- float x = pixelCoord.x;
- float y = pixelCoord.y;
- float xHi = floor(x / 256.0);
- float xLo = x - xHi * 256.0;
- float yHi = floor(y / 256.0);
- float yLo = y - yHi * 256.0;
- gl_FragColor = vec4(xHi / 255.0, xLo / 255.0, yHi / 255.0, yLo / 255.0);
- } else {
- gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
- }
-}
diff --git a/apps/web/src/lib/shaders/jfa-step.frag.glsl b/apps/web/src/lib/shaders/jfa-step.frag.glsl
deleted file mode 100644
index 9b76a70b..00000000
--- a/apps/web/src/lib/shaders/jfa-step.frag.glsl
+++ /dev/null
@@ -1,60 +0,0 @@
-precision mediump float;
-
-uniform sampler2D u_texture;
-uniform vec2 u_resolution;
-uniform float u_step_size;
-
-varying vec2 v_texCoord;
-
-vec2 decodeSeed(vec4 encoded) {
- float x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5);
- float y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5);
- return vec2(x, y);
-}
-
-vec4 encodeSeed(vec2 seed) {
- float xHi = floor(seed.x / 256.0);
- float xLo = seed.x - xHi * 256.0;
- float yHi = floor(seed.y / 256.0);
- float yLo = seed.y - yHi * 256.0;
- return vec4(xHi / 255.0, xLo / 255.0, yHi / 255.0, yLo / 255.0);
-}
-
-bool isNoSeed(vec4 encoded) {
- return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99;
-}
-
-void main() {
- vec2 pixelCoord = floor(v_texCoord * u_resolution);
- vec2 texelSize = 1.0 / u_resolution;
-
- float bestDist = 1e10;
- vec2 bestSeed = vec2(65535.0);
-
- for (int dy = -1; dy <= 1; dy++) {
- for (int dx = -1; dx <= 1; dx++) {
- vec2 offset = vec2(float(dx), float(dy)) * u_step_size;
- vec2 sampleUV = v_texCoord + offset * texelSize;
-
- if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0)
- continue;
-
- vec4 encoded = texture2D(u_texture, sampleUV);
- if (isNoSeed(encoded))
- continue;
-
- vec2 seed = decodeSeed(encoded);
- float dist = distance(pixelCoord, seed);
- if (dist < bestDist) {
- bestDist = dist;
- bestSeed = seed;
- }
- }
- }
-
- if (bestDist < 1e9) {
- gl_FragColor = encodeSeed(bestSeed);
- } else {
- gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
- }
-}
diff --git a/apps/web/src/services/renderer/effect-preview.ts b/apps/web/src/services/renderer/effect-preview.ts
index b2f6f51a..8e081039 100644
--- a/apps/web/src/services/renderer/effect-preview.ts
+++ b/apps/web/src/services/renderer/effect-preview.ts
@@ -2,18 +2,14 @@ import { createOffscreenCanvas } from "./canvas-utils";
import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
import { buildDefaultParamValues } from "@/lib/registry";
import type { ParamValues } from "@/lib/params";
-import { applyMultiPassEffect } from "./webgl/webgl-utils";
-import type { EffectPassData } from "./webgl/webgl-utils";
+import { gpuRenderer } from "./gpu-renderer";
const PREVIEW_SIZE = 160;
const PREVIEW_IMAGE_PATH = "/effects/preview.jpg";
class EffectPreviewService {
- private previewGl: WebGLRenderingContext | null = null;
- private previewCanvas: OffscreenCanvas | HTMLCanvasElement | null = null;
private testSourceCanvas: OffscreenCanvas | HTMLCanvasElement | null = null;
private previewImageElement: HTMLImageElement | null = null;
- private programCache = new Map<string, WebGLProgram>();
private onReadyCallbacks = new Set<() => void>();
readonly PREVIEW_SIZE = PREVIEW_SIZE;
@@ -58,7 +54,7 @@ class EffectPreviewService {
width: uniformDimensions?.width ?? size,
height: uniformDimensions?.height ?? size,
});
- const result = this.applyWebGlEffect({
+ const result = this.applyGpuEffect({
source,
width: size,
height: size,
@@ -114,29 +110,6 @@ class EffectPreviewService {
return canvas;
}
- private getOrCreatePreviewContext({
- width,
- height,
- }: {
- width: number;
- height: number;
- }): { canvas: OffscreenCanvas | HTMLCanvasElement; gl: WebGLRenderingContext } {
- if (!this.previewCanvas || !this.previewGl) {
- this.previewCanvas = createOffscreenCanvas({ width, height });
- this.previewGl = this.previewCanvas.getContext("webgl", {
- premultipliedAlpha: false,
- }) as WebGLRenderingContext | null;
- if (!this.previewGl) {
- throw new Error("WebGL not supported");
- }
- }
- if (this.previewCanvas.width !== width || this.previewCanvas.height !== height) {
- this.previewCanvas.width = width;
- this.previewCanvas.height = height;
- }
- return { canvas: this.previewCanvas, gl: this.previewGl };
- }
-
private getTestSource({
width,
height,
@@ -154,7 +127,7 @@ class EffectPreviewService {
return this.testSourceCanvas;
}
- private applyWebGlEffect({
+ private applyGpuEffect({
source,
width,
height,
@@ -163,28 +136,14 @@ class EffectPreviewService {
source: CanvasImageSource;
width: number;
height: number;
- passes: EffectPassData[];
+ passes: ReturnType<typeof resolveEffectPasses>;
}): OffscreenCanvas | HTMLCanvasElement {
- const { canvas: glCanvas, gl } = this.getOrCreatePreviewContext({ width, height });
-
- applyMultiPassEffect({
- context: gl,
+ return gpuRenderer.applyEffect({
source,
width,
height,
passes,
- programCache: this.programCache,
- });
-
- const outputCanvas = createOffscreenCanvas({ width, height });
- const outputCtx = outputCanvas.getContext("2d") as
- | CanvasRenderingContext2D
- | OffscreenCanvasRenderingContext2D
- | null;
- if (outputCtx) {
- outputCtx.drawImage(glCanvas, 0, 0, width, height);
- }
- return outputCanvas;
+ }) as OffscreenCanvas | HTMLCanvasElement;
}
}
diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts
new file mode 100644
index 00000000..91b03603
--- /dev/null
+++ b/apps/web/src/services/renderer/gpu-renderer.ts
@@ -0,0 +1,118 @@
+import {
+ applyEffectPasses,
+ applyMaskFeather as applyMaskFeatherWasm,
+ initializeGpu,
+} from "opencut-wasm";
+import type { EffectPass, EffectUniformValue } from "@/lib/effects/types";
+
+let initializeGpuRendererPromise: Promise<void> | null = null;
+
+export function initializeGpuRenderer(): Promise<void> {
+ if (!initializeGpuRendererPromise) {
+ initializeGpuRendererPromise = initializeGpu();
+ }
+ const promise = initializeGpuRendererPromise;
+ if (!promise) {
+ throw new Error("GPU renderer initialization promise was not created");
+ }
+ return promise;
+}
+
+export const gpuRenderer = {
+ applyEffect({
+ source,
+ width,
+ height,
+ passes,
+ }: {
+ source: CanvasImageSource;
+ width: number;
+ height: number;
+ passes: EffectPass[];
+ }): CanvasImageSource {
+ if (passes.length === 0) {
+ return source;
+ }
+
+ const sourceCanvas = ensureOffscreenCanvas({
+ source,
+ width,
+ height,
+ label: "effect source",
+ });
+ return applyEffectPasses({
+ source: sourceCanvas,
+ width,
+ height,
+ passes: serializeEffectPasses(passes),
+ });
+ },
+
+ applyMaskFeather({
+ maskCanvas,
+ width,
+ height,
+ feather,
+ }: {
+ maskCanvas: CanvasImageSource;
+ width: number;
+ height: number;
+ feather: number;
+ }): CanvasImageSource {
+ const sourceCanvas = ensureOffscreenCanvas({
+ source: maskCanvas,
+ width,
+ height,
+ label: "mask source",
+ });
+ return applyMaskFeatherWasm({
+ mask: sourceCanvas,
+ width,
+ height,
+ feather,
+ });
+ },
+};
+
+function ensureOffscreenCanvas({
+ source,
+ width,
+ height,
+ label,
+}: {
+ source: CanvasImageSource;
+ width: number;
+ height: number;
+ label: string;
+}): OffscreenCanvas {
+ if (source instanceof OffscreenCanvas) {
+ return source;
+ }
+
+ if (typeof OffscreenCanvas === "undefined") {
+ throw new Error(`OffscreenCanvas is required for the GPU ${label}`);
+ }
+
+ const canvas = new OffscreenCanvas(width, height);
+ const context = canvas.getContext("2d");
+ if (!context) {
+ throw new Error(`Failed to get 2d context for the GPU ${label}`);
+ }
+ context.clearRect(0, 0, width, height);
+ context.drawImage(source, 0, 0, width, height);
+ return canvas;
+}
+
+function serializeEffectPasses(passes: EffectPass[]) {
+ return passes.map((pass) => ({
+ shader: pass.shader,
+ uniforms: Object.entries(pass.uniforms).map(([name, value]) => ({
+ name,
+ value: normalizeUniformValue(value),
+ })),
+ }));
+}
+
+function normalizeUniformValue(value: EffectUniformValue): number[] {
+ return typeof value === "number" ? [value] : value;
+}
diff --git a/apps/web/src/services/renderer/mask-feather.ts b/apps/web/src/services/renderer/mask-feather.ts
index f1515117..4ea7eb2b 100644
--- a/apps/web/src/services/renderer/mask-feather.ts
+++ b/apps/web/src/services/renderer/mask-feather.ts
@@ -1,54 +1,20 @@
-import jfaDistanceShader from "@/lib/masks/shaders/jfa-distance.frag.glsl";
-import { getWebGLContext, readResult } from "./webgl/webgl-context";
-import { computeSignedDistanceField, runPass } from "./webgl/jfa";
-import { compileProgram, createTexture } from "./webgl/webgl-utils";
-
-export function applyMaskFeather({
- maskCanvas,
- width,
- height,
- feather,
-}: {
- maskCanvas: CanvasImageSource;
- width: number;
- height: number;
- feather: number;
-}): OffscreenCanvas | HTMLCanvasElement {
- const { context, programCache } = getWebGLContext({ width, height });
- const sourceTexture = createTexture({ context, source: maskCanvas });
-
- const sdf = computeSignedDistanceField({
- context,
- programCache,
- sourceTexture,
- width,
- height,
- });
-
- const distanceProgram = compileProgram({
- context,
- fragmentShaderSource: jfaDistanceShader,
- programCache,
- });
-
- runPass({
- context,
- program: distanceProgram,
- inputTexture: sdf.insideTexture,
- target: null,
- width,
- height,
- uniforms: { u_feather_half: feather / 2.0 },
- extraBindings: [
- { unit: 1, texture: sdf.outsideTexture, name: "u_jfa_outside" },
- ],
- });
-
- context.deleteTexture(sourceTexture);
- sdf.cleanup();
-
- context.bindTexture(context.TEXTURE_2D, null);
- context.bindFramebuffer(context.FRAMEBUFFER, null);
-
- return readResult({ width, height });
-}
+import { gpuRenderer } from "./gpu-renderer";
+
+export function applyMaskFeather({
+ maskCanvas,
+ width,
+ height,
+ feather,
+}: {
+ maskCanvas: CanvasImageSource;
+ width: number;
+ height: number;
+ feather: number;
+}): OffscreenCanvas | HTMLCanvasElement {
+ return gpuRenderer.applyMaskFeather({
+ maskCanvas,
+ width,
+ height,
+ feather,
+ }) as OffscreenCanvas | HTMLCanvasElement;
+}
diff --git a/apps/web/src/services/renderer/nodes/blur-background-node.ts b/apps/web/src/services/renderer/nodes/blur-background-node.ts
index 7f4d06fc..f790ebb4 100644
--- a/apps/web/src/services/renderer/nodes/blur-background-node.ts
+++ b/apps/web/src/services/renderer/nodes/blur-background-node.ts
@@ -5,7 +5,7 @@ import { videoCache } from "@/services/video-cache/service";
import type { RetimeConfig } from "@/lib/timeline";
import type { CanvasRenderer } from "../canvas-renderer";
import { createOffscreenCanvas } from "../canvas-utils";
-import { webglEffectRenderer } from "../webgl/webgl-effect-renderer";
+import { gpuRenderer } from "../gpu-renderer";
import { BaseNode } from "./base-node";
import { loadImageSource, type CachedImageSource } from "./image-node";
@@ -131,7 +131,7 @@ export class BlurBackgroundNode extends BaseNode<BlurBackgroundNodeParams> {
sigmaX: this.params.blurIntensity * (renderer.width / 1920),
sigmaY: this.params.blurIntensity * (renderer.height / 1080),
});
- const effectResult = webglEffectRenderer.applyEffect({
+ const effectResult = gpuRenderer.applyEffect({
source: offscreen as CanvasImageSource,
width: renderer.width,
height: renderer.height,
diff --git a/apps/web/src/services/renderer/nodes/effect-layer-node.ts b/apps/web/src/services/renderer/nodes/effect-layer-node.ts
index 51de2398..d8389fb5 100644
--- a/apps/web/src/services/renderer/nodes/effect-layer-node.ts
+++ b/apps/web/src/services/renderer/nodes/effect-layer-node.ts
@@ -1,81 +1,81 @@
-import type { CanvasRenderer } from "../canvas-renderer";
-import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
-import type { ParamValues } from "@/lib/params";
-import { BaseNode } from "./base-node";
-import { webglEffectRenderer } from "../webgl/webgl-effect-renderer";
-
-const TIME_EPSILON = 1e-6;
-
-export type EffectLayerNodeParams = {
- effectType: string;
- effectParams: ParamValues;
- timeOffset: number;
- duration: number;
-};
-
-function isInRange({
- time,
- timeOffset,
- duration,
-}: {
- time: number;
- timeOffset: number;
- duration: number;
-}): boolean {
- return (
- time >= timeOffset - TIME_EPSILON &&
- time < timeOffset + duration + TIME_EPSILON
- );
-}
-
-// snapshots whatever is currently on the canvas, applies the effect, draws it back
-export class EffectLayerNode extends BaseNode<EffectLayerNodeParams> {
- async render({
- renderer,
- time,
- }: {
- renderer: CanvasRenderer;
- time: number;
- }): Promise<void> {
- if (
- !isInRange({
- time,
- timeOffset: this.params.timeOffset,
- duration: this.params.duration,
- })
- ) {
- return;
- }
-
- const source = renderer.context.canvas as CanvasImageSource;
-
- const effectDefinition = effectsRegistry.get(this.params.effectType);
-
- const passes = resolveEffectPasses({
- definition: effectDefinition,
- effectParams: this.params.effectParams,
- width: renderer.width,
- height: renderer.height,
- });
- if (passes.length === 0) {
- return;
- }
- const effectResult = webglEffectRenderer.applyEffect({
- source,
- width: renderer.width,
- height: renderer.height,
- passes,
- });
-
- renderer.context.save();
- renderer.context.clearRect(0, 0, renderer.width, renderer.height);
- renderer.context.drawImage(
- effectResult,
- 0,
- 0,
- renderer.width,
- renderer.height,
- );
- renderer.context.restore();
- }
-}
+import type { CanvasRenderer } from "../canvas-renderer";
+import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
+import type { ParamValues } from "@/lib/params";
+import { BaseNode } from "./base-node";
+import { gpuRenderer } from "../gpu-renderer";
+
+const TIME_EPSILON = 1e-6;
+
+export type EffectLayerNodeParams = {
+ effectType: string;
+ effectParams: ParamValues;
+ timeOffset: number;
+ duration: number;
+};
+
+function isInRange({
+ time,
+ timeOffset,
+ duration,
+}: {
+ time: number;
+ timeOffset: number;
+ duration: number;
+}): boolean {
+ return (
+ time >= timeOffset - TIME_EPSILON &&
+ time < timeOffset + duration + TIME_EPSILON
+ );
+}
+
+// snapshots whatever is currently on the canvas, applies the effect, draws it back
+export class EffectLayerNode extends BaseNode<EffectLayerNodeParams> {
+ async render({
+ renderer,
+ time,
+ }: {
+ renderer: CanvasRenderer;
+ time: number;
+ }): Promise<void> {
+ if (
+ !isInRange({
+ time,
+ timeOffset: this.params.timeOffset,
+ duration: this.params.duration,
+ })
+ ) {
+ return;
+ }
+
+ const source = renderer.context.canvas as CanvasImageSource;
+
+ const effectDefinition = effectsRegistry.get(this.params.effectType);
+
+ const passes = resolveEffectPasses({
+ definition: effectDefinition,
+ effectParams: this.params.effectParams,
+ width: renderer.width,
+ height: renderer.height,
+ });
+ if (passes.length === 0) {
+ return;
+ }
+ const effectResult = gpuRenderer.applyEffect({
+ source,
+ width: renderer.width,
+ height: renderer.height,
+ passes,
+ });
+
+ renderer.context.save();
+ renderer.context.clearRect(0, 0, renderer.width, renderer.height);
+ renderer.context.drawImage(
+ effectResult,
+ 0,
+ 0,
+ renderer.width,
+ renderer.height,
+ );
+ renderer.context.restore();
+ }
+}
diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts
index 031c43b3..643e538a 100644
--- a/apps/web/src/services/renderer/nodes/text-node.ts
+++ b/apps/web/src/services/renderer/nodes/text-node.ts
@@ -21,7 +21,7 @@ import {
} from "@/lib/animation";
import { resolveEffectParamsAtTime } from "@/lib/animation/effect-param-channel";
import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
-import { webglEffectRenderer } from "../webgl/webgl-effect-renderer";
+import { gpuRenderer } from "../gpu-renderer";
import { clamp } from "@/utils/math";
const TEXT_DECORATION_THICKNESS_RATIO = 0.07;
@@ -272,7 +272,7 @@ export class TextNode extends BaseNode<TextNodeParams> {
width: renderer.width,
height: renderer.height,
});
- currentSource = webglEffectRenderer.applyEffect({
+ currentSource = gpuRenderer.applyEffect({
source: currentSource,
width: renderer.width,
height: renderer.height,
diff --git a/apps/web/src/services/renderer/nodes/visual-node.ts b/apps/web/src/services/renderer/nodes/visual-node.ts
index e0f87c52..82093da4 100644
--- a/apps/web/src/services/renderer/nodes/visual-node.ts
+++ b/apps/web/src/services/renderer/nodes/visual-node.ts
@@ -16,7 +16,7 @@ import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants";
import { effectsRegistry, resolveEffectPasses } from "@/lib/effects";
import { masksRegistry } from "@/lib/masks";
import { getSourceTimeAtClipTime } from "@/lib/retime";
-import { webglEffectRenderer } from "../webgl/webgl-effect-renderer";
+import { gpuRenderer } from "../gpu-renderer";
import { applyMaskFeather } from "../mask-feather";
export interface VisualNodeParams {
@@ -205,7 +205,7 @@ export abstract class VisualNode<
width,
height,
});
- current = webglEffectRenderer.applyEffect({
+ current = gpuRenderer.applyEffect({
source: current,
width: Math.round(width),
height: Math.round(height),
diff --git a/apps/web/src/services/renderer/webgl/jfa.ts b/apps/web/src/services/renderer/webgl/jfa.ts
deleted file mode 100644
index 9c87e7fa..00000000
--- a/apps/web/src/services/renderer/webgl/jfa.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import jfaInitShader from "@/lib/shaders/jfa-init.frag.glsl";
-import jfaStepShader from "@/lib/shaders/jfa-step.frag.glsl";
-import {
- compileProgram,
- createFramebufferTexture,
- setUniforms,
- drawFullscreenQuad,
-} from "./webgl-utils";
-
-interface FBPair {
- texture: WebGLTexture;
- framebuffer: WebGLFramebuffer;
-}
-
-function runPass({
- context,
- program,
- inputTexture,
- target,
- width,
- height,
- uniforms,
- extraBindings,
-}: {
- context: WebGLRenderingContext;
- program: WebGLProgram;
- inputTexture: WebGLTexture;
- target: WebGLFramebuffer | null;
- width: number;
- height: number;
- uniforms: Record<string, number | number[]>;
- extraBindings?: Array<{ unit: number; texture: WebGLTexture; name: string }>;
-}): void {
- context.bindFramebuffer(context.FRAMEBUFFER, target);
- // biome-ignore lint/correctness/useHookAtTopLevel: WebGL API method, not a React hook
- context.useProgram(program);
-
- context.activeTexture(context.TEXTURE0);
- context.bindTexture(context.TEXTURE_2D, inputTexture);
- const uTexLoc = context.getUniformLocation(program, "u_texture");
- if (uTexLoc) context.uniform1i(uTexLoc, 0);
-
- if (extraBindings) {
- for (const binding of extraBindings) {
- context.activeTexture(context.TEXTURE0 + binding.unit);
- context.bindTexture(context.TEXTURE_2D, binding.texture);
- const loc = context.getUniformLocation(program, binding.name);
- if (loc) context.uniform1i(loc, binding.unit);
- }
- }
-
- setUniforms({
- context,
- program,
- uniforms: { ...uniforms, u_resolution: [width, height] },
- });
- drawFullscreenQuad({ context, program, width, height });
-}
-
-function runJFA({
- context,
- programCache,
- sourceTexture,
- width,
- height,
- isInverted,
-}: {
- context: WebGLRenderingContext;
- programCache: Map<string, WebGLProgram>;
- sourceTexture: WebGLTexture;
- width: number;
- height: number;
- isInverted: boolean;
-}): {
- resultTexture: WebGLTexture;
- resultFB: WebGLFramebuffer;
- tempFBs: FBPair[];
-} {
- const numSteps = Math.ceil(Math.log2(Math.max(width, height)));
-
- const fbA = createFramebufferTexture({ context, width, height });
- const fbB = createFramebufferTexture({ context, width, height });
-
- const initProgram = compileProgram({
- context,
- fragmentShaderSource: jfaInitShader,
- programCache,
- });
- runPass({
- context,
- program: initProgram,
- inputTexture: sourceTexture,
- target: fbA.framebuffer,
- width,
- height,
- uniforms: { u_invert: isInverted ? 1.0 : 0.0 },
- });
-
- const stepProgram = compileProgram({
- context,
- fragmentShaderSource: jfaStepShader,
- programCache,
- });
-
- let readFB = fbA;
- let writeFB = fbB;
-
- for (let i = numSteps - 1; i >= 0; i--) {
- const stepSize = 2 ** i;
- runPass({
- context,
- program: stepProgram,
- inputTexture: readFB.texture,
- target: writeFB.framebuffer,
- width,
- height,
- uniforms: { u_step_size: stepSize },
- });
- const tmp = readFB;
- readFB = writeFB;
- writeFB = tmp;
- }
-
- return {
- resultTexture: readFB.texture,
- resultFB: readFB.framebuffer,
- tempFBs: [writeFB],
- };
-}
-
-function cleanupJFAResult({
- context,
- result,
-}: {
- context: WebGLRenderingContext;
- result: {
- resultTexture: WebGLTexture;
- resultFB: WebGLFramebuffer;
- tempFBs: FBPair[];
- };
-}): void {
- context.deleteTexture(result.resultTexture);
- context.deleteFramebuffer(result.resultFB);
- for (const fb of result.tempFBs) {
- context.deleteTexture(fb.texture);
- context.deleteFramebuffer(fb.framebuffer);
- }
-}
-
-export { runPass };
-
-export function computeSignedDistanceField({
- context,
- programCache,
- sourceTexture,
- width,
- height,
-}: {
- context: WebGLRenderingContext;
- programCache: Map<string, WebGLProgram>;
- sourceTexture: WebGLTexture;
- width: number;
- height: number;
-}): {
- insideTexture: WebGLTexture;
- outsideTexture: WebGLTexture;
- cleanup: () => void;
-} {
- const inside = runJFA({
- context,
- programCache,
- sourceTexture,
- width,
- height,
- isInverted: false,
- });
- const outside = runJFA({
- context,
- programCache,
- sourceTexture,
- width,
- height,
- isInverted: true,
- });
-
- return {
- insideTexture: inside.resultTexture,
- outsideTexture: outside.resultTexture,
- cleanup: () => {
- cleanupJFAResult({ context, result: inside });
- cleanupJFAResult({ context, result: outside });
- },
- };
-}
diff --git a/apps/web/src/services/renderer/webgl/webgl-context.ts b/apps/web/src/services/renderer/webgl/webgl-context.ts
deleted file mode 100644
index 39869a02..00000000
--- a/apps/web/src/services/renderer/webgl/webgl-context.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { createOffscreenCanvas } from "../canvas-utils";
-
-let gl: WebGLRenderingContext | null = null;
-let webglCanvas: OffscreenCanvas | HTMLCanvasElement | null = null;
-const programCache = new Map<string, WebGLProgram>();
-
-export function getWebGLContext({
- width,
- height,
-}: {
- width: number;
- height: number;
-}): {
- context: WebGLRenderingContext;
- programCache: Map<string, WebGLProgram>;
-} {
- if (!webglCanvas) {
- webglCanvas = createOffscreenCanvas({ width, height });
- gl = webglCanvas.getContext("webgl", {
- premultipliedAlpha: false,
- }) as WebGLRenderingContext | null;
- if (!gl) throw new Error("WebGL not supported");
- }
- if (webglCanvas.width !== width || webglCanvas.height !== height) {
- webglCanvas.width = width;
- webglCanvas.height = height;
- }
- if (!gl) throw new Error("WebGL context lost");
- return { context: gl, programCache };
-}
-
-export function readResult({
- width,
- height,
-}: {
- width: number;
- height: number;
-}): OffscreenCanvas | HTMLCanvasElement {
- if (!webglCanvas) throw new Error("WebGL canvas not initialized");
- const outputCanvas = createOffscreenCanvas({ width, height });
- const outputCtx = outputCanvas.getContext("2d") as
- | CanvasRenderingContext2D
- | OffscreenCanvasRenderingContext2D
- | null;
- if (outputCtx) {
- outputCtx.drawImage(webglCanvas, 0, 0, width, height);
- }
- return outputCanvas;
-}
diff --git a/apps/web/src/services/renderer/webgl/webgl-effect-renderer.ts b/apps/web/src/services/renderer/webgl/webgl-effect-renderer.ts
deleted file mode 100644
index 95ef990b..00000000
--- a/apps/web/src/services/renderer/webgl/webgl-effect-renderer.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { getWebGLContext, readResult } from "./webgl-context";
-import { applyMultiPassEffect } from "./webgl-utils";
-import type { EffectPassData } from "./webgl-utils";
-
-export interface ApplyEffectParams {
- source: CanvasImageSource;
- width: number;
- height: number;
- passes: EffectPassData[];
-}
-
-function applyEffect({
- source,
- width,
- height,
- passes,
-}: ApplyEffectParams): CanvasImageSource {
- if (passes.length === 0) {
- return source;
- }
- const { context, programCache } = getWebGLContext({ width, height });
- applyMultiPassEffect({
- context,
- source,
- width,
- height,
- passes,
- programCache,
- });
- return readResult({ width, height });
-}
-
-export const webglEffectRenderer = {
- applyEffect,
-};
diff --git a/apps/web/src/services/renderer/webgl/webgl-utils.ts b/apps/web/src/services/renderer/webgl/webgl-utils.ts
deleted file mode 100644
index ec207546..00000000
--- a/apps/web/src/services/renderer/webgl/webgl-utils.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import VERTEX_SHADER_SOURCE from "@/lib/effects/effect.vert.glsl";
-
-export interface EffectPassData {
- fragmentShader: string;
- uniforms: Record<string, number | number[]>;
-}
-
-export const QUAD_POSITIONS = new Float32Array([
- -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,
-]);
-
-export function compileProgram({
- context,
- fragmentShaderSource,
- programCache,
-}: {
- context: WebGLRenderingContext;
- fragmentShaderSource: string;
- programCache: Map<string, WebGLProgram>;
-}): WebGLProgram {
- const cached = programCache.get(fragmentShaderSource);
- if (cached) {
- return cached;
- }
- const vertexShader = compileShader({
- context,
- source: VERTEX_SHADER_SOURCE,
- type: context.VERTEX_SHADER,
- });
- const fragmentShader = compileShader({
- context,
- source: fragmentShaderSource,
- type: context.FRAGMENT_SHADER,
- });
- const program = context.createProgram();
- if (!program) {
- throw new Error("Failed to create WebGL program");
- }
- context.attachShader(program, vertexShader);
- context.attachShader(program, fragmentShader);
- context.linkProgram(program);
- if (!context.getProgramParameter(program, context.LINK_STATUS)) {
- const info = context.getProgramInfoLog(program);
- context.deleteProgram(program);
- throw new Error(`WebGL program link failed: ${info}`);
- }
- context.deleteShader(vertexShader);
- context.deleteShader(fragmentShader);
- programCache.set(fragmentShaderSource, program);
- return program;
-}
-
-export function compileShader({
- context,
- source,
- type,
-}: {
- context: WebGLRenderingContext;
- source: string;
- type: number;
-}): WebGLShader {
- const shader = context.createShader(type);
- if (!shader) {
- throw new Error("Failed to create WebGL shader");
- }
- context.shaderSource(shader, source);
- context.compileShader(shader);
- if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
- const info = context.getShaderInfoLog(shader);
- context.deleteShader(shader);
- throw new Error(`WebGL shader compile failed: ${info}`);
- }
- return shader;
-}
-
-export function createTexture({
- context,
- source,
-}: {
- context: WebGLRenderingContext;
- source: CanvasImageSource;
-}): WebGLTexture {
- const texture = context.createTexture();
- if (!texture) {
- throw new Error("Failed to create WebGL texture");
- }
- context.activeTexture(context.TEXTURE0);
- context.bindTexture(context.TEXTURE_2D, texture);
- context.pixelStorei(context.UNPACK_FLIP_Y_WEBGL, 1);
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_WRAP_S,
- context.CLAMP_TO_EDGE,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_WRAP_T,
- context.CLAMP_TO_EDGE,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_MIN_FILTER,
- context.LINEAR,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_MAG_FILTER,
- context.LINEAR,
- );
- context.texImage2D(
- context.TEXTURE_2D,
- 0,
- context.RGBA,
- context.RGBA,
- context.UNSIGNED_BYTE,
- source as TexImageSource,
- );
- return texture;
-}
-
-export function setUniforms({
- context,
- program,
- uniforms,
-}: {
- context: WebGLRenderingContext;
- program: WebGLProgram;
- uniforms: Record<string, number | number[]>;
-}): void {
- for (const [name, value] of Object.entries(uniforms)) {
- const location = context.getUniformLocation(program, name);
- if (location === null) continue;
-
- if (typeof value === "number") {
- context.uniform1f(location, value);
- } else if (Array.isArray(value)) {
- if (value.length === 2) {
- context.uniform2fv(location, new Float32Array(value));
- } else if (value.length === 3) {
- context.uniform3fv(location, new Float32Array(value));
- } else if (value.length === 4) {
- context.uniform4fv(location, new Float32Array(value));
- }
- }
- }
-}
-
-export function drawFullscreenQuad({
- context,
- program,
- width,
- height,
-}: {
- context: WebGLRenderingContext;
- program: WebGLProgram;
- width: number;
- height: number;
-}): void {
- const positionLocation = context.getAttribLocation(program, "a_position");
- const buffer = context.createBuffer();
- context.bindBuffer(context.ARRAY_BUFFER, buffer);
- context.bufferData(context.ARRAY_BUFFER, QUAD_POSITIONS, context.STATIC_DRAW);
- context.enableVertexAttribArray(positionLocation);
- context.vertexAttribPointer(positionLocation, 2, context.FLOAT, false, 0, 0);
-
- context.viewport(0, 0, width, height);
- context.clearColor(0, 0, 0, 0);
- context.clear(context.COLOR_BUFFER_BIT);
- context.drawArrays(context.TRIANGLES, 0, 6);
-}
-
-export function createFramebufferTexture({
- context,
- width,
- height,
-}: {
- context: WebGLRenderingContext;
- width: number;
- height: number;
-}): { texture: WebGLTexture; framebuffer: WebGLFramebuffer } {
- const texture = context.createTexture();
- if (!texture) throw new Error("Failed to create framebuffer texture");
- context.bindTexture(context.TEXTURE_2D, texture);
- context.texImage2D(
- context.TEXTURE_2D,
- 0,
- context.RGBA,
- width,
- height,
- 0,
- context.RGBA,
- context.UNSIGNED_BYTE,
- null,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_WRAP_S,
- context.CLAMP_TO_EDGE,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_WRAP_T,
- context.CLAMP_TO_EDGE,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_MIN_FILTER,
- context.LINEAR,
- );
- context.texParameteri(
- context.TEXTURE_2D,
- context.TEXTURE_MAG_FILTER,
- context.LINEAR,
- );
- context.bindTexture(context.TEXTURE_2D, null);
-
- const framebuffer = context.createFramebuffer();
- if (!framebuffer) throw new Error("Failed to create framebuffer");
- context.bindFramebuffer(context.FRAMEBUFFER, framebuffer);
- context.framebufferTexture2D(
- context.FRAMEBUFFER,
- context.COLOR_ATTACHMENT0,
- context.TEXTURE_2D,
- texture,
- 0,
- );
- context.bindFramebuffer(context.FRAMEBUFFER, null);
-
- return { texture, framebuffer };
-}
-
-export function applyMultiPassEffect({
- context,
- source,
- width,
- height,
- passes,
- programCache,
-}: {
- context: WebGLRenderingContext;
- source: CanvasImageSource;
- width: number;
- height: number;
- passes: EffectPassData[];
- programCache: Map<string, WebGLProgram>;
-}): void {
- const sourceTexture = createTexture({ context, source });
- let currentTexture: WebGLTexture = sourceTexture;
-
- const intermediates: Array<{
- texture: WebGLTexture;
- framebuffer: WebGLFramebuffer;
- }> = [];
- for (let i = 0; i < passes.length - 1; i++) {
- intermediates.push(createFramebufferTexture({ context, width, height }));
- }
-
- for (let i = 0; i < passes.length; i++) {
- const pass = passes[i];
- const program = compileProgram({
- context,
- fragmentShaderSource: pass.fragmentShader,
- programCache,
- });
- const isLastPass = i === passes.length - 1;
- const targetFramebuffer = isLastPass ? null : intermediates[i].framebuffer;
-
- context.bindFramebuffer(context.FRAMEBUFFER, targetFramebuffer);
- // biome-ignore lint/correctness/useHookAtTopLevel: WebGL API method, not a React hook
- context.useProgram(program);
- context.activeTexture(context.TEXTURE0);
- context.bindTexture(context.TEXTURE_2D, currentTexture);
-
- const uTextureLocation = context.getUniformLocation(program, "u_texture");
- if (uTextureLocation) {
- context.uniform1i(uTextureLocation, 0);
- }
-
- setUniforms({
- context,
- program,
- uniforms: { ...pass.uniforms, u_resolution: [width, height] },
- });
- drawFullscreenQuad({ context, program, width, height });
-
- if (!isLastPass) {
- currentTexture = intermediates[i].texture;
- }
- }
-
- context.deleteTexture(sourceTexture);
- for (const intermediate of intermediates) {
- context.deleteTexture(intermediate.texture);
- context.deleteFramebuffer(intermediate.framebuffer);
- }
- context.bindTexture(context.TEXTURE_2D, null);
- context.bindFramebuffer(context.FRAMEBUFFER, null);
-}
diff --git a/apps/web/src/types/glsl.d.ts b/apps/web/src/types/glsl.d.ts
deleted file mode 100644
index 2a5de52c..00000000
--- a/apps/web/src/types/glsl.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module "*.glsl" {
- const value: string;
- export default value;
-}
diff --git a/bun.lock b/bun.lock
index d528cc56..bc1dff3b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -96,7 +96,6 @@
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.4",
"postcss": "^8",
- "raw-loader": "^4.0.2",
"sharp": "^0.34.5",
"tailwindcss": "^4.2.1",
"typescript": "^5.8.3",
@@ -105,7 +104,7 @@
},
"rust/wasm/pkg": {
"name": "opencut-wasm",
- "version": "0.1.0",
+ "version": "0.1.2",
},
},
"trustedDependencies": [
@@ -800,18 +799,12 @@
"@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
- "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
-
- "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
-
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
- "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
-
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -838,56 +831,14 @@
"@upstash/redis": ["@upstash/redis@1.35.4", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-WE1ZnhFyBiIjTDW13GbO6JjkiMVVjw5VsvS8ENmvvJsze/caMQ5paxVD44+U68IUVmkXcbsLSoE+VIYsHtbQEw=="],
- "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
-
- "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
-
- "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
-
- "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
-
- "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
-
- "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
-
- "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
-
- "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
-
- "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
-
- "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
-
- "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
-
- "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
-
- "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
-
- "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
-
- "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
-
- "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
-
- "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
-
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
- "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
-
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
- "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
-
- "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
-
- "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
-
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -914,8 +865,6 @@
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
- "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="],
-
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
@@ -928,8 +877,6 @@
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
- "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
-
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
@@ -960,8 +907,6 @@
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
- "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
-
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@@ -1042,8 +987,6 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
-
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
"embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
@@ -1052,8 +995,6 @@
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
- "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
-
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
@@ -1068,8 +1009,6 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
- "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
-
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
@@ -1086,14 +1025,8 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
-
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
- "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
-
- "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
-
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
@@ -1102,8 +1035,6 @@
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
- "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
-
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
@@ -1112,12 +1043,6 @@
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
- "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
-
- "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
-
- "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
-
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
"fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
@@ -1168,8 +1093,6 @@
"glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="],
- "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
-
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
@@ -1184,8 +1107,6 @@
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
- "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
-
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -1258,8 +1179,6 @@
"jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
- "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
-
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
@@ -1268,14 +1187,8 @@
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
- "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
-
- "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
-
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
- "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
-
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -1306,10 +1219,6 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
- "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
-
- "loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
-
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
@@ -1392,9 +1301,9 @@
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
- "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
- "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
@@ -1424,8 +1333,6 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
- "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
-
"next": ["next@16.1.3", "", { "dependencies": { "@next/env": "16.1.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.3", "@next/swc-darwin-x64": "16.1.3", "@next/swc-linux-arm64-gnu": "16.1.3", "@next/swc-linux-arm64-musl": "16.1.3", "@next/swc-linux-x64-gnu": "16.1.3", "@next/swc-linux-x64-musl": "16.1.3", "@next/swc-win32-arm64-msvc": "16.1.3", "@next/swc-win32-x64-msvc": "16.1.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -1434,8 +1341,6 @@
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
- "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
-
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -1524,8 +1429,6 @@
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
- "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
@@ -1538,8 +1441,6 @@
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
- "raw-loader": ["raw-loader@4.0.2", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA=="],
-
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
@@ -1582,8 +1483,6 @@
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
- "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
-
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
@@ -1600,8 +1499,6 @@
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
- "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
-
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@@ -1672,7 +1569,7 @@
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
- "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+ "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
@@ -1684,9 +1581,7 @@
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
- "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
-
- "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="],
+ "terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
@@ -1734,10 +1629,6 @@
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
- "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
-
- "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@@ -1758,8 +1649,6 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
- "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
-
"wavesurfer.js": ["wavesurfer.js@7.10.0", "", {}, "sha512-GiyAHdorqGtUYG5fe4BfTf5lmtSLhrXoHeNlMsR80JOiOZxOrIOv9QaIR8RnqlleJ6D8R9cqvZKR9lfJcWcapg=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
@@ -1768,10 +1657,6 @@
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
- "webpack": ["webpack@5.105.3", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg=="],
-
- "webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="],
-
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1842,12 +1727,8 @@
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
- "@node-minify/terser/terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="],
-
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
- "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
-
"@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1956,16 +1837,10 @@
"@upstash/core-analytics/@upstash/redis": ["@upstash/redis@1.35.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA=="],
- "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
-
- "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
-
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
- "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
-
"cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
@@ -1980,16 +1855,14 @@
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
- "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
-
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
- "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
-
"finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
@@ -2014,18 +1887,10 @@
"send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
- "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
-
"string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
- "terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
-
- "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
-
- "webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
-
"wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
@@ -2088,8 +1953,6 @@
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
- "@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
-
"@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
@@ -2204,10 +2067,6 @@
"@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
- "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
-
- "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
@@ -2276,24 +2135,12 @@
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
- "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
- "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
-
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "terser-webpack-plugin/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
-
- "terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
-
- "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
-
- "webpack/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
-
- "webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
-
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -2360,10 +2207,6 @@
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
- "terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
- "webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
"@node-minify/core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
diff --git a/docs/effects-renderer.md b/docs/effects-renderer.md
index a26bcae6..d68cec21 100644
--- a/docs/effects-renderer.md
+++ b/docs/effects-renderer.md
@@ -1,4 +1,4 @@
-# Effects & WebGL Renderer
+# Effects & GPU Renderer
## How to add a new effect
@@ -11,19 +11,18 @@ An effect definition has:
- `name` — display name
- `keywords` — for search
- `params` — user-facing controls (sliders, toggles, etc.)
-- `renderer` — always `webgl`
+- `renderer` — GPU pass templates resolved into shader identifiers + uniforms
-All effects use WebGL. Even simple single-value effects like brightness or contrast are trivial shaders — there's no reason to leave the GPU pipeline for them.
+All effects use the shared GPU renderer. TypeScript decides which shader identifiers to run and which uniforms to pass. Rust/wgpu owns device creation, textures, and pass execution.
## Single-pass vs multi-pass
-The `webgl` renderer supports a `passes` array. Single-pass effects (e.g. color grading) just have one entry. Multi-pass is needed when an effect has to process its own output — blur (H then V), bloom (extract → blur → composite), glow, etc.
+The renderer supports a `passes` array. Single-pass effects (e.g. color grading) just have one entry. Multi-pass is needed when an effect has to process its own output — blur (H then V), bloom (extract → blur → composite), glow, etc.
```typescript
renderer: {
- type: "webgl",
passes: [
- { fragmentShader: myShader, uniforms: ({ effectParams }) => ({ ... }) },
+ { shader: "my-effect-shader", uniforms: ({ effectParams }) => ({ ... }) },
],
}
```
@@ -34,10 +33,9 @@ Some effects need a variable number of passes depending on their parameters (e.g
```typescript
renderer: {
- type: "webgl",
passes: [ /* static fallback — used if buildPasses is absent */ ],
buildPasses: ({ effectParams, width, height }) => {
- // return ResolvedEffectPass[] with pre-computed uniforms
+ // return EffectPass[] with pre-computed uniforms
},
}
```
@@ -58,11 +56,17 @@ This handles the `buildPasses` vs static `passes` dispatch automatically.
### Pipeline
-Linear effect chains (blur, color grading, bloom) go through `applyMultiPassEffect` in `apps/web/src/services/renderer/webgl-utils.ts`. Non-linear GPU pipelines that need branching or multi-texture passes (like JFA for signed distance fields) get their own orchestrator in `services/renderer/` and share the WebGL context via `webgl-context.ts`.
+Linear effect chains go through `gpuRenderer.applyEffect()` in `apps/web/src/services/renderer/gpu-renderer.ts`.
-## Writing fragment shaders
+TypeScript resolves `EffectPass[]` from effect definitions. Each pass contains:
+- `shader` — a stable identifier such as `"gaussian-blur"`
+- `uniforms` — resolved numeric values for that pass
-Effect-specific shaders live in `apps/web/src/lib/effects/definitions/`. General-purpose GPU algorithm shaders (like JFA) live in `apps/web/src/lib/shaders/`. Domain-specific shaders that consume a general algorithm (like the mask feather smoothstep) live with their domain (e.g. `lib/masks/shaders/`). The shared vertex shader (`effect.vert.glsl`) maps clip space to UV coordinates — don't replace it unless you have a specific reason.
+Rust maps the shader identifier to a precompiled WGSL pipeline in `rust/crates/gpu/src/shader_registry.rs`. Non-linear GPU work such as signed-distance-field generation and mask feathering lives in dedicated Rust pipeline modules, not in TypeScript orchestration.
+
+## Writing shaders
+
+Effect-specific WGSL shaders live in `rust/crates/gpu/src/shaders/`. Add the shader file there, then register its identifier in `rust/crates/gpu/src/shader_registry.rs`.
Available uniforms (automatically injected, no need to pass them manually):
- `u_texture` — the input texture (sampler2D)
@@ -78,17 +82,15 @@ The fix is a `u_step` uniform that spaces samples further apart. With step=4 the
Keep the step size moderate (≤4) to avoid visible banding. If you need more blur than step=4 allows in a single iteration, add iterations instead of increasing the step further.
-```glsl
+```wgsl
// u_step scales the distance between samples
-float pos = float(i) * u_step;
-float weight = exp(-(pos * pos) / (2.0 * u_sigma * u_sigma));
-color += texture2D(u_texture, v_texCoord + texelSize * u_direction * pos) * weight;
+let position = f32(sample_index) * uniforms.step;
+let weight = exp(-(position * position) / (2.0 * uniforms.sigma * uniforms.sigma));
+color += textureSample(input_texture, input_sampler, uv + texel_size * uniforms.direction * position) * weight;
```
Do **not** use large step sizes (>6) in a single pass — it creates visible banding regardless of bilinear interpolation. Use multiple iterations instead.
-## Y-flip and coordinate systems
-
-Source textures (uploaded from canvas) are Y-flipped via `UNPACK_FLIP_Y_WEBGL`. Intermediate FBO textures (rendered by WebGL between passes) are not. In practice this cancels out correctly as long as you use the shared vertex shader — it maps clip space Y consistently so both texture types sample correctly.
+## Coordinate systems
-If you write a custom vertex shader or do manual coordinate math, be aware that canvas and WebGL have opposite Y origins (canvas: top-left, WebGL: bottom-left). Getting this wrong produces an upside-down result with no obvious error.
+Source canvases are imported through `copy_external_image_to_texture()`, which is the boundary where browser canvas data enters the GPU pipeline. If a shader or import path changes, validate orientation explicitly — the renderer assumes a consistent top-left canvas origin by the time results come back to TypeScript.
diff --git a/rust/crates/gpu/Cargo.toml b/rust/crates/gpu/Cargo.toml
new file mode 100644
index 00000000..d68e8a65
--- /dev/null
+++ b/rust/crates/gpu/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "gpu"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+bytemuck = { version = "1.25.0", features = ["derive"] }
+thiserror = "2.0.18"
+wgpu = "29.0.1"
diff --git a/rust/crates/gpu/src/context.rs b/rust/crates/gpu/src/context.rs
new file mode 100644
index 00000000..702f65ba
--- /dev/null
+++ b/rust/crates/gpu/src/context.rs
@@ -0,0 +1,250 @@
+use wgpu::util::DeviceExt;
+
+use crate::{
+ GPU_TEXTURE_FORMAT, GpuError,
+ effect_pipeline::{ApplyEffectsOptions, apply_effects},
+ mask_feather::{ApplyMaskFeatherOptions, MaskFeatherPipeline},
+ sdf_pipeline::SdfPipeline,
+ shader_registry::ShaderRegistry,
+};
+
+const FULLSCREEN_QUAD_POSITIONS: [[f32; 2]; 6] = [
+ [-1.0, -1.0],
+ [1.0, -1.0],
+ [-1.0, 1.0],
+ [-1.0, 1.0],
+ [1.0, -1.0],
+ [1.0, 1.0],
+];
+
+pub struct GpuContext {
+ instance: wgpu::Instance,
+ adapter: wgpu::Adapter,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ fullscreen_quad: wgpu::Buffer,
+ linear_sampler: wgpu::Sampler,
+ nearest_sampler: wgpu::Sampler,
+ shader_registry: ShaderRegistry,
+ sdf_pipeline: SdfPipeline,
+ mask_feather_pipeline: MaskFeatherPipeline,
+}
+
+impl GpuContext {
+ pub async fn new() -> Result<Self, GpuError> {
+ let instance = wgpu::Instance::default();
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::HighPerformance,
+ compatible_surface: None,
+ force_fallback_adapter: false,
+ })
+ .await
+ .map_err(|_| GpuError::AdapterUnavailable)?;
+ let (device, queue) = adapter
+ .request_device(&wgpu::DeviceDescriptor {
+ label: Some("gpu-device"),
+ required_features: wgpu::Features::empty(),
+ required_limits: wgpu::Limits::downlevel_webgl2_defaults()
+ .using_resolution(adapter.limits()),
+ memory_hints: wgpu::MemoryHints::Performance,
+ experimental_features: wgpu::ExperimentalFeatures::disabled(),
+ trace: wgpu::Trace::Off,
+ })
+ .await?;
+ let fullscreen_quad = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("gpu-fullscreen-quad-buffer"),
+ contents: bytemuck::cast_slice(&FULLSCREEN_QUAD_POSITIONS),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+ let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("gpu-linear-sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::MipmapFilterMode::Nearest,
+ ..Default::default()
+ });
+ let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("gpu-nearest-sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Nearest,
+ min_filter: wgpu::FilterMode::Nearest,
+ mipmap_filter: wgpu::MipmapFilterMode::Nearest,
+ ..Default::default()
+ });
+ let shader_registry = ShaderRegistry::new(&device);
+ let sdf_pipeline = SdfPipeline::new(&device);
+ let mask_feather_pipeline = MaskFeatherPipeline::new(&device);
+
+ Ok(Self {
+ instance,
+ adapter,
+ device,
+ queue,
+ fullscreen_quad,
+ linear_sampler,
+ nearest_sampler,
+ shader_registry,
+ sdf_pipeline,
+ mask_feather_pipeline,
+ })
+ }
+
+ pub fn apply_effects(
+ &self,
+ options: ApplyEffectsOptions<'_>,
+ ) -> Result<wgpu::Texture, GpuError> {
+ apply_effects(self, options)
+ }
+
+ pub fn apply_mask_feather(
+ &self,
+ options: ApplyMaskFeatherOptions<'_>,
+ ) -> wgpu::Texture {
+ self.mask_feather_pipeline.apply_mask_feather(self, options)
+ }
+
+ pub fn create_render_texture(
+ &self,
+ width: u32,
+ height: u32,
+ label: &'static str,
+ ) -> wgpu::Texture {
+ self.device.create_texture(&wgpu::TextureDescriptor {
+ label: Some(label),
+ size: wgpu::Extent3d {
+ width,
+ height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: GPU_TEXTURE_FORMAT,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING
+ | wgpu::TextureUsages::COPY_DST
+ | wgpu::TextureUsages::COPY_SRC
+ | wgpu::TextureUsages::RENDER_ATTACHMENT,
+ view_formats: &[],
+ })
+ }
+
+ pub fn instance(&self) -> &wgpu::Instance {
+ &self.instance
+ }
+
+ pub fn adapter(&self) -> &wgpu::Adapter {
+ &self.adapter
+ }
+
+ pub fn device(&self) -> &wgpu::Device {
+ &self.device
+ }
+
+ pub fn queue(&self) -> &wgpu::Queue {
+ &self.queue
+ }
+
+ pub fn fullscreen_quad(&self) -> &wgpu::Buffer {
+ &self.fullscreen_quad
+ }
+
+ pub fn linear_sampler(&self) -> &wgpu::Sampler {
+ &self.linear_sampler
+ }
+
+ pub fn nearest_sampler(&self) -> &wgpu::Sampler {
+ &self.nearest_sampler
+ }
+
+ pub fn shader_registry(&self) -> &ShaderRegistry {
+ &self.shader_registry
+ }
+
+ pub fn sdf_pipeline(&self) -> &SdfPipeline {
+ &self.sdf_pipeline
+ }
+
+ pub fn render_texture_to_surface(
+ &self,
+ texture: &wgpu::Texture,
+ surface: &wgpu::Surface<'_>,
+ width: u32,
+ height: u32,
+ ) -> Result<(), GpuError> {
+ let Some(config) = surface.get_default_config(&self.adapter, width, height) else {
+ return Err(GpuError::UnsupportedSurfaceFormat);
+ };
+ if config.format != GPU_TEXTURE_FORMAT {
+ return Err(GpuError::UnsupportedSurfaceFormat);
+ }
+ surface.configure(&self.device, &config);
+
+ let surface_texture = match surface.get_current_texture() {
+ wgpu::CurrentSurfaceTexture::Success(surface_texture)
+ | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture,
+ wgpu::CurrentSurfaceTexture::Timeout
+ | wgpu::CurrentSurfaceTexture::Occluded
+ | wgpu::CurrentSurfaceTexture::Outdated
+ | wgpu::CurrentSurfaceTexture::Lost
+ | wgpu::CurrentSurfaceTexture::Validation => {
+ return Err(GpuError::UnsupportedSurfaceFormat);
+ }
+ };
+ let source_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let target_view = surface_texture
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-blit-bind-group"),
+ layout: self.shader_registry.effect_texture_bind_group_layout(),
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&source_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&self.linear_sampler),
+ },
+ ],
+ });
+ let mut encoder = self
+ .device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("gpu-surface-blit-encoder"),
+ });
+
+ {
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("gpu-surface-blit-pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &target_view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ multiview_mask: None,
+ });
+ render_pass.set_pipeline(self.shader_registry.blit_pipeline());
+ render_pass.set_vertex_buffer(0, self.fullscreen_quad.slice(..));
+ render_pass.set_bind_group(0, &bind_group, &[]);
+ render_pass.draw(0..6, 0..1);
+ }
+
+ self.queue.submit([encoder.finish()]);
+ surface_texture.present();
+ Ok(())
+ }
+}
diff --git a/rust/crates/gpu/src/effect_pipeline.rs b/rust/crates/gpu/src/effect_pipeline.rs
new file mode 100644
index 00000000..c600551a
--- /dev/null
+++ b/rust/crates/gpu/src/effect_pipeline.rs
@@ -0,0 +1,194 @@
+use std::collections::HashMap;
+
+use bytemuck::{Pod, Zeroable};
+use wgpu::util::DeviceExt;
+
+use crate::{GpuError, context::GpuContext};
+
+pub struct ApplyEffectsOptions<'a> {
+ pub source: &'a wgpu::Texture,
+ pub width: u32,
+ pub height: u32,
+ pub passes: &'a [EffectPass],
+}
+
+#[derive(Clone, Debug)]
+pub struct EffectPass {
+ pub shader: String,
+ pub uniforms: HashMap<String, UniformValue>,
+}
+
+#[derive(Clone, Debug)]
+pub enum UniformValue {
+ Number(f32),
+ Vector(Vec<f32>),
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Pod, Zeroable)]
+struct EffectUniformBuffer {
+ resolution: [f32; 2],
+ direction: [f32; 2],
+ scalars: [f32; 4],
+}
+
+pub fn apply_effects(
+ context: &GpuContext,
+ ApplyEffectsOptions {
+ source,
+ width,
+ height,
+ passes,
+ }: ApplyEffectsOptions<'_>,
+) -> Result<wgpu::Texture, GpuError> {
+ let mut current_texture: Option<wgpu::Texture> = None;
+
+ for pass in passes {
+ let input_texture = current_texture.as_ref().unwrap_or(source);
+ let output_texture =
+ context.create_render_texture(width, height, "gpu-effect-pass-output");
+ let input_view = input_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let texture_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-effect-texture-bind-group"),
+ layout: context
+ .shader_registry()
+ .effect_texture_bind_group_layout(),
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&input_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(context.linear_sampler()),
+ },
+ ],
+ });
+ let uniform_buffer = context
+ .device()
+ .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("gpu-effect-uniform-buffer"),
+ contents: bytemuck::bytes_of(&pack_effect_uniforms(pass, width, height)?),
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ });
+ let uniform_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-effect-uniform-bind-group"),
+ layout: context
+ .shader_registry()
+ .effect_uniform_bind_group_layout(),
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: uniform_buffer.as_entire_binding(),
+ }],
+ });
+ let pipeline = context.shader_registry().get_effect_pipeline(&pass.shader)?;
+ let mut encoder = context
+ .device()
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("gpu-effect-command-encoder"),
+ });
+
+ {
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("gpu-effect-render-pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &output_view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ multiview_mask: None,
+ });
+ render_pass.set_pipeline(pipeline);
+ render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..));
+ render_pass.set_bind_group(0, &texture_bind_group, &[]);
+ render_pass.set_bind_group(1, &uniform_bind_group, &[]);
+ render_pass.draw(0..6, 0..1);
+ }
+
+ context.queue().submit([encoder.finish()]);
+ current_texture = Some(output_texture);
+ }
+
+ current_texture.ok_or_else(|| GpuError::UnknownEffectShader {
+ shader: "missing-effect-pass".to_string(),
+ })
+}
+
+fn pack_effect_uniforms(
+ pass: &EffectPass,
+ width: u32,
+ height: u32,
+) -> Result<EffectUniformBuffer, GpuError> {
+ let shader = pass.shader.as_str();
+ let sigma = read_number_uniform(pass, "u_sigma")?;
+ let step = read_number_uniform(pass, "u_step")?;
+ let direction = read_vec2_uniform(pass, "u_direction")?;
+
+ for uniform in pass.uniforms.keys() {
+ if uniform == "u_sigma" || uniform == "u_step" || uniform == "u_direction" {
+ continue;
+ }
+ return Err(GpuError::UnsupportedUniform {
+ shader: shader.to_string(),
+ uniform: uniform.clone(),
+ });
+ }
+
+ Ok(EffectUniformBuffer {
+ resolution: [width as f32, height as f32],
+ direction,
+ scalars: [sigma, step, 0.0, 0.0],
+ })
+}
+
+fn read_number_uniform(pass: &EffectPass, uniform: &str) -> Result<f32, GpuError> {
+ let Some(value) = pass.uniforms.get(uniform) else {
+ return Err(GpuError::MissingUniform {
+ shader: pass.shader.clone(),
+ uniform: uniform.to_string(),
+ });
+ };
+ match value {
+ UniformValue::Number(value) => Ok(*value),
+ UniformValue::Vector(_) => Err(GpuError::InvalidNumberUniform {
+ shader: pass.shader.clone(),
+ uniform: uniform.to_string(),
+ }),
+ }
+}
+
+fn read_vec2_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 2], GpuError> {
+ let Some(value) = pass.uniforms.get(uniform) else {
+ return Err(GpuError::MissingUniform {
+ shader: pass.shader.clone(),
+ uniform: uniform.to_string(),
+ });
+ };
+ let UniformValue::Vector(values) = value else {
+ return Err(GpuError::InvalidVectorUniform {
+ shader: pass.shader.clone(),
+ uniform: uniform.to_string(),
+ expected_length: 2,
+ });
+ };
+ if values.len() != 2 {
+ return Err(GpuError::InvalidVectorUniform {
+ shader: pass.shader.clone(),
+ uniform: uniform.to_string(),
+ expected_length: 2,
+ });
+ }
+ Ok([values[0], values[1]])
+}
diff --git a/rust/crates/gpu/src/lib.rs b/rust/crates/gpu/src/lib.rs
new file mode 100644
index 00000000..c53e63ab
--- /dev/null
+++ b/rust/crates/gpu/src/lib.rs
@@ -0,0 +1,40 @@
+mod context;
+mod effect_pipeline;
+mod mask_feather;
+mod sdf_pipeline;
+mod shader_registry;
+
+use thiserror::Error;
+
+pub use wgpu;
+pub use context::GpuContext;
+pub use effect_pipeline::{ApplyEffectsOptions, EffectPass, UniformValue};
+pub use mask_feather::ApplyMaskFeatherOptions;
+
+pub const GPU_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
+
+#[derive(Debug, Error)]
+pub enum GpuError {
+ #[error("No WebGPU adapter is available")]
+ AdapterUnavailable,
+ #[error("Failed to request a WebGPU device: {0}")]
+ RequestDevice(#[from] wgpu::RequestDeviceError),
+ #[error("Failed to create a WebGPU surface: {0}")]
+ CreateSurface(#[from] wgpu::CreateSurfaceError),
+ #[error("The output surface does not support the required texture format")]
+ UnsupportedSurfaceFormat,
+ #[error("Unknown effect shader '{shader}'")]
+ UnknownEffectShader { shader: String },
+ #[error("Missing uniform '{uniform}' for shader '{shader}'")]
+ MissingUniform { shader: String, uniform: String },
+ #[error("Uniform '{uniform}' for shader '{shader}' must be a number")]
+ InvalidNumberUniform { shader: String, uniform: String },
+ #[error("Uniform '{uniform}' for shader '{shader}' must be a vector of length {expected_length}")]
+ InvalidVectorUniform {
+ shader: String,
+ uniform: String,
+ expected_length: usize,
+ },
+ #[error("Shader '{shader}' does not support uniform '{uniform}'")]
+ UnsupportedUniform { shader: String, uniform: String },
+}
diff --git a/rust/crates/gpu/src/mask_feather.rs b/rust/crates/gpu/src/mask_feather.rs
new file mode 100644
index 00000000..029b59ed
--- /dev/null
+++ b/rust/crates/gpu/src/mask_feather.rs
@@ -0,0 +1,259 @@
+use bytemuck::{Pod, Zeroable};
+use wgpu::util::DeviceExt;
+
+use crate::{GPU_TEXTURE_FORMAT, context::GpuContext};
+
+const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl");
+const JFA_DISTANCE_SHADER_SOURCE: &str = include_str!("shaders/jfa_distance.wgsl");
+
+pub struct ApplyMaskFeatherOptions<'a> {
+ pub mask: &'a wgpu::Texture,
+ pub width: u32,
+ pub height: u32,
+ pub feather: f32,
+}
+
+pub struct MaskFeatherPipeline {
+ inside_texture_bind_group_layout: wgpu::BindGroupLayout,
+ outside_texture_bind_group_layout: wgpu::BindGroupLayout,
+ uniform_bind_group_layout: wgpu::BindGroupLayout,
+ distance_pipeline: wgpu::RenderPipeline,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Pod, Zeroable)]
+struct DistanceUniformBuffer {
+ resolution: [f32; 2],
+ feather_half: f32,
+ _padding: f32,
+}
+
+impl MaskFeatherPipeline {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let inside_texture_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-mask-distance-inside-layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+ let outside_texture_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-mask-distance-outside-layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+ let uniform_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-mask-distance-uniform-layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ });
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("gpu-mask-distance-pipeline-layout"),
+ bind_group_layouts: &[
+ Some(&inside_texture_bind_group_layout),
+ Some(&outside_texture_bind_group_layout),
+ Some(&uniform_bind_group_layout),
+ ],
+ immediate_size: 0,
+ });
+ let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-mask-distance-fullscreen-shader"),
+ source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()),
+ });
+ let fragment_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-mask-distance-fragment-shader"),
+ source: wgpu::ShaderSource::Wgsl(JFA_DISTANCE_SHADER_SOURCE.into()),
+ });
+ let distance_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("gpu-mask-distance-pipeline"),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &vertex_shader_module,
+ entry_point: Some("vertex_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<[f32; 2]>() as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[wgpu::VertexAttribute {
+ format: wgpu::VertexFormat::Float32x2,
+ offset: 0,
+ shader_location: 0,
+ }],
+ }],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &fragment_shader_module,
+ entry_point: Some("fragment_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: GPU_TEXTURE_FORMAT,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+
+ Self {
+ inside_texture_bind_group_layout,
+ outside_texture_bind_group_layout,
+ uniform_bind_group_layout,
+ distance_pipeline,
+ }
+ }
+
+ pub fn apply_mask_feather(
+ &self,
+ context: &GpuContext,
+ ApplyMaskFeatherOptions {
+ mask,
+ width,
+ height,
+ feather,
+ }: ApplyMaskFeatherOptions<'_>,
+ ) -> wgpu::Texture {
+ let sdf = context
+ .sdf_pipeline()
+ .compute_signed_distance_field(context, mask, width, height);
+ let output_texture =
+ context.create_render_texture(width, height, "gpu-mask-feather-output");
+ let inside_view = sdf
+ .inside_texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+ let outside_view = sdf
+ .outside_texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+ let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let inside_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-mask-distance-inside-bind-group"),
+ layout: &self.inside_texture_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&inside_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(context.nearest_sampler()),
+ },
+ ],
+ });
+ let outside_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-mask-distance-outside-bind-group"),
+ layout: &self.outside_texture_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&outside_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(context.nearest_sampler()),
+ },
+ ],
+ });
+ let uniform_buffer = context
+ .device()
+ .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("gpu-mask-distance-uniform-buffer"),
+ contents: bytemuck::bytes_of(&DistanceUniformBuffer {
+ resolution: [width as f32, height as f32],
+ feather_half: feather / 2.0,
+ _padding: 0.0,
+ }),
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ });
+ let uniform_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-mask-distance-uniform-bind-group"),
+ layout: &self.uniform_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: uniform_buffer.as_entire_binding(),
+ }],
+ });
+ let mut encoder = context
+ .device()
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("gpu-mask-distance-command-encoder"),
+ });
+
+ {
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("gpu-mask-distance-render-pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &output_view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ multiview_mask: None,
+ });
+ render_pass.set_pipeline(&self.distance_pipeline);
+ render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..));
+ render_pass.set_bind_group(0, &inside_bind_group, &[]);
+ render_pass.set_bind_group(1, &outside_bind_group, &[]);
+ render_pass.set_bind_group(2, &uniform_bind_group, &[]);
+ render_pass.draw(0..6, 0..1);
+ }
+
+ context.queue().submit([encoder.finish()]);
+ output_texture
+ }
+}
diff --git a/rust/crates/gpu/src/sdf_pipeline.rs b/rust/crates/gpu/src/sdf_pipeline.rs
new file mode 100644
index 00000000..d2612fe1
--- /dev/null
+++ b/rust/crates/gpu/src/sdf_pipeline.rs
@@ -0,0 +1,307 @@
+use bytemuck::{Pod, Zeroable};
+use wgpu::util::DeviceExt;
+
+use crate::{GPU_TEXTURE_FORMAT, context::GpuContext};
+
+const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl");
+const JFA_INIT_SHADER_SOURCE: &str = include_str!("shaders/jfa_init.wgsl");
+const JFA_STEP_SHADER_SOURCE: &str = include_str!("shaders/jfa_step.wgsl");
+
+pub struct SignedDistanceFieldTextures {
+ pub inside_texture: wgpu::Texture,
+ pub outside_texture: wgpu::Texture,
+}
+
+pub struct SdfPipeline {
+ texture_bind_group_layout: wgpu::BindGroupLayout,
+ uniform_bind_group_layout: wgpu::BindGroupLayout,
+ init_pipeline: wgpu::RenderPipeline,
+ step_pipeline: wgpu::RenderPipeline,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Pod, Zeroable)]
+struct JfaInitUniformBuffer {
+ resolution: [f32; 2],
+ invert: f32,
+ _padding: f32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Pod, Zeroable)]
+struct JfaStepUniformBuffer {
+ resolution: [f32; 2],
+ step_size: f32,
+ _padding: f32,
+}
+
+impl SdfPipeline {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let texture_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-sdf-texture-bind-group-layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+ let uniform_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-sdf-uniform-bind-group-layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ });
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("gpu-sdf-pipeline-layout"),
+ bind_group_layouts: &[
+ Some(&texture_bind_group_layout),
+ Some(&uniform_bind_group_layout),
+ ],
+ immediate_size: 0,
+ });
+ let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-sdf-fullscreen-shader"),
+ source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()),
+ });
+ let init_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-jfa-init-shader"),
+ source: wgpu::ShaderSource::Wgsl(JFA_INIT_SHADER_SOURCE.into()),
+ });
+ let step_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-jfa-step-shader"),
+ source: wgpu::ShaderSource::Wgsl(JFA_STEP_SHADER_SOURCE.into()),
+ });
+ let init_pipeline = create_pipeline(
+ device,
+ "gpu-jfa-init-pipeline",
+ &pipeline_layout,
+ &vertex_shader_module,
+ &init_shader_module,
+ );
+ let step_pipeline = create_pipeline(
+ device,
+ "gpu-jfa-step-pipeline",
+ &pipeline_layout,
+ &vertex_shader_module,
+ &step_shader_module,
+ );
+
+ Self {
+ texture_bind_group_layout,
+ uniform_bind_group_layout,
+ init_pipeline,
+ step_pipeline,
+ }
+ }
+
+ pub fn compute_signed_distance_field(
+ &self,
+ context: &GpuContext,
+ source_texture: &wgpu::Texture,
+ width: u32,
+ height: u32,
+ ) -> SignedDistanceFieldTextures {
+ SignedDistanceFieldTextures {
+ inside_texture: self.run_jfa(context, source_texture, width, height, false),
+ outside_texture: self.run_jfa(context, source_texture, width, height, true),
+ }
+ }
+
+ fn run_jfa(
+ &self,
+ context: &GpuContext,
+ source_texture: &wgpu::Texture,
+ width: u32,
+ height: u32,
+ is_inverted: bool,
+ ) -> wgpu::Texture {
+ let ping_texture = context.create_render_texture(width, height, "gpu-jfa-ping-texture");
+ let pong_texture = context.create_render_texture(width, height, "gpu-jfa-pong-texture");
+
+ self.run_pass(
+ context,
+ source_texture,
+ &ping_texture,
+ &self.init_pipeline,
+ bytemuck::bytes_of(&JfaInitUniformBuffer {
+ resolution: [width as f32, height as f32],
+ invert: if is_inverted { 1.0 } else { 0.0 },
+ _padding: 0.0,
+ }),
+ );
+
+ let mut source_is_ping = true;
+ let steps = (width.max(height) as f32).log2().ceil() as u32;
+ for step_index in (0..steps).rev() {
+ let step_size = 2u32.pow(step_index).max(1);
+ let input_texture = if source_is_ping {
+ &ping_texture
+ } else {
+ &pong_texture
+ };
+ let output_texture = if source_is_ping {
+ &pong_texture
+ } else {
+ &ping_texture
+ };
+ self.run_pass(
+ context,
+ input_texture,
+ output_texture,
+ &self.step_pipeline,
+ bytemuck::bytes_of(&JfaStepUniformBuffer {
+ resolution: [width as f32, height as f32],
+ step_size: step_size as f32,
+ _padding: 0.0,
+ }),
+ );
+ source_is_ping = !source_is_ping;
+ }
+
+ if source_is_ping {
+ ping_texture
+ } else {
+ pong_texture
+ }
+ }
+
+ fn run_pass(
+ &self,
+ context: &GpuContext,
+ input_texture: &wgpu::Texture,
+ output_texture: &wgpu::Texture,
+ pipeline: &wgpu::RenderPipeline,
+ uniform_buffer_bytes: &[u8],
+ ) {
+ let input_view = input_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let texture_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-sdf-texture-bind-group"),
+ layout: &self.texture_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&input_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(context.nearest_sampler()),
+ },
+ ],
+ });
+ let uniform_buffer = context
+ .device()
+ .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("gpu-sdf-uniform-buffer"),
+ contents: uniform_buffer_bytes,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ });
+ let uniform_bind_group = context
+ .device()
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("gpu-sdf-uniform-bind-group"),
+ layout: &self.uniform_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: uniform_buffer.as_entire_binding(),
+ }],
+ });
+ let mut encoder = context
+ .device()
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("gpu-sdf-command-encoder"),
+ });
+
+ {
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("gpu-sdf-render-pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &output_view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ multiview_mask: None,
+ });
+ render_pass.set_pipeline(pipeline);
+ render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..));
+ render_pass.set_bind_group(0, &texture_bind_group, &[]);
+ render_pass.set_bind_group(1, &uniform_bind_group, &[]);
+ render_pass.draw(0..6, 0..1);
+ }
+
+ context.queue().submit([encoder.finish()]);
+ }
+}
+
+fn create_pipeline(
+ device: &wgpu::Device,
+ label: &'static str,
+ layout: &wgpu::PipelineLayout,
+ vertex_shader_module: &wgpu::ShaderModule,
+ fragment_shader_module: &wgpu::ShaderModule,
+) -> wgpu::RenderPipeline {
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some(label),
+ layout: Some(layout),
+ vertex: wgpu::VertexState {
+ module: vertex_shader_module,
+ entry_point: Some("vertex_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<[f32; 2]>() as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[wgpu::VertexAttribute {
+ format: wgpu::VertexFormat::Float32x2,
+ offset: 0,
+ shader_location: 0,
+ }],
+ }],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: fragment_shader_module,
+ entry_point: Some("fragment_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: GPU_TEXTURE_FORMAT,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ })
+}
diff --git a/rust/crates/gpu/src/shader_registry.rs b/rust/crates/gpu/src/shader_registry.rs
new file mode 100644
index 00000000..b6af173e
--- /dev/null
+++ b/rust/crates/gpu/src/shader_registry.rs
@@ -0,0 +1,183 @@
+use std::collections::HashMap;
+
+use crate::{GPU_TEXTURE_FORMAT, GpuError};
+
+const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl");
+const GAUSSIAN_BLUR_SHADER_SOURCE: &str = include_str!("shaders/gaussian_blur.wgsl");
+const BLIT_SHADER_SOURCE: &str = include_str!("shaders/blit.wgsl");
+pub const GAUSSIAN_BLUR_SHADER_ID: &str = "gaussian-blur";
+
+pub struct ShaderRegistry {
+ effect_texture_bind_group_layout: wgpu::BindGroupLayout,
+ effect_uniform_bind_group_layout: wgpu::BindGroupLayout,
+ effect_pipelines: HashMap<String, wgpu::RenderPipeline>,
+ blit_pipeline: wgpu::RenderPipeline,
+}
+
+impl ShaderRegistry {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let effect_texture_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-effect-texture-bind-group-layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ });
+ let effect_uniform_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("gpu-effect-uniform-bind-group-layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ });
+
+ let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-fullscreen-shader"),
+ source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()),
+ });
+ let gaussian_blur_shader_module =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-gaussian-blur-shader"),
+ source: wgpu::ShaderSource::Wgsl(GAUSSIAN_BLUR_SHADER_SOURCE.into()),
+ });
+ let blit_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("gpu-blit-shader"),
+ source: wgpu::ShaderSource::Wgsl(BLIT_SHADER_SOURCE.into()),
+ });
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("gpu-effect-pipeline-layout"),
+ bind_group_layouts: &[
+ Some(&effect_texture_bind_group_layout),
+ Some(&effect_uniform_bind_group_layout),
+ ],
+ immediate_size: 0,
+ });
+ let blit_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("gpu-blit-pipeline-layout"),
+ bind_group_layouts: &[Some(&effect_texture_bind_group_layout)],
+ immediate_size: 0,
+ });
+ let gaussian_blur_pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("gpu-gaussian-blur-pipeline"),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &vertex_shader_module,
+ entry_point: Some("vertex_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<[f32; 2]>() as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[wgpu::VertexAttribute {
+ format: wgpu::VertexFormat::Float32x2,
+ offset: 0,
+ shader_location: 0,
+ }],
+ }],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &gaussian_blur_shader_module,
+ entry_point: Some("fragment_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: GPU_TEXTURE_FORMAT,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+ let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("gpu-blit-pipeline"),
+ layout: Some(&blit_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &vertex_shader_module,
+ entry_point: Some("vertex_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<[f32; 2]>() as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[wgpu::VertexAttribute {
+ format: wgpu::VertexFormat::Float32x2,
+ offset: 0,
+ shader_location: 0,
+ }],
+ }],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &blit_shader_module,
+ entry_point: Some("fragment_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: GPU_TEXTURE_FORMAT,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+
+ let effect_pipelines = HashMap::from([(
+ GAUSSIAN_BLUR_SHADER_ID.to_string(),
+ gaussian_blur_pipeline,
+ )]);
+
+ Self {
+ effect_texture_bind_group_layout,
+ effect_uniform_bind_group_layout,
+ effect_pipelines,
+ blit_pipeline,
+ }
+ }
+
+ pub fn get_effect_pipeline(&self, shader: &str) -> Result<&wgpu::RenderPipeline, GpuError> {
+ self.effect_pipelines
+ .get(shader)
+ .ok_or_else(|| GpuError::UnknownEffectShader {
+ shader: shader.to_string(),
+ })
+ }
+
+ pub fn effect_texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
+ &self.effect_texture_bind_group_layout
+ }
+
+ pub fn effect_uniform_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
+ &self.effect_uniform_bind_group_layout
+ }
+
+ pub fn blit_pipeline(&self) -> &wgpu::RenderPipeline {
+ &self.blit_pipeline
+ }
+}
diff --git a/rust/crates/gpu/src/shaders/blit.wgsl b/rust/crates/gpu/src/shaders/blit.wgsl
new file mode 100644
index 00000000..2be67288
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/blit.wgsl
@@ -0,0 +1,12 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+@group(0) @binding(0) var input_texture: texture_2d<f32>;
+@group(0) @binding(1) var input_sampler: sampler;
+
+@fragment
+fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
+ return textureSample(input_texture, input_sampler, input.tex_coord);
+}
diff --git a/rust/crates/gpu/src/shaders/fullscreen.wgsl b/rust/crates/gpu/src/shaders/fullscreen.wgsl
new file mode 100644
index 00000000..1ef26e48
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/fullscreen.wgsl
@@ -0,0 +1,12 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+@vertex
+fn vertex_main(@location(0) position: vec2f) -> VertexOutput {
+ var output: VertexOutput;
+ output.position = vec4f(position, 0.0, 1.0);
+ output.tex_coord = (position * 0.5) + vec2f(0.5, 0.5);
+ return output;
+}
diff --git a/rust/crates/gpu/src/shaders/gaussian_blur.wgsl b/rust/crates/gpu/src/shaders/gaussian_blur.wgsl
new file mode 100644
index 00000000..6c8d311d
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/gaussian_blur.wgsl
@@ -0,0 +1,34 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+struct EffectUniforms {
+ resolution: vec2f,
+ direction: vec2f,
+ scalars: vec4f,
+}
+
+@group(0) @binding(0) var input_texture: texture_2d<f32>;
+@group(0) @binding(1) var input_sampler: sampler;
+@group(1) @binding(0) var<uniform> uniforms: EffectUniforms;
+
+@fragment
+fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
+ let texel_size = vec2f(1.0, 1.0) / uniforms.resolution;
+ let sigma = uniforms.scalars.x;
+ let step_size = uniforms.scalars.y;
+
+ var color = vec4f(0.0, 0.0, 0.0, 0.0);
+ var total_weight = 0.0;
+
+ for (var index = -30; index <= 30; index = index + 1) {
+ let position = f32(index) * step_size;
+ let weight = exp(-(position * position) / (2.0 * sigma * sigma));
+ let sample_uv = input.tex_coord + (texel_size * uniforms.direction * position);
+ color = color + textureSample(input_texture, input_sampler, sample_uv) * weight;
+ total_weight = total_weight + weight;
+ }
+
+ return color / total_weight;
+}
diff --git a/rust/crates/gpu/src/shaders/jfa_distance.wgsl b/rust/crates/gpu/src/shaders/jfa_distance.wgsl
new file mode 100644
index 00000000..e65a7023
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/jfa_distance.wgsl
@@ -0,0 +1,50 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+struct DistanceUniforms {
+ resolution: vec2f,
+ feather_half: f32,
+ _padding: f32,
+}
+
+@group(0) @binding(0) var inside_texture: texture_2d<f32>;
+@group(0) @binding(1) var inside_sampler: sampler;
+@group(1) @binding(0) var outside_texture: texture_2d<f32>;
+@group(1) @binding(1) var outside_sampler: sampler;
+@group(2) @binding(0) var<uniform> uniforms: DistanceUniforms;
+
+fn decode_seed(encoded: vec4f) -> vec2f {
+ let x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5);
+ let y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5);
+ return vec2f(x, y);
+}
+
+fn is_no_seed(encoded: vec4f) -> bool {
+ return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99;
+}
+
+@fragment
+fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
+ let pixel_coord = floor(input.tex_coord * uniforms.resolution);
+ let inside_encoded = textureSample(inside_texture, inside_sampler, input.tex_coord);
+ let outside_encoded = textureSample(outside_texture, outside_sampler, input.tex_coord);
+
+ let has_inside = !is_no_seed(inside_encoded);
+ let has_outside = !is_no_seed(outside_encoded);
+ let distance_to_inside = select(
+ 100000.0,
+ distance(pixel_coord, decode_seed(inside_encoded)),
+ has_inside,
+ );
+ let distance_to_outside = select(
+ 100000.0,
+ distance(pixel_coord, decode_seed(outside_encoded)),
+ has_outside,
+ );
+ let signed_distance = distance_to_outside - distance_to_inside;
+ let alpha = smoothstep(-uniforms.feather_half, uniforms.feather_half, signed_distance);
+
+ return vec4f(alpha, alpha, alpha, alpha);
+}
diff --git a/rust/crates/gpu/src/shaders/jfa_init.wgsl b/rust/crates/gpu/src/shaders/jfa_init.wgsl
new file mode 100644
index 00000000..29aa38c7
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/jfa_init.wgsl
@@ -0,0 +1,35 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+struct JfaInitUniforms {
+ resolution: vec2f,
+ invert: f32,
+ _padding: f32,
+}
+
+@group(0) @binding(0) var input_texture: texture_2d<f32>;
+@group(0) @binding(1) var input_sampler: sampler;
+@group(1) @binding(0) var<uniform> uniforms: JfaInitUniforms;
+
+fn encode_seed(seed: vec2f) -> vec4f {
+ let x_hi = floor(seed.x / 256.0);
+ let x_lo = seed.x - (x_hi * 256.0);
+ let y_hi = floor(seed.y / 256.0);
+ let y_lo = seed.y - (y_hi * 256.0);
+ return vec4f(x_hi / 255.0, x_lo / 255.0, y_hi / 255.0, y_lo / 255.0);
+}
+
+@fragment
+fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
+ let mask = textureSample(input_texture, input_sampler, input.tex_coord).r;
+ let is_seed = select(mask > 0.5, mask < 0.5, uniforms.invert > 0.5);
+
+ if (is_seed) {
+ let pixel_coord = floor(input.tex_coord * uniforms.resolution);
+ return encode_seed(pixel_coord);
+ }
+
+ return vec4f(1.0, 1.0, 1.0, 1.0);
+}
diff --git a/rust/crates/gpu/src/shaders/jfa_step.wgsl b/rust/crates/gpu/src/shaders/jfa_step.wgsl
new file mode 100644
index 00000000..e20d7fb0
--- /dev/null
+++ b/rust/crates/gpu/src/shaders/jfa_step.wgsl
@@ -0,0 +1,75 @@
+struct VertexOutput {
+ @builtin(position) position: vec4f,
+ @location(0) tex_coord: vec2f,
+}
+
+struct JfaStepUniforms {
+ resolution: vec2f,
+ step_size: f32,
+ _padding: f32,
+}
+
+@group(0) @binding(0) var input_texture: texture_2d<f32>;
+@group(0) @binding(1) var input_sampler: sampler;
+@group(1) @binding(0) var<uniform> uniforms: JfaStepUniforms;
+
+fn decode_seed(encoded: vec4f) -> vec2f {
+ let x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5);
+ let y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5);
+ return vec2f(x, y);
+}
+
+fn encode_seed(seed: vec2f) -> vec4f {
+ let x_hi = floor(seed.x / 256.0);
+ let x_lo = seed.x - (x_hi * 256.0);
+ let y_hi = floor(seed.y / 256.0);
+ let y_lo = seed.y - (y_hi * 256.0);
+ return vec4f(x_hi / 255.0, x_lo / 255.0, y_hi / 255.0, y_lo / 255.0);
+}
+
+fn is_no_seed(encoded: vec4f) -> bool {
+ return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99;
+}
+
+@fragment
+fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
+ let pixel_coord = floor(input.tex_coord * uniforms.resolution);
+ let texel_size = vec2f(1.0, 1.0) / uniforms.resolution;
+
+ var best_distance = 10000000000.0;
+ var best_seed = vec2f(65535.0, 65535.0);
+
+ for (var y = -1; y <= 1; y = y + 1) {
+ for (var x = -1; x <= 1; x = x + 1) {
+ let offset = vec2f(f32(x), f32(y)) * uniforms.step_size;
+ let sample_uv = input.tex_coord + (offset * texel_size);
+
+ if (
+ sample_uv.x < 0.0 ||
+ sample_uv.x > 1.0 ||
+ sample_uv.y < 0.0 ||
+ sample_uv.y > 1.0
+ ) {
+ continue;
+ }
+
+ let encoded = textureSample(input_texture, input_sampler, sample_uv);
+ if (is_no_seed(encoded)) {
+ continue;
+ }
+
+ let seed = decode_seed(encoded);
+ let distance_to_seed = distance(pixel_coord, seed);
+ if (distance_to_seed < best_distance) {
+ best_distance = distance_to_seed;
+ best_seed = seed;
+ }
+ }
+ }
+
+ if (best_distance < 1000000000.0) {
+ return encode_seed(best_seed);
+ }
+
+ return vec4f(1.0, 1.0, 1.0, 1.0);
+}
diff --git a/rust/wasm/Cargo.toml b/rust/wasm/Cargo.toml
index 031437e5..06be926f 100644
--- a/rust/wasm/Cargo.toml
+++ b/rust/wasm/Cargo.toml
@@ -11,4 +11,11 @@ path = "src/wasm.rs"
crate-type = ["cdylib", "rlib"]
[dependencies]
+gpu = { version = "0.1.0", path = "../crates/gpu" }
+js-sys = "0.3.93"
+serde = { version = "1.0.228", features = ["derive"] }
+serde-wasm-bindgen = "0.6.5"
time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] }
+wasm-bindgen = "0.2.116"
+wasm-bindgen-futures = "0.4.66"
+web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window"] }
diff --git a/rust/wasm/src/gpu_bridge.rs b/rust/wasm/src/gpu_bridge.rs
new file mode 100644
index 00000000..85db0c8b
--- /dev/null
+++ b/rust/wasm/src/gpu_bridge.rs
@@ -0,0 +1,256 @@
+#![cfg(target_arch = "wasm32")]
+
+use std::cell::RefCell;
+
+use gpu::{EffectPass, GpuContext, UniformValue, wgpu};
+use js_sys::{Object, Reflect};
+use serde::Deserialize;
+use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
+
+thread_local! {
+ static GPU_CONTEXT: RefCell<Option<GpuContext>> = const { RefCell::new(None) };
+}
+
+struct ApplyEffectPassesOptions {
+ source: wgpu::web_sys::OffscreenCanvas,
+ width: u32,
+ height: u32,
+ passes: Vec<EffectPassInput>,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct EffectPassInput {
+ shader: String,
+ uniforms: Vec<EffectUniformInput>,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct EffectUniformInput {
+ name: String,
+ value: Vec<f32>,
+}
+
+struct ApplyMaskFeatherOptions {
+ mask: wgpu::web_sys::OffscreenCanvas,
+ width: u32,
+ height: u32,
+ feather: f32,
+}
+
+#[wasm_bindgen(js_name = initializeGpu)]
+pub async fn initialize_gpu() -> Result<(), JsValue> {
+ if GPU_CONTEXT.with(|context| context.borrow().is_some()) {
+ return Ok(());
+ }
+
+ let context = GpuContext::new()
+ .await
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ GPU_CONTEXT.with(|gpu_context| {
+ gpu_context.replace(Some(context));
+ });
+ Ok(())
+}
+
+#[wasm_bindgen(js_name = applyEffectPasses)]
+pub fn apply_effect_passes(
+ options: JsValue,
+) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
+ let ApplyEffectPassesOptions {
+ source,
+ width,
+ height,
+ passes,
+ } = parse_apply_effect_passes_options(options)?;
+
+ with_gpu_context(|context| {
+ let source_texture = import_canvas_texture(context, &source, width, height)?;
+ let effect_passes = map_effect_passes(passes);
+ let result_texture = context
+ .apply_effects(gpu::ApplyEffectsOptions {
+ source: &source_texture,
+ width,
+ height,
+ passes: &effect_passes,
+ })
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?;
+ let surface = context
+ .instance()
+ .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone()))
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+
+ context
+ .render_texture_to_surface(&result_texture, &surface, width, height)
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ Ok(output_canvas)
+ })
+}
+
+#[wasm_bindgen(js_name = applyMaskFeather)]
+pub fn apply_mask_feather(
+ options: JsValue,
+) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
+ let ApplyMaskFeatherOptions {
+ mask,
+ width,
+ height,
+ feather,
+ } = parse_apply_mask_feather_options(options)?;
+
+ with_gpu_context(|context| {
+ let mask_texture = import_canvas_texture(context, &mask, width, height)?;
+ let result_texture = context.apply_mask_feather(gpu::ApplyMaskFeatherOptions {
+ mask: &mask_texture,
+ width,
+ height,
+ feather,
+ });
+ let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?;
+ let surface = context
+ .instance()
+ .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone()))
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+
+ context
+ .render_texture_to_surface(&result_texture, &surface, width, height)
+ .map_err(|error| JsValue::from_str(&error.to_string()))?;
+ Ok(output_canvas)
+ })
+}
+
+fn with_gpu_context<T>(
+ action: impl FnOnce(&GpuContext) -> Result<T, JsValue>,
+) -> Result<T, JsValue> {
+ GPU_CONTEXT.with(|context| {
+ let borrow = context.borrow();
+ let Some(gpu_context) = borrow.as_ref() else {
+ return Err(JsValue::from_str(
+ "GPU context not initialized. Call initializeGpu() first.",
+ ));
+ };
+ action(gpu_context)
+ })
+}
+
+fn import_canvas_texture(
+ context: &GpuContext,
+ canvas: &wgpu::web_sys::OffscreenCanvas,
+ width: u32,
+ height: u32,
+) -> Result<wgpu::Texture, JsValue> {
+ let texture = context.create_render_texture(width, height, "gpu-bridge-input-texture");
+ context.queue().copy_external_image_to_texture(
+ &wgpu::CopyExternalImageSourceInfo {
+ source: wgpu::ExternalImageSource::OffscreenCanvas(canvas.clone()),
+ origin: wgpu::Origin2d::ZERO,
+ flip_y: true,
+ },
+ wgpu::CopyExternalImageDestInfo {
+ texture: &texture,
+ mip_level: 0,
+ origin: wgpu::Origin3d::ZERO,
+ aspect: wgpu::TextureAspect::All,
+ color_space: wgpu::PredefinedColorSpace::Srgb,
+ premultiplied_alpha: false,
+ },
+ wgpu::Extent3d {
+ width,
+ height,
+ depth_or_array_layers: 1,
+ },
+ );
+ Ok(texture)
+}
+
+fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> {
+ effect_passes
+ .into_iter()
+ .map(|pass| EffectPass {
+ shader: pass.shader,
+ uniforms: pass
+ .uniforms
+ .into_iter()
+ .map(|uniform| {
+ let value = if uniform.value.len() == 1 {
+ UniformValue::Number(uniform.value[0])
+ } else {
+ UniformValue::Vector(uniform.value)
+ };
+ (uniform.name, value)
+ })
+ .collect(),
+ })
+ .collect()
+}
+
+fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> {
+ let object: Object = value
+ .dyn_into()
+ .map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?;
+
+ Ok(ApplyEffectPassesOptions {
+ source: read_offscreen_canvas_property(&object, "source")?,
+ width: read_u32_property(&object, "width")?,
+ height: read_u32_property(&object, "height")?,
+ passes: read_serde_property(&object, "passes")?,
+ })
+}
+
+fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> {
+ let object: Object = value
+ .dyn_into()
+ .map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?;
+
+ Ok(ApplyMaskFeatherOptions {
+ mask: read_offscreen_canvas_property(&object, "mask")?,
+ width: read_u32_property(&object, "width")?,
+ height: read_u32_property(&object, "height")?,
+ feather: read_f32_property(&object, "feather")?,
+ })
+}
+
+fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> {
+ Reflect::get(object, &JsValue::from_str(name))
+ .map_err(|_| JsValue::from_str(&format!("Missing property '{name}'")))
+}
+
+fn read_offscreen_canvas_property(
+ object: &Object,
+ name: &str,
+) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> {
+ read_property(object, name)?
+ .dyn_into::<wgpu::web_sys::OffscreenCanvas>()
+ .map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas")))
+}
+
+fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> {
+ let value = read_property(object, name)?;
+ let Some(number) = value.as_f64() else {
+ return Err(JsValue::from_str(&format!(
+ "Property '{name}' must be a number"
+ )));
+ };
+ Ok(number as u32)
+}
+
+fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> {
+ let value = read_property(object, name)?;
+ let Some(number) = value.as_f64() else {
+ return Err(JsValue::from_str(&format!(
+ "Property '{name}' must be a number"
+ )));
+ };
+ Ok(number as f32)
+}
+
+fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue>
+where
+ T: for<'de> Deserialize<'de>,
+{
+ let value = read_property(object, name)?;
+ serde_wasm_bindgen::from_value(value)
+ .map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}")))
+}
diff --git a/rust/wasm/src/wasm.rs b/rust/wasm/src/wasm.rs
index e852df5f..0a78a314 100644
--- a/rust/wasm/src/wasm.rs
+++ b/rust/wasm/src/wasm.rs
@@ -1 +1,6 @@
+#[cfg(target_arch = "wasm32")]
+mod gpu_bridge;
+
+#[cfg(target_arch = "wasm32")]
+pub use gpu_bridge::*;
pub use time::*;
diff --git a/turbo.json b/turbo.json
index bcea3ffb..244b2f95 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,7 +1,7 @@
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
- "build:wasm": {
+ "//#build:wasm": {
"outputs": ["rust/wasm/pkg/**"]
},
"build": {