mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:55:54 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60977d1190 |
Generated
+46
-219
@@ -609,7 +609,6 @@ dependencies = [
|
||||
"candle-core",
|
||||
"candle-nn",
|
||||
"candle-transformers",
|
||||
"cudarc",
|
||||
"filesys",
|
||||
"hf-hub",
|
||||
"image",
|
||||
@@ -1425,14 +1424,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "candle-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef79e00300acaaa6b0b33168e2ed5050f06125d1add87e7103c40a119083581"
|
||||
checksum = "855dfedff437d2681d68e1f34ae559d88b0dd84aa5a6b63f2c8e75ebdd875bbf"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"candle-kernels",
|
||||
"cudarc",
|
||||
"gemm 0.17.1",
|
||||
"gemm",
|
||||
"half",
|
||||
"memmap2",
|
||||
"num-traits",
|
||||
@@ -1450,9 +1449,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "candle-kernels"
|
||||
version = "0.8.3"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3134cc19a080a326f95bdcbe057ecaa9658598e5146e37ba1a8cd550c342a9b"
|
||||
checksum = "53343628fa470b7075c28c589b98735b4220b464e37ddbb8e117040e199f4787"
|
||||
dependencies = [
|
||||
"bindgen_cuda",
|
||||
]
|
||||
@@ -2229,9 +2228,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cudarc"
|
||||
version = "0.13.7"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e29ce3bfa797c1183053ceb496316203ef561c183941c3c181500d9ade6daf4"
|
||||
checksum = "38cd60a9a42ec83a2ed7effb0b1f073270264ea99da7acfc44f7e8d74dee0384"
|
||||
dependencies = [
|
||||
"half",
|
||||
"libloading 0.8.6",
|
||||
@@ -2892,15 +2891,6 @@ dependencies = [
|
||||
"reborrow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-stack"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easyenv"
|
||||
version = "0.1.0"
|
||||
@@ -3753,37 +3743,17 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-c32 0.17.1",
|
||||
"gemm-c64 0.17.1",
|
||||
"gemm-common 0.17.1",
|
||||
"gemm-f16 0.17.1",
|
||||
"gemm-f32 0.17.1",
|
||||
"gemm-f64 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-c32",
|
||||
"gemm-c64",
|
||||
"gemm-common",
|
||||
"gemm-f16",
|
||||
"gemm-f32",
|
||||
"gemm-f64",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-c32 0.18.2",
|
||||
"gemm-c64 0.18.2",
|
||||
"gemm-common 0.18.2",
|
||||
"gemm-f16 0.18.2",
|
||||
"gemm-f32 0.18.2",
|
||||
"gemm-f64 0.18.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3793,27 +3763,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-c32"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-common 0.18.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3823,27 +3778,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-c64"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-common 0.18.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3854,38 +3794,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"dyn-stack 0.10.0",
|
||||
"dyn-stack",
|
||||
"half",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"pulp 0.18.22",
|
||||
"raw-cpuid 10.7.0",
|
||||
"pulp",
|
||||
"raw-cpuid",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
"sysctl 0.5.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-common"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"dyn-stack 0.13.0",
|
||||
"half",
|
||||
"libm",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"pulp 0.21.4",
|
||||
"raw-cpuid 11.4.0",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
"sysctl 0.6.0",
|
||||
"sysctl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3894,32 +3813,14 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"gemm-f32 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"gemm-f32",
|
||||
"half",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-f16"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-common 0.18.2",
|
||||
"gemm-f32 0.18.2",
|
||||
"half",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
]
|
||||
@@ -3930,27 +3831,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-f32"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-common 0.18.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3960,27 +3846,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0"
|
||||
dependencies = [
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 10.7.0",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemm-f64"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd"
|
||||
dependencies = [
|
||||
"dyn-stack 0.13.0",
|
||||
"gemm-common 0.18.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid 11.4.0",
|
||||
"raw-cpuid",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -5366,9 +5237,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.11"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@@ -6367,7 +6238,7 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
@@ -7452,20 +7323,6 @@ dependencies = [
|
||||
"reborrow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulp"
|
||||
version = "0.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95fb7a99b37aaef4c7dd2fd15a819eb8010bfc7a2c2155230d51f497316cad6d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if 1.0.0",
|
||||
"libm",
|
||||
"num-complex",
|
||||
"reborrow",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
@@ -7779,15 +7636,6 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -9926,20 +9774,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysctl"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"byteorder",
|
||||
"enum-as-inner",
|
||||
"libc",
|
||||
"thiserror 1.0.63",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
@@ -11012,34 +10846,27 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ug"
|
||||
version = "0.1.0"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437"
|
||||
checksum = "c4eef2ebfc18c67a6dbcacd9d8a4d85e0568cc58c82515552382312c2730ea13"
|
||||
dependencies = [
|
||||
"gemm 0.18.2",
|
||||
"half",
|
||||
"libloading 0.8.6",
|
||||
"memmap2",
|
||||
"num",
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
"rayon",
|
||||
"safetensors",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.63",
|
||||
"tracing",
|
||||
"yoke",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ug-cuda"
|
||||
version = "0.1.0"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50758486d7941f8b0a636ba7e29455c07071f41590beac1fd307ec893e8db69a"
|
||||
checksum = "1c4dcab280ad0ef3957e153a82dcad608c954d02cf253b695322f502d1f8902e"
|
||||
dependencies = [
|
||||
"cudarc",
|
||||
"half",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.63",
|
||||
"ug",
|
||||
]
|
||||
@@ -11365,9 +11192,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "videos"
|
||||
|
||||
@@ -23,9 +23,6 @@ tauri-build = { version = "2.0.4", features = [] }
|
||||
filesys = { path = "../../lib/files/filesys" }
|
||||
memory_caching = { path = "../../lib/caching/memory_caching" }
|
||||
|
||||
# Bypass cudarc issues on Windows https://github.com/coreylowman/cudarc/issues/332
|
||||
cudarc = "0.13.7"
|
||||
|
||||
# External
|
||||
anyhow = "1.0"
|
||||
bytes = "1.10"
|
||||
|
||||
@@ -12,7 +12,7 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
backup
|
||||
.venv
|
||||
|
||||
# Editor directories and files
|
||||
~
|
||||
.vscode
|
||||
|
||||
Generated
-10
@@ -4525,8 +4525,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -5267,8 +5265,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/cross-spawn/node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -5317,8 +5313,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/debug/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
@@ -7145,8 +7139,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -7184,8 +7176,6 @@
|
||||
},
|
||||
"node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any, Optional, Tuple, Callable, List
|
||||
|
||||
import torch
|
||||
import websockets
|
||||
from PIL import Image
|
||||
from diffusers import (
|
||||
StableDiffusionXLPipeline,
|
||||
StableDiffusionXLImg2ImgPipeline,
|
||||
AutoPipelineForImage2Image,
|
||||
LCMScheduler,
|
||||
UNet2DConditionModel,
|
||||
)
|
||||
from tqdm import tqdm
|
||||
from huggingface_hub import HfApi, hf_hub_download
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global variables
|
||||
connected = False
|
||||
pipe = None
|
||||
model_initialized = False
|
||||
current_settings = {}
|
||||
|
||||
# Custom TQDMProgressCallback for tracking download progress
|
||||
class ProgressCallback:
|
||||
def __init__(self, websocket):
|
||||
self.websocket = websocket
|
||||
self.total = 0
|
||||
self.current = 0
|
||||
self.last_update_time = 0
|
||||
self.last_progress = 0
|
||||
self.stage = "downloading"
|
||||
self.current_file = ""
|
||||
|
||||
async def __call__(self, current, total, file_name=""):
|
||||
self.total = total
|
||||
self.current = current
|
||||
self.current_file = file_name or self.current_file
|
||||
|
||||
# Only send updates at most once every 500ms to avoid flooding the connection
|
||||
current_time = time.time()
|
||||
progress = int(100 * current / total) if total > 0 else 0
|
||||
|
||||
if (current_time - self.last_update_time > 0.5 and progress != self.last_progress) or current >= total:
|
||||
self.last_update_time = current_time
|
||||
self.last_progress = progress
|
||||
|
||||
await self.websocket.send(json.dumps({
|
||||
'type': 'progress_update',
|
||||
'stage': self.stage,
|
||||
'progress': progress,
|
||||
'file': self.current_file
|
||||
}))
|
||||
|
||||
# Pure functions
|
||||
def decode_base64_to_image(base64_string: str) -> Image.Image:
|
||||
"""Decode a base64 string to a PIL Image."""
|
||||
try:
|
||||
image_data = base64.b64decode(base64_string)
|
||||
return Image.open(BytesIO(image_data))
|
||||
except Exception as e:
|
||||
logger.error(f"Error decoding base64 image: {e}")
|
||||
raise ValueError(f"Invalid base64 image: {e}")
|
||||
|
||||
def encode_image_to_base64(image: Image.Image) -> str:
|
||||
"""Encode a PIL Image to a base64 string."""
|
||||
try:
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
return base64.b64encode(buffered.getvalue()).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.error(f"Error encoding image to base64: {e}")
|
||||
raise ValueError(f"Error encoding image: {e}")
|
||||
|
||||
async def load_model(sdxl_checkpoint_path: pathlib.Path, progress_callback: Optional[ProgressCallback] = None) -> Tuple[AutoPipelineForImage2Image, bool]:
|
||||
"""Load the SDXL model with LCM scheduler and report progress."""
|
||||
try:
|
||||
logger.info(f"Loading model from {sdxl_checkpoint_path}")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback.stage = "loading_unet"
|
||||
progress_callback.current_file = "latent-consistency/lcm-sdxl"
|
||||
await progress_callback(0, 100)
|
||||
|
||||
# Load the UNet with LCM
|
||||
unet = UNet2DConditionModel.from_pretrained(
|
||||
"latent-consistency/lcm-sdxl",
|
||||
torch_dtype=torch.float16,
|
||||
variant="fp16",
|
||||
use_safetensors=True,
|
||||
)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback.stage = "loading_base_model"
|
||||
progress_callback.current_file = str(sdxl_checkpoint_path)
|
||||
await progress_callback(25, 100)
|
||||
|
||||
# Load the base model using the checkpoint path
|
||||
pipe = AutoPipelineForImage2Image.from_pretrained(
|
||||
"stabilityai/stable-diffusion-xl-base-1.0",
|
||||
unet=unet,
|
||||
torch_dtype=torch.float16,
|
||||
variant="fp16",
|
||||
use_safetensors=True
|
||||
).to("cuda")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback.stage = "configuring_scheduler"
|
||||
await progress_callback(75, 100)
|
||||
|
||||
# Replace the scheduler with LCM scheduler
|
||||
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback.stage = "loading_lcm_adapter"
|
||||
progress_callback.current_file = "latent-consistency/lcm-lora-sdxl"
|
||||
await progress_callback(85, 100)
|
||||
|
||||
# Load the default LCM adapter
|
||||
pipe.load_lora_weights("latent-consistency/lcm-lora-sdxl", adapter_name="lcm",from_safetensors=True)
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback(100, 100)
|
||||
|
||||
return pipe, True
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading model: {e}")
|
||||
if progress_callback:
|
||||
progress_callback.stage = "error"
|
||||
progress_callback.current_file = str(e)
|
||||
await progress_callback(0, 100)
|
||||
return None, False
|
||||
|
||||
async def load_lora(pipe: AutoPipelineForImage2Image, lora_path: pathlib.Path, adapter_name: str = "lora", progress_callback: Optional[ProgressCallback] = None) -> bool:
|
||||
"""Load a LoRA adapter into the pipeline and report progress."""
|
||||
try:
|
||||
logger.info(f"Loading LoRA from {lora_path} as {adapter_name}")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback.stage = "loading_lora"
|
||||
progress_callback.current_file = str(lora_path)
|
||||
await progress_callback(0, 100)
|
||||
|
||||
# Load the LoRA weights
|
||||
pipe.load_lora_weights(lora_path, adapter_name=adapter_name)
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback(100, 100)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading LoRA: {e}")
|
||||
if progress_callback:
|
||||
progress_callback.stage = "error"
|
||||
progress_callback.current_file = str(e)
|
||||
await progress_callback(0, 100)
|
||||
return False
|
||||
|
||||
def generate_image(
|
||||
pipe: AutoPipelineForImage2Image,
|
||||
image: Image.Image,
|
||||
prompt: str,
|
||||
lora_strength: float = 1.0,
|
||||
image_to_image_strength: float = 1.0,
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
num_inference_steps: int = 4,
|
||||
guidance_scale: float = 1.0,
|
||||
seed: Optional[int] = None
|
||||
) -> Optional[Image.Image]:
|
||||
"""Generate an image using the LCM model."""
|
||||
try:
|
||||
# Resize input image to match desired dimensions
|
||||
image = image.resize((width, height))
|
||||
|
||||
# Set up generator for reproducibility if seed is provided
|
||||
generator = torch.manual_seed(seed) if seed is not None else None
|
||||
|
||||
# Set lora adapter weights if available
|
||||
if hasattr(pipe, 'set_adapters') and len(pipe.adapters) > 0:
|
||||
# If we have both lcm and lora adapters
|
||||
if "lcm" in pipe.adapters and "lora" in pipe.adapters:
|
||||
pipe.set_adapters(["lcm", "lora"], adapter_weights=[1.0, lora_strength])
|
||||
# If we only have lcm adapter
|
||||
elif "lcm" in pipe.adapters:
|
||||
pipe.set_adapters(["lcm"], adapter_weights=[1.0])
|
||||
|
||||
# Generate the image
|
||||
result = pipe(
|
||||
prompt=prompt,
|
||||
image=image,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=guidance_scale,
|
||||
strength=image_to_image_strength,
|
||||
width=width,
|
||||
height=height,
|
||||
generator=generator
|
||||
)
|
||||
|
||||
return result.images[0]
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating image: {e}")
|
||||
return None
|
||||
|
||||
async def handle_client(websocket):
|
||||
"""Handle a client connection."""
|
||||
global connected, pipe, model_initialized, current_settings
|
||||
|
||||
if connected:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'connection_error',
|
||||
'error': "Another client is already connected"
|
||||
}))
|
||||
await websocket.close(1008, "Another client is already connected")
|
||||
return
|
||||
|
||||
connected = True
|
||||
logger.info("Client connected")
|
||||
|
||||
# Create a progress callback for this connection
|
||||
progress_callback = ProgressCallback(websocket)
|
||||
|
||||
try:
|
||||
async for message in websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
logger.info(f"Received message type: {data.get('type', 'unknown')}")
|
||||
logger.info(f"Full payload: {json.dumps(data, indent=2)}")
|
||||
|
||||
# Check message type
|
||||
message_type = data.get('type', '')
|
||||
|
||||
# Handle setup request - initializes the model and settings
|
||||
if message_type == 'setup':
|
||||
logger.info("Processing setup message")
|
||||
model_data = data.get('model', {})
|
||||
lora_data = data.get('lora')
|
||||
device = data.get('device', 'cuda')
|
||||
|
||||
logger.info(f"Model data: {json.dumps(model_data, indent=2)}")
|
||||
logger.info(f"LoRA data: {json.dumps(lora_data, indent=2) if lora_data else 'None'}")
|
||||
logger.info(f"Device: {device}")
|
||||
|
||||
# Extract setup parameters with detailed logging
|
||||
model_path = model_data.get('path', '')
|
||||
logger.info(f"Raw model path from message: '{model_path}'")
|
||||
|
||||
# Check if the path is valid
|
||||
if not model_path:
|
||||
logger.error("Model path is empty")
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'setup_response',
|
||||
'success': False,
|
||||
'error': 'Model path is required'
|
||||
}))
|
||||
continue
|
||||
|
||||
# Convert to Path object
|
||||
sdxl_checkpoint_path = pathlib.Path(model_path)
|
||||
logger.info(f"Converted to pathlib.Path: '{sdxl_checkpoint_path}'")
|
||||
logger.info(f"Path is absolute: {sdxl_checkpoint_path.is_absolute()}")
|
||||
logger.info(f"Path exists check: {sdxl_checkpoint_path.exists()}")
|
||||
|
||||
# If the path exists as a file
|
||||
if sdxl_checkpoint_path.is_file():
|
||||
logger.info(f"Path is a file: Yes")
|
||||
logger.info(f"File size: {sdxl_checkpoint_path.stat().st_size / (1024*1024):.2f} MB")
|
||||
else:
|
||||
logger.info(f"Path is a file: No")
|
||||
|
||||
# Store current settings
|
||||
current_settings = {
|
||||
'sdxl_checkpoint_path': str(sdxl_checkpoint_path),
|
||||
'lora_path': str(lora_data['path']) if lora_data else None,
|
||||
'lora_alpha': float(lora_data['alpha']) if lora_data else 1.0,
|
||||
'device': device
|
||||
}
|
||||
logger.info(f"Current settings: {json.dumps(current_settings, indent=2)}")
|
||||
|
||||
# Validate required parameters
|
||||
if not sdxl_checkpoint_path.exists():
|
||||
logger.error(f"Path does not exist: {sdxl_checkpoint_path}")
|
||||
# Try alternative path format
|
||||
alt_path = pathlib.Path(model_path.replace('\\', '/'))
|
||||
logger.info(f"Trying alternative path format: {alt_path}")
|
||||
logger.info(f"Alternative path exists check: {alt_path.exists()}")
|
||||
|
||||
if alt_path.exists():
|
||||
logger.info("Using alternative path format")
|
||||
sdxl_checkpoint_path = alt_path
|
||||
else:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'setup_response',
|
||||
'success': False,
|
||||
'error': f'SDXL checkpoint path does not exist: {sdxl_checkpoint_path}'
|
||||
}))
|
||||
continue
|
||||
|
||||
# Load the model with progress reporting
|
||||
logger.info(f"Loading model from: {sdxl_checkpoint_path}")
|
||||
pipe, success = await load_model(sdxl_checkpoint_path, progress_callback)
|
||||
|
||||
if not success:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'setup_response',
|
||||
'success': False,
|
||||
'error': 'Failed to load model'
|
||||
}))
|
||||
continue
|
||||
|
||||
# Load LoRA if specified
|
||||
lora_success = True
|
||||
if lora_data and lora_data['path'] and pathlib.Path(lora_data['path']).exists():
|
||||
lora_success = await load_lora(pipe, pathlib.Path(lora_data['path']), progress_callback=progress_callback)
|
||||
if not lora_success:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'setup_response',
|
||||
'success': False,
|
||||
'error': f'Failed to load LoRA from {pathlib.Path(lora_data["path"])}'
|
||||
}))
|
||||
continue
|
||||
|
||||
model_initialized = True
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'setup_response',
|
||||
'success': True,
|
||||
'message': 'Model initialized successfully',
|
||||
'settings': current_settings
|
||||
}))
|
||||
|
||||
# Handle settings update - changes settings without full reinitialization
|
||||
elif message_type == 'update_settings':
|
||||
if not model_initialized:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'update_settings_response',
|
||||
'success': False,
|
||||
'error': 'Model not initialized. Send a setup request first.'
|
||||
}))
|
||||
continue
|
||||
|
||||
settings_data = data.get('settings', {})
|
||||
updates_applied = []
|
||||
errors = []
|
||||
|
||||
# Check for LoRA path update
|
||||
if 'lora_path' in settings_data and settings_data['lora_path'] != current_settings.get('lora_path'):
|
||||
new_lora_path = pathlib.Path(settings_data['lora_path']) if settings_data['lora_path'] else None
|
||||
|
||||
if new_lora_path and new_lora_path.exists():
|
||||
# Load the new LoRA with progress reporting
|
||||
lora_success = await load_lora(pipe, new_lora_path, progress_callback=progress_callback)
|
||||
if lora_success:
|
||||
current_settings['lora_path'] = str(new_lora_path)
|
||||
updates_applied.append('lora_path')
|
||||
else:
|
||||
errors.append(f'Failed to load LoRA from {new_lora_path}')
|
||||
elif new_lora_path:
|
||||
errors.append(f'LoRA path does not exist: {new_lora_path}')
|
||||
else:
|
||||
# If LoRA path is None, we'll just update the setting
|
||||
current_settings['lora_path'] = None
|
||||
updates_applied.append('lora_path')
|
||||
|
||||
# Handle any other settings that can be updated without reloading the model
|
||||
# For example, default generation parameters
|
||||
for param in ['default_lora_strength', 'default_image_to_image_strength',
|
||||
'default_width', 'default_height', 'default_steps',
|
||||
'default_guidance_scale']:
|
||||
if param in settings_data:
|
||||
current_settings[param] = settings_data[param]
|
||||
updates_applied.append(param)
|
||||
|
||||
# Send response
|
||||
if errors:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'update_settings_response',
|
||||
'success': False,
|
||||
'error': ', '.join(errors),
|
||||
'updates_applied': updates_applied,
|
||||
'current_settings': current_settings
|
||||
}))
|
||||
else:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'update_settings_response',
|
||||
'success': True,
|
||||
'message': 'Settings updated successfully',
|
||||
'updates_applied': updates_applied,
|
||||
'current_settings': current_settings
|
||||
}))
|
||||
|
||||
# Handle generation request
|
||||
elif message_type == 'generate':
|
||||
if not model_initialized:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'generate_response',
|
||||
'success': False,
|
||||
'error': 'Model not initialized. Send a setup request first.'
|
||||
}))
|
||||
continue
|
||||
|
||||
generate_data = data.get('generate', {})
|
||||
|
||||
# Extract generation parameters
|
||||
base64_image = generate_data.get('image')
|
||||
prompt = generate_data.get('prompt', '')
|
||||
|
||||
# Use default settings if not provided
|
||||
lora_strength = float(generate_data.get('lora_strength', current_settings.get('default_lora_strength', 1.0)))
|
||||
image_to_image_strength = float(generate_data.get('image_to_image_strength', current_settings.get('default_image_to_image_strength', 1.0)))
|
||||
width = int(generate_data.get('generated_image_width', current_settings.get('default_width', 1024)))
|
||||
height = int(generate_data.get('generated_image_height', current_settings.get('default_height', 1024)))
|
||||
num_inference_steps = int(generate_data.get('num_inference_steps', current_settings.get('default_steps', 4)))
|
||||
guidance_scale = float(generate_data.get('guidance_scale', current_settings.get('default_guidance_scale', 1.0)))
|
||||
seed = int(generate_data.get('seed')) if 'seed' in generate_data else None
|
||||
|
||||
# Validate required parameters
|
||||
if not base64_image:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'generate_response',
|
||||
'success': False,
|
||||
'error': 'Missing base64 image'
|
||||
}))
|
||||
continue
|
||||
|
||||
# Decode the input image
|
||||
input_image = decode_base64_to_image(base64_image)
|
||||
|
||||
# Update for generation in progress
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'progress_update',
|
||||
'stage': 'generating',
|
||||
'progress': 0,
|
||||
'file': ""
|
||||
}))
|
||||
|
||||
# Generate the image
|
||||
output_image = generate_image(
|
||||
pipe=pipe,
|
||||
image=input_image,
|
||||
prompt=prompt,
|
||||
lora_strength=lora_strength,
|
||||
image_to_image_strength=image_to_image_strength,
|
||||
width=width,
|
||||
height=height,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=guidance_scale,
|
||||
seed=seed
|
||||
)
|
||||
|
||||
# Update for generation complete
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'progress_update',
|
||||
'stage': 'generating',
|
||||
'progress': 100,
|
||||
'file': ""
|
||||
}))
|
||||
|
||||
if output_image is None:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'generate_response',
|
||||
'success': False,
|
||||
'error': 'Failed to generate image'
|
||||
}))
|
||||
continue
|
||||
|
||||
# Encode the output image to base64
|
||||
output_base64 = encode_image_to_base64(output_image)
|
||||
|
||||
# Send the result back to the client
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'generate_response',
|
||||
'success': True,
|
||||
'image': output_base64
|
||||
}))
|
||||
|
||||
# Handle status request
|
||||
elif message_type == 'status':
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'status_response',
|
||||
'success': True,
|
||||
'model_initialized': model_initialized,
|
||||
'current_settings': current_settings
|
||||
}))
|
||||
|
||||
else:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'error',
|
||||
'error': f'Unknown message type: {message_type}'
|
||||
}))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'error',
|
||||
'error': 'Invalid JSON'
|
||||
}))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
await websocket.send(json.dumps({
|
||||
'type': 'error',
|
||||
'error': str(e)
|
||||
}))
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.info("Connection closed")
|
||||
finally:
|
||||
connected = False
|
||||
logger.info("Client disconnected")
|
||||
|
||||
async def main():
|
||||
"""Start the WebSocket server."""
|
||||
server = await websockets.serve(handle_client, "localhost", 8765)
|
||||
logger.info("Server started on ws://localhost:8765")
|
||||
await server.wait_closed()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
@@ -1,9 +1,8 @@
|
||||
import Konva from "konva";
|
||||
|
||||
|
||||
import { uiAccess, uiEvents } from "~/signals";
|
||||
import { ShapeNode } from "./Nodes";
|
||||
import { PaintNode } from "./Nodes/PaintNode";
|
||||
import { PreviewCopyNode } from "./Nodes/PreviewCopy";
|
||||
|
||||
import { UndoStackManager } from "./UndoRedo";
|
||||
import { CommandManager, MatteBox, SceneManager } from "./EngineUtitlities";
|
||||
import {
|
||||
@@ -28,8 +27,7 @@ import { LoadingVideosProvider } from "./EngineUtitlities/LoadingVideosProvider"
|
||||
|
||||
import { VideoExtractionHandler } from "./EngineUtitlities/VideoExtractionHandler/VideoExtractionHandler";
|
||||
import { RealTimeDrawEngine } from "./RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { NodeColor } from "~/signals/uiEvents/toolbarNode";
|
||||
import { ShapeType } from "./Nodes";
|
||||
|
||||
// for testing loading files from system
|
||||
// import { FileUtilities } from "./FileUtilities/FileUtilities";
|
||||
|
||||
@@ -111,8 +109,17 @@ export class Engine {
|
||||
});
|
||||
|
||||
// core layer for all the work done.
|
||||
|
||||
this.offScreenCanvas = new OffscreenCanvas(0, 0);
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
width: VideoResolutions.SQUARE_1024.width,
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
bgLayerRef: this.bgLayer,
|
||||
offScreenCanvas: this.offScreenCanvas
|
||||
});
|
||||
|
||||
// Collection of all Nodes
|
||||
this.nodesManager = new NodesManager();
|
||||
// Partial Collection of selected Nodes
|
||||
@@ -120,20 +127,6 @@ export class Engine {
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
width: VideoResolutions.SQUARE_1024.width,
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
offScreenCanvas: this.offScreenCanvas,
|
||||
onDraw: async (canvas, lineBounds) => {
|
||||
await this.addPaintNode(canvas, lineBounds);
|
||||
},
|
||||
onPreviewCopy: async (image) => {
|
||||
await this.addPreviewCopy(image);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
@@ -145,9 +138,7 @@ export class Engine {
|
||||
this.uiLayer.add(this.selectorSquare.getKonvaNode());
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
this.realTimeDrawEngine.render();
|
||||
});
|
||||
this.undoStackManager = new UndoStackManager();
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
@@ -185,31 +176,16 @@ export class Engine {
|
||||
// some of the managers has events
|
||||
// hence, lastly, setup these events
|
||||
this.setupEventSystem();
|
||||
this.setAppMode(this.appMode);
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private setAppMode(newAppMode: AppModes) {
|
||||
this.appMode = newAppMode;
|
||||
switch (this.appMode) {
|
||||
case AppModes.PAINT: {
|
||||
console.log("APPMODE: PAINT");
|
||||
this.selectorSquare.disable(); // this breaks the paint if you use it in the wrong spot
|
||||
this.selectionManager.disable(); // this prevents selection.
|
||||
this.realTimeDrawEngine.enablePaintMode();
|
||||
this.realTimeDrawEngine.disableDragging();
|
||||
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.disable();
|
||||
return;
|
||||
}
|
||||
case AppModes.SELECT: {
|
||||
console.log("APPMODE: SELECT");
|
||||
this.realTimeDrawEngine.disablePaintMode();
|
||||
this.realTimeDrawEngine.enableDragging();
|
||||
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
uiAccess.toolbarMain.enable();
|
||||
@@ -381,30 +357,12 @@ export class Engine {
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
this.undoStackManager.undo();
|
||||
});
|
||||
uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
this.undoStackManager.redo();
|
||||
this.realTimeDrawEngine.render();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.UNDO.onClick(() => this.undoStackManager.undo());
|
||||
uiEvents.toolbarMain.REDO.onClick(() => this.undoStackManager.redo());
|
||||
|
||||
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
await this.realTimeDrawEngine.saveOutput();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
console.log("Toolbar Main >> Select");
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
console.log("Toolbar Main >> Paint");
|
||||
this.setAppMode(AppModes.PAINT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.ERASER.onClick(() => {
|
||||
console.log("Toolbar Main >> Eraser");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
|
||||
@@ -435,6 +393,7 @@ export class Engine {
|
||||
this.addImage(image);
|
||||
});
|
||||
|
||||
|
||||
uiEvents.onAddTextToEngine((textdata) => {
|
||||
this.addText(textdata);
|
||||
});
|
||||
@@ -465,7 +424,7 @@ export class Engine {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
uiEvents.promptEvents.onPromptStrengthChanged((strength) => {
|
||||
this.realTimeDrawEngine.currentStrength = strength;
|
||||
});
|
||||
@@ -473,36 +432,6 @@ export class Engine {
|
||||
uiEvents.promptEvents.onPromptTextChanged((prompt) => {
|
||||
this.realTimeDrawEngine.currentPrompt = prompt;
|
||||
});
|
||||
|
||||
uiEvents.onAddShapeToEngine((shapeData) => {
|
||||
switch (shapeData.shape) {
|
||||
case "circle":
|
||||
this.addShape(ShapeType.CIRCLE, 100);
|
||||
break;
|
||||
case "square":
|
||||
this.addShape(ShapeType.SQUARE, 100);
|
||||
break;
|
||||
case "triangle":
|
||||
this.addShape(ShapeType.TRIANGLE, 100);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => {
|
||||
console.log("Color change event triggered with:", nodeColor);
|
||||
if (!nodeColor) {
|
||||
console.warn("nodeColor is undefined or null");
|
||||
return;
|
||||
}
|
||||
if (!nodeColor.kNodeId) {
|
||||
console.warn("nodeColor.kNodeId is undefined or null");
|
||||
return;
|
||||
}
|
||||
this.changeNodeColor(nodeColor);
|
||||
});
|
||||
uiEvents.toolbarMain.onPaintColorChanged((color) => {
|
||||
this.realTimeDrawEngine.paintColor = color;
|
||||
});
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
@@ -566,9 +495,9 @@ export class Engine {
|
||||
x: 10,
|
||||
y: 80,
|
||||
text: "",
|
||||
fontSize: 18,
|
||||
fontSize: 24,
|
||||
fontFamily: "Source Sans 3",
|
||||
fill: "white",
|
||||
fill: "black",
|
||||
});
|
||||
const anim = new Konva.Animation((frame) => {
|
||||
if (frame && import.meta.env.DEV) {
|
||||
@@ -585,45 +514,6 @@ export class Engine {
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
private changeNodeColor(nodeColor: NodeColor) {
|
||||
console.log("Changing node color:", nodeColor);
|
||||
// Find the node that is selected with the id
|
||||
const idSelector = "#" + nodeColor.kNodeId;
|
||||
const selectedNode = this.mediaLayer.findOne(idSelector);
|
||||
console.log("Selected node:", selectedNode);
|
||||
console.log("ID selector:", idSelector);
|
||||
console.log("Media layer:", this.mediaLayer);
|
||||
// TODO: Change the color of the node
|
||||
// Turns out shapes are image nodes... how do we change the image's fill??
|
||||
if (selectedNode) {
|
||||
// Get the shape node from the real time drawing engine's image nodes
|
||||
const shapeNode = this.realTimeDrawEngine.findImageNodeById(
|
||||
nodeColor.kNodeId,
|
||||
);
|
||||
if (shapeNode && shapeNode instanceof ShapeNode) {
|
||||
// Create new shape node with updated color
|
||||
const newShapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: shapeNode.shapeType,
|
||||
size: shapeNode.size,
|
||||
color: nodeColor.color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
newShapeNode.kNode.position(shapeNode.kNode.position());
|
||||
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
|
||||
|
||||
// Remove old node and add new one
|
||||
shapeNode.kNode.destroy();
|
||||
this.commandManager.createNode(newShapeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addText(textNodeData: TextNodeData) {
|
||||
const textNode = new TextNode({
|
||||
textNodeData: textNodeData,
|
||||
@@ -635,38 +525,6 @@ export class Engine {
|
||||
this.commandManager.createNode(textNode);
|
||||
}
|
||||
|
||||
public addShape(type: ShapeType, size: number, color?: string) {
|
||||
const shapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: type,
|
||||
size: size,
|
||||
color: color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
shapeNode.kNode.zIndex(1); // Replace desiredZIndex with a number
|
||||
|
||||
this.commandManager.createNode(shapeNode);
|
||||
|
||||
console.debug("Added shapenode:", shapeNode);
|
||||
console.debug("Added node's ID:", shapeNode.kNode._id);
|
||||
}
|
||||
public addPreviewCopy(image: Image.Konva) {
|
||||
// Start of Selection
|
||||
const copyNode = new PreviewCopyNode({
|
||||
image: image,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(copyNode);
|
||||
}
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
@@ -674,32 +532,10 @@ export class Engine {
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
imageNode.kNode.zIndex(1);
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
public addPaintNode(
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) {
|
||||
var node = new PaintNode({
|
||||
canvasElement: canvas,
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
this.realTimeDrawEngine.addNodes(imageNode);
|
||||
}
|
||||
|
||||
public addVideo(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Konva from "konva";
|
||||
import { ImageNode, ShapeNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { MediaNode, Position, RGBColor, Transformation } from "../types";
|
||||
import {
|
||||
AddChromaKey,
|
||||
@@ -20,15 +20,14 @@ import {
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
import { RenderEngine } from "../RenderingPrimitives/RenderEngine";
|
||||
|
||||
interface EngineReferences {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
renderEngineRef: RenderEngine;
|
||||
undoStackManagerRef: UndoStackManager;
|
||||
}
|
||||
|
||||
@@ -37,7 +36,7 @@ export class CommandManager {
|
||||
private nodesManagerRef: NodesManager;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private renderEngineRef: RealTimeDrawEngine;
|
||||
private renderEngineRef: RenderEngine;
|
||||
private undoStackManagerRef: UndoStackManager;
|
||||
|
||||
constructor(engineRefs: EngineReferences) {
|
||||
@@ -49,7 +48,7 @@ export class CommandManager {
|
||||
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
|
||||
}
|
||||
|
||||
createNode(node: MediaNode) {
|
||||
createNode(node: VideoNode | ImageNode | TextNode) {
|
||||
const command = new CreateCommand({
|
||||
nodes: new Set<MediaNode>([node]),
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
@@ -59,11 +58,6 @@ export class CommandManager {
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
|
||||
// Set the kNode's manual id to same as kNode's internal _id
|
||||
// This helps identify the kNode in the Konva layer with find
|
||||
node.kNode.id(node.kNode._id.toString());
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
deleteNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
@@ -76,7 +70,6 @@ export class CommandManager {
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
toggleLockNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
@@ -104,7 +97,6 @@ export class CommandManager {
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
moveNodesDown() {
|
||||
const command = new MoveLayerDown({
|
||||
@@ -113,7 +105,6 @@ export class CommandManager {
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
translateNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
@@ -125,7 +116,6 @@ export class CommandManager {
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
transformNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
@@ -137,7 +127,6 @@ export class CommandManager {
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
addChromaKey(props: { videoNode: VideoNode; newChromaColor: RGBColor }) {
|
||||
const command = new AddChromaKey(props);
|
||||
|
||||
@@ -29,7 +29,7 @@ export abstract class BaseNode {
|
||||
}: {
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
kNode: Konva.Image | Konva.Group | Konva.Shape;
|
||||
kNode: Konva.Image | Konva.Group;
|
||||
}) {
|
||||
// console.log("Node constructed");
|
||||
// this.uuid = uuidv4();
|
||||
@@ -67,11 +67,8 @@ export abstract class BaseNode {
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect != null) {
|
||||
wrapperRect.stroke(primaryOrange);
|
||||
wrapperRect.strokeWidth(highlightStrokeWidth);
|
||||
}
|
||||
wrapperRect.stroke(primaryOrange);
|
||||
wrapperRect.strokeWidth(highlightStrokeWidth);
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
@@ -89,11 +86,7 @@ export abstract class BaseNode {
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect != null) {
|
||||
// Issue investigate
|
||||
wrapperRect.strokeWidth(0);
|
||||
}
|
||||
wrapperRect.strokeWidth(0);
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
@@ -138,7 +131,7 @@ export abstract class BaseNode {
|
||||
// console.log("handle select");
|
||||
if (!isMultiSelect) {
|
||||
// clear selection if not multislect
|
||||
console.log("No Shift >> no multiselect");
|
||||
//console.log("No Shift >> no multiselect");
|
||||
this.selectionManagerRef.clearSelection();
|
||||
}
|
||||
if (this.selectionManagerRef.isNodeSelected(this)) {
|
||||
@@ -151,25 +144,8 @@ export abstract class BaseNode {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = () => {
|
||||
this.selectionManagerRef.clearSelection();
|
||||
this.selectionManagerRef.selectNode(this, undefined, true);
|
||||
}
|
||||
|
||||
this.kNode.on("contextmenu", (e) => {
|
||||
console.log("CONTEXT MENU", e);
|
||||
e.evt.preventDefault();
|
||||
handleRightClick();
|
||||
});
|
||||
|
||||
this.kNode.on("mousedown", (e) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
|
||||
// 3: Right mouse button
|
||||
// If right click, let context event handle it
|
||||
if (e.evt.button === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("MOUSE DOWN");
|
||||
// Selection of Node
|
||||
if (!this.selectionManagerRef.isNodeSelected(this)) {
|
||||
this.isSelecting = true;
|
||||
@@ -179,13 +155,7 @@ export abstract class BaseNode {
|
||||
});
|
||||
|
||||
this.kNode.on("mouseup", (e) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
|
||||
// 3: Right mouse button
|
||||
// If right click, let context event handle it
|
||||
if (e.evt.button === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("MOUSE UP");
|
||||
// just coming out of dragging or selecting mode
|
||||
// no need to handle select
|
||||
if (this.selectionManagerRef.isDragging() || this.isSelecting) {
|
||||
|
||||
@@ -17,13 +17,12 @@ interface ImageNodeContructor {
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class ImageNode extends NetworkedNode {
|
||||
public kNode: Konva.Image;
|
||||
public imageSize?: Size;
|
||||
private loaded!: () => Promise<void>;
|
||||
|
||||
constructor({
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
@@ -33,7 +32,6 @@ export class ImageNode extends NetworkedNode {
|
||||
transform: existingTransform,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: ImageNodeContructor) {
|
||||
// kNodes need to be created first to guaruntee
|
||||
// that it is not undefined in parent's context
|
||||
@@ -55,7 +53,6 @@ export class ImageNode extends NetworkedNode {
|
||||
localFile: imageFile,
|
||||
});
|
||||
this.kNode = kNode;
|
||||
this.loaded = loaded;
|
||||
// this.imageSize = minNodeSize;
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
|
||||
@@ -86,10 +83,8 @@ export class ImageNode extends NetworkedNode {
|
||||
}) {
|
||||
const imageComponent = new Image();
|
||||
imageComponent.crossOrigin = "anonymous";
|
||||
// this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
|
||||
|
||||
imageComponent.onload = () => {
|
||||
//this.setProgress({ progress: 0, status: UploadStatus.FILE_STAGED });
|
||||
this.setProgress({ progress: 0, status: UploadStatus.FILE_STAGED });
|
||||
this.imageSize = {
|
||||
width: imageComponent.width,
|
||||
height: imageComponent.height,
|
||||
@@ -110,34 +105,33 @@ export class ImageNode extends NetworkedNode {
|
||||
this.kNode.fill(transparent);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
//this.uploadImage(imageFile);
|
||||
this.loaded();
|
||||
this.uploadImage(imageFile);
|
||||
};
|
||||
imageComponent.onerror = () => {
|
||||
//this.setProgress({ progress: 0, status: UploadStatus.ERROR_ON_FILE });
|
||||
this.setProgress({ progress: 0, status: UploadStatus.ERROR_ON_FILE });
|
||||
};
|
||||
imageComponent.src = URL.createObjectURL(imageFile);
|
||||
}
|
||||
private async loadImageFromUrl(mediaFileUrl: string) {
|
||||
//this.setProgress({ progress: 75, status: UploadStatus.LOADING });
|
||||
this.setProgress({ progress: 75, status: UploadStatus.LOADING });
|
||||
|
||||
const newImage = new Image();
|
||||
newImage.crossOrigin = "anonymous";
|
||||
newImage.onerror = () => {
|
||||
//this.setProgress({ progress: 90, status: UploadStatus.ERROR_ON_LOAD });
|
||||
this.setProgress({ progress: 90, status: UploadStatus.ERROR_ON_LOAD });
|
||||
};
|
||||
newImage.onload = () => {
|
||||
// console.log("network image", newImage);
|
||||
this.kNode.image(newImage);
|
||||
this.kNode.draw();
|
||||
//this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
|
||||
this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
|
||||
};
|
||||
newImage.src = mediaFileUrl;
|
||||
this.listenToBaseKNode();
|
||||
}
|
||||
|
||||
private async uploadImage(imageFile: File) {
|
||||
//this.setProgress({ progress: 10, status: UploadStatus.UPLOADING });
|
||||
this.setProgress({ progress: 10, status: UploadStatus.UPLOADING });
|
||||
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const uploadResponse = await mediaUploadApi.UploadImage({
|
||||
@@ -155,7 +149,7 @@ export class ImageNode extends NetworkedNode {
|
||||
}
|
||||
|
||||
private async retreiveImage(mediaFileToken: string) {
|
||||
//this.setProgress({ progress: 50, status: UploadStatus.RETREIVING });
|
||||
this.setProgress({ progress: 50, status: UploadStatus.RETREIVING });
|
||||
const mediaFileApi = new MediaFilesApi();
|
||||
const mediaFileResponse = await mediaFileApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaFileToken,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, NodeData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
interface PaintNodeConstructor {
|
||||
canvasElement:HTMLCanvasElement;
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class PaintNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor({
|
||||
canvasElement,
|
||||
lineBounds,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: PaintNodeConstructor) {
|
||||
|
||||
const cl = canvasElement
|
||||
|
||||
// Convert circle to image
|
||||
const dataURL = cl.toDataURL();
|
||||
const imageObj = new Image();
|
||||
imageObj.src = dataURL;
|
||||
|
||||
const paintShape = new Konva.Image({
|
||||
x: lineBounds.x,
|
||||
y: lineBounds.y,
|
||||
width: lineBounds.width,
|
||||
height: lineBounds.height,
|
||||
image: imageObj,
|
||||
listening: false, // Disable event listening to allow captureCanvas to receive events
|
||||
draggable: true,
|
||||
// stroke: '#000000',
|
||||
// strokeWidth: 2,
|
||||
// dash: [5, 5], // Creates dotted outline THIS IS FOR DEBUG
|
||||
globalCompositeOperation: 'source-over',
|
||||
fill: 'transparent'
|
||||
});
|
||||
|
||||
imageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: paintShape,
|
||||
});
|
||||
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, Size, NodeData, TransformationData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
interface PreviewCopyConstructor {
|
||||
image: Konva.Image;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class PreviewCopyNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor({
|
||||
image,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: PreviewCopyConstructor) {
|
||||
// Create the actual shape inside the group
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: image,
|
||||
});
|
||||
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, Size, NodeData, TransformationData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
export enum ShapeType {
|
||||
CIRCLE = "circle",
|
||||
SQUARE = "square",
|
||||
TRIANGLE = "triangle",
|
||||
}
|
||||
|
||||
interface ShapeNodeConstructor {
|
||||
canvasPosition: Position;
|
||||
canvasSize: Size;
|
||||
shapeType: ShapeType;
|
||||
size: number;
|
||||
color?: string; // Hex color string
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class ShapeNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
public shapeType: ShapeType;
|
||||
public size: number;
|
||||
|
||||
constructor({
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
shapeType,
|
||||
size,
|
||||
color = "#2D81FF", // Default red if no color provided
|
||||
transform: existingTransform,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: ShapeNodeConstructor) {
|
||||
// Create the actual shape inside the group
|
||||
const transform = NodeUtilities.getInitialTransform({
|
||||
existingTransform,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
});
|
||||
|
||||
let shape = null;
|
||||
|
||||
switch (shapeType) {
|
||||
case ShapeType.CIRCLE:
|
||||
const circleShape = new Konva.Circle({
|
||||
radius: 50,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Convert circle to image
|
||||
const dataURL = circleShape.toDataURL();
|
||||
const imageObj = new Image();
|
||||
imageObj.src = dataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: imageObj,
|
||||
x: circleShape.x(),
|
||||
y: circleShape.y(),
|
||||
width: circleShape.width(),
|
||||
height: circleShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
imageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
// Clean up the temporary circle
|
||||
circleShape.destroy();
|
||||
break;
|
||||
|
||||
case ShapeType.SQUARE:
|
||||
const squareShape = new Konva.Rect({
|
||||
width: 100,
|
||||
height: 100,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Convert square to image
|
||||
const squareDataURL = squareShape.toDataURL();
|
||||
const squareImageObj = new Image();
|
||||
squareImageObj.src = squareDataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: squareImageObj,
|
||||
x: squareShape.x(),
|
||||
y: squareShape.y(),
|
||||
width: squareShape.width(),
|
||||
height: squareShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
squareImageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded()
|
||||
};
|
||||
|
||||
// Clean up the temporary square
|
||||
squareShape.destroy();
|
||||
break;
|
||||
|
||||
case ShapeType.TRIANGLE:
|
||||
const triangleShape = new Konva.RegularPolygon({
|
||||
sides: 3,
|
||||
radius: 50,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Convert triangle to image
|
||||
const triangleDataURL = triangleShape.toDataURL();
|
||||
const triangleImageObj = new Image();
|
||||
triangleImageObj.src = triangleDataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: triangleImageObj,
|
||||
x: triangleShape.x(),
|
||||
y: triangleShape.y(),
|
||||
width: triangleShape.width(),
|
||||
height: triangleShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
triangleImageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded()
|
||||
};
|
||||
|
||||
// Clean up the temporary triangle
|
||||
triangleShape.destroy();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid shape type");
|
||||
}
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: shape,
|
||||
});
|
||||
|
||||
this.shapeType = shapeType;
|
||||
this.size = size;
|
||||
|
||||
// Add shape to group
|
||||
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: canvasPosition,
|
||||
componentSize: canvasSize,
|
||||
maxSize: canvasSize,
|
||||
});
|
||||
this.kNode.setPosition(centerPosition);
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,11 @@ export enum NodeType {
|
||||
IMAGE = "image",
|
||||
TEXT = "text",
|
||||
VIDEO = "video",
|
||||
SHAPE = "shape",
|
||||
}
|
||||
export const transparent = Colors.transparent;
|
||||
export const primaryOrange = Colors.primaryOrange;
|
||||
|
||||
export const highlightStrokeWidth = 2;
|
||||
export const highlightStrokeWidth = 10;
|
||||
export const minNodeSize = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
|
||||
@@ -3,4 +3,3 @@ export { BaseNode } from "./BaseNode";
|
||||
export { ImageNode } from "./ImageNode";
|
||||
export { VideoNode } from "./VideoNode";
|
||||
export { TextNode } from "./TextNode";
|
||||
export { ShapeNode , ShapeType} from "./ShapeNode";
|
||||
|
||||
@@ -4,12 +4,11 @@ import { MediaNode, Position, Transformation } from "../types";
|
||||
import { uiAccess, uiEvents } from "~/signals";
|
||||
import { NetworkedNode } from "../Nodes/NetworkedNode";
|
||||
import { LoadingBarStatus } from "~/components/ui";
|
||||
import { ImageNode, ShapeNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
import {
|
||||
calculateContextualsPosition,
|
||||
getImageNodeButtonStates,
|
||||
getMultiSelectButtonStates,
|
||||
getShapeNodeButtonStates,
|
||||
getTextNodeButtonStates,
|
||||
getVideoNodeButtonStates,
|
||||
} from "./ToolbarNodeUtilities";
|
||||
@@ -80,7 +79,7 @@ export class SelectionManager {
|
||||
public isDisabled() {
|
||||
return this.disable;
|
||||
}
|
||||
public selectNode(node: MediaNode, doNotDraw?: boolean, showContextMenu = false): boolean {
|
||||
public selectNode(node: MediaNode, doNotDraw?: boolean): boolean {
|
||||
if (
|
||||
this.disabled || // no-op when disabled
|
||||
this.selectedNodes.has(node) || // if the node is already selected
|
||||
@@ -100,11 +99,7 @@ export class SelectionManager {
|
||||
this.selectedNodes.add(node);
|
||||
this.updateNodeTransformer();
|
||||
this.updateContextComponents();
|
||||
|
||||
console.log("SelectionManager > selectNode > showContextMenu", showContextMenu);
|
||||
if (showContextMenu) {
|
||||
this.showContextComponents();
|
||||
}
|
||||
this.showContextComponents();
|
||||
|
||||
if (!doNotDraw) {
|
||||
this.mediaLayerRef.batchDraw();
|
||||
@@ -193,8 +188,8 @@ export class SelectionManager {
|
||||
// done, clear data and show menu
|
||||
this.initialPositions.clear();
|
||||
this.updateContextComponents();
|
||||
this.showContextComponents();
|
||||
}
|
||||
|
||||
private getKNodeTransformation(kNode: Konva.Node) {
|
||||
return {
|
||||
kNodeId: kNode._id,
|
||||
@@ -278,29 +273,17 @@ export class SelectionManager {
|
||||
if (this.selectedNodes.size === 1) {
|
||||
if (node instanceof ImageNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }),
|
||||
});
|
||||
}
|
||||
else if (node instanceof ShapeNode) {
|
||||
console.debug("Selected node is of type ShapeNode", node.kNode.id())
|
||||
if (node instanceof TextNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getShapeNodeButtonStates({ locked: node.isLocked() }),
|
||||
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
|
||||
});
|
||||
}
|
||||
else if (node instanceof TextNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }),
|
||||
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
|
||||
});
|
||||
}
|
||||
else if (node instanceof VideoNode) {
|
||||
if (node instanceof VideoNode) {
|
||||
showOrUpdate({
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }),
|
||||
|
||||
@@ -40,9 +40,8 @@ export class SelectorSquare {
|
||||
stageRef: Konva.Stage;
|
||||
}) {
|
||||
this.kSquare = new Konva.Rect({
|
||||
fill: "rgba(45, 129, 255, 0.3)",
|
||||
stroke: "#00a1ff",
|
||||
strokeWidth: 1,
|
||||
fill: " rgb(250, 128, 114, 0.3)",
|
||||
stroke: "salmon",
|
||||
visible: false,
|
||||
// disable events to not interrupt with events
|
||||
listening: false,
|
||||
|
||||
@@ -51,8 +51,7 @@ export function getImageNodeButtonStates(
|
||||
buttonName === ButtonNames.AI_STYLIZE ||
|
||||
buttonName === ButtonNames.SEGMENTATION ||
|
||||
buttonName === ButtonNames.DOWNLOAD ||
|
||||
buttonName === ButtonNames.CHROMA ||
|
||||
buttonName === ButtonNames.COLOR
|
||||
buttonName === ButtonNames.CHROMA
|
||||
) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
@@ -131,9 +130,7 @@ export function getVideoNodeButtonStates(
|
||||
return buttonStates;
|
||||
}
|
||||
// soon to come feature is disabled
|
||||
if (buttonName === ButtonNames.AI_STYLIZE ||
|
||||
buttonName === ButtonNames.COLOR
|
||||
) {
|
||||
if (buttonName === ButtonNames.AI_STYLIZE) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
@@ -152,8 +149,7 @@ export function getVideoNodeButtonStates(
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
export function getShapeNodeButtonStates(
|
||||
export function getMultiSelectButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
@@ -161,7 +157,7 @@ export function getShapeNodeButtonStates(
|
||||
(buttonStates, buttonName) => {
|
||||
// transform button depends on locked state
|
||||
if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) {
|
||||
buttonStates.TRANSFORM = {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: props.locked === "unknown" || props.locked === true,
|
||||
hidden: false,
|
||||
active: true,
|
||||
@@ -195,47 +191,3 @@ export function getShapeNodeButtonStates(
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
export function getMultiSelectButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
// transform button depends on locked state
|
||||
if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: props.locked === "unknown" || props.locked === true,
|
||||
hidden: false,
|
||||
active: true,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
|
||||
// hidden buttons
|
||||
if (
|
||||
buttonName === ButtonNames.AI_STYLIZE ||
|
||||
buttonName === ButtonNames.SEGMENTATION ||
|
||||
buttonName === ButtonNames.DOWNLOAD ||
|
||||
buttonName === ButtonNames.CHROMA ||
|
||||
buttonName === ButtonNames.COLOR
|
||||
) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,22 @@ import { Container } from "konva/lib/Container";
|
||||
import { Shape } from "konva/lib/Shape";
|
||||
import { Group } from "konva/lib/Group";
|
||||
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
|
||||
import { FileUtilities } from "../FileUtilities/FileUtilities";
|
||||
import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes";
|
||||
import { ImageNode, VideoNode, TextNode } from "../Nodes";
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
import { RenderTask } from "./RenderTask";
|
||||
import { OffScreenSceneCanvas } from "./OffScreenSceneCanvas";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
import {
|
||||
ServerSetupPayload,
|
||||
ServerSettingsPayload,
|
||||
ServerResponse,
|
||||
} from "../types/ServerTypes";
|
||||
|
||||
// https://www.aiseesoft.com/resource/phone-aspect-ratio-screen-resolution.html#:~:text=16%3A9%20Aspect%20Ratio
|
||||
|
||||
export class RealTimeDrawEngine {
|
||||
private videoNodes: VideoNode[];
|
||||
private imageNodes: (ImageNode | TextNode | ShapeNode | PaintNode)[];
|
||||
private imageNodes: (ImageNode | TextNode)[];
|
||||
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private outputBitmap: ImageBitmap | undefined;
|
||||
@@ -33,9 +26,8 @@ export class RealTimeDrawEngine {
|
||||
// private frames: ImageBitmap[];
|
||||
|
||||
// capturing composite within window
|
||||
|
||||
private bgLayerRef: Konva.Layer;
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private drawingsLayer: Konva.Layer; // New Layer for Drawings
|
||||
|
||||
private height: number;
|
||||
private width: number;
|
||||
@@ -56,55 +48,22 @@ export class RealTimeDrawEngine {
|
||||
public currentPrompt: string;
|
||||
public currentStrength: number;
|
||||
|
||||
// Paint Color
|
||||
// paint Brush Size
|
||||
// has to exit out of paint mode when shape or image are used.
|
||||
public paintColor: string = "#000000";
|
||||
public paintBrushSize: number = 5;
|
||||
public isPaintMode: boolean = false;
|
||||
|
||||
private onDrawCallback?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
|
||||
private onPreviewCopyCallback?: (previewCopy: Konva.Image) => void; // New Callback
|
||||
|
||||
private serverSocket: WebSocket | null = null;
|
||||
|
||||
constructor({
|
||||
width,
|
||||
height,
|
||||
bgLayerRef,
|
||||
mediaLayerRef,
|
||||
offScreenCanvas,
|
||||
onDraw,
|
||||
onPreviewCopy, // New Parameter
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
bgLayerRef: Konva.Layer;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
offScreenCanvas: OffscreenCanvas;
|
||||
onDraw?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
onPreviewCopy?: (previewCopy: Konva.Image) => void; // New Parameter
|
||||
}) {
|
||||
this.videoLoadingCanvas = undefined;
|
||||
this.videoNodes = [];
|
||||
this.imageNodes = [];
|
||||
this.onDrawCallback = onDraw;
|
||||
this.onPreviewCopyCallback = onPreviewCopy; // Assign Callback
|
||||
|
||||
// TODO: Make this dynamic and update this on change of canvas.
|
||||
|
||||
@@ -121,21 +80,17 @@ export class RealTimeDrawEngine {
|
||||
this.offScreenCanvas.width = this.width;
|
||||
this.offScreenCanvas.height = this.height;
|
||||
|
||||
this.bgLayerRef = bgLayerRef;
|
||||
|
||||
// this is the whole canvas
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
|
||||
// Create a separate layer for drawings
|
||||
this.drawingsLayer = new Konva.Layer({
|
||||
clearBeforeDraw: true, // Ensures transparent background
|
||||
});
|
||||
this.mediaLayerRef.getStage()?.add(this.drawingsLayer); // to od pass in stage
|
||||
|
||||
// Set background layer to red and media layer to green for visibility
|
||||
|
||||
this.port = undefined;
|
||||
|
||||
this.fps = 24;
|
||||
|
||||
|
||||
this.currentPrompt = "";
|
||||
this.currentStrength = 100;
|
||||
// This is captures a subset of the medialayer ref
|
||||
@@ -151,6 +106,20 @@ export class RealTimeDrawEngine {
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
// Add preview canvas with same dimensions but different style
|
||||
// this.previewCanvas = new Konva.Rect({
|
||||
// name: "PreviewCanvas",
|
||||
// x: this.positionX,
|
||||
// y: this.positionY,
|
||||
// width: this.width,
|
||||
// height: this.height,
|
||||
// fill: "white",
|
||||
// stroke: "blue",
|
||||
// strokeWidth: 1,
|
||||
// dash: [5, 5],
|
||||
// draggable: false,
|
||||
// });
|
||||
|
||||
this.previewCanvas = new Konva.Image({
|
||||
name: "PreviewCanvas",
|
||||
x: this.positionX,
|
||||
@@ -158,336 +127,21 @@ export class RealTimeDrawEngine {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
image: undefined,
|
||||
stroke: "black",
|
||||
stroke: "blue",
|
||||
strokeWidth: 1,
|
||||
dash: [5, 5],
|
||||
draggable: false,
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
this.mediaLayerRef.add(this.captureCanvas);
|
||||
this.mediaLayerRef.add(this.previewCanvas);
|
||||
this.bgLayerRef.add(this.captureCanvas);
|
||||
this.bgLayerRef.add(this.previewCanvas);
|
||||
// send back
|
||||
this.captureCanvas.setZIndex(0);
|
||||
this.previewCanvas.setZIndex(1);
|
||||
// Add mouse events for preview canvas copying
|
||||
//this.previewCopyListener();
|
||||
this.previewCanvas.setZIndex(0);
|
||||
|
||||
//this.startServer();
|
||||
//this.debug();
|
||||
}
|
||||
|
||||
private isEnabled: boolean = false;
|
||||
private cleanupFunction: (() => void) | null = null;
|
||||
|
||||
// this starts the python server
|
||||
public startServer() {
|
||||
// Create WebSocket connection
|
||||
const socket = new WebSocket("ws://localhost:8765");
|
||||
|
||||
// Setup event handlers
|
||||
socket.onopen = () => {
|
||||
console.log("Connected to inference server");
|
||||
// Send initial setup payload to load models
|
||||
const setupPayload: ServerSetupPayload = {
|
||||
type: "setup",
|
||||
model: {
|
||||
name: "stabilityai/sdxl-turbo",
|
||||
path: "F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/checkpoints/ponyDiffusionV6XL_v6StartWithThisOne.safetensors",
|
||||
precision: "fp16",
|
||||
},
|
||||
lora: {
|
||||
path: "F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
|
||||
alpha: 0.75,
|
||||
},
|
||||
device: "cuda",
|
||||
};
|
||||
|
||||
socket.send(JSON.stringify(setupPayload));
|
||||
};
|
||||
|
||||
// Handle incoming messages from server
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const response: ServerResponse = JSON.parse(event.data);
|
||||
console.log("Server response:", response);
|
||||
// Handle model loading progress updates
|
||||
if (response.type === "progress") {
|
||||
console.log(`Loading progress: ${response.percent}%`);
|
||||
// You might want to update UI with loading progress here
|
||||
}
|
||||
// Handle successful model load
|
||||
else if (
|
||||
response.type === "success" &&
|
||||
response.status === "model_loaded"
|
||||
) {
|
||||
console.log("Model loaded successfully");
|
||||
// Enable UI elements that depend on model being loaded
|
||||
}
|
||||
// Handle image generation results
|
||||
else if (
|
||||
response.type === "success" &&
|
||||
response.status === "generation_complete"
|
||||
) {
|
||||
if (response.image) {
|
||||
// Convert base64 image to ImageBitmap for preview
|
||||
this.base64ToImageBitmap(response.image).then((bitmap) => {
|
||||
this.outputBitmap = bitmap;
|
||||
this.previewCanvas.image(bitmap);
|
||||
this.mediaLayerRef.draw();
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing server response:", error);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log("Disconnected from inference server");
|
||||
};
|
||||
|
||||
// Store socket reference for later use
|
||||
this.serverSocket = socket;
|
||||
}
|
||||
|
||||
// Add method to update model settings
|
||||
public updateServerSettings(settings: ServerSettingsPayload) {
|
||||
if (this.serverSocket && this.serverSocket.readyState === WebSocket.OPEN) {
|
||||
this.serverSocket.send(JSON.stringify(settings));
|
||||
} else {
|
||||
console.error(
|
||||
"Cannot update settings: Server connection not established",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public paintMode() {
|
||||
let isDrawing = false;
|
||||
let currentLine: Konva.Line | null = null;
|
||||
|
||||
const startDrawing = (pos: { x: number; y: number }) => {
|
||||
if (!this.isEnabled) return;
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
// Convert pointer position to relative position within capture canvas
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
const relativeX = pos.x - captureBox.x;
|
||||
const relativeY = pos.y - captureBox.y;
|
||||
|
||||
currentLine = new Konva.Line({
|
||||
points: [relativeX, relativeY],
|
||||
stroke: this.paintColor,
|
||||
strokeWidth: 5,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
x: this.captureCanvas.x(),
|
||||
y: this.captureCanvas.y(),
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
this.drawingsLayer.add(currentLine); // Add to drawingsLayer
|
||||
isDrawing = true;
|
||||
};
|
||||
|
||||
const draw = (pos: { x: number; y: number }) => {
|
||||
if (!this.isEnabled) return;
|
||||
if (!isDrawing || !currentLine) return;
|
||||
|
||||
// Convert pointer position to relative position
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
const relativeX = pos.x - captureBox.x;
|
||||
const relativeY = pos.y - captureBox.y;
|
||||
|
||||
const newPoints = currentLine.points().concat([relativeX, relativeY]);
|
||||
currentLine.points(newPoints);
|
||||
this.drawingsLayer.batchDraw();
|
||||
};
|
||||
|
||||
const stopDrawing = () => {
|
||||
if (!this.isEnabled) return;
|
||||
if (!isDrawing || !currentLine) return;
|
||||
|
||||
// Store current line before resetting state
|
||||
const lineToConvert = currentLine;
|
||||
|
||||
// Reset drawing state immediately so we can start a new stroke
|
||||
isDrawing = false;
|
||||
currentLine = null;
|
||||
|
||||
// Create a temporary layer to render the line
|
||||
const tempLayer = new Konva.Layer({
|
||||
clearBeforeDraw: true,
|
||||
});
|
||||
this.mediaLayerRef.getStage()?.add(tempLayer);
|
||||
tempLayer.add(lineToConvert);
|
||||
|
||||
// Get the line canvas with transparent background
|
||||
// Get the bounding box of the line
|
||||
const lineBounds = lineToConvert.getClientRect();
|
||||
|
||||
// Create canvas with just enough size to contain the line
|
||||
const lineCanvas = tempLayer.toCanvas({
|
||||
x: lineBounds.x,
|
||||
y: lineBounds.y,
|
||||
width: lineBounds.width,
|
||||
height: lineBounds.height,
|
||||
pixelRatio: 1,
|
||||
});
|
||||
|
||||
if (this.onDrawCallback) {
|
||||
this.onDrawCallback(lineCanvas, lineBounds);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
lineToConvert.destroy();
|
||||
tempLayer.destroy();
|
||||
|
||||
this.drawingsLayer.batchDraw();
|
||||
};
|
||||
|
||||
// Check if point is within capture canvas bounds
|
||||
const isWithinCaptureCanvas = (pos: { x: number; y: number }) => {
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
return (
|
||||
pos.x >= captureBox.x &&
|
||||
pos.x <= captureBox.x + captureBox.width &&
|
||||
pos.y >= captureBox.y &&
|
||||
pos.y <= captureBox.y + captureBox.height
|
||||
);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
stage.on("mousedown touchstart", (e) => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos && isWithinCaptureCanvas(pos)) {
|
||||
startDrawing(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mousemove touchmove", (e) => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos && isWithinCaptureCanvas(pos)) {
|
||||
draw(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mouseup touchend", async () => {
|
||||
stopDrawing();
|
||||
await this.render();
|
||||
});
|
||||
|
||||
// Store cleanup function
|
||||
this.cleanupFunction = () => {
|
||||
stage.off("mousedown touchstart");
|
||||
stage.off("mousemove touchmove");
|
||||
stage.off("mouseup touchend");
|
||||
};
|
||||
}
|
||||
|
||||
public enablePaintMode() {
|
||||
this.isEnabled = true;
|
||||
if (!this.cleanupFunction) {
|
||||
this.paintMode();
|
||||
}
|
||||
}
|
||||
|
||||
public enableDragging() {
|
||||
// Enable dragging for all nodes in media layer
|
||||
this.imageNodes?.forEach((node) => {
|
||||
node.kNode.draggable(true);
|
||||
node.kNode.listening(true);
|
||||
});
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
public disableDragging() {
|
||||
// Disable dragging for all nodes in media layer
|
||||
this.imageNodes?.forEach((node) => {
|
||||
node.kNode.draggable(false);
|
||||
node.kNode.listening(false);
|
||||
});
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
public disablePaintMode() {
|
||||
this.isEnabled = false;
|
||||
if (this.cleanupFunction) {
|
||||
this.cleanupFunction();
|
||||
this.cleanupFunction = null;
|
||||
}
|
||||
}
|
||||
|
||||
public previewCopyListener() {
|
||||
this.previewCanvas.on("mousedown touchstart", () => {
|
||||
if (!this.outputBitmap) {
|
||||
console.log("No preview image to copy");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create draggable preview copy
|
||||
const previewCopy = new Konva.Image({
|
||||
x: this.previewCanvas.x(),
|
||||
y: this.previewCanvas.y(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
image: this.outputBitmap,
|
||||
draggable: true,
|
||||
listening: true,
|
||||
});
|
||||
|
||||
// this.drawingsLayer.add(previewCopy); // Add to drawingsLayer instead of mediaLayerRef
|
||||
// this.drawingsLayer.batchDraw();
|
||||
|
||||
// Invoke the callback with the preview copy
|
||||
if (this.onPreviewCopyCallback) {
|
||||
this.onPreviewCopyCallback(previewCopy);
|
||||
}
|
||||
|
||||
// Start dragging immediately
|
||||
previewCopy.startDrag();
|
||||
|
||||
// Handle drag events
|
||||
previewCopy.on("dragmove", () => {
|
||||
// this.drawingsLayer.draw();
|
||||
});
|
||||
|
||||
previewCopy.on("dragend", () => {
|
||||
const previewBox = previewCopy.getClientRect();
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
|
||||
if (Konva.Util.haveIntersection(previewBox, captureBox)) {
|
||||
// Snap to capture canvas position
|
||||
previewCopy.position({
|
||||
x: this.captureCanvas.x(),
|
||||
y: this.captureCanvas.y(),
|
||||
});
|
||||
//this.drawingsLayer.batchDraw();
|
||||
} else {
|
||||
// Remove if dropped outside capture area
|
||||
//previewCopy.destroy();
|
||||
//this.drawingsLayer.batchDraw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public findImageNodeById(
|
||||
id: string,
|
||||
): ImageNode | TextNode | ShapeNode | PaintNode | undefined {
|
||||
return this.imageNodes.find((node) => {
|
||||
if (node.kNode) {
|
||||
return node.kNode.id() === id;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
async updateCaptureCanvas(
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
@@ -624,33 +278,20 @@ export class RealTimeDrawEngine {
|
||||
private handleNodeDragEnd = async () => {
|
||||
// Clean up any existing state
|
||||
if (this.isProcessing) {
|
||||
console.log("isProcessing Returning");
|
||||
return;
|
||||
}
|
||||
console.log("Node drag ended");
|
||||
this.isProcessing = true;
|
||||
await this.render();
|
||||
};
|
||||
|
||||
public async addNodes(node: MediaNode) {
|
||||
if (
|
||||
node instanceof ImageNode ||
|
||||
node instanceof TextNode ||
|
||||
node instanceof ShapeNode ||
|
||||
node instanceof PaintNode
|
||||
) {
|
||||
console.debug("Adding node:", node);
|
||||
public addNodes(node: MediaNode) {
|
||||
if (node instanceof VideoNode) {
|
||||
this.videoNodes.push(node);
|
||||
node.kNode.on("dragend", this.handleNodeDragEnd);
|
||||
} else if (node instanceof ImageNode || node instanceof TextNode) {
|
||||
this.imageNodes.push(node);
|
||||
console.log(this.imageNodes);
|
||||
//node.kNode.on("dragend", this.handleNodeDragEnd);
|
||||
node.kNode.on("dragend", this.handleNodeDragEnd);
|
||||
}
|
||||
|
||||
// ensure the layer doesn't move if added while painting.
|
||||
if (this.isEnabled) {
|
||||
this.disableDragging();
|
||||
}
|
||||
|
||||
await this.render();
|
||||
}
|
||||
|
||||
public removeNodes(node: MediaNode) {
|
||||
@@ -660,12 +301,7 @@ export class RealTimeDrawEngine {
|
||||
node.kNode.off("dragend", this.handleNodeDragEnd);
|
||||
this.videoNodes.splice(index, 1);
|
||||
}
|
||||
} else if (
|
||||
node instanceof ImageNode ||
|
||||
node instanceof TextNode ||
|
||||
node instanceof ShapeNode ||
|
||||
node instanceof PaintNode
|
||||
) {
|
||||
} else if (node instanceof ImageNode || node instanceof TextNode) {
|
||||
const index = this.imageNodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
node.kNode.off("dragend", this.handleNodeDragEnd);
|
||||
@@ -748,6 +384,8 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async imageBitmapToBase64(imageBitmap: ImageBitmap): Promise<string> {
|
||||
// Create a temporary canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -774,7 +412,7 @@ export class RealTimeDrawEngine {
|
||||
base64String: string,
|
||||
): Promise<ImageBitmap> {
|
||||
// Create an image element
|
||||
const img = document.createElement("img");
|
||||
const img = new Image();
|
||||
|
||||
// Convert base64 to data URL if it doesn't include the prefix
|
||||
const dataUrl = base64String.startsWith("data:")
|
||||
@@ -824,12 +462,9 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
|
||||
public async render() {
|
||||
// only pick nodes that intersect wi th the canvas on screen bounds to freeze.
|
||||
|
||||
// only pick nodes that intersect with the canvas on screen bounds to freeze.
|
||||
this.mediaLayerRef.draw();
|
||||
// Output all nodes in mediaLayerRef
|
||||
const nodes = this.mediaLayerRef.getChildren();
|
||||
console.log("All nodes in mediaLayer:", nodes);
|
||||
|
||||
console.log(
|
||||
`context: x:${this.positionX} y:${this.positionY} ${this.width} x ${this.height}`,
|
||||
);
|
||||
@@ -847,20 +482,20 @@ export class RealTimeDrawEngine {
|
||||
})) as ImageBitmap;
|
||||
|
||||
// Test code
|
||||
if (true) {
|
||||
if (false) {
|
||||
this.outputBitmap = bitmap;
|
||||
this.previewCanvas.image(bitmap);
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const base64Bitmap = await this.imageBitmapToBase64(bitmap);
|
||||
|
||||
const base64BitmapResponse = await invoke("infer_image", {
|
||||
image: base64Bitmap,
|
||||
prompt: this.currentPrompt,
|
||||
strength: this.currentStrength,
|
||||
prompt:this.currentPrompt,
|
||||
strength:this.currentStrength,
|
||||
});
|
||||
|
||||
console.log(base64BitmapResponse);
|
||||
@@ -875,5 +510,6 @@ export class RealTimeDrawEngine {
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { RenderEngine } from "../RenderingPrimitives/RenderEngine";
|
||||
|
||||
import { toolbarNode } from "~/signals/uiAccess/toolbarNode";
|
||||
|
||||
@@ -19,7 +19,7 @@ export class CreateCommand implements ICommand {
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
|
||||
private renderEngineRef: RealTimeDrawEngine;
|
||||
private renderEngineRef: RenderEngine;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
@@ -33,7 +33,7 @@ export class CreateCommand implements ICommand {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
renderEngineRef: RenderEngine;
|
||||
selectionManagerRef: SelectionManager;
|
||||
}) {
|
||||
this.nodes = nodes;
|
||||
@@ -53,7 +53,6 @@ export class CreateCommand implements ICommand {
|
||||
this.renderEngineRef.addNodes(node);
|
||||
});
|
||||
this.mediaLayerRef.draw();
|
||||
|
||||
}
|
||||
|
||||
undo() {
|
||||
@@ -72,7 +71,5 @@ export class CreateCommand implements ICommand {
|
||||
this.nodeTransformerRef.clear();
|
||||
toolbarNode.hide();
|
||||
this.mediaLayerRef.draw();
|
||||
this.renderEngineRef.render();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ export class UndoStackManager {
|
||||
private undoStack: ICommand[] = [];
|
||||
private redoStack: ICommand[] = [];
|
||||
private readonly STACK_LIMIT = 20;
|
||||
private onCommandExecuted: () => void;
|
||||
|
||||
constructor(onCommandExecuted: () => void) {
|
||||
this.onCommandExecuted = onCommandExecuted;
|
||||
}
|
||||
|
||||
setDisabled(newState: boolean) {
|
||||
this.disabled = newState;
|
||||
@@ -55,7 +50,6 @@ export class UndoStackManager {
|
||||
}
|
||||
console.log("undo:", this.undoStack);
|
||||
}
|
||||
this.onCommandExecuted();
|
||||
}
|
||||
|
||||
redo() {
|
||||
@@ -74,6 +68,5 @@ export class UndoStackManager {
|
||||
|
||||
console.log("Redo:", this.redoStack);
|
||||
}
|
||||
this.onCommandExecuted();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ export enum AppModes {
|
||||
INIT = "INIT",
|
||||
PREVIEW = "PREVIEW",
|
||||
SELECT = "SELECT",
|
||||
PAINT = "PAINT",
|
||||
ERASER = "ERASER",
|
||||
RENDERING = "RENDERING",
|
||||
}
|
||||
|
||||
@@ -16,5 +14,5 @@ export const VideoResolutions = {
|
||||
export const Colors = {
|
||||
transparent: "rgba(0,0,0,0)",
|
||||
translucentBlack: "rgba(0,0,0,0.3)",
|
||||
primaryOrange: "rgba(45, 129, 255, 1)",
|
||||
primaryOrange: "rgba(230,100,98,1)",
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { NodeType, BaseNode, ImageNode, TextNode, VideoNode, ShapeNode } from "../Nodes";
|
||||
import { NodeType, BaseNode, ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
|
||||
import { TransformationData } from "./Transformation";
|
||||
|
||||
import { TextNodeData } from "./Text";
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
export type MediaNode =
|
||||
// | NetworkedNode
|
||||
BaseNode | VideoNode | ImageNode | TextNode | ShapeNode | PaintNode;
|
||||
BaseNode | VideoNode | ImageNode | TextNode;
|
||||
|
||||
export type ImageNodeData = {
|
||||
mediaFileUrl: string;
|
||||
@@ -27,10 +26,6 @@ export type VideoNodeData = {
|
||||
extractionUrl?: string;
|
||||
};
|
||||
|
||||
export type ShapeNodeData = {
|
||||
shape: "triangle" | "square" | "circle";
|
||||
}
|
||||
|
||||
export type NodeData = {
|
||||
type: NodeType;
|
||||
transform: TransformationData;
|
||||
|
||||
@@ -50,20 +50,6 @@ export const ContextualToolbarNode = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
// Don't do anything if no node is known to be bound
|
||||
if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the UI component
|
||||
uiAccess.toolbarNode.setColor(color);
|
||||
|
||||
// Signal the engine with the node ID (just grab the first -and expected only- node)
|
||||
const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0];
|
||||
dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color });
|
||||
}
|
||||
|
||||
const boundedTopPosition = Math.min(position.y, winSize.height - 164);
|
||||
return (
|
||||
<Transition
|
||||
@@ -85,8 +71,6 @@ export const ContextualToolbarNode = () => {
|
||||
onLockClicked={(e) => {
|
||||
dispatchUiEvents.toolbarNode.lock(e);
|
||||
}}
|
||||
color={uiAccess.toolbarNode.signal.value.color}
|
||||
onColorChange={handleColorChange}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { Slider } from "~/components/ui";
|
||||
import { dispatchUiEvents } from "~/signals";
|
||||
import { Slider } from "~/components/ui"
|
||||
import { dispatchUiEvents } from "~/signals"
|
||||
import { promptStrength } from "~/signals/uiEvents/promptSettings";
|
||||
|
||||
export const SignaledPromptSlider = () => {
|
||||
const sliderValue = promptStrength.value;
|
||||
const onSliderValueChanged =
|
||||
dispatchUiEvents.promptSettings.setPromptStrength;
|
||||
|
||||
// This makes component re-render whenever the signal value changes
|
||||
useSignalEffect(() => {
|
||||
promptStrength.value;
|
||||
});
|
||||
const sliderValue = promptStrength.value
|
||||
const onSliderValueChanged = dispatchUiEvents.promptSettings.setPromptStrength
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-[138px] left-1/2 h-4 w-[400px] -translate-x-1/2 shadow-lg">
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValue}
|
||||
onChange={onSliderValueChanged}
|
||||
step={1}
|
||||
innerLabel={`Strength - ${sliderValue}%`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// This makes component re-render whenever the signal value changes
|
||||
useSignalEffect(() => {
|
||||
promptStrength.value;
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-80 left-1/2 -translate-x-1/2 h-4 w-96">
|
||||
<Slider min={0} max={100} value={sliderValue} onChange={onSliderValueChanged} step={1} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,31 @@
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { ResizeType, Textarea } from "~/components/ui";
|
||||
import { dispatchUiEvents } from "~/signals";
|
||||
import { promptText } from "~/signals/uiEvents/promptSettings";
|
||||
|
||||
export const SignaledPromptText = ({}) => {
|
||||
export const SignaledPromptText = ({
|
||||
}) => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const focusOnTextArea = useCallback(() => {
|
||||
if (textAreaRef.current === null) {
|
||||
return;
|
||||
}
|
||||
textAreaRef.current.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
focusOnTextArea();
|
||||
}, []);
|
||||
|
||||
const TextAreaStates = {
|
||||
placeholder: "Type your prompt here...",
|
||||
style: {
|
||||
width: "530px",
|
||||
height: "200px",
|
||||
},
|
||||
placeholder: "",
|
||||
label: "",
|
||||
resize: "none" as ResizeType,
|
||||
rows: 3,
|
||||
};
|
||||
|
||||
const textContent = promptText.value;
|
||||
@@ -22,18 +36,11 @@ export const SignaledPromptText = ({}) => {
|
||||
// This makes component re-render whenever the signal value changes
|
||||
useSignalEffect(() => {
|
||||
promptText.value;
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-5 left-1/2 w-[400px] -translate-x-1/2 text-[16px] font-medium shadow-lg">
|
||||
<Textarea
|
||||
className="glass rounded-xl"
|
||||
{...TextAreaStates}
|
||||
ref={textAreaRef}
|
||||
onChange={onTextChanged}
|
||||
value={textContent}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="fixed bottom-24 left-1/2 -translate-x-1/2">
|
||||
<Textarea {...TextAreaStates} ref={textAreaRef} onChange={onTextChanged} value={textContent} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import { LayoutSignalType } from "./contextSignals/layout";
|
||||
import { AppUiContextInterface } from "./contextSignals/appUi";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { dispatchUiEvents } from "~/signals";
|
||||
|
||||
export const SignaledToolbarMain = ({
|
||||
// layoutSignal,
|
||||
@@ -44,19 +43,6 @@ export const SignaledToolbarMain = ({
|
||||
return () => appUiContext.openAddImage();
|
||||
case ToolbarMainButtonNames.ADD_VIDEO:
|
||||
return () => appUiContext.openAddVideo();
|
||||
case ToolbarMainButtonNames.ADD_CIRCLE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "circle" });
|
||||
case ToolbarMainButtonNames.ADD_TRIANGLE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "triangle" });
|
||||
case ToolbarMainButtonNames.ADD_SQUARE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "square" });
|
||||
case ToolbarMainButtonNames.SELECT:
|
||||
case ToolbarMainButtonNames.ERASER:
|
||||
case ToolbarMainButtonNames.PAINT:
|
||||
return (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
toolbarMain.setMode(buttonName);
|
||||
dispatchers[buttonName](e);
|
||||
};
|
||||
default:
|
||||
return dispatchers[buttonName];
|
||||
}
|
||||
@@ -83,7 +69,7 @@ export const SignaledToolbarMain = ({
|
||||
dispatchers.loadingBarRetry(e);
|
||||
};
|
||||
return (
|
||||
<div className="fixed left-4 top-1/2 -translate-y-1/2">
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2">
|
||||
<Transition
|
||||
as="div"
|
||||
className={twMerge(
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import {
|
||||
ServerMessage,
|
||||
WebSocketHandlers,
|
||||
SetupRequest,
|
||||
UpdateSettingsRequest,
|
||||
GenerateRequest,
|
||||
StatusRequest,
|
||||
GenerationParameters,
|
||||
ServerSettings,
|
||||
createSetupRequest,
|
||||
createUpdateSettingsRequest,
|
||||
createGenerateRequest,
|
||||
createStatusRequest,
|
||||
} from "./ServerTypes";
|
||||
|
||||
/**
|
||||
* ServerClient provides a typed interface for communicating with the Python WebSocket server
|
||||
*/
|
||||
export class ServerClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private handlers: WebSocketHandlers = {};
|
||||
private connectionPromise: Promise<void> | null = null;
|
||||
private connectionResolve: (() => void) | null = null;
|
||||
private connectionReject: ((error: Error) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Create a new ServerClient
|
||||
* @param url The WebSocket URL to connect to
|
||||
* @param handlers Optional event handlers for different message types
|
||||
*/
|
||||
constructor(
|
||||
private url: string,
|
||||
handlers: Partial<WebSocketHandlers> = {},
|
||||
) {
|
||||
this.handlers = { ...handlers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the WebSocket server
|
||||
* @returns A promise that resolves when connected
|
||||
*/
|
||||
public connect(): Promise<void> {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = new Promise<void>((resolve, reject) => {
|
||||
this.connectionResolve = resolve;
|
||||
this.connectionReject = reject;
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
if (this.connectionResolve) {
|
||||
this.connectionResolve();
|
||||
}
|
||||
if (this.handlers.onOpen) {
|
||||
this.handlers.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
if (this.handlers.onClose) {
|
||||
this.handlers.onClose();
|
||||
}
|
||||
this.ws = null;
|
||||
this.connectionPromise = null;
|
||||
};
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
const error = new Error(`WebSocket error: ${event}`);
|
||||
if (this.connectionReject) {
|
||||
this.connectionReject(error);
|
||||
}
|
||||
if (this.handlers.onError) {
|
||||
this.handlers.onError({
|
||||
type: "error",
|
||||
error: `Connection error: ${event}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data) as ServerMessage;
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error("Error parsing message:", error);
|
||||
if (this.handlers.onError) {
|
||||
this.handlers.onError({
|
||||
type: "error",
|
||||
error: `Failed to parse message: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.connectionReject) {
|
||||
this.connectionReject(error as Error);
|
||||
}
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the WebSocket server
|
||||
*/
|
||||
public disconnect(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the server
|
||||
* @param message The message to send
|
||||
* @returns A promise that resolves when the message is sent
|
||||
*/
|
||||
private async send(message: ServerMessage): Promise<void> {
|
||||
await this.connect();
|
||||
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming message from the server
|
||||
* @param message The message received from the server
|
||||
*/
|
||||
private handleMessage(message: ServerMessage): void {
|
||||
switch (message.type) {
|
||||
case "setup_response":
|
||||
if (this.handlers.onSetupResponse) {
|
||||
this.handlers.onSetupResponse(message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "update_settings_response":
|
||||
if (this.handlers.onUpdateSettingsResponse) {
|
||||
this.handlers.onUpdateSettingsResponse(message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "generate_response":
|
||||
if (this.handlers.onGenerateResponse) {
|
||||
this.handlers.onGenerateResponse(message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "status_response":
|
||||
if (this.handlers.onStatusResponse) {
|
||||
this.handlers.onStatusResponse(message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "progress_update":
|
||||
if (this.handlers.onProgressUpdate) {
|
||||
this.handlers.onProgressUpdate(message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "error":
|
||||
case "connection_error":
|
||||
if (this.handlers.onError) {
|
||||
this.handlers.onError(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers
|
||||
* @param handlers The event handlers to set
|
||||
*/
|
||||
public setHandlers(handlers: Partial<WebSocketHandlers>): void {
|
||||
this.handlers = { ...this.handlers, ...handlers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a setup request to initialize the model
|
||||
* @param sdxl_checkpoint_path Path to the SDXL checkpoint
|
||||
* @param lora_path Optional path to a LoRA file
|
||||
*/
|
||||
public async setup(
|
||||
sdxl_checkpoint_path: string,
|
||||
lora_path?: string,
|
||||
): Promise<void> {
|
||||
const request = createSetupRequest(sdxl_checkpoint_path, lora_path);
|
||||
await this.send(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an update settings request
|
||||
* @param settings The settings to update
|
||||
*/
|
||||
public async updateSettings(
|
||||
settings: Partial<ServerSettings>,
|
||||
): Promise<void> {
|
||||
const request = createUpdateSettingsRequest(settings);
|
||||
await this.send(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a generate request
|
||||
* @param params The generation parameters
|
||||
*/
|
||||
public async generate(params: GenerationParameters): Promise<void> {
|
||||
const request = createGenerateRequest(params);
|
||||
await this.send(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a status request
|
||||
*/
|
||||
public async requestStatus(): Promise<void> {
|
||||
const request = createStatusRequest();
|
||||
await this.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a base64 string from an image
|
||||
* @param file The image file
|
||||
* @returns A promise that resolves to the base64 string
|
||||
*/
|
||||
export function fileToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
// Remove the data URL prefix (e.g., "data:image/png;base64,")
|
||||
const base64 = result.split(",")[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an image from a base64 string
|
||||
* @param base64 The base64 string
|
||||
* @returns The image URL
|
||||
*/
|
||||
export function base64ToImageUrl(base64: string): string {
|
||||
return `data:image/png;base64,${base64}`;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// Server Message Types - Common base type for all messages
|
||||
export type MessageType =
|
||||
| "setup"
|
||||
| "setup_response"
|
||||
| "update_settings"
|
||||
| "update_settings_response"
|
||||
| "generate"
|
||||
| "generate_response"
|
||||
| "status"
|
||||
| "status_response"
|
||||
| "progress_update"
|
||||
| "error"
|
||||
| "connection_error";
|
||||
|
||||
// Base interface for all messages
|
||||
interface BaseMessage {
|
||||
type: MessageType;
|
||||
}
|
||||
|
||||
// ===== Setup Messages =====
|
||||
|
||||
// Setup Request
|
||||
export interface SetupRequest extends BaseMessage {
|
||||
type: "setup";
|
||||
setup: {
|
||||
sdxl_checkpoint_path: string;
|
||||
lora_path?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Setup Response
|
||||
export interface SetupResponse extends BaseMessage {
|
||||
type: "setup_response";
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
settings?: ServerSettings;
|
||||
}
|
||||
|
||||
// ===== Update Settings Messages =====
|
||||
|
||||
// Default generation settings that can be configured
|
||||
export interface GenerationSettings {
|
||||
default_lora_strength?: number;
|
||||
default_image_to_image_strength?: number;
|
||||
default_width?: number;
|
||||
default_height?: number;
|
||||
default_steps?: number;
|
||||
default_guidance_scale?: number;
|
||||
}
|
||||
|
||||
// Server settings - includes both paths and generation defaults
|
||||
export interface ServerSettings extends GenerationSettings {
|
||||
sdxl_checkpoint_path: string;
|
||||
lora_path?: string | null;
|
||||
}
|
||||
|
||||
// Update Settings Request
|
||||
export interface UpdateSettingsRequest extends BaseMessage {
|
||||
type: "update_settings";
|
||||
settings: Partial<ServerSettings>;
|
||||
}
|
||||
|
||||
// Update Settings Response
|
||||
export interface UpdateSettingsResponse extends BaseMessage {
|
||||
type: "update_settings_response";
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
updates_applied?: string[];
|
||||
current_settings: ServerSettings;
|
||||
}
|
||||
|
||||
// ===== Generate Messages =====
|
||||
|
||||
// Generation parameters
|
||||
export interface GenerationParameters {
|
||||
image: string; // base64 encoded image
|
||||
prompt: string;
|
||||
lora_strength?: number;
|
||||
image_to_image_strength?: number;
|
||||
generated_image_width?: number;
|
||||
generated_image_height?: number;
|
||||
num_inference_steps?: number;
|
||||
guidance_scale?: number;
|
||||
seed?: number;
|
||||
}
|
||||
|
||||
// Generate Request
|
||||
export interface GenerateRequest extends BaseMessage {
|
||||
type: "generate";
|
||||
generate: GenerationParameters;
|
||||
}
|
||||
|
||||
// Generate Response
|
||||
export interface GenerateResponse extends BaseMessage {
|
||||
type: "generate_response";
|
||||
success: boolean;
|
||||
image?: string; // base64 encoded image
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ===== Status Messages =====
|
||||
|
||||
// Status Request
|
||||
export interface StatusRequest extends BaseMessage {
|
||||
type: "status";
|
||||
}
|
||||
|
||||
// Status Response
|
||||
export interface StatusResponse extends BaseMessage {
|
||||
type: "status_response";
|
||||
success: boolean;
|
||||
model_initialized: boolean;
|
||||
current_settings: ServerSettings;
|
||||
}
|
||||
|
||||
// ===== Progress Update =====
|
||||
|
||||
// Progress stages
|
||||
export type ProgressStage =
|
||||
| "downloading"
|
||||
| "loading_unet"
|
||||
| "loading_base_model"
|
||||
| "configuring_scheduler"
|
||||
| "loading_lcm_adapter"
|
||||
| "loading_lora"
|
||||
| "generating"
|
||||
| "error";
|
||||
|
||||
// Progress Update Message
|
||||
export interface ProgressUpdateMessage extends BaseMessage {
|
||||
type: "progress_update";
|
||||
stage: ProgressStage;
|
||||
progress: number; // 0-100
|
||||
file: string;
|
||||
}
|
||||
|
||||
// ===== Error Messages =====
|
||||
|
||||
// Error Message
|
||||
export interface ErrorMessage extends BaseMessage {
|
||||
type: "error";
|
||||
error: string;
|
||||
}
|
||||
|
||||
// Connection Error Message
|
||||
export interface ConnectionErrorMessage extends BaseMessage {
|
||||
type: "connection_error";
|
||||
error: string;
|
||||
}
|
||||
|
||||
// Union type for all possible server messages
|
||||
export type ServerMessage =
|
||||
| SetupRequest
|
||||
| SetupResponse
|
||||
| UpdateSettingsRequest
|
||||
| UpdateSettingsResponse
|
||||
| GenerateRequest
|
||||
| GenerateResponse
|
||||
| StatusRequest
|
||||
| StatusResponse
|
||||
| ProgressUpdateMessage
|
||||
| ErrorMessage
|
||||
| ConnectionErrorMessage;
|
||||
|
||||
// WebSocket event handlers for different message types
|
||||
export interface WebSocketHandlers {
|
||||
onSetupResponse?: (response: SetupResponse) => void;
|
||||
onUpdateSettingsResponse?: (response: UpdateSettingsResponse) => void;
|
||||
onGenerateResponse?: (response: GenerateResponse) => void;
|
||||
onStatusResponse?: (response: StatusResponse) => void;
|
||||
onProgressUpdate?: (update: ProgressUpdateMessage) => void;
|
||||
onError?: (error: ErrorMessage | ConnectionErrorMessage) => void;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// Helper function to create a typed setup request
|
||||
export function createSetupRequest(
|
||||
sdxl_checkpoint_path: string,
|
||||
lora_path?: string,
|
||||
): SetupRequest {
|
||||
return {
|
||||
type: "setup",
|
||||
setup: {
|
||||
sdxl_checkpoint_path,
|
||||
lora_path,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create a typed update settings request
|
||||
export function createUpdateSettingsRequest(
|
||||
settings: Partial<ServerSettings>,
|
||||
): UpdateSettingsRequest {
|
||||
return {
|
||||
type: "update_settings",
|
||||
settings,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create a typed generate request
|
||||
export function createGenerateRequest(
|
||||
params: GenerationParameters,
|
||||
): GenerateRequest {
|
||||
return {
|
||||
type: "generate",
|
||||
generate: params,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create a typed status request
|
||||
export function createStatusRequest(): StatusRequest {
|
||||
return { type: "status" };
|
||||
}
|
||||
@@ -4,11 +4,10 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@apply bg-ui-background font-sans text-white;
|
||||
@apply bg-ui-background font-sans text-[#222222];
|
||||
}
|
||||
.pegboard {
|
||||
background-image: radial-gradient(#ffffff25 1px, transparent 0);
|
||||
background-color: #1b1b1f;
|
||||
background-image: radial-gradient(#3f3f466b 1px, transparent 0);
|
||||
background-size: 36px 36px;
|
||||
@apply bg-center;
|
||||
}
|
||||
@@ -42,24 +41,7 @@
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-ui-controls border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply bg-ui-controls placeholder:text-white/50;
|
||||
|
||||
/* Webkit scrollbar styles */
|
||||
&::-webkit-scrollbar {
|
||||
@apply w-1;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply rounded-full bg-white/20 hover:bg-white/30;
|
||||
}
|
||||
@apply border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +54,10 @@
|
||||
.dotted-pattern {
|
||||
background-image: radial-gradient(
|
||||
circle at 1px 1px,
|
||||
rgb(255 255 255 / 0.25) 1px,
|
||||
rgb(0 0 0 / 0.25) 1px,
|
||||
transparent 0
|
||||
);
|
||||
background-size: 40px 40px;
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
@@ -105,11 +87,6 @@
|
||||
background-position: 100% 0;
|
||||
animation: highlight-sweep 3s infinite linear;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@apply relative border border-ui-border shadow-lg backdrop-blur-xl transition-colors duration-300;
|
||||
@apply bg-ui-controls/50 hover:bg-ui-controls/90 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
|
||||
@@ -1,169 +1,107 @@
|
||||
import Navbar from "../../../components/landing/Navbar";
|
||||
import Hero from "../../../components/landing/Hero";
|
||||
import FeatureCard from "../../../components/landing/FeatureCard";
|
||||
import Footer from "../../../components/landing/Footer";
|
||||
import { Button } from "~/components/ui";
|
||||
import { faApple } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDesktop } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useIsMobile } from "~/hooks/useIsMobile";
|
||||
|
||||
const Landing = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const features = [
|
||||
{
|
||||
title: "Lorem ipsum dolor sit",
|
||||
title: "Step 1: Compose Your Scene",
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.",
|
||||
image: "/images/features/ai-creation.jpg",
|
||||
tag: "Core",
|
||||
"Begin your story on our intuitive canvas. Drop and arrange photos and videos to create layered scenes. Position, scale, and transform elements with precision to build your perfect composition. Our dynamic canvas makes it easy to bring your vision to life.",
|
||||
image: "/images/feature_compositing.png",
|
||||
gradient: "bg-gradient-to-br from-purple-500/20 to-blue-500/20",
|
||||
},
|
||||
{
|
||||
title: "Ut enim ad minim",
|
||||
title: "Step 2: Smart Segmentation",
|
||||
description:
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea.",
|
||||
image: "/images/features/collaboration.jpg",
|
||||
tag: "Workflow",
|
||||
"Clean up your scenes with powerful AI segmentation. Remove green screens instantly, isolate subjects from any background, and perfect your layer masks with intelligent tools. Create professional-quality compositions with just a few clicks.",
|
||||
image: "/images/feature_segmentation.png",
|
||||
gradient: "bg-gradient-to-br from-emerald-500/20 to-cyan-500/20",
|
||||
},
|
||||
{
|
||||
title: "Duis aute irure dolor",
|
||||
title: "Step 3: AI Style Transfer",
|
||||
description:
|
||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla.",
|
||||
image: "/images/features/style-models.jpg",
|
||||
tag: "Tools",
|
||||
"Transform your composition with AI style transfer magic. Choose from our curated collection of artistic styles. Apply consistent styling across your entire scene to create a cohesive visual masterpiece.",
|
||||
image: "/images/feature_ai_stylize.png",
|
||||
gradient: "bg-gradient-to-br from-rose-500/20 to-orange-500/20",
|
||||
},
|
||||
{
|
||||
title: "Step 4: Render Your Video",
|
||||
description:
|
||||
"Render your styled composition into a stunning final movie. Export in high quality, ready to share with the world.",
|
||||
image: "/images/feature_render.png",
|
||||
gradient: "bg-gradient-to-br from-blue-500/20 to-indigo-500/20",
|
||||
},
|
||||
];
|
||||
|
||||
const scrollToFeature = (index: number) => {
|
||||
const element = document.getElementById(`feature-${index + 1}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-[#101014] text-white">
|
||||
<div className="min-h-screen bg-white">
|
||||
<Navbar />
|
||||
|
||||
<div className="dotted-pattern absolute inset-0 z-[0] opacity-50" />
|
||||
{/* Hero Section */}
|
||||
<div className="relative flex overflow-hidden pt-24 xl:min-h-[1000px] xl:items-center xl:pt-0">
|
||||
{/* Content Container */}
|
||||
<div className="relative z-10 mx-auto grid h-full w-full max-w-[1920px] grid-cols-12 items-center px-6 md:px-16 xl:px-32">
|
||||
<div className="z-10 col-span-12 w-full xl:absolute xl:left-0 xl:top-1/2 xl:max-w-4xl xl:-translate-y-1/2 xl:pl-32 before:xl:absolute before:xl:-inset-0 before:xl:-z-10 before:xl:mr-40 before:xl:rounded-full before:xl:bg-black/90 before:xl:to-transparent before:xl:blur-[180px]">
|
||||
<div className="max-w-2xl">
|
||||
<div className="mb-6">
|
||||
<span className="text-lg font-semibold uppercase tracking-widest text-gray-400">
|
||||
MIRA
|
||||
<div>
|
||||
<Hero onFeatureClick={scrollToFeature} />
|
||||
|
||||
<section className="relative overflow-hidden py-32">
|
||||
{/* Decorative elements */}
|
||||
<div className="dotted-pattern absolute inset-0 opacity-50" />
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
{/* Section header with enhanced styling */}
|
||||
<div className="mx-auto mb-24 max-w-3xl text-center">
|
||||
<div className="mb-6 inline-flex items-center rounded-full bg-gradient-to-r from-purple-500/10 via-blue-500/10 to-purple-500/10 px-8 py-2.5">
|
||||
<span className="whitespace-nowrap bg-gradient-to-r from-purple-600 via-blue-600 to-purple-600 bg-clip-text text-sm font-semibold text-transparent">
|
||||
AI-POWERED WORKFLOW
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mb-6 font-bold leading-tight lg:text-6xl">
|
||||
Realtime AI editor.
|
||||
<br />
|
||||
On your local machine.
|
||||
</h1>
|
||||
|
||||
<p className="mb-10 max-w-xl text-lg leading-relaxed">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation.
|
||||
<h2 className="mb-6 text-4xl font-bold tracking-tight text-gray-900">
|
||||
Create Stunning Videos in
|
||||
<span className="relative">
|
||||
<span className="gradient-text relative z-10">
|
||||
{" "}
|
||||
Four Simple Steps
|
||||
</span>
|
||||
<span className="bg-rose-200/50 absolute -bottom-2 left-0 z-0 h-3 w-full" />
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xl leading-relaxed text-gray-600">
|
||||
From composition to final render, transform your media into
|
||||
cinematic stories with our powerful AI-enhanced workflow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<Button
|
||||
className={`w-full transform rounded-lg px-8 py-4 font-semibold transition sm:w-auto ${
|
||||
isMobile
|
||||
? "cursor-not-allowed bg-[#2D81FF]"
|
||||
: "bg-[#2D81FF] hover:scale-105 hover:bg-[#438AF6]"
|
||||
}`}
|
||||
disabled={isMobile}
|
||||
icon={isMobile ? faDesktop : faApple}
|
||||
{/* Features stack with enhanced visual separation */}
|
||||
<div className="relative space-y-32">
|
||||
{/* Vertical timeline line */}
|
||||
<div className="to-rose-500/20 absolute left-1/2 top-0 h-full w-px bg-gradient-to-b from-purple-500/20 via-blue-500/20" />
|
||||
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
id={`feature-${index + 1}`}
|
||||
className="relative"
|
||||
>
|
||||
{isMobile ? "Download on desktop" : "Download for MacOS"}
|
||||
</Button>
|
||||
<button className="w-full transform rounded-lg border border-white/30 px-8 py-4 font-semibold transition hover:scale-105 hover:bg-white/10 sm:w-auto">
|
||||
Learn more
|
||||
</button>
|
||||
</div>
|
||||
{/* Step number bubble */}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 -translate-y-16">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full border bg-white shadow-lg">
|
||||
<span className="bg-gradient-to-br from-purple-600 to-purple-600 bg-clip-text text-xl font-bold text-transparent">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FeatureCard {...feature} isReversed={index % 2 === 1} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 mt-12 aspect-video w-full transform overflow-hidden rounded-xl border border-white/[16%] bg-transparent md:mt-16 xl:col-span-9 xl:col-start-4 xl:mt-0">
|
||||
<img
|
||||
src="/images/landing_hero.png"
|
||||
alt="AI Video Creation"
|
||||
className="h-full w-full object-cover object-top opacity-90"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="relative z-10 mx-auto mb-32 max-w-[1920px] px-4 py-20 sm:px-24 lg:px-32">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<h1 className="text-4xl font-bold">Features</h1>
|
||||
{/* <button className="text-sm text-[#0096CE] hover:text-[#0084B5]">
|
||||
View all
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{features.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group cursor-pointer overflow-hidden rounded-xl bg-[#1C1C20] transition hover:bg-[#27272B]"
|
||||
>
|
||||
<div className="aspect-video overflow-hidden">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover transition group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{/* <div className="mb-4 text-sm text-[#0096CE]">{item.tag}</div> */}
|
||||
<h3 className="mb-2 text-xl font-semibold">{item.title}</h3>
|
||||
<p className="text-gray-400">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Installation Steps Section */}
|
||||
<div className="mt-36">
|
||||
<h1 className="mb-12 text-5xl font-bold">
|
||||
How to install Mira AI Editor
|
||||
</h1>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="rounded-xl bg-[#1C1C20] p-8">
|
||||
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
|
||||
STEP 1
|
||||
</div>
|
||||
<h3 className="mb-4 text-2xl font-semibold">Download the app</h3>
|
||||
<p className="mb-6 text-gray-400">
|
||||
Download and install our application to start creating amazing
|
||||
AI-powered content.
|
||||
</p>
|
||||
<Button className="rounded-lg bg-[#2D81FF] px-4 py-2 text-sm font-medium hover:bg-[#438AF6]">
|
||||
Download App
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-[#1C1C20] p-8">
|
||||
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
|
||||
STEP 2
|
||||
</div>
|
||||
<h3 className="mb-4 text-2xl font-semibold">Create an account</h3>
|
||||
<p className="mb-6 text-gray-400">
|
||||
Sign up for an account to access all features and start your
|
||||
journey.
|
||||
</p>
|
||||
<Button className="text-sm font-medium" variant="secondary">
|
||||
Sign up
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-[#1C1C20] p-8">
|
||||
<div className="mb-4 w-fit rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white">
|
||||
STEP 3
|
||||
</div>
|
||||
<h3 className="mb-4 text-2xl font-semibold">Start creating</h3>
|
||||
<p className="mb-6 text-gray-400">
|
||||
Begin creating with our realtime AI editor!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ToolbarUserProfile } from "~/components/features";
|
||||
//Components of the Konva App are all in the KonvaComponent
|
||||
import { KonvaRootComponent } from "~/KonvaRootComponent";
|
||||
import { ToolbarTopLeft } from "~/components/features/ToolbarTopLeft";
|
||||
import { ToolbarTopRight } from "~/components/features/ToolbarTopRight";
|
||||
|
||||
export const Main = withProtectionRoute(() => {
|
||||
// This is a hook that will log the number of times the component has rerendered
|
||||
@@ -26,7 +25,6 @@ export const Main = withProtectionRoute(() => {
|
||||
/>
|
||||
<div className="absolute top-0 flex w-full justify-between px-2">
|
||||
<ToolbarTopLeft />
|
||||
<ToolbarTopRight />
|
||||
{/* <ToolbarUserProfile /> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
ServerClient,
|
||||
fileToBase64,
|
||||
base64ToImageUrl,
|
||||
} from "../PyServer/ServerClient";
|
||||
import {
|
||||
ProgressUpdateMessage,
|
||||
SetupResponse,
|
||||
GenerateResponse,
|
||||
StatusResponse,
|
||||
UpdateSettingsResponse,
|
||||
} from "../PyServer/ServerTypes";
|
||||
|
||||
// Default server URL
|
||||
const DEFAULT_SERVER_URL = "ws://localhost:8765";
|
||||
|
||||
export const ImageGenerator: React.FC = () => {
|
||||
// Server connection state
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [serverUrl, setServerUrl] = useState(DEFAULT_SERVER_URL);
|
||||
const clientRef = useRef<ServerClient | null>(null);
|
||||
|
||||
// Model state
|
||||
const [modelPath, setModelPath] = useState("");
|
||||
const [loraPath, setLoraPath] = useState("");
|
||||
const [isModelInitialized, setIsModelInitialized] = useState(false);
|
||||
|
||||
// Generation parameters
|
||||
const [sourceImage, setSourceImage] = useState<File | null>(null);
|
||||
const [sourceImageUrl, setSourceImageUrl] = useState<string | null>(null);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [loraStrength, setLoraStrength] = useState(1.0);
|
||||
const [imageToImageStrength, setImageToImageStrength] = useState(0.75);
|
||||
const [steps, setSteps] = useState(4);
|
||||
const [guidanceScale, setGuidanceScale] = useState(1.0);
|
||||
const [width, setWidth] = useState(1024);
|
||||
const [height, setHeight] = useState(1024);
|
||||
|
||||
// Results and UI state
|
||||
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [progress, setProgress] = useState<ProgressUpdateMessage | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Initialize the client
|
||||
useEffect(() => {
|
||||
const client = new ServerClient(serverUrl, {
|
||||
onOpen: () => {
|
||||
console.log("Connected to server");
|
||||
setIsConnected(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
// Request status to see if model is already initialized
|
||||
client.requestStatus().catch((error) => {
|
||||
console.error("Error requesting status:", error);
|
||||
});
|
||||
},
|
||||
|
||||
onClose: () => {
|
||||
console.log("Disconnected from server");
|
||||
setIsConnected(false);
|
||||
setIsModelInitialized(false);
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
console.error("Error:", error);
|
||||
setErrorMessage(error.error);
|
||||
setIsLoading(false);
|
||||
},
|
||||
|
||||
onProgressUpdate: (update) => {
|
||||
console.log("Progress:", update);
|
||||
setProgress(update);
|
||||
|
||||
if (update.stage !== "error") {
|
||||
setErrorMessage(null);
|
||||
}
|
||||
},
|
||||
|
||||
onSetupResponse: (response) => {
|
||||
console.log("Setup response:", response);
|
||||
setIsLoading(false);
|
||||
|
||||
if (response.success) {
|
||||
setIsModelInitialized(true);
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage(response.error || "Unknown error during setup");
|
||||
}
|
||||
},
|
||||
|
||||
onUpdateSettingsResponse: (response) => {
|
||||
console.log("Update settings response:", response);
|
||||
setIsLoading(false);
|
||||
|
||||
if (response.success) {
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage(response.error || "Unknown error updating settings");
|
||||
}
|
||||
},
|
||||
|
||||
onGenerateResponse: (response) => {
|
||||
console.log("Generate response:", response);
|
||||
setIsLoading(false);
|
||||
|
||||
if (response.success && response.image) {
|
||||
setGeneratedImageUrl(base64ToImageUrl(response.image));
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage(response.error || "Unknown error generating image");
|
||||
}
|
||||
},
|
||||
|
||||
onStatusResponse: (response) => {
|
||||
console.log("Status response:", response);
|
||||
|
||||
if (response.success) {
|
||||
setIsModelInitialized(response.model_initialized);
|
||||
|
||||
if (response.current_settings) {
|
||||
// Update UI with current settings
|
||||
setModelPath(response.current_settings.sdxl_checkpoint_path || "");
|
||||
setLoraPath(response.current_settings.lora_path || "");
|
||||
|
||||
// Update generation parameters if they exist
|
||||
if (response.current_settings.default_lora_strength !== undefined) {
|
||||
setLoraStrength(response.current_settings.default_lora_strength);
|
||||
}
|
||||
|
||||
if (
|
||||
response.current_settings.default_image_to_image_strength !==
|
||||
undefined
|
||||
) {
|
||||
setImageToImageStrength(
|
||||
response.current_settings.default_image_to_image_strength,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.current_settings.default_steps !== undefined) {
|
||||
setSteps(response.current_settings.default_steps);
|
||||
}
|
||||
|
||||
if (
|
||||
response.current_settings.default_guidance_scale !== undefined
|
||||
) {
|
||||
setGuidanceScale(
|
||||
response.current_settings.default_guidance_scale,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.current_settings.default_width !== undefined) {
|
||||
setWidth(response.current_settings.default_width);
|
||||
}
|
||||
|
||||
if (response.current_settings.default_height !== undefined) {
|
||||
setHeight(response.current_settings.default_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
clientRef.current = client;
|
||||
|
||||
// Connect to the server
|
||||
client.connect().catch((error) => {
|
||||
console.error("Error connecting to server:", error);
|
||||
setErrorMessage(`Failed to connect to server: ${error.message}`);
|
||||
});
|
||||
|
||||
return () => {
|
||||
client.disconnect();
|
||||
clientRef.current = null;
|
||||
};
|
||||
}, [serverUrl]);
|
||||
|
||||
// Handle source image selection
|
||||
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSourceImage(file);
|
||||
setSourceImageUrl(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle setup submission
|
||||
const handleSetup = async () => {
|
||||
if (!clientRef.current) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
await clientRef.current.setup(modelPath, loraPath || undefined);
|
||||
} catch (error) {
|
||||
console.error("Error setting up model:", error);
|
||||
setErrorMessage(`Error setting up model: ${(error as Error).message}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle update settings
|
||||
const handleUpdateSettings = async () => {
|
||||
if (!clientRef.current) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
await clientRef.current.updateSettings({
|
||||
lora_path: loraPath || null,
|
||||
default_lora_strength: loraStrength,
|
||||
default_image_to_image_strength: imageToImageStrength,
|
||||
default_steps: steps,
|
||||
default_guidance_scale: guidanceScale,
|
||||
default_width: width,
|
||||
default_height: height,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating settings:", error);
|
||||
setErrorMessage(`Error updating settings: ${(error as Error).message}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle generation
|
||||
const handleGenerate = async () => {
|
||||
if (!clientRef.current || !sourceImage) {
|
||||
setErrorMessage("Please select a source image first");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
setGeneratedImageUrl(null);
|
||||
|
||||
try {
|
||||
const base64Image = await fileToBase64(sourceImage);
|
||||
|
||||
await clientRef.current.generate({
|
||||
image: base64Image,
|
||||
prompt,
|
||||
lora_strength: loraStrength,
|
||||
image_to_image_strength: imageToImageStrength,
|
||||
num_inference_steps: steps,
|
||||
guidance_scale: guidanceScale,
|
||||
generated_image_width: width,
|
||||
generated_image_height: height,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating image:", error);
|
||||
setErrorMessage(`Error generating image: ${(error as Error).message}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl p-6">
|
||||
<h1 className="mb-6 text-2xl font-bold">SDXL Image Generator</h1>
|
||||
|
||||
{/* Connection and Setup */}
|
||||
<div className="mb-6 rounded-lg bg-gray-100 p-4">
|
||||
<h2 className="mb-2 text-xl font-semibold">Server Connection</h2>
|
||||
<div className="mb-4 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
className="flex-grow rounded border p-2"
|
||||
placeholder="WebSocket URL"
|
||||
/>
|
||||
<button
|
||||
onClick={() => clientRef.current?.connect()}
|
||||
disabled={isConnected}
|
||||
className="rounded bg-blue-500 px-4 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<button
|
||||
onClick={() => clientRef.current?.disconnect()}
|
||||
disabled={!isConnected}
|
||||
className="rounded bg-red-500 px-4 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-center">
|
||||
<span className="mr-2">Status:</span>
|
||||
<span
|
||||
className={`rounded px-2 py-1 text-white ${isConnected ? "bg-green-500" : "bg-red-500"}`}
|
||||
>
|
||||
{isConnected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
<span className="ml-4 mr-2">Model:</span>
|
||||
<span
|
||||
className={`rounded px-2 py-1 text-white ${isModelInitialized ? "bg-green-500" : "bg-yellow-500"}`}
|
||||
>
|
||||
{isModelInitialized ? "Initialized" : "Not Initialized"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Setup */}
|
||||
<div className="mb-6 rounded-lg bg-gray-100 p-4">
|
||||
<h2 className="mb-2 text-xl font-semibold">Model Setup</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block">SDXL Checkpoint Path:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelPath}
|
||||
onChange={(e) => setModelPath(e.target.value)}
|
||||
className="w-full rounded border p-2"
|
||||
placeholder="/path/to/sdxl/model"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block">LoRA Path (optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
value={loraPath}
|
||||
onChange={(e) => setLoraPath(e.target.value)}
|
||||
className="w-full rounded border p-2"
|
||||
placeholder="/path/to/lora"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSetup}
|
||||
disabled={!isConnected || !modelPath || isLoading}
|
||||
className="rounded bg-green-500 px-4 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
Initialize Model
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdateSettings}
|
||||
disabled={!isConnected || !isModelInitialized || isLoading}
|
||||
className="rounded bg-purple-500 px-4 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
Update Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generation Parameters */}
|
||||
<div className="mb-6 rounded-lg bg-gray-100 p-4">
|
||||
<h2 className="mb-2 text-xl font-semibold">Generation Parameters</h2>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1 block">Prompt:</label>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="h-20 w-full rounded border p-2"
|
||||
placeholder="Enter your prompt..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">Source Image:</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="mb-2"
|
||||
/>
|
||||
{sourceImageUrl && (
|
||||
<img
|
||||
src={sourceImageUrl}
|
||||
alt="Source"
|
||||
className="max-h-40 rounded border"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">
|
||||
LoRA Strength: {loraStrength.toFixed(2)}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={loraStrength}
|
||||
onChange={(e) => setLoraStrength(parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">
|
||||
Image-to-Image Strength: {imageToImageStrength.toFixed(2)}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={imageToImageStrength}
|
||||
onChange={(e) =>
|
||||
setImageToImageStrength(parseFloat(e.target.value))
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">Steps: {steps}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="20"
|
||||
step="1"
|
||||
value={steps}
|
||||
onChange={(e) => setSteps(parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">
|
||||
Guidance Scale: {guidanceScale.toFixed(2)}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.1"
|
||||
value={guidanceScale}
|
||||
onChange={(e) => setGuidanceScale(parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">Width:</label>
|
||||
<select
|
||||
value={width}
|
||||
onChange={(e) => setWidth(parseInt(e.target.value))}
|
||||
className="w-full rounded border p-2"
|
||||
>
|
||||
<option value="512">512</option>
|
||||
<option value="768">768</option>
|
||||
<option value="1024">1024</option>
|
||||
<option value="1280">1280</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block">Height:</label>
|
||||
<select
|
||||
value={height}
|
||||
onChange={(e) => setHeight(parseInt(e.target.value))}
|
||||
className="w-full rounded border p-2"
|
||||
>
|
||||
<option value="512">512</option>
|
||||
<option value="768">768</option>
|
||||
<option value="1024">1024</option>
|
||||
<option value="1280">1280</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={
|
||||
!isConnected || !isModelInitialized || !sourceImage || isLoading
|
||||
}
|
||||
className="mt-4 rounded bg-blue-500 px-6 py-2 text-white disabled:bg-gray-400"
|
||||
>
|
||||
Generate Image
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress and Results */}
|
||||
{isLoading && progress && (
|
||||
<div className="mb-6 rounded-lg bg-gray-100 p-4">
|
||||
<h2 className="mb-2 text-xl font-semibold">Progress</h2>
|
||||
<div className="mb-2">
|
||||
<div className="mb-1 flex justify-between">
|
||||
<span>{progress.stage}</span>
|
||||
<span>{progress.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2.5 w-full rounded-full bg-gray-300">
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-600"
|
||||
style={{ width: `${progress.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
{progress.file && (
|
||||
<div className="mt-1 text-sm text-gray-600">{progress.file}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<div className="mb-6 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||
<p>{errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generatedImageUrl && (
|
||||
<div className="mb-6 rounded-lg bg-gray-100 p-4">
|
||||
<h2 className="mb-2 text-xl font-semibold">Generated Image</h2>
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={generatedImageUrl}
|
||||
alt="Generated"
|
||||
className="max-w-full rounded border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -42,6 +42,12 @@ export const DialogAddImage = ({
|
||||
closeCallback();
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (currFile) {
|
||||
onAddImage(currFile);
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
@@ -52,7 +58,7 @@ export const DialogAddImage = ({
|
||||
"w-full max-w-2xl",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="text-2xl font-bold">Upload Image</DialogTitle>
|
||||
<DialogTitle className="text-3xl font-bold">Upload Image</DialogTitle>
|
||||
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
|
||||
<FileUploader
|
||||
title=""
|
||||
@@ -60,10 +66,6 @@ export const DialogAddImage = ({
|
||||
file={currFile}
|
||||
setFile={(file: File | null) => {
|
||||
setAssetFile(file);
|
||||
if (file) {
|
||||
onAddImage(file);
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{currFile && (
|
||||
@@ -82,6 +84,9 @@ export const DialogAddImage = ({
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEnter} disabled={currFile === null}>
|
||||
Enter
|
||||
</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const DialogEditText = ({
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
<DialogPanel className={twMerge(paperWrapperStyles, dialogPanelStyles)}>
|
||||
<DialogTitle className="text-2xl font-bold">Edit Text</DialogTitle>
|
||||
<DialogTitle className="text-3xl font-bold">Edit Text</DialogTitle>
|
||||
<TextEditor
|
||||
text={text}
|
||||
formatData={textFormatData}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DragAndDropZone = ({ file, fileTypes }: Props) => {
|
||||
|
||||
// const fileName = file && getFileName(file).toUpperCase();
|
||||
const wrapperClassName = twMerge(
|
||||
"group cursor-pointer px-6 py-12 bg-ui-controls hover:bg-white/25 transition-colors duration-150 ease-in-out rounded-lg",
|
||||
"group cursor-pointer px-6 py-12 bg-gray-100 hover:bg-gray-200/60 transition-colors duration-150 ease-in-out",
|
||||
!file && "flex flex-col items-center justify-center gap-",
|
||||
file && "flex items-center gap-2 py-6",
|
||||
// "rounded-lg border-2 border-dashed border-ui-border",
|
||||
|
||||
@@ -44,12 +44,12 @@ export const ToolbarButton = ({
|
||||
}
|
||||
|
||||
const mergedButtonClasses = twMerge(
|
||||
"rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2",
|
||||
children ? "w-fit flex items-center gap-2.5 text-nowrap" : "size-10",
|
||||
active && "bg-primary/30 border-2 border-primary hover:bg-primary/30",
|
||||
"rounded-lg py-2 px-3 hover:bg-gray-200/50 transition-all duration-100",
|
||||
children ? "w-fit flex items-center gap-2 text-nowrap" : "size-10",
|
||||
active && "pointer-events-none text-primary ",
|
||||
disabled && "pointer-events-none text-secondary-300",
|
||||
buttonProps.prominent &&
|
||||
"border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
|
||||
"highlight-button border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
|
||||
customButtonClassNames,
|
||||
);
|
||||
|
||||
@@ -57,7 +57,6 @@ export const ToolbarButton = ({
|
||||
<button
|
||||
className={mergedButtonClasses}
|
||||
disabled={disabled}
|
||||
autoFocus={false}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -74,11 +73,7 @@ export const ToolbarButton = ({
|
||||
</button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip position="right" tip={tooltip}>
|
||||
{Button}
|
||||
</Tooltip>
|
||||
);
|
||||
return <Tooltip tip={tooltip}>{Button}</Tooltip>;
|
||||
}
|
||||
return Button;
|
||||
};
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import { Fragment, MouseEventHandler } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowRotateLeft,
|
||||
faArrowRotateRight,
|
||||
faCircle,
|
||||
faEraser,
|
||||
faFilePlus,
|
||||
faFilm,
|
||||
faFloppyDisk,
|
||||
faImage,
|
||||
faLocationArrow,
|
||||
faPaintbrush,
|
||||
faShapes,
|
||||
faSquare,
|
||||
faMagicWandSparkles,
|
||||
faText,
|
||||
faTriangle,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Fragment, MouseEventHandler } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ToolbarButton } from "../ToolbarButton";
|
||||
|
||||
// style and constants
|
||||
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
|
||||
import { ToolbarMainButtonNames } from "./enum";
|
||||
import { dispatchUiEvents, uiEvents } from "~/signals/uiEvents";
|
||||
import { paintColor } from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
|
||||
import { PaintModeMenu } from "~/components/ui/ToolbarMain/PaintModeMenu";
|
||||
import { EraseModeMenu } from "~/components/ui/ToolbarMain/EraseModeMenu";
|
||||
import { ButtonPreviewAndRender } from "./ButtonPreviewAndRender";
|
||||
|
||||
export const ToolbarMain = ({
|
||||
disabled = false,
|
||||
@@ -45,14 +38,13 @@ export const ToolbarMain = ({
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-fit flex-col items-center gap-y-2",
|
||||
"flex w-fit items-center divide-x divide-ui-border",
|
||||
paperWrapperStyles,
|
||||
"glass",
|
||||
disabled &&
|
||||
"pointer-events-none cursor-default bg-ui-border shadow-md",
|
||||
"pointer-events-none cursor-default bg-ui-border shadow-md",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2 pr-2">
|
||||
<ToolbarButton
|
||||
icon={faLocationArrow}
|
||||
iconProps={{ className: "fa-flip-horizontal" }}
|
||||
@@ -64,152 +56,37 @@ export const ToolbarMain = ({
|
||||
buttonProps={buttonProps.ADD_TEXT}
|
||||
tooltip="Add Text"
|
||||
/>
|
||||
|
||||
<Popover className="relative">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<PopoverButton as={Fragment}>
|
||||
<button
|
||||
data-tooltip="Add Shapes"
|
||||
className={twMerge(
|
||||
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
|
||||
toolTipStyles.base,
|
||||
toolTipStyles.right,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShapes} />
|
||||
</button>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="right"
|
||||
className={twMerge(
|
||||
"flex flex-row [--anchor-gap:12px]",
|
||||
paperWrapperStyles,
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
icon={faSquare}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_SQUARE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_SQUARE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faCircle}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_CIRCLE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_CIRCLE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faTriangle}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_TRIANGLE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_TRIANGLE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</PopoverPanel>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
<Popover className="relative">
|
||||
<PopoverButton as={Fragment}>
|
||||
<button
|
||||
data-tooltip="Add Media"
|
||||
data-tooltip="Add..."
|
||||
className={twMerge(
|
||||
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
|
||||
toolTipStyles.base,
|
||||
toolTipStyles.right,
|
||||
"size-10 rounded-lg p-2 hover:bg-gray-200/50",
|
||||
toolTipStyles,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilePlus} />
|
||||
</button>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="right"
|
||||
anchor="bottom"
|
||||
className={twMerge(
|
||||
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
|
||||
"flex [--anchor-gap:12px]",
|
||||
"flex flex-col [--anchor-gap:16px]",
|
||||
paperWrapperStyles,
|
||||
)}
|
||||
>
|
||||
<ToolbarButton icon={faImage} buttonProps={buttonProps.ADD_IMAGE}>
|
||||
<span className="text-[16px]">Add Image</span>
|
||||
Add Image
|
||||
</ToolbarButton>
|
||||
{/* <ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
|
||||
<ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
|
||||
Add Video
|
||||
</ToolbarButton> */}
|
||||
</ToolbarButton>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
||||
</div>
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{ /* Conditionally show paint menu on mode selection */}
|
||||
{buttonProps.PAINT.active ? (
|
||||
<div className="relative">
|
||||
<PaintModeMenu
|
||||
color={paintColor.value}
|
||||
onChange={dispatchUiEvents.toolbarMain.setPaintColor}
|
||||
faIcon={faPaintbrush}
|
||||
borderStyle="border-2 bg-primary/30 border-2 border-primary hover:bg-primary/30 text-white"
|
||||
showBar={false}
|
||||
staticIconColor="white"
|
||||
streamChanges
|
||||
defaultOpen={true}
|
||||
anchor="right"
|
||||
anchorGap={12}
|
||||
closeOnMouseLeave={true}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute -bottom-1.5 -right-1.5 h-[18px] w-[18px] rounded-full border border-gray-400"
|
||||
style={{ backgroundColor: paintColor.value }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ToolbarButton
|
||||
icon={faPaintbrush}
|
||||
buttonProps={buttonProps.PAINT}
|
||||
tooltip="Paint Brush"
|
||||
/>
|
||||
)}
|
||||
|
||||
{ /* Conditionally show eraser menu on mode selection */}
|
||||
{buttonProps.ERASER.active ? (
|
||||
<EraseModeMenu
|
||||
faIcon={faEraser}
|
||||
borderStyle="border-2 bg-primary/30 border-2 border-primary hover:bg-primary/30 text-white"
|
||||
defaultOpen={true}
|
||||
anchor="right"
|
||||
anchorGap={12}
|
||||
closeOnMouseLeave={true}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ToolbarButton
|
||||
icon={faEraser}
|
||||
buttonProps={buttonProps.ERASER}
|
||||
tooltip="Eraser"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<ToolbarButton
|
||||
icon={faArrowRotateLeft}
|
||||
buttonProps={buttonProps.UNDO}
|
||||
@@ -220,11 +97,15 @@ export const ToolbarMain = ({
|
||||
buttonProps={buttonProps.REDO}
|
||||
tooltip="Redo"
|
||||
/>
|
||||
{/* <ToolbarButton
|
||||
<ToolbarButton
|
||||
icon={faFloppyDisk}
|
||||
buttonProps={buttonProps.SAVE}
|
||||
tooltip="Save"
|
||||
/> */}
|
||||
/>
|
||||
<ButtonPreviewAndRender
|
||||
buttonPreviewProps={buttonProps.PREVIEW}
|
||||
buttonRenderProps={buttonProps.DOWNLOAD}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -4,11 +4,6 @@ export enum ToolbarMainButtonNames {
|
||||
ADD_TEXT = "ADD_TEXT",
|
||||
ADD_IMAGE = "ADD_IMAGE",
|
||||
ADD_VIDEO = "ADD_VIDEO",
|
||||
ADD_CIRCLE = "ADD_CIRCLE",
|
||||
ADD_TRIANGLE = "ADD_TRIANGLE",
|
||||
ADD_SQUARE = "ADD_SQUARE",
|
||||
PAINT = "PAINT",
|
||||
ERASER = "ERASER",
|
||||
AI_STYLIZE = "AI_STYLIZE",
|
||||
UNDO = "UNDO",
|
||||
REDO = "REDO",
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
faLockKeyholeOpen,
|
||||
faLockKeyhole,
|
||||
faUnlockKeyhole,
|
||||
faPalette,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarButtonProps,
|
||||
} from "~/components/features/ToolbarButton";
|
||||
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { ToolbarNodeButtonNames } from "./enums";
|
||||
import { ToolbarNodeButtonData } from "./data";
|
||||
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
|
||||
|
||||
export interface ToolbarNodeProps {
|
||||
disabled?: boolean;
|
||||
@@ -26,8 +24,6 @@ export interface ToolbarNodeProps {
|
||||
buttonsProps?: {
|
||||
[key in ToolbarNodeButtonNames]: ToolbarButtonProps;
|
||||
};
|
||||
color?: string;
|
||||
onColorChange?: (color: string) => void;
|
||||
}
|
||||
export const ToolbarNode = ({
|
||||
disabled,
|
||||
@@ -35,8 +31,6 @@ export const ToolbarNode = ({
|
||||
lockDisabled,
|
||||
onLockClicked,
|
||||
buttonsProps,
|
||||
color = "#000000",
|
||||
onColorChange = () => { },
|
||||
}: ToolbarNodeProps) => {
|
||||
const handleOnLockClicked: React.MouseEventHandler<HTMLButtonElement> = (
|
||||
e,
|
||||
@@ -51,7 +45,7 @@ export const ToolbarNode = ({
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
disabled && "pointer-events-none cursor-default bg-ui-border shadow-md",
|
||||
"flex flex-col gap-2 transition",
|
||||
"flex gap-2 transition",
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
@@ -59,7 +53,7 @@ export const ToolbarNode = ({
|
||||
className: twMerge(
|
||||
locked && "text-primary hover:bg-primary hover:text-white",
|
||||
lockDisabledOrUnknown &&
|
||||
"text-secondary-300 hover:text-secondary-300",
|
||||
"text-secondary-300 hover:text-secondary-300",
|
||||
),
|
||||
disabled: lockDisabledOrUnknown,
|
||||
}}
|
||||
@@ -79,24 +73,14 @@ export const ToolbarNode = ({
|
||||
{ToolbarNodeButtonData.map((buttonDatum, idx) => {
|
||||
const buttonProps = buttonsProps?.[buttonDatum.name];
|
||||
|
||||
if (!buttonProps || buttonProps.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonDatum.name !== ToolbarNodeButtonNames.COLOR) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
icon={buttonDatum.icon}
|
||||
tooltip={buttonDatum.tooltip}
|
||||
key={idx}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ColorPicker color={color} onChange={onColorChange} faIcon={faPalette} borderStyle="" key={idx} />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ToolbarButton
|
||||
icon={buttonDatum.icon}
|
||||
tooltip={buttonDatum.tooltip}
|
||||
key={idx}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
faTrashCan,
|
||||
faVectorSquare,
|
||||
faDownload,
|
||||
faPalette,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export const ToolbarNodeButtonData = [
|
||||
@@ -17,11 +16,6 @@ export const ToolbarNodeButtonData = [
|
||||
icon: faVectorSquare,
|
||||
tooltip: "Move",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.COLOR,
|
||||
icon: faPalette,
|
||||
tooltip: "Color",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.CHROMA,
|
||||
icon: faTransporter,
|
||||
|
||||
@@ -7,5 +7,4 @@ export enum ToolbarNodeButtonNames {
|
||||
MOVE_LAYER_UP = "MOVE_LAYER_UP",
|
||||
MOVE_LAYER_DOWN = "MOVE_LAYER_DOWN",
|
||||
DOWNLOAD = "DOWNLOAD",
|
||||
COLOR = "COLOR",
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
|
||||
export const ToolbarTopLeft = () => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"z-20 mt-2 flex h-fit w-fit items-center gap-1 py-2.5 pl-4",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src="/brand/mira-logo.png"
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="logo"
|
||||
className="h-[30px] select-none pb-1"
|
||||
className="mr-4 h-8"
|
||||
/>
|
||||
{/* <ToolbarButton icon={faPlus}>
|
||||
<ToolbarButton icon={faPlus}>
|
||||
<span className="font-semibold">New Board</span>
|
||||
</ToolbarButton>
|
||||
<ToolbarButton icon={faQuestion}>
|
||||
<span className="font-semibold">Help</span>
|
||||
</ToolbarButton> */}
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { faSave } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toolbarMain } from "~/signals/uiAccess/toolbarMain";
|
||||
import { dispatchers } from "~/signals/uiEvents/toolbarMain";
|
||||
import { ToolbarMainButtonNames } from "../ToolbarMain/enum";
|
||||
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
|
||||
export const ToolbarTopRight = () => {
|
||||
const saveButtonState =
|
||||
toolbarMain.signal.value.buttonStates[ToolbarMainButtonNames.SAVE];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-20 mt-2 flex h-fit w-fit items-center gap-1 p-4 py-2.5",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
icon={faSave}
|
||||
onClick={dispatchers[ToolbarMainButtonNames.SAVE]}
|
||||
disabled={saveButtonState?.disabled}
|
||||
>
|
||||
Save Image
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,18 +9,18 @@ const Footer = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<footer className="relative z-10 bg-[#18181C]">
|
||||
<footer className="border-t border-gray-100 bg-gray-50">
|
||||
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center space-y-10 text-center">
|
||||
<img
|
||||
src="/brand/mira-logo.png"
|
||||
alt="Mira Realtime AI Editor"
|
||||
className="h-[34px] pb-1"
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Board"
|
||||
className="h-9"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate("/signup")}
|
||||
className="text-md group rounded-xl bg-[#2D81FF] px-8 py-4 hover:bg-[#438AF6]"
|
||||
className="group rounded-xl px-8 py-4 text-lg"
|
||||
>
|
||||
<span className="relative flex items-center">
|
||||
Start Creating for Free
|
||||
|
||||
@@ -3,31 +3,26 @@ import { Button } from "../ui";
|
||||
|
||||
const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 z-50 w-full bg-[rgba(16,16,20,0.7)] backdrop-blur-3xl">
|
||||
<div className="mx-auto max-w-[1920px] px-4 sm:px-24 lg:px-32">
|
||||
<div className="flex h-[72px] items-center justify-between">
|
||||
<nav className="fixed top-0 z-50 w-full border-b border-gray-100 bg-white/80 backdrop-blur-md">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-20 items-center justify-between">
|
||||
<a href="/" className="flex items-center">
|
||||
<img
|
||||
src="/brand/mira-logo.png"
|
||||
alt="Mira Realtime AI Editor"
|
||||
className="h-[34px] pb-1"
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Board"
|
||||
className="h-9"
|
||||
/>
|
||||
</a>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className=""
|
||||
onClick={() => navigate("/signup")}
|
||||
<div className="flex items-center space-x-5">
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium !text-gray-600 hover:!text-gray-900"
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden bg-[#2D81FF] hover:bg-[#438AF6] md:block"
|
||||
onClick={() => []}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
Login
|
||||
</a>
|
||||
<Button onClick={() => navigate("/signup")}>Sign Up</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const paperWrapperStyles =
|
||||
"rounded-xl border border-ui-border p-1.5 shadow-lg bg-ui-panel";
|
||||
"rounded-xl border border-ui-border bg-ui-panel p-2 shadow-lg";
|
||||
|
||||
export const transitionTimingStyles = "transition duration-150 ease-in-out";
|
||||
|
||||
export const dialogBackgroundStyles =
|
||||
"fixed inset-0 flex w-screen items-center justify-center bg-black/50";
|
||||
|
||||
export const toolTipStyles = {
|
||||
base: twMerge(
|
||||
"relative",
|
||||
"after:absolute after:z-40",
|
||||
"after:content-[attr(data-tooltip)] after:text-black after:text-nowrap after:text-sm after:font-medium after:text-white/80 after:glass",
|
||||
"after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1 after:shadow-xl",
|
||||
"after:hidden hover:after:block",
|
||||
),
|
||||
top: twMerge(
|
||||
"after:left-1/2 after:bottom-full after:-translate-x-1/2 after:mb-3.5",
|
||||
),
|
||||
bottom: twMerge(
|
||||
"after:left-1/2 after:top-full after:-translate-x-1/2 after:mt-3.5",
|
||||
),
|
||||
left: twMerge(
|
||||
"after:top-1/2 after:right-full after:-translate-y-1/2 after:mr-3.5",
|
||||
),
|
||||
right: twMerge(
|
||||
"after:top-1/2 after:left-full after:-translate-y-1/2 after:ml-3.5",
|
||||
),
|
||||
};
|
||||
export const toolTipStyles = twMerge(
|
||||
"relative",
|
||||
"before:w-0 before:h-0 before:absolute",
|
||||
"before:left-1/2 before:bottom-full before:mb-[1px] before:-translate-x-1/2 before:z-50",
|
||||
"before:border-l-8 before:border-l-transparent",
|
||||
"before:border-r-8 before:border-r-transparent",
|
||||
"before:border-t-8 before:border-t-white",
|
||||
"after:absolute after:left-1/2 after:bottom-full after:-translate-x-1/2 after:z-40",
|
||||
"after:content-[attr(data-tooltip)] after:text-black after:text-nowrap",
|
||||
"after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1.5 after:mb-2 after:shadow-xl",
|
||||
"after:hidden before:hidden hover:after:block hover:before:block",
|
||||
);
|
||||
|
||||
export const dialogPanelStyles = twMerge(
|
||||
"flex max-w-5xl flex-col justify-between gap-4 p-6 bg-[#1C1C20]",
|
||||
"flex max-w-5xl flex-col justify-between gap-4 p-6",
|
||||
);
|
||||
|
||||
@@ -31,14 +31,14 @@ export const Button = ({
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600";
|
||||
}
|
||||
case "secondary": {
|
||||
return "bg-ui-controls hover:bg-ui-controls/80 text-white focus-visible:outline-secondary";
|
||||
return "bg-gray-600 hover:bg-gray-500 text-white focus-visible:outline-secondary";
|
||||
}
|
||||
case "action": {
|
||||
return " bg-action hover:bg-action-500 text-white focus-visible:outline-action";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-primary hover:bg-primary-600 text-white focus-visible:outline-primary-600";
|
||||
return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,25 +29,25 @@ export const Combobox = ({ options, value, onChange }: ComboBoxInterface) => {
|
||||
onChange={onChange}
|
||||
onClose={() => setQuery("")}
|
||||
>
|
||||
<div className="relative flex h-10 rounded-md border border-ui-border">
|
||||
<div className="relative flex h-10 rounded-md border">
|
||||
<ComboboxInput
|
||||
className="rounded-lg px-3"
|
||||
className="rounded-md"
|
||||
displayValue={(font: string) => font}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center px-3 transition-colors duration-300 hover:text-primary">
|
||||
<FontAwesomeIcon icon={faChevronDown} className="size-4" />
|
||||
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center hover:text-primary">
|
||||
<FontAwesomeIcon icon={faChevronDown} className="size-4 p-2" />
|
||||
</ComboboxButton>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
anchor="bottom"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border border-ui-border empty:invisible"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border empty:invisible"
|
||||
>
|
||||
{filteredOptions.map((option, idx) => (
|
||||
<ComboboxOption
|
||||
key={idx}
|
||||
value={option}
|
||||
className="bg-ui-controls cursor-pointer px-3 py-2 data-[focus]:bg-gray-600 data-[selected]:font-medium"
|
||||
className="cursor-pointer bg-white p-2 data-[focus]:bg-primary-200 data-[selected]:font-medium"
|
||||
>
|
||||
{option}
|
||||
</ComboboxOption>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import React, { useState, useRef, useEffect, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "./Button";
|
||||
import { faMinus, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface SliderProps {
|
||||
min: number;
|
||||
@@ -14,9 +12,6 @@ interface SliderProps {
|
||||
showTooltip?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
suffix?: string;
|
||||
innerLabel?: string;
|
||||
showDecrement?: boolean,
|
||||
showIncrement?: boolean,
|
||||
}
|
||||
|
||||
export const Slider = ({
|
||||
@@ -29,9 +24,6 @@ export const Slider = ({
|
||||
showTooltip,
|
||||
tooltipContent,
|
||||
suffix,
|
||||
innerLabel,
|
||||
showDecrement,
|
||||
showIncrement
|
||||
}: SliderProps) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -42,19 +34,15 @@ export const Slider = ({
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const setClampedValue = (value: number) => {
|
||||
const roundedValue = Math.round(value / step) * step;
|
||||
const clampedValue = Math.min(Math.max(roundedValue, min), max);
|
||||
setLocalValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
}
|
||||
|
||||
const updatePosition = (x: number) => {
|
||||
if (!sliderRef.current) return;
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const fraction = (x - rect.left) / rect.width;
|
||||
const newValue = fraction * (max - min) + min;
|
||||
setClampedValue(newValue);
|
||||
const roundedValue = Math.round(newValue / step) * step;
|
||||
const clampedValue = Math.min(Math.max(roundedValue, min), max);
|
||||
setLocalValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -91,75 +79,34 @@ export const Slider = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
setClampedValue(localValue - step);
|
||||
}
|
||||
|
||||
const handleIncrement = () => {
|
||||
setClampedValue(localValue + step);
|
||||
}
|
||||
|
||||
const percentage = ((localValue - min) / (max - min)) * 100;
|
||||
const displayValue =
|
||||
localValue.toFixed(step.toString().split(".")[1]?.length || 0) +
|
||||
(suffix || "");
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
{showDecrement && (
|
||||
<Button
|
||||
icon={faMinus}
|
||||
className="size-6 text-white/80 bg-transparent rounded-full hover:bg-white/10 focus-visible:outline-primary active:bg-primary/30 my-auto mr-1"
|
||||
onClick={handleDecrement}
|
||||
/>
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"relative h-2.5 w-full cursor-pointer rounded-full bg-gray-500",
|
||||
className,
|
||||
)}
|
||||
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className="absolute h-2.5 rounded-full bg-primary"
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"glass group relative h-7 w-full cursor-pointer overflow-hidden rounded-lg border border-ui-border",
|
||||
isDragging && "!bg-ui-controls/90",
|
||||
className,
|
||||
"absolute -top-1 h-[18px] w-[18px] -translate-x-1/2 transform rounded-full bg-gray-200 transition-colors",
|
||||
isDragging ? "bg-white" : "bg-gray-300 hover:bg-white",
|
||||
)}
|
||||
style={{ left: `${percentage}%` }}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute h-7 bg-primary/30 transition-colors duration-300 group-hover:bg-primary/50",
|
||||
isDragging && "!bg-primary/50",
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
>
|
||||
{innerLabel && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute top-1/2 ml-2.5 -translate-y-1/2 text-nowrap text-sm font-medium text-white/60 transition-colors duration-300 group-hover:text-white",
|
||||
isDragging && "!text-white",
|
||||
)}
|
||||
>
|
||||
{innerLabel}
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute right-0 top-1/2 mr-1.5 h-3.5 w-0.5 -translate-y-1/2",
|
||||
isDragging ? "bg-white" : "bg-white/50",
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showIncrement && (
|
||||
<Button
|
||||
icon={faPlus}
|
||||
className="size-6 text-white/80 bg-transparent rounded-full hover:bg-white/10 focus-visible:outline-primary active:bg-primary/30 my-auto ml-1"
|
||||
onClick={handleIncrement}
|
||||
/>
|
||||
)}
|
||||
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
{showTooltip && (
|
||||
<Transition
|
||||
as={"div"}
|
||||
|
||||
@@ -15,7 +15,7 @@ const resizeStyles = {
|
||||
horizontal: "resize-x",
|
||||
vertical: "resize-y",
|
||||
undefined: "",
|
||||
};
|
||||
}
|
||||
|
||||
export interface TextareaInterface
|
||||
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
@@ -27,6 +27,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
|
||||
{ className, label, resize = "vertical", id, ...rest }: TextareaInterface,
|
||||
ref,
|
||||
) => {
|
||||
|
||||
const resizeStyle = resizeStyles[resize];
|
||||
|
||||
return (
|
||||
@@ -37,16 +38,17 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
|
||||
ref={ref}
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"rounded-lg border border-ui-border px-4 py-2.5 text-white outline-none focus:outline-none",
|
||||
"border-ui-panel-border bg-ui-controls rounded-lg border px-3 py-2",
|
||||
className,
|
||||
resizeStyle,
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
transition: "outline-color 0.15s ease-in-out",
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
// disableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "#2d81ff";
|
||||
e.currentTarget.style.outlineColor = "#e66462";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// enableHotkeyInput(DomLevels.INPUT);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
faAlignCenter,
|
||||
// faAlignJustify,
|
||||
faAlignJustify,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
@@ -1,46 +1,21 @@
|
||||
import {
|
||||
faFont,
|
||||
faHashtag,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faFont, faHashtag } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { HexAlphaColorPicker } from "react-colorful";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
|
||||
export const ColorPicker = ({
|
||||
color: prevColor,
|
||||
onChange,
|
||||
faIcon = faFont,
|
||||
borderStyle,
|
||||
showBar = true,
|
||||
anchor = "bottom",
|
||||
anchorGap = 0,
|
||||
fillBg = false,
|
||||
staticIconColor,
|
||||
streamChanges = false,
|
||||
defaultOpen = false,
|
||||
closeOnMouseLeave = false,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (newColor: string) => void;
|
||||
faIcon?: IconDefinition;
|
||||
borderStyle?: string;
|
||||
showBar?: boolean;
|
||||
anchor?: "top" | "right" | "bottom" | "left";
|
||||
anchorGap?: number;
|
||||
fillBg?: boolean;
|
||||
staticIconColor?: string;
|
||||
streamChanges?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
closeOnMouseLeave?: boolean;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [{ currColor, textInput }, setStates] = useState<{
|
||||
currColor: string;
|
||||
textInput: string;
|
||||
@@ -49,18 +24,13 @@ export const ColorPicker = ({
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
|
||||
const handleMouseLeave = (close: () => void) => {
|
||||
if (closeOnMouseLeave) {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}
|
||||
const handleSelectColor = (newColor: string) => {
|
||||
setStates({ currColor: newColor, textInput: newColor.substring(1) });
|
||||
};
|
||||
const isHexColorCode = (color: string) => {
|
||||
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (isHexColorCode(e.target.value)) {
|
||||
setStates({
|
||||
@@ -74,101 +44,55 @@ export const ColorPicker = ({
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const isHexColorCode = (color: string) => {
|
||||
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
className={twMerge(
|
||||
"flex size-10 items-center gap-1 rounded-lg bg-ui-controls p-2",
|
||||
borderStyle,
|
||||
showBar ? "flex-col" : "justify-center",
|
||||
)}
|
||||
style={fillBg ? { backgroundColor: prevColor } : {}}
|
||||
onMouseEnter={() => defaultOpen && setIsOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faIcon}
|
||||
color={staticIconColor ?? (fillBg ? undefined : prevColor)}
|
||||
/>
|
||||
{showBar && (
|
||||
<span
|
||||
className="h-1 w-full"
|
||||
style={{ backgroundColor: prevColor }}
|
||||
<PopoverButton className="flex size-10 flex-col items-center gap-1 rounded-md border p-2">
|
||||
<FontAwesomeIcon icon={faFont} />
|
||||
<span className="h-1 w-full" style={{ backgroundColor: prevColor }} />
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="bottom"
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2",
|
||||
)}
|
||||
>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<HexColorPicker color={currColor} onChange={handleSelectColor} />
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
)}
|
||||
</PopoverButton>
|
||||
{(open || isOpen) && (
|
||||
<div onMouseLeave={() => handleMouseLeave(close)}>
|
||||
<PopoverPanel
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--anchor-gap": `${anchorGap}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexAlphaColorPicker
|
||||
className="overflow-hidden"
|
||||
color={currColor}
|
||||
onChange={(color) => {
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
{!closeOnMouseLeave && (
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStates({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(currColor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -151,11 +151,7 @@ export const TextEditor = ({
|
||||
onChangeTextDecoration={onChangeTextDecoration}
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
className="bg-white"
|
||||
{...unionedTextAreaProps}
|
||||
ref={textAreaRef}
|
||||
/>
|
||||
<Textarea {...unionedTextAreaProps} ref={textAreaRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const buttonGroupButton =
|
||||
"size-10 rounded-none bg-ui-controls text-white/80 hover:bg-white/30 data-[selected=true]:text-white data-[selected=true]:bg-primary/30 disabled:hover:bg-transparent disabled:text-gray-400 border border-ui-border";
|
||||
"size-10 rounded-none bg-white text-black hover:bg-gray-200 data-[selected=true]:text-primary-400 disabled:hover:bg-transparent disabled:text-gray-400";
|
||||
export const buttonGroupGrouper =
|
||||
"flex divide-x overflow-hidden rounded-md border border-ui-border";
|
||||
"flex divide-x overflow-hidden rounded-md border";
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
faFont,
|
||||
faHashtag,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, paintBrushSize, setPaintBrushSize } from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
import { eraseBrushSize, setEraseBrushSize } from "~/signals/uiEvents/toolbarMain/eraseMode";
|
||||
|
||||
export const EraseModeMenu = ({
|
||||
faIcon = faFont,
|
||||
borderStyle,
|
||||
anchor = "bottom",
|
||||
anchorGap = 0,
|
||||
defaultOpen = false,
|
||||
closeOnMouseLeave = false,
|
||||
}: {
|
||||
faIcon?: IconDefinition;
|
||||
borderStyle?: string;
|
||||
anchor?: "top" | "right" | "bottom" | "left";
|
||||
anchorGap?: number;
|
||||
defaultOpen?: boolean;
|
||||
closeOnMouseLeave?: boolean;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
|
||||
const handleMouseLeave = (close: () => void) => {
|
||||
if (closeOnMouseLeave) {
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
// Rerender the component when these signals change:
|
||||
useSignals();
|
||||
const brushSize = eraseBrushSize.value;
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
className={twMerge(
|
||||
"flex size-10 items-center justify-center gap-1 rounded-lg bg-ui-controls p-2",
|
||||
borderStyle,
|
||||
)}
|
||||
onMouseEnter={() => defaultOpen && setIsOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faIcon}
|
||||
/>
|
||||
</PopoverButton>
|
||||
{(open || isOpen) && (
|
||||
<div onMouseLeave={() => handleMouseLeave(close)}>
|
||||
<PopoverPanel
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden w-[198px]",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--anchor-gap": `${anchorGap}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
static
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<p className="text-white font-medium text-sm w-full justify-start">Eraser Size:</p>
|
||||
<Slider
|
||||
min={BRUSH_MIN_SIZE}
|
||||
max={BRUSH_MAX_SIZE}
|
||||
value={brushSize}
|
||||
onChange={setEraseBrushSize}
|
||||
step={1}
|
||||
innerLabel={brushSize.toString()}
|
||||
showDecrement
|
||||
showIncrement
|
||||
/>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
import {
|
||||
faFont,
|
||||
faHashtag,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { HexAlphaColorPicker } from "react-colorful";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, paintBrushSize, setPaintBrushSize } from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
|
||||
export const PaintModeMenu = ({
|
||||
color: prevColor,
|
||||
onChange,
|
||||
faIcon = faFont,
|
||||
borderStyle,
|
||||
showBar = true,
|
||||
anchor = "bottom",
|
||||
anchorGap = 0,
|
||||
fillBg = false,
|
||||
staticIconColor,
|
||||
streamChanges = false,
|
||||
defaultOpen = false,
|
||||
closeOnMouseLeave = false,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (newColor: string) => void;
|
||||
faIcon?: IconDefinition;
|
||||
borderStyle?: string;
|
||||
showBar?: boolean;
|
||||
anchor?: "top" | "right" | "bottom" | "left";
|
||||
anchorGap?: number;
|
||||
fillBg?: boolean;
|
||||
staticIconColor?: string;
|
||||
streamChanges?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
closeOnMouseLeave?: boolean;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [{ currColor, textInput }, setStates] = useState<{
|
||||
currColor: string;
|
||||
textInput: string;
|
||||
}>({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
|
||||
const handleMouseLeave = (close: () => void) => {
|
||||
if (closeOnMouseLeave) {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (isHexColorCode(e.target.value)) {
|
||||
setStates({
|
||||
currColor: `#${e.target.value}`,
|
||||
textInput: e.target.value,
|
||||
});
|
||||
} else {
|
||||
setStates((curr) => ({
|
||||
...curr,
|
||||
textInput: e.target.value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const isHexColorCode = (color: string) => {
|
||||
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
// Rerender the component when these signals change:
|
||||
useSignals();
|
||||
const brushSize = paintBrushSize.value;
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
className={twMerge(
|
||||
"flex size-10 items-center gap-1 rounded-lg bg-ui-controls p-2",
|
||||
borderStyle,
|
||||
showBar ? "flex-col" : "justify-center",
|
||||
)}
|
||||
style={fillBg ? { backgroundColor: prevColor } : {}}
|
||||
onMouseEnter={() => defaultOpen && setIsOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faIcon}
|
||||
color={staticIconColor ?? (fillBg ? undefined : prevColor)}
|
||||
/>
|
||||
{showBar && (
|
||||
<span
|
||||
className="h-1 w-full"
|
||||
style={{ backgroundColor: prevColor }}
|
||||
/>
|
||||
)}
|
||||
</PopoverButton>
|
||||
{(open || isOpen) && (
|
||||
<div onMouseLeave={() => handleMouseLeave(close)}>
|
||||
<PopoverPanel
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--anchor-gap": `${anchorGap}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexAlphaColorPicker
|
||||
className="overflow-hidden"
|
||||
color={currColor}
|
||||
onChange={(color) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
<div className="flex flex-col w-full mt-2 gap-2">
|
||||
<p className="text-white font-medium text-sm w-full justify-start">Brush Size:</p>
|
||||
<Slider
|
||||
min={BRUSH_MIN_SIZE}
|
||||
max={BRUSH_MAX_SIZE}
|
||||
value={brushSize}
|
||||
onChange={setPaintBrushSize}
|
||||
step={1}
|
||||
innerLabel={brushSize.toString()}
|
||||
showDecrement
|
||||
showIncrement
|
||||
/>
|
||||
</div>
|
||||
{!streamChanges && (
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStates({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -7,18 +7,15 @@ export const Tooltip = ({
|
||||
tip,
|
||||
children,
|
||||
forceShow,
|
||||
position = "top",
|
||||
}: {
|
||||
tip: string;
|
||||
children: ReactElement<any, string | JSXElementConstructor<any>>;
|
||||
forceShow?: boolean;
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
}) => {
|
||||
const clonedChildren = cloneElement(children, {
|
||||
"data-tooltip": tip,
|
||||
className: twMerge(
|
||||
toolTipStyles.base,
|
||||
toolTipStyles[position],
|
||||
toolTipStyles,
|
||||
forceShow && "after:block before:block",
|
||||
children.props.className,
|
||||
),
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useIsMobile = () => {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent,
|
||||
);
|
||||
};
|
||||
|
||||
setIsMobile(checkMobile());
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
|
||||
import { isToolbarMode } from "~/signals/uiEvents/toolbarMain/toolbarMain";
|
||||
|
||||
interface ToolbarMainSignalInterface {
|
||||
disabled: boolean;
|
||||
@@ -43,17 +42,6 @@ export const toolbarMain = {
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
setMode(mode: ToolbarMainButtonNames) {
|
||||
if (!isToolbarMode(mode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeButtonState(ToolbarMainButtonNames.SELECT, { active: false });
|
||||
this.changeButtonState(ToolbarMainButtonNames.ERASER, { active: false });
|
||||
this.changeButtonState(ToolbarMainButtonNames.PAINT, { active: false });
|
||||
this.changeButtonState(mode, { active: true });
|
||||
}
|
||||
};
|
||||
|
||||
function initButtonStates() {
|
||||
|
||||
@@ -10,13 +10,12 @@ type ButtonStates = {
|
||||
};
|
||||
};
|
||||
export interface ContextualToolbarProps extends ContextualUi {
|
||||
knodeIds: string[];
|
||||
knodeIds: number[];
|
||||
disabled: boolean;
|
||||
downloadUrl?: string;
|
||||
locked: boolean | "unknown";
|
||||
lockDisabled: boolean;
|
||||
buttonStates: ButtonStates;
|
||||
color: string;
|
||||
}
|
||||
interface PartialContextualToolbarProps
|
||||
extends Partial<Omit<ContextualToolbarProps, "buttonStates">> {
|
||||
@@ -33,7 +32,6 @@ const toolbarNodeSignal = signal<ContextualToolbarProps>({
|
||||
locked: false,
|
||||
lockDisabled: false,
|
||||
buttonStates: initButtonStates(),
|
||||
color: "#000000",
|
||||
});
|
||||
|
||||
export const toolbarNode = {
|
||||
@@ -79,11 +77,6 @@ export const toolbarNode = {
|
||||
hidden: false,
|
||||
active: buttonStates[ButtonNames.TRANSFORM].active,
|
||||
},
|
||||
[ButtonNames.COLOR]: {
|
||||
disabled: locked,
|
||||
hidden: false,
|
||||
active: buttonStates[ButtonNames.COLOR].active,
|
||||
},
|
||||
},
|
||||
locked: locked,
|
||||
};
|
||||
@@ -142,12 +135,6 @@ export const toolbarNode = {
|
||||
},
|
||||
};
|
||||
},
|
||||
setColor(color: string) {
|
||||
toolbarNodeSignal.value = {
|
||||
...toolbarNodeSignal.value,
|
||||
color,
|
||||
}
|
||||
},
|
||||
batchButtonStates({ buttonStates }: { buttonStates: Partial<ButtonStates> }) {
|
||||
const prevButtonStates = toolbarNodeSignal.value.buttonStates;
|
||||
toolbarNodeSignal.value = {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { signal, effect } from "@preact/signals-react";
|
||||
import { ShapeNodeData, TextNodeData } from "~/KonvaApp/types";
|
||||
import { TextNodeData } from "~/KonvaApp/types";
|
||||
|
||||
// ADDING IMAGES
|
||||
const stagedImage = signal<File | null>(null);
|
||||
const addImageToEngine = (image: File) => {
|
||||
stagedImage.value = image;
|
||||
};
|
||||
|
||||
const onGetStagedImage = (callback: (file: File) => void) => {
|
||||
effect(() => {
|
||||
if (stagedImage.value) {
|
||||
@@ -49,30 +48,15 @@ const onAddTextToEngine = (callback: (data: TextNodeData) => void) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Add shapes
|
||||
const stagedShape = signal<ShapeNodeData | null>(null);
|
||||
const addShapeToEngine = (data: ShapeNodeData) => {
|
||||
stagedShape.value = data;
|
||||
}
|
||||
const onAddShapeToEngine = (callback: (data: ShapeNodeData) => void) => {
|
||||
effect(() => {
|
||||
if (stagedShape.value) {
|
||||
callback(stagedShape.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//EXPORTS
|
||||
export const dispatchers = {
|
||||
addImageToEngine,
|
||||
addVideoToEngine,
|
||||
addTextToEngine,
|
||||
addShapeToEngine,
|
||||
};
|
||||
|
||||
export const events = {
|
||||
onGetStagedImage,
|
||||
onGetStagedVideo,
|
||||
onAddTextToEngine,
|
||||
onAddShapeToEngine,
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { effect, signal } from "@preact/signals-react";
|
||||
import { clamp, normalize } from "~/utilities";
|
||||
import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, DEFAULT_PAINT_BRUSH_SIZE, paintBrushSize } from "./paintMode";
|
||||
|
||||
export const eraseBrushSize = signal<number>(DEFAULT_PAINT_BRUSH_SIZE);
|
||||
export const setEraseBrushSize = (brushSize: number) => {
|
||||
eraseBrushSize.value = clamp(brushSize, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE);
|
||||
}
|
||||
|
||||
export const onEraseBrushSizeChanged = (callback: (normalizedSize: number) => void) => {
|
||||
effect(() => {
|
||||
callback(normalize(paintBrushSize.value, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE));
|
||||
});
|
||||
};
|
||||
@@ -3,18 +3,11 @@ import {
|
||||
loadingBarRetryDispatch,
|
||||
loadingBarRetryEventHandler,
|
||||
} from "./loadingBar";
|
||||
import { onEraseBrushSizeChanged, setEraseBrushSize } from "./eraseMode";
|
||||
import { onPaintColorChanged, setPaintBrushSize, setPaintColor } from "./paintMode";
|
||||
export const dispatchers = {
|
||||
...buttonDispatchers,
|
||||
loadingBarRetry: loadingBarRetryDispatch,
|
||||
setPaintColor,
|
||||
setPaintBrushSize,
|
||||
setEraseBrushSize
|
||||
};
|
||||
export const eventsHandlers = {
|
||||
...buttonEventsHandlers,
|
||||
loadingBarRetry: loadingBarRetryEventHandler,
|
||||
onPaintColorChanged,
|
||||
onEraseBrushSizeChanged
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { effect, signal } from "@preact/signals-react";
|
||||
import { clamp, normalize } from "~/utilities";
|
||||
|
||||
export const DEFAULT_PAINT_COLOR: string = "#ff0000";
|
||||
export const paintColor = signal<string>(DEFAULT_PAINT_COLOR);
|
||||
export const setPaintColor = (data: string) => {
|
||||
paintColor.value = data;
|
||||
};
|
||||
|
||||
export const BRUSH_MIN_SIZE: number = 0;
|
||||
export const BRUSH_MAX_SIZE: number = 100;
|
||||
// NOTE: Must be between MIN and MAX sizes - there's no runtime checks
|
||||
export const DEFAULT_PAINT_BRUSH_SIZE: number = 50;
|
||||
export const paintBrushSize = signal<number>(DEFAULT_PAINT_BRUSH_SIZE);
|
||||
export const setPaintBrushSize = (brushSize: number) => {
|
||||
paintBrushSize.value = clamp(brushSize, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE);
|
||||
console.debug(`Paint brush size set to ${paintBrushSize.value}`);
|
||||
}
|
||||
|
||||
export const onPaintColorChanged = (callback: (data: string) => void) => {
|
||||
effect(() => {
|
||||
if (paintColor.value) {
|
||||
callback(paintColor.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const onPaintBrushSizeChanged = (callback: (normalizedSize: number) => void) => {
|
||||
effect(() => {
|
||||
callback(normalize(paintBrushSize.value, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE));
|
||||
});
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { MouseEventHandler } from "react";
|
||||
import { signal, effect, Signal } from "@preact/signals-react";
|
||||
|
||||
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
|
||||
import { AppModes } from "~/KonvaApp/constants";
|
||||
|
||||
const events = Object.values(ToolbarMainButtonNames).reduce(
|
||||
(acc, buttonName) => {
|
||||
@@ -26,35 +25,22 @@ const effectsCleanups = Object.values(ToolbarMainButtonNames).reduce(
|
||||
[key in ToolbarMainButtonNames]: (() => void) | undefined;
|
||||
},
|
||||
);
|
||||
|
||||
export const isToolbarMode = (buttonName: ToolbarMainButtonNames) => {
|
||||
return appModeMap[buttonName] !== undefined;
|
||||
}
|
||||
|
||||
export const appModeMap: Record<string, AppModes> = {
|
||||
[ToolbarMainButtonNames.SELECT]: AppModes.SELECT,
|
||||
[ToolbarMainButtonNames.ERASER]: AppModes.ERASER,
|
||||
[ToolbarMainButtonNames.PAINT]: AppModes.PAINT,
|
||||
}
|
||||
|
||||
const defaultHandler = (buttonName: ToolbarMainButtonNames, callback: MouseEventHandler<HTMLButtonElement>) => {
|
||||
if (effectsCleanups[buttonName]) {
|
||||
effectsCleanups[buttonName]();
|
||||
}
|
||||
effectsCleanups[buttonName] = effect(() => {
|
||||
if (events[buttonName].value) {
|
||||
callback(events[buttonName].value);
|
||||
events[buttonName].value = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const buttonEventsHandlers = Object.values(
|
||||
ToolbarMainButtonNames,
|
||||
).reduce(
|
||||
(acc, buttonName) => {
|
||||
acc[buttonName] = {
|
||||
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => { defaultHandler(buttonName, callback) },
|
||||
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
|
||||
if (effectsCleanups[buttonName]) {
|
||||
effectsCleanups[buttonName]();
|
||||
}
|
||||
effectsCleanups[buttonName] = effect(() => {
|
||||
if (events[buttonName].value) {
|
||||
callback(events[buttonName].value);
|
||||
events[buttonName].value = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -44,11 +44,6 @@ const buttonEventsRecords = Object.values(ButtonNames).reduce(
|
||||
|
||||
const buttonEventsHandlers = Object.values(ButtonNames).reduce(
|
||||
(acc, buttonName) => {
|
||||
// We define custom events/handlers for color changes
|
||||
if (buttonName === ButtonNames.COLOR) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[buttonName] = {
|
||||
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => {
|
||||
const { eventSignal, effectCleanup, lastEventTimestamp } =
|
||||
@@ -116,27 +111,9 @@ const retryEventHandler = (callback: MouseEventHandler<HTMLButtonElement>) => {
|
||||
const retryDispatcher = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
retryEvent.value = e;
|
||||
};
|
||||
|
||||
export type NodeColor = {
|
||||
kNodeId: string;
|
||||
color: string;
|
||||
}
|
||||
const selectedColorEvent = signal<NodeColor | null>();
|
||||
const setSelectedColor = (nodeColor: NodeColor) => {
|
||||
selectedColorEvent.value = nodeColor;
|
||||
}
|
||||
const onSelectedColorChanged = (callback: (color: NodeColor) => void) => {
|
||||
effect(() => {
|
||||
if (selectedColorEvent.value) {
|
||||
callback(selectedColorEvent.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const dispatchers = {
|
||||
lock: lockDispatcher,
|
||||
retry: retryDispatcher,
|
||||
setSelectedColor,
|
||||
...buttonDispatchers,
|
||||
};
|
||||
export const eventsHandlers = {
|
||||
@@ -146,8 +123,5 @@ export const eventsHandlers = {
|
||||
retry: {
|
||||
onClick: retryEventHandler,
|
||||
},
|
||||
color: {
|
||||
onConfirmChanged: onSelectedColorChanged,
|
||||
},
|
||||
...buttonEventsHandlers,
|
||||
};
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Server communication types
|
||||
export interface ModelConfig {
|
||||
name: string;
|
||||
path: string;
|
||||
precision: "fp16" | "fp32";
|
||||
}
|
||||
|
||||
export interface LoraConfig {
|
||||
path: string;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
export interface ServerSetupPayload {
|
||||
type: "setup";
|
||||
model: ModelConfig;
|
||||
lora?: LoraConfig;
|
||||
device: "cuda" | "cpu";
|
||||
}
|
||||
|
||||
export interface ServerSettingsPayload {
|
||||
type: "settings";
|
||||
prompt?: string;
|
||||
strength?: number;
|
||||
steps?: number;
|
||||
guidance_scale?: number;
|
||||
seed?: number;
|
||||
negative_prompt?: string;
|
||||
}
|
||||
|
||||
export interface ServerResponse {
|
||||
type: "progress" | "success" | "error";
|
||||
status?: "model_loaded" | "generation_complete";
|
||||
message?: string;
|
||||
percent?: number;
|
||||
image?: string; // base64 encoded image
|
||||
}
|
||||
@@ -10,9 +10,3 @@ export const getFileName = (file: File) => {
|
||||
export const getFileExtension = (file: File) => {
|
||||
return file.name.substring(file.name.lastIndexOf(".") + 1);
|
||||
};
|
||||
export const clamp = (num: number, min: number, max: number) => {
|
||||
return Math.min(Math.max(num, min), max);
|
||||
}
|
||||
export const normalize = (num: number, min: number, max: number) => {
|
||||
return (num - min) / (max - min);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Activate virtual environment
|
||||
deactivate conda
|
||||
.\.venv\Scripts\activate.bat
|
||||
cd .\paint_py_server
|
||||
python server.py
|
||||
@@ -44,21 +44,16 @@ export default {
|
||||
info: colors.sky[500],
|
||||
|
||||
// brand colors
|
||||
primary: storytellerColors.azureRadiance,
|
||||
primary: storytellerColors.sunglo,
|
||||
secondary: storytellerColors.gunpowder,
|
||||
tertiary: storytellerColors.aquamarineBlue,
|
||||
|
||||
ui: {
|
||||
background: "#101014",
|
||||
panel: "#3E3E41",
|
||||
border: "#FFFFFF0D",
|
||||
controls: "#3E3E41",
|
||||
background: colors.zinc[100],
|
||||
panel: colors.white,
|
||||
border: colors.zinc[200],
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
"radial-gradient":
|
||||
"radial-gradient(circle at left, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
export const storytellerColors = {
|
||||
azureRadiance: {
|
||||
DEFAULT: "#2d81ff", //=500
|
||||
50: "#eef6ff",
|
||||
100: "#d9ebff",
|
||||
200: "#bcdcff",
|
||||
300: "#8ec7ff",
|
||||
400: "#59a7ff",
|
||||
500: "#2d81ff",
|
||||
600: "#1b63f5",
|
||||
700: "#144ee1",
|
||||
800: "#173fb6",
|
||||
900: "#19398f",
|
||||
950: "#142457",
|
||||
sunglo: {
|
||||
//sunglo
|
||||
DEFAULT: "#e66462", //=500
|
||||
50: "#fdf3f3",
|
||||
100: "#fce4e4",
|
||||
200: "#facfce",
|
||||
300: "#f6acab",
|
||||
400: "#ef7c7a",
|
||||
500: "#e66462",
|
||||
600: "#cf3533",
|
||||
700: "#ae2927",
|
||||
800: "#902624",
|
||||
900: "#782524",
|
||||
950: "#410f0e",
|
||||
},
|
||||
|
||||
gunpowder: {
|
||||
DEFAULT: "#39394c", //=950
|
||||
50: "#f5f6f9",
|
||||
|
||||
Reference in New Issue
Block a user