feat: migrate GPU renderer from WebGL to wgpu/WASM

This commit is contained in:
Maze Winther 2026-04-01 13:57:32 +02:00
parent b048b739bd
commit e579ae1202
44 changed files with 2745 additions and 1333 deletions

597
Cargo.lock generated
View File

@ -60,6 +60,21 @@ dependencies = [
"equator",
]
[[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"
@ -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]]
@ -511,6 +535,12 @@ version = "0.8.0"
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"
@ -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",
@ -898,6 +928,17 @@ dependencies = [
"unicode-width",
]
[[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"
@ -1332,6 +1373,15 @@ dependencies = [
"libloading",
]
[[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"
@ -1665,6 +1715,12 @@ version = "0.1.5"
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"
@ -1954,6 +2010,17 @@ dependencies = [
"weezl",
]
[[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"
@ -2010,6 +2077,36 @@ dependencies = [
"web-sys",
]
[[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"
@ -2040,6 +2137,40 @@ dependencies = [
"bitflags 2.11.0",
]
[[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"
@ -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"
@ -2789,6 +2925,34 @@ version = "1.0.18"
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"
@ -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,8 +2983,15 @@ 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"
@ -2939,6 +3110,12 @@ version = "0.8.1"
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"
@ -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,12 +3354,38 @@ 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"
@ -3192,6 +3395,15 @@ dependencies = [
"getrandom 0.2.17",
]
[[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"
@ -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]]
@ -3582,6 +3801,15 @@ version = "0.2.0"
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"
@ -3781,6 +4009,21 @@ version = "0.2.5"
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"
@ -3816,6 +4059,12 @@ dependencies = [
"zerocopy",
]
[[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"
@ -4069,6 +4318,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[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"
@ -4143,6 +4398,18 @@ dependencies = [
"raw-window-handle",
]
[[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"
@ -4260,6 +4527,12 @@ version = "0.8.10"
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"
@ -4882,6 +5155,15 @@ dependencies = [
"bitflags 2.11.0",
]
[[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"
@ -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",
@ -6150,6 +6432,173 @@ version = "0.1.12"
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"
@ -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]]
@ -6238,6 +6699,15 @@ dependencies = [
"windows-core 0.61.2",
]
[[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"
@ -6263,6 +6733,19 @@ dependencies = [
"windows-strings 0.4.2",
]
[[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"
@ -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]]
@ -6340,6 +6834,16 @@ dependencies = [
"windows-link 0.1.3",
]
[[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"
@ -6380,6 +6884,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[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"
@ -6398,6 +6911,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[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"
@ -6500,6 +7022,15 @@ dependencies = [
"windows-link 0.1.3",
]
[[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"
@ -6878,6 +7409,12 @@ version = "0.2.1"
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"

View File

@ -4,5 +4,5 @@ members = [
"apps/desktop",
"rust/crates/time",
"rust/crates/bridge",
"rust/wasm",
"rust/wasm", "rust/crates/gpu",
]

View File

@ -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",
},

View File

@ -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",

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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 }),
}));
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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),

View File

@ -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 });
},
};
}

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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);
}

View File

@ -1,4 +0,0 @@
declare module "*.glsl" {
const value: string;
export default value;
}

173
bun.lock
View File

@ -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=="],
}
}

View File

@ -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
## 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.
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.

View File

@ -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"

View File

@ -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(())
}
}

View File

@ -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]])
}

View File

@ -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 },
}

View File

@ -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
}
}

View File

@ -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,
})
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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"] }

256
rust/wasm/src/gpu_bridge.rs Normal file
View File

@ -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}")))
}

View File

@ -1 +1,6 @@
#[cfg(target_arch = "wasm32")]
mod gpu_bridge;
#[cfg(target_arch = "wasm32")]
pub use gpu_bridge::*;
pub use time::*;

View File

@ -1,7 +1,7 @@
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build:wasm": {
"//#build:wasm": {
"outputs": ["rust/wasm/pkg/**"]
},
"build": {