feat: migrate GPU renderer from WebGL to wgpu/WASM
This commit is contained in:
parent
b048b739bd
commit
e579ae1202
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ members = [
|
|||
"apps/desktop",
|
||||
"rust/crates/time",
|
||||
"rust/crates/bridge",
|
||||
"rust/wasm",
|
||||
"rust/wasm", "rust/crates/gpu",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
declare module "*.glsl" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
173
bun.lock
173
bun.lock
|
|
@ -96,7 +96,6 @@
|
|||
"dotenv": "^16.5.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"postcss": "^8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "^5.8.3",
|
||||
|
|
@ -105,7 +104,7 @@
|
|||
},
|
||||
"rust/wasm/pkg": {
|
||||
"name": "opencut-wasm",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
|
|
@ -800,18 +799,12 @@
|
|||
|
||||
"@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
|
@ -838,56 +831,14 @@
|
|||
|
||||
"@upstash/redis": ["@upstash/redis@1.35.4", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-WE1ZnhFyBiIjTDW13GbO6JjkiMVVjw5VsvS8ENmvvJsze/caMQ5paxVD44+U68IUVmkXcbsLSoE+VIYsHtbQEw=="],
|
||||
|
||||
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
||||
|
||||
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
||||
|
||||
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
|
||||
|
||||
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
|
||||
|
||||
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
|
||||
|
||||
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
|
||||
|
||||
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
|
||||
|
||||
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
|
||||
|
||||
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
|
||||
|
||||
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
|
||||
|
||||
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
|
||||
|
||||
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
|
||||
|
||||
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
@ -914,8 +865,6 @@
|
|||
|
||||
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||
|
||||
"big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
|
@ -928,8 +877,6 @@
|
|||
|
||||
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
|
@ -960,8 +907,6 @@
|
|||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
|
@ -1042,8 +987,6 @@
|
|||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||
|
||||
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
|
||||
|
||||
"embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
|
||||
|
|
@ -1052,8 +995,6 @@
|
|||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
|
@ -1068,8 +1009,6 @@
|
|||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
|
@ -1086,14 +1025,8 @@
|
|||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
|
@ -1102,8 +1035,6 @@
|
|||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
|
@ -1112,12 +1043,6 @@
|
|||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
|
@ -1168,8 +1093,6 @@
|
|||
|
||||
"glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
|
@ -1184,8 +1107,6 @@
|
|||
|
||||
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
|
@ -1258,8 +1179,6 @@
|
|||
|
||||
"jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
|
||||
|
||||
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
|
@ -1268,14 +1187,8 @@
|
|||
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
|
@ -1306,10 +1219,6 @@
|
|||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
|
||||
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
|
||||
|
||||
"loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
|
@ -1392,9 +1301,9 @@
|
|||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
|
|
@ -1424,8 +1333,6 @@
|
|||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"next": ["next@16.1.3", "", { "dependencies": { "@next/env": "16.1.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.3", "@next/swc-darwin-x64": "16.1.3", "@next/swc-linux-arm64-gnu": "16.1.3", "@next/swc-linux-arm64-musl": "16.1.3", "@next/swc-linux-x64-gnu": "16.1.3", "@next/swc-linux-x64-musl": "16.1.3", "@next/swc-win32-arm64-msvc": "16.1.3", "@next/swc-win32-x64-msvc": "16.1.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
|
@ -1434,8 +1341,6 @@
|
|||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
|
@ -1524,8 +1429,6 @@
|
|||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
|
||||
|
|
@ -1538,8 +1441,6 @@
|
|||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"raw-loader": ["raw-loader@4.0.2", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA=="],
|
||||
|
||||
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||
|
||||
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
|
||||
|
|
@ -1582,8 +1483,6 @@
|
|||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
|
@ -1600,8 +1499,6 @@
|
|||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
|
@ -1672,7 +1569,7 @@
|
|||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||
|
||||
|
|
@ -1684,9 +1581,7 @@
|
|||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||
|
||||
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="],
|
||||
"terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
|
|
@ -1734,10 +1629,6 @@
|
|||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
|
@ -1758,8 +1649,6 @@
|
|||
|
||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||
|
||||
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
|
||||
|
||||
"wavesurfer.js": ["wavesurfer.js@7.10.0", "", {}, "sha512-GiyAHdorqGtUYG5fe4BfTf5lmtSLhrXoHeNlMsR80JOiOZxOrIOv9QaIR8RnqlleJ6D8R9cqvZKR9lfJcWcapg=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
|
@ -1768,10 +1657,6 @@
|
|||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack": ["webpack@5.105.3", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg=="],
|
||||
|
||||
"webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
|
@ -1842,12 +1727,8 @@
|
|||
|
||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"@node-minify/terser/terser": ["terser@5.16.9", "", { "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
|
@ -1956,16 +1837,10 @@
|
|||
|
||||
"@upstash/core-analytics/@upstash/redis": ["@upstash/redis@1.35.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA=="],
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||
|
||||
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
|
@ -1980,16 +1855,14 @@
|
|||
|
||||
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
|
||||
|
|
@ -2014,18 +1887,10 @@
|
|||
|
||||
"send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||
|
||||
"type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||
|
||||
"wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
|
@ -2088,8 +1953,6 @@
|
|||
|
||||
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
|
@ -2204,10 +2067,6 @@
|
|||
|
||||
"@types/pg/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
|
@ -2276,24 +2135,12 @@
|
|||
|
||||
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
|
||||
|
||||
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"webpack/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
|
@ -2360,10 +2207,6 @@
|
|||
|
||||
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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]])
|
||||
}
|
||||
|
|
@ -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 },
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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}")))
|
||||
}
|
||||
|
|
@ -1 +1,6 @@
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
mod gpu_bridge;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use gpu_bridge::*;
|
||||
pub use time::*;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://turborepo.com/schema.json",
|
||||
"tasks": {
|
||||
"build:wasm": {
|
||||
"//#build:wasm": {
|
||||
"outputs": ["rust/wasm/pkg/**"]
|
||||
},
|
||||
"build": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue