mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 13:54:53 -05:00
Compare commits
25
Commits
bt-cfg
...
bflat/redesign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bb4383f9 | ||
|
|
95c4293fe8 | ||
|
|
737edda502 | ||
|
|
d7092e9492 | ||
|
|
c121d1ac8c | ||
|
|
40ceb2e08b | ||
|
|
fb2ec6baa4 | ||
|
|
6077232cd3 | ||
|
|
f013373180 | ||
|
|
f0c1fcf516 | ||
|
|
9f392de9c5 | ||
|
|
f5fb340de9 | ||
|
|
0392bda03c | ||
|
|
1bca478621 | ||
|
|
f9c966003a | ||
|
|
2d8245cd55 | ||
|
|
9d1d605f5c | ||
|
|
1e38fbad5c | ||
|
|
6e03df968d | ||
|
|
13e0e4bb02 | ||
|
|
442c3579e1 | ||
|
|
b8a305e271 | ||
|
|
a70061bfd3 | ||
|
|
e7a392b8e2 | ||
|
|
9147da22f2 |
Generated
+219
-46
@@ -609,6 +609,7 @@ dependencies = [
|
||||
"candle-core",
|
||||
"candle-nn",
|
||||
"candle-transformers",
|
||||
"cudarc",
|
||||
"filesys",
|
||||
"hf-hub",
|
||||
"image",
|
||||
@@ -1424,14 +1425,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "candle-core"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "855dfedff437d2681d68e1f34ae559d88b0dd84aa5a6b63f2c8e75ebdd875bbf"
|
||||
checksum = "bef79e00300acaaa6b0b33168e2ed5050f06125d1add87e7103c40a119083581"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"candle-kernels",
|
||||
"cudarc",
|
||||
"gemm",
|
||||
"gemm 0.17.1",
|
||||
"half",
|
||||
"memmap2",
|
||||
"num-traits",
|
||||
@@ -1449,9 +1450,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "candle-kernels"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53343628fa470b7075c28c589b98735b4220b464e37ddbb8e117040e199f4787"
|
||||
checksum = "e3134cc19a080a326f95bdcbe057ecaa9658598e5146e37ba1a8cd550c342a9b"
|
||||
dependencies = [
|
||||
"bindgen_cuda",
|
||||
]
|
||||
@@ -2228,9 +2229,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cudarc"
|
||||
version = "0.12.1"
|
||||
version = "0.13.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38cd60a9a42ec83a2ed7effb0b1f073270264ea99da7acfc44f7e8d74dee0384"
|
||||
checksum = "4e29ce3bfa797c1183053ceb496316203ef561c183941c3c181500d9ade6daf4"
|
||||
dependencies = [
|
||||
"half",
|
||||
"libloading 0.8.6",
|
||||
@@ -2891,6 +2892,15 @@ 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"
|
||||
@@ -3743,17 +3753,37 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-c32",
|
||||
"gemm-c64",
|
||||
"gemm-common",
|
||||
"gemm-f16",
|
||||
"gemm-f32",
|
||||
"gemm-f64",
|
||||
"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",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3763,12 +3793,27 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3778,12 +3823,27 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3794,17 +3854,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"dyn-stack",
|
||||
"dyn-stack 0.10.0",
|
||||
"half",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"pulp",
|
||||
"raw-cpuid",
|
||||
"pulp 0.18.22",
|
||||
"raw-cpuid 10.7.0",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
"sysctl",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3813,14 +3894,32 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"gemm-f32",
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"gemm-f32 0.17.1",
|
||||
"half",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
]
|
||||
@@ -3831,12 +3930,27 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -3846,12 +3960,27 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0"
|
||||
dependencies = [
|
||||
"dyn-stack",
|
||||
"gemm-common",
|
||||
"dyn-stack 0.10.0",
|
||||
"gemm-common 0.17.1",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"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",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -5237,9 +5366,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@@ -6238,7 +6367,7 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.1.0",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
@@ -7323,6 +7452,20 @@ 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"
|
||||
@@ -7636,6 +7779,15 @@ 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"
|
||||
@@ -9774,6 +9926,20 @@ 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"
|
||||
@@ -10846,27 +11012,34 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ug"
|
||||
version = "0.0.2"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4eef2ebfc18c67a6dbcacd9d8a4d85e0568cc58c82515552382312c2730ea13"
|
||||
checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437"
|
||||
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.0.2"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c4dcab280ad0ef3957e153a82dcad608c954d02cf253b695322f502d1f8902e"
|
||||
checksum = "50758486d7941f8b0a636ba7e29455c07071f41590beac1fd307ec893e8db69a"
|
||||
dependencies = [
|
||||
"cudarc",
|
||||
"half",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.63",
|
||||
"ug",
|
||||
]
|
||||
@@ -11192,9 +11365,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "videos"
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ image_height: 512
|
||||
# NB(bt): It looks like we can get down to 2 sampling steps, perhaps ?
|
||||
scheduler_steps: 2
|
||||
scheduler_samples: 1
|
||||
|
||||
cfg_scale: 1.1
|
||||
|
||||
@@ -23,6 +23,9 @@ 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"
|
||||
|
||||
@@ -59,11 +59,11 @@ fn do_infer_image(
|
||||
prompt: prompt.to_string(),
|
||||
uncond_prompt: NEGATIVE_PROMPT.to_string(),
|
||||
//guidance_scale: Some(0.0),
|
||||
guidance_scale: None,
|
||||
model_cache,
|
||||
configs: config,
|
||||
prompt_cache: &prompt_cache,
|
||||
strength: strength,
|
||||
i2i_strength: strength,
|
||||
cfg_scale: config.cfg_scale,
|
||||
};
|
||||
|
||||
match stable_diffusion_pipeline(args) {
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn infer_clip_text_embeddings(
|
||||
false, // use_f16
|
||||
&device,
|
||||
dtype,
|
||||
false, // use_guide_scale
|
||||
use_guide_scale,
|
||||
*first,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3,27 +3,27 @@ use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use crate::ml::image::dynamic_image_to_tensor::dynamic_image_to_tensor;
|
||||
use crate::ml::image::tensor_to_image_buffer::{tensor_to_image_buffer, RgbImage};
|
||||
use crate::ml::model_cache::ModelCache;
|
||||
use crate::ml::model_file::StableDiffusionVersion;
|
||||
use crate::ml::prompt_cache::PromptCache;
|
||||
use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale;
|
||||
use crate::ml::stable_diffusion::infer_clip_text_embeddings::infer_clip_text_embeddings;
|
||||
use crate::state::app_config::AppConfig;
|
||||
use anyhow::{Error as E, Result};
|
||||
use candle_core::{DType, IndexOp, D};
|
||||
use anyhow::{anyhow, Error as E, Result};
|
||||
use candle_core::{DType, IndexOp, Tensor, D};
|
||||
use candle_transformers::models::stable_diffusion::vae::DiagonalGaussianDistribution;
|
||||
use image::DynamicImage;
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
|
||||
|
||||
pub struct Args<'a> {
|
||||
pub image: &'a DynamicImage,
|
||||
pub prompt: String,
|
||||
pub uncond_prompt: String,
|
||||
pub guidance_scale: Option<f64>,
|
||||
pub cfg_scale: Option<f64>,
|
||||
pub i2i_strength: Option<u8>,
|
||||
pub configs: &'a AppConfig,
|
||||
pub model_cache: &'a ModelCache,
|
||||
pub prompt_cache: &'a PromptCache,
|
||||
pub strength: Option<u8>,
|
||||
}
|
||||
|
||||
pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
@@ -38,20 +38,33 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
// TODO(bt,2025-02-18): The scheduler is `EulerAncestralDiscreteScheduler`, but we may want to port an LCM scheduler.
|
||||
// This is a target for performance improvement
|
||||
// See: https://github.com/huggingface/candle/issues/1331
|
||||
println!("Building scheduler...");
|
||||
let mut scheduler = args.configs.sd_config.build_scheduler(
|
||||
args.configs.scheduler_steps)?;
|
||||
|
||||
let seed = args.configs.seed.unwrap_or_else(|| rand::thread_rng().gen());
|
||||
args.configs.device.set_seed(seed)?;
|
||||
|
||||
println!("Using seed: {}", seed);
|
||||
args.configs.device.set_seed(seed)?;
|
||||
|
||||
let guidance_scale = match args.cfg_scale {
|
||||
Some(guidance_scale) => guidance_scale,
|
||||
None => match args.configs.sd_version {
|
||||
StableDiffusionVersion::V1_5
|
||||
| StableDiffusionVersion::V2_1
|
||||
| StableDiffusionVersion::Xl => 7.5,
|
||||
StableDiffusionVersion::Turbo => 0.,
|
||||
_ => 0., // NB: Not sure what the other model families should use, so sticking with "0"
|
||||
},
|
||||
};
|
||||
|
||||
let use_guide_scale = guidance_scale > 1.0;
|
||||
|
||||
println!("Using guide scale: {}", use_guide_scale);
|
||||
|
||||
info!("Checking if prompt is cached");
|
||||
let maybe_cached = args.prompt_cache.get_copy(&args.prompt)?;
|
||||
|
||||
let text_embeddings = if let Some(tensor) = maybe_cached {
|
||||
info!("Prompt is cached!");
|
||||
|
||||
let mut text_embeddings = if let Some(tensor) = maybe_cached {
|
||||
tensor
|
||||
} else {
|
||||
info!("Prompt is NOT cached! Calculating embedding...");
|
||||
@@ -66,12 +79,12 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
false, // use_f16
|
||||
&args.configs.device,
|
||||
args.configs.dtype,
|
||||
false, // use_guide_scale
|
||||
use_guide_scale,
|
||||
)?;
|
||||
args.prompt_cache.store_copy(&args.prompt, &tensor)?;
|
||||
tensor
|
||||
};
|
||||
|
||||
|
||||
println!("Text embeddings shape: {:?}", text_embeddings.shape());
|
||||
|
||||
println!("Loading input image into tensor...");
|
||||
@@ -97,8 +110,8 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
println!("Initial latents shape: {:?}", latents.shape());
|
||||
|
||||
println!("Calculating start step for diffusion process...");
|
||||
|
||||
let img2img_strength = match args.strength {
|
||||
|
||||
let img2img_strength = match args.i2i_strength {
|
||||
None => 0.75f64,
|
||||
Some(strength) => (strength as f64) / 100.0f64,
|
||||
};
|
||||
@@ -136,20 +149,29 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
}
|
||||
|
||||
println!("Processing step {}/{} @ timestamp = {}", timestep_index + 1, timesteps.len(), timestep);
|
||||
|
||||
// TODO(bt,2025-08-18): Do we have to clone the tensor?
|
||||
let latent_model_input = scheduler.scale_model_input(latents.clone(), timestep)?;
|
||||
|
||||
let noise_pred = match args.model_cache.unet_inference(&latent_model_input, timestep as f64, &text_embeddings) {
|
||||
Ok(pred) => {
|
||||
pred
|
||||
},
|
||||
|
||||
let latent_model_input = if use_guide_scale {
|
||||
Tensor::cat(&[&latents, &latents], 0)?
|
||||
} else {
|
||||
latents.clone() // TODO(bt,2025-08-18): Do we have to clone the tensor? `scale_model_input` requires ownership
|
||||
};
|
||||
|
||||
let latent_model_input = scheduler.scale_model_input(latent_model_input, timestep)?;
|
||||
|
||||
let mut noise_pred = match args.model_cache.unet_inference(&latent_model_input, timestep as f64, &text_embeddings) {
|
||||
Ok(pred) => pred,
|
||||
Err(e) => {
|
||||
println!("UNet inference failed with error: {}", e);
|
||||
return Err(anyhow::anyhow!("UNet inference failed: {}", e));
|
||||
},
|
||||
};
|
||||
|
||||
if use_guide_scale {
|
||||
let chunks = noise_pred.chunk(2, 0)?;
|
||||
let (noise_pred_uncond, noise_pred_text) = (&chunks[0], &chunks[1]);
|
||||
noise_pred = (noise_pred_uncond + (guidance_scale * (noise_pred_text - noise_pred_uncond)?)?)?;
|
||||
}
|
||||
|
||||
latents = scheduler.step(&noise_pred, timestep, &latents)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct AppConfig {
|
||||
pub scheduler_samples: usize,
|
||||
|
||||
pub seed: Option<u64>,
|
||||
|
||||
pub cfg_scale: Option<f64>,
|
||||
|
||||
/// Probably shouldn't be here.
|
||||
pub hf_api: Api,
|
||||
@@ -72,6 +74,7 @@ impl AppConfig {
|
||||
scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(1),
|
||||
scheduler_samples: yaml_configs.scheduler_samples.unwrap_or(15),
|
||||
seed: yaml_configs.seed,
|
||||
cfg_scale: yaml_configs.cfg_scale,
|
||||
hf_api,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct YamlConfig {
|
||||
pub scheduler_steps: Option<usize>,
|
||||
pub scheduler_samples: Option<usize>,
|
||||
pub seed: Option<u64>,
|
||||
pub cfg_scale: Option<f64>,
|
||||
}
|
||||
|
||||
impl YamlConfig {
|
||||
|
||||
Generated
+10
@@ -4525,6 +4525,8 @@
|
||||
},
|
||||
"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",
|
||||
@@ -5265,6 +5267,8 @@
|
||||
},
|
||||
"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",
|
||||
@@ -5313,6 +5317,8 @@
|
||||
},
|
||||
"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"
|
||||
@@ -7139,6 +7145,8 @@
|
||||
},
|
||||
"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",
|
||||
@@ -7176,6 +7184,8 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -1,7 +1,7 @@
|
||||
import Konva from "konva";
|
||||
|
||||
|
||||
import { uiAccess, uiEvents } from "~/signals";
|
||||
import { ShapeNode } from "./Nodes";
|
||||
|
||||
import { UndoStackManager } from "./UndoRedo";
|
||||
import { CommandManager, MatteBox, SceneManager } from "./EngineUtitlities";
|
||||
@@ -27,7 +27,8 @@ 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";
|
||||
|
||||
@@ -117,7 +118,7 @@ export class Engine {
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
bgLayerRef: this.bgLayer,
|
||||
offScreenCanvas: this.offScreenCanvas
|
||||
offScreenCanvas: this.offScreenCanvas,
|
||||
});
|
||||
|
||||
// Collection of all Nodes
|
||||
@@ -179,8 +180,6 @@ export class Engine {
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private setAppMode(newAppMode: AppModes) {
|
||||
this.appMode = newAppMode;
|
||||
switch (this.appMode) {
|
||||
@@ -359,10 +358,21 @@ export class Engine {
|
||||
// Listen to Toolbar Main
|
||||
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");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
console.log("Toolbar Main >> Paint");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.ERASER.onClick(() => {
|
||||
console.log("Toolbar Main >> Eraser");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
|
||||
@@ -393,7 +403,6 @@ export class Engine {
|
||||
this.addImage(image);
|
||||
});
|
||||
|
||||
|
||||
uiEvents.onAddTextToEngine((textdata) => {
|
||||
this.addText(textdata);
|
||||
});
|
||||
@@ -424,7 +433,7 @@ export class Engine {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
uiEvents.promptEvents.onPromptStrengthChanged((strength) => {
|
||||
this.realTimeDrawEngine.currentStrength = strength;
|
||||
});
|
||||
@@ -432,6 +441,33 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
@@ -495,9 +531,9 @@ export class Engine {
|
||||
x: 10,
|
||||
y: 80,
|
||||
text: "",
|
||||
fontSize: 24,
|
||||
fontSize: 18,
|
||||
fontFamily: "Source Sans 3",
|
||||
fill: "black",
|
||||
fill: "white",
|
||||
});
|
||||
const anim = new Konva.Animation((frame) => {
|
||||
if (frame && import.meta.env.DEV) {
|
||||
@@ -514,6 +550,42 @@ 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,
|
||||
});
|
||||
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,
|
||||
@@ -525,6 +597,24 @@ 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,
|
||||
});
|
||||
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);
|
||||
//this.realTimeDrawEngine.addNodes(shapeNode);
|
||||
}
|
||||
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
@@ -533,9 +623,9 @@ export class Engine {
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
});
|
||||
|
||||
imageNode.kNode.zIndex(1);
|
||||
this.commandManager.createNode(imageNode);
|
||||
this.realTimeDrawEngine.addNodes(imageNode);
|
||||
//this.realTimeDrawEngine.addNodes(imageNode);
|
||||
}
|
||||
|
||||
public addVideo(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Konva from "konva";
|
||||
import { ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { ImageNode, ShapeNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { MediaNode, Position, RGBColor, Transformation } from "../types";
|
||||
import {
|
||||
AddChromaKey,
|
||||
@@ -48,7 +48,7 @@ export class CommandManager {
|
||||
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
|
||||
}
|
||||
|
||||
createNode(node: VideoNode | ImageNode | TextNode) {
|
||||
createNode(node: VideoNode | ImageNode | TextNode | ShapeNode) {
|
||||
const command = new CreateCommand({
|
||||
nodes: new Set<MediaNode>([node]),
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
@@ -58,6 +58,10 @@ 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());
|
||||
}
|
||||
deleteNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
|
||||
@@ -29,7 +29,7 @@ export abstract class BaseNode {
|
||||
}: {
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
kNode: Konva.Image | Konva.Group;
|
||||
kNode: Konva.Image | Konva.Group | Konva.Shape;
|
||||
}) {
|
||||
// console.log("Node constructed");
|
||||
// this.uuid = uuidv4();
|
||||
@@ -67,8 +67,11 @@ export abstract class BaseNode {
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
wrapperRect.stroke(primaryOrange);
|
||||
wrapperRect.strokeWidth(highlightStrokeWidth);
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect !=null) {
|
||||
wrapperRect.stroke(primaryOrange);
|
||||
wrapperRect.strokeWidth(highlightStrokeWidth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
@@ -86,7 +89,11 @@ export abstract class BaseNode {
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
wrapperRect.strokeWidth(0);
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect !=null) {
|
||||
// Issue investigate
|
||||
wrapperRect.strokeWidth(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
|
||||
@@ -83,8 +83,10 @@ 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,
|
||||
@@ -105,33 +107,33 @@ export class ImageNode extends NetworkedNode {
|
||||
this.kNode.fill(transparent);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
this.uploadImage(imageFile);
|
||||
//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({
|
||||
@@ -149,7 +151,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,
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
}: 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();
|
||||
};
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
// 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,11 +4,12 @@ export enum NodeType {
|
||||
IMAGE = "image",
|
||||
TEXT = "text",
|
||||
VIDEO = "video",
|
||||
SHAPE = "shape",
|
||||
}
|
||||
export const transparent = Colors.transparent;
|
||||
export const primaryOrange = Colors.primaryOrange;
|
||||
|
||||
export const highlightStrokeWidth = 10;
|
||||
export const highlightStrokeWidth = 2;
|
||||
export const minNodeSize = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
|
||||
@@ -3,3 +3,4 @@ export { BaseNode } from "./BaseNode";
|
||||
export { ImageNode } from "./ImageNode";
|
||||
export { VideoNode } from "./VideoNode";
|
||||
export { TextNode } from "./TextNode";
|
||||
export { ShapeNode , ShapeType} from "./ShapeNode";
|
||||
|
||||
@@ -4,11 +4,12 @@ import { MediaNode, Position, Transformation } from "../types";
|
||||
import { uiAccess, uiEvents } from "~/signals";
|
||||
import { NetworkedNode } from "../Nodes/NetworkedNode";
|
||||
import { LoadingBarStatus } from "~/components/ui";
|
||||
import { ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { ImageNode, ShapeNode, TextNode, VideoNode } from "../Nodes";
|
||||
import {
|
||||
calculateContextualsPosition,
|
||||
getImageNodeButtonStates,
|
||||
getMultiSelectButtonStates,
|
||||
getShapeNodeButtonStates,
|
||||
getTextNodeButtonStates,
|
||||
getVideoNodeButtonStates,
|
||||
} from "./ToolbarNodeUtilities";
|
||||
@@ -273,17 +274,29 @@ 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() }),
|
||||
});
|
||||
}
|
||||
if (node instanceof TextNode) {
|
||||
else if (node instanceof ShapeNode) {
|
||||
console.debug("Selected node is of type ShapeNode", node.kNode.id())
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }),
|
||||
buttonStates: getShapeNodeButtonStates({ locked: node.isLocked() }),
|
||||
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
|
||||
});
|
||||
}
|
||||
if (node instanceof VideoNode) {
|
||||
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) {
|
||||
showOrUpdate({
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }),
|
||||
|
||||
@@ -40,8 +40,9 @@ export class SelectorSquare {
|
||||
stageRef: Konva.Stage;
|
||||
}) {
|
||||
this.kSquare = new Konva.Rect({
|
||||
fill: " rgb(250, 128, 114, 0.3)",
|
||||
stroke: "salmon",
|
||||
fill: "rgba(45, 129, 255, 0.3)",
|
||||
stroke: "#00a1ff",
|
||||
strokeWidth: 1,
|
||||
visible: false,
|
||||
// disable events to not interrupt with events
|
||||
listening: false,
|
||||
|
||||
@@ -51,7 +51,8 @@ export function getImageNodeButtonStates(
|
||||
buttonName === ButtonNames.AI_STYLIZE ||
|
||||
buttonName === ButtonNames.SEGMENTATION ||
|
||||
buttonName === ButtonNames.DOWNLOAD ||
|
||||
buttonName === ButtonNames.CHROMA
|
||||
buttonName === ButtonNames.CHROMA ||
|
||||
buttonName === ButtonNames.COLOR
|
||||
) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
@@ -130,7 +131,9 @@ export function getVideoNodeButtonStates(
|
||||
return buttonStates;
|
||||
}
|
||||
// soon to come feature is disabled
|
||||
if (buttonName === ButtonNames.AI_STYLIZE) {
|
||||
if (buttonName === ButtonNames.AI_STYLIZE ||
|
||||
buttonName === ButtonNames.COLOR
|
||||
) {
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
@@ -149,7 +152,8 @@ export function getVideoNodeButtonStates(
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
export function getMultiSelectButtonStates(
|
||||
|
||||
export function getShapeNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
@@ -157,7 +161,7 @@ export function getMultiSelectButtonStates(
|
||||
(buttonStates, buttonName) => {
|
||||
// transform button depends on locked state
|
||||
if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) {
|
||||
buttonStates[buttonName] = {
|
||||
buttonStates.TRANSFORM = {
|
||||
disabled: props.locked === "unknown" || props.locked === true,
|
||||
hidden: false,
|
||||
active: true,
|
||||
@@ -191,3 +195,47 @@ export function getMultiSelectButtonStates(
|
||||
{} 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,12 +3,10 @@ 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 } from "../Nodes";
|
||||
import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes";
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
import { RenderTask } from "./RenderTask";
|
||||
@@ -18,7 +16,7 @@ import { OffScreenSceneCanvas } from "./OffScreenSceneCanvas";
|
||||
|
||||
export class RealTimeDrawEngine {
|
||||
private videoNodes: VideoNode[];
|
||||
private imageNodes: (ImageNode | TextNode)[];
|
||||
private imageNodes: (ImageNode | TextNode | ShapeNode)[];
|
||||
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private outputBitmap: ImageBitmap | undefined;
|
||||
@@ -26,7 +24,7 @@ export class RealTimeDrawEngine {
|
||||
// private frames: ImageBitmap[];
|
||||
|
||||
// capturing composite within window
|
||||
private bgLayerRef: Konva.Layer;
|
||||
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
|
||||
private height: number;
|
||||
@@ -51,13 +49,11 @@ export class RealTimeDrawEngine {
|
||||
constructor({
|
||||
width,
|
||||
height,
|
||||
bgLayerRef,
|
||||
mediaLayerRef,
|
||||
offScreenCanvas,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
bgLayerRef: Konva.Layer;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
offScreenCanvas: OffscreenCanvas;
|
||||
}) {
|
||||
@@ -80,8 +76,6 @@ export class RealTimeDrawEngine {
|
||||
this.offScreenCanvas.width = this.width;
|
||||
this.offScreenCanvas.height = this.height;
|
||||
|
||||
this.bgLayerRef = bgLayerRef;
|
||||
|
||||
// this is the whole canvas
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
|
||||
@@ -90,7 +84,7 @@ export class RealTimeDrawEngine {
|
||||
this.port = undefined;
|
||||
|
||||
this.fps = 24;
|
||||
|
||||
|
||||
this.currentPrompt = "";
|
||||
this.currentStrength = 100;
|
||||
// This is captures a subset of the medialayer ref
|
||||
@@ -106,20 +100,6 @@ 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,
|
||||
@@ -127,21 +107,90 @@ export class RealTimeDrawEngine {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
image: undefined,
|
||||
stroke: "blue",
|
||||
stroke: "black",
|
||||
strokeWidth: 1,
|
||||
dash: [5, 5],
|
||||
draggable: false,
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
this.bgLayerRef.add(this.captureCanvas);
|
||||
this.bgLayerRef.add(this.previewCanvas);
|
||||
this.mediaLayerRef.add(this.captureCanvas);
|
||||
this.mediaLayerRef.add(this.previewCanvas);
|
||||
// send back
|
||||
this.captureCanvas.setZIndex(0);
|
||||
this.previewCanvas.setZIndex(0);
|
||||
this.previewCanvas.setZIndex(1);
|
||||
// Add mouse events for preview canvas copying
|
||||
//this.previewCopyListener();
|
||||
|
||||
//this.debug();
|
||||
}
|
||||
|
||||
public previewCopyListener() {
|
||||
this.previewCanvas.on('mousedown touchstart', () => {
|
||||
if (!this.outputBitmap) {
|
||||
console.log("No preview image to copy");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO create it as a node.
|
||||
|
||||
// const imageNode = new ImageNode({
|
||||
// mediaLayerRef: this.mediaLayer,
|
||||
// canvasPosition: this.renderEngine.captureCanvas.position(),
|
||||
// canvasSize: this.renderEngine.captureCanvas.size(),
|
||||
// imageFile: imageFile,
|
||||
// selectionManagerRef: this.selectionManager,
|
||||
// });
|
||||
|
||||
// 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.mediaLayerRef.add(previewCopy);
|
||||
previewCopy.moveToTop();
|
||||
this.mediaLayerRef.draw();
|
||||
|
||||
// Start dragging immediately
|
||||
previewCopy.startDrag();
|
||||
|
||||
// Handle drag events
|
||||
previewCopy.on('dragmove', () => {
|
||||
this.mediaLayerRef.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.mediaLayerRef.batchDraw();
|
||||
} else {
|
||||
// Remove if dropped outside capture area
|
||||
previewCopy.destroy();
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public findImageNodeById(id: string): (ImageNode | TextNode | ShapeNode | undefined) {
|
||||
return this.imageNodes.find(node => {
|
||||
if (node.kNode) {
|
||||
return node.kNode.id() === id;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
async updateCaptureCanvas(
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
@@ -284,12 +333,13 @@ export class RealTimeDrawEngine {
|
||||
this.isProcessing = true;
|
||||
await this.render();
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
if (node instanceof ImageNode || node instanceof TextNode || node instanceof ShapeNode) {
|
||||
console.debug("Adding node:", node);
|
||||
this.imageNodes.push(node);
|
||||
console.log(this.imageNodes)
|
||||
node.kNode.on("dragend", this.handleNodeDragEnd);
|
||||
}
|
||||
}
|
||||
@@ -325,6 +375,7 @@ export class RealTimeDrawEngine {
|
||||
try {
|
||||
const box = config.layerOfInterest.getClientRect();
|
||||
const stage = config.layerOfInterest.getStage();
|
||||
|
||||
|
||||
const x = config.x !== undefined ? config.x : Math.floor(box.x);
|
||||
const y = config.y !== undefined ? config.y : Math.floor(box.y);
|
||||
@@ -384,8 +435,6 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async imageBitmapToBase64(imageBitmap: ImageBitmap): Promise<string> {
|
||||
// Create a temporary canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
@@ -462,9 +511,11 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
|
||||
public async render() {
|
||||
// only pick nodes that intersect with the canvas on screen bounds to freeze.
|
||||
// only pick nodes that intersect wi th 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}`,
|
||||
);
|
||||
@@ -482,20 +533,20 @@ export class RealTimeDrawEngine {
|
||||
})) as ImageBitmap;
|
||||
|
||||
// Test code
|
||||
if (false) {
|
||||
if (true) {
|
||||
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);
|
||||
@@ -510,6 +561,5 @@ export class RealTimeDrawEngine {
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ export enum AppModes {
|
||||
INIT = "INIT",
|
||||
PREVIEW = "PREVIEW",
|
||||
SELECT = "SELECT",
|
||||
PAINT = "PAINT",
|
||||
ERASER = "ERASER",
|
||||
RENDERING = "RENDERING",
|
||||
}
|
||||
|
||||
@@ -14,5 +16,5 @@ export const VideoResolutions = {
|
||||
export const Colors = {
|
||||
transparent: "rgba(0,0,0,0)",
|
||||
translucentBlack: "rgba(0,0,0,0.3)",
|
||||
primaryOrange: "rgba(230,100,98,1)",
|
||||
primaryOrange: "rgba(45, 129, 255, 1)",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NodeType, BaseNode, ImageNode, TextNode, VideoNode } from "../Nodes";
|
||||
import { NodeType, BaseNode, ImageNode, TextNode, VideoNode, ShapeNode } from "../Nodes";
|
||||
|
||||
import { TransformationData } from "./Transformation";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TextNodeData } from "./Text";
|
||||
|
||||
export type MediaNode =
|
||||
// | NetworkedNode
|
||||
BaseNode | VideoNode | ImageNode | TextNode;
|
||||
BaseNode | VideoNode | ImageNode | TextNode | ShapeNode;
|
||||
|
||||
export type ImageNodeData = {
|
||||
mediaFileUrl: string;
|
||||
@@ -26,6 +26,10 @@ export type VideoNodeData = {
|
||||
extractionUrl?: string;
|
||||
};
|
||||
|
||||
export type ShapeNodeData = {
|
||||
shape: "triangle" | "square" | "circle";
|
||||
}
|
||||
|
||||
export type NodeData = {
|
||||
type: NodeType;
|
||||
transform: TransformationData;
|
||||
|
||||
@@ -50,6 +50,20 @@ 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
|
||||
@@ -71,6 +85,8 @@ export const ContextualToolbarNode = () => {
|
||||
onLockClicked={(e) => {
|
||||
dispatchUiEvents.toolbarNode.lock(e);
|
||||
}}
|
||||
color={uiAccess.toolbarNode.signal.value.color}
|
||||
onColorChange={handleColorChange}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
|
||||
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;
|
||||
|
||||
const sliderValue = promptStrength.value
|
||||
const onSliderValueChanged = dispatchUiEvents.promptSettings.setPromptStrength
|
||||
// This makes component re-render whenever the signal value changes
|
||||
useSignalEffect(() => {
|
||||
promptStrength.value;
|
||||
});
|
||||
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { 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 = {
|
||||
style: {
|
||||
width: "530px",
|
||||
height: "200px",
|
||||
},
|
||||
placeholder: "",
|
||||
placeholder: "Type your prompt here...",
|
||||
label: "",
|
||||
resize: "none" as ResizeType,
|
||||
rows: 3,
|
||||
};
|
||||
|
||||
const textContent = promptText.value;
|
||||
@@ -36,11 +22,18 @@ export const SignaledPromptText = ({
|
||||
// This makes component re-render whenever the signal value changes
|
||||
useSignalEffect(() => {
|
||||
promptText.value;
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 left-1/2 -translate-x-1/2">
|
||||
<Textarea {...TextAreaStates} ref={textAreaRef} onChange={onTextChanged} value={textContent} />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ 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,
|
||||
@@ -43,6 +44,19 @@ 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];
|
||||
}
|
||||
@@ -69,7 +83,7 @@ export const SignaledToolbarMain = ({
|
||||
dispatchers.loadingBarRetry(e);
|
||||
};
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2">
|
||||
<div className="fixed left-4 top-1/2 -translate-y-1/2">
|
||||
<Transition
|
||||
as="div"
|
||||
className={twMerge(
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@apply bg-ui-background font-sans text-[#222222];
|
||||
@apply bg-ui-background font-sans text-white;
|
||||
}
|
||||
.pegboard {
|
||||
background-image: radial-gradient(#3f3f466b 1px, transparent 0);
|
||||
background-image: radial-gradient(#ffffff25 1px, transparent 0);
|
||||
background-color: #1b1b1f;
|
||||
background-size: 36px 36px;
|
||||
@apply bg-center;
|
||||
}
|
||||
@@ -41,7 +42,24 @@
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +72,10 @@
|
||||
.dotted-pattern {
|
||||
background-image: radial-gradient(
|
||||
circle at 1px 1px,
|
||||
rgb(0 0 0 / 0.25) 1px,
|
||||
rgb(255 255 255 / 0.25) 1px,
|
||||
transparent 0
|
||||
);
|
||||
background-size: 32px 32px;
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
@@ -87,6 +105,11 @@
|
||||
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,107 +1,169 @@
|
||||
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: "Step 1: Compose Your Scene",
|
||||
title: "Lorem ipsum dolor sit",
|
||||
description:
|
||||
"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",
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.",
|
||||
image: "/images/features/ai-creation.jpg",
|
||||
tag: "Core",
|
||||
},
|
||||
{
|
||||
title: "Step 2: Smart Segmentation",
|
||||
title: "Ut enim ad minim",
|
||||
description:
|
||||
"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",
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea.",
|
||||
image: "/images/features/collaboration.jpg",
|
||||
tag: "Workflow",
|
||||
},
|
||||
{
|
||||
title: "Step 3: AI Style Transfer",
|
||||
title: "Duis aute irure dolor",
|
||||
description:
|
||||
"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",
|
||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla.",
|
||||
image: "/images/features/style-models.jpg",
|
||||
tag: "Tools",
|
||||
},
|
||||
];
|
||||
|
||||
const scrollToFeature = (index: number) => {
|
||||
const element = document.getElementById(`feature-${index + 1}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="relative min-h-screen bg-[#101014] text-white">
|
||||
<Navbar />
|
||||
|
||||
<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
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
<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.
|
||||
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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"
|
||||
<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}
|
||||
>
|
||||
{/* 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>
|
||||
))}
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -25,6 +26,7 @@ export const Main = withProtectionRoute(() => {
|
||||
/>
|
||||
<div className="absolute top-0 flex w-full justify-between px-2">
|
||||
<ToolbarTopLeft />
|
||||
<ToolbarTopRight />
|
||||
{/* <ToolbarUserProfile /> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,12 +42,6 @@ export const DialogAddImage = ({
|
||||
closeCallback();
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (currFile) {
|
||||
onAddImage(currFile);
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
@@ -58,7 +52,7 @@ export const DialogAddImage = ({
|
||||
"w-full max-w-2xl",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="text-3xl font-bold">Upload Image</DialogTitle>
|
||||
<DialogTitle className="text-2xl font-bold">Upload Image</DialogTitle>
|
||||
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
|
||||
<FileUploader
|
||||
title=""
|
||||
@@ -66,6 +60,10 @@ export const DialogAddImage = ({
|
||||
file={currFile}
|
||||
setFile={(file: File | null) => {
|
||||
setAssetFile(file);
|
||||
if (file) {
|
||||
onAddImage(file);
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{currFile && (
|
||||
@@ -84,9 +82,6 @@ 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-3xl font-bold">Edit Text</DialogTitle>
|
||||
<DialogTitle className="text-2xl 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-gray-100 hover:bg-gray-200/60 transition-colors duration-150 ease-in-out",
|
||||
"group cursor-pointer px-6 py-12 bg-ui-controls hover:bg-white/25 transition-colors duration-150 ease-in-out rounded-lg",
|
||||
!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 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 ",
|
||||
"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",
|
||||
disabled && "pointer-events-none text-secondary-300",
|
||||
buttonProps.prominent &&
|
||||
"highlight-button border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
|
||||
"border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
|
||||
customButtonClassNames,
|
||||
);
|
||||
|
||||
@@ -57,6 +57,7 @@ export const ToolbarButton = ({
|
||||
<button
|
||||
className={mergedButtonClasses}
|
||||
disabled={disabled}
|
||||
autoFocus={false}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -73,7 +74,11 @@ export const ToolbarButton = ({
|
||||
</button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return <Tooltip tip={tooltip}>{Button}</Tooltip>;
|
||||
return (
|
||||
<Tooltip position="right" tip={tooltip}>
|
||||
{Button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return Button;
|
||||
};
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
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,
|
||||
faMagicWandSparkles,
|
||||
faPaintbrush,
|
||||
faShapes,
|
||||
faSquare,
|
||||
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 { ButtonPreviewAndRender } from "./ButtonPreviewAndRender";
|
||||
|
||||
export const ToolbarMain = ({
|
||||
disabled = false,
|
||||
@@ -38,13 +40,14 @@ export const ToolbarMain = ({
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-fit items-center divide-x divide-ui-border",
|
||||
"flex h-fit flex-col items-center gap-y-2",
|
||||
paperWrapperStyles,
|
||||
"glass",
|
||||
disabled &&
|
||||
"pointer-events-none cursor-default bg-ui-border shadow-md",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 pr-2">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ToolbarButton
|
||||
icon={faLocationArrow}
|
||||
iconProps={{ className: "fa-flip-horizontal" }}
|
||||
@@ -56,37 +59,113 @@ 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..."
|
||||
data-tooltip="Add Media"
|
||||
className={twMerge(
|
||||
"size-10 rounded-lg p-2 hover:bg-gray-200/50",
|
||||
toolTipStyles,
|
||||
"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={faFilePlus} />
|
||||
</button>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="bottom"
|
||||
anchor="right"
|
||||
className={twMerge(
|
||||
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
|
||||
"flex flex-col [--anchor-gap:16px]",
|
||||
"flex [--anchor-gap:12px]",
|
||||
paperWrapperStyles,
|
||||
)}
|
||||
>
|
||||
<ToolbarButton icon={faImage} buttonProps={buttonProps.ADD_IMAGE}>
|
||||
Add Image
|
||||
<span className="text-[16px]">Add Image</span>
|
||||
</ToolbarButton>
|
||||
<ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
|
||||
{/* <ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
|
||||
Add Video
|
||||
</ToolbarButton>
|
||||
</ToolbarButton> */}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ToolbarButton
|
||||
icon={faPaintbrush}
|
||||
buttonProps={buttonProps.PAINT}
|
||||
tooltip="Paint Brush"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faEraser}
|
||||
buttonProps={buttonProps.ERASER}
|
||||
tooltip="Eraser"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ToolbarButton
|
||||
icon={faArrowRotateLeft}
|
||||
buttonProps={buttonProps.UNDO}
|
||||
@@ -97,15 +176,11 @@ 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,6 +4,11 @@ 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,14 +4,19 @@ import {
|
||||
faLockKeyholeOpen,
|
||||
faLockKeyhole,
|
||||
faUnlockKeyhole,
|
||||
faPalette,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarButtonProps,
|
||||
} from "~/components/features/ToolbarButton";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
|
||||
import { ToolbarNodeButtonNames } from "./enums";
|
||||
import { ToolbarNodeButtonData } from "./data";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
|
||||
|
||||
export interface ToolbarNodeProps {
|
||||
disabled?: boolean;
|
||||
@@ -24,6 +29,8 @@ export interface ToolbarNodeProps {
|
||||
buttonsProps?: {
|
||||
[key in ToolbarNodeButtonNames]: ToolbarButtonProps;
|
||||
};
|
||||
color?: string;
|
||||
onColorChange?: (color: string) => void;
|
||||
}
|
||||
export const ToolbarNode = ({
|
||||
disabled,
|
||||
@@ -31,6 +38,8 @@ export const ToolbarNode = ({
|
||||
lockDisabled,
|
||||
onLockClicked,
|
||||
buttonsProps,
|
||||
color = "#000000",
|
||||
onColorChange = () => { },
|
||||
}: ToolbarNodeProps) => {
|
||||
const handleOnLockClicked: React.MouseEventHandler<HTMLButtonElement> = (
|
||||
e,
|
||||
@@ -53,7 +62,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,
|
||||
}}
|
||||
@@ -73,14 +82,24 @@ export const ToolbarNode = ({
|
||||
{ToolbarNodeButtonData.map((buttonDatum, idx) => {
|
||||
const buttonProps = buttonsProps?.[buttonDatum.name];
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
icon={buttonDatum.icon}
|
||||
tooltip={buttonDatum.tooltip}
|
||||
key={idx}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
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} />
|
||||
)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
faTrashCan,
|
||||
faVectorSquare,
|
||||
faDownload,
|
||||
faPalette,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export const ToolbarNodeButtonData = [
|
||||
@@ -16,6 +17,11 @@ export const ToolbarNodeButtonData = [
|
||||
icon: faVectorSquare,
|
||||
tooltip: "Move",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.COLOR,
|
||||
icon: faPalette,
|
||||
tooltip: "Color",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.CHROMA,
|
||||
icon: faTransporter,
|
||||
|
||||
@@ -7,4 +7,5 @@ export enum ToolbarNodeButtonNames {
|
||||
MOVE_LAYER_UP = "MOVE_LAYER_UP",
|
||||
MOVE_LAYER_DOWN = "MOVE_LAYER_DOWN",
|
||||
DOWNLOAD = "DOWNLOAD",
|
||||
COLOR = "COLOR",
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
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/Storyteller-Logo-Black.png"
|
||||
src="/brand/mira-logo.png"
|
||||
alt="logo"
|
||||
className="mr-4 h-8"
|
||||
className="h-[30px] select-none pb-1"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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="border-t border-gray-100 bg-gray-50">
|
||||
<footer className="relative z-10 bg-[#18181C]">
|
||||
<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/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Board"
|
||||
className="h-9"
|
||||
src="/brand/mira-logo.png"
|
||||
alt="Mira Realtime AI Editor"
|
||||
className="h-[34px] pb-1"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate("/signup")}
|
||||
className="group rounded-xl px-8 py-4 text-lg"
|
||||
className="text-md group rounded-xl bg-[#2D81FF] px-8 py-4 hover:bg-[#438AF6]"
|
||||
>
|
||||
<span className="relative flex items-center">
|
||||
Start Creating for Free
|
||||
|
||||
@@ -3,26 +3,31 @@ import { Button } from "../ui";
|
||||
|
||||
const Navbar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<a href="/" className="flex items-center">
|
||||
<img
|
||||
src="/brand/Storyteller-Logo-Black.png"
|
||||
alt="Storyteller Board"
|
||||
className="h-9"
|
||||
src="/brand/mira-logo.png"
|
||||
alt="Mira Realtime AI Editor"
|
||||
className="h-[34px] pb-1"
|
||||
/>
|
||||
</a>
|
||||
<div className="flex items-center space-x-5">
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium !text-gray-600 hover:!text-gray-900"
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className=""
|
||||
onClick={() => navigate("/signup")}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
<Button onClick={() => navigate("/signup")}>Sign Up</Button>
|
||||
Sign up
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden bg-[#2D81FF] hover:bg-[#438AF6] md:block"
|
||||
onClick={() => []}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const paperWrapperStyles =
|
||||
"rounded-xl border border-ui-border bg-ui-panel p-2 shadow-lg";
|
||||
"rounded-xl border border-ui-border p-1.5 shadow-lg bg-ui-panel";
|
||||
|
||||
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 = 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 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 dialogPanelStyles = twMerge(
|
||||
"flex max-w-5xl flex-col justify-between gap-4 p-6",
|
||||
"flex max-w-5xl flex-col justify-between gap-4 p-6 bg-[#1C1C20]",
|
||||
);
|
||||
|
||||
@@ -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-gray-600 hover:bg-gray-500 text-white focus-visible:outline-secondary";
|
||||
return "bg-ui-controls hover:bg-ui-controls/80 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-400 text-white focus-visible:outline-primary-600";
|
||||
return "bg-primary hover:bg-primary-600 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">
|
||||
<div className="relative flex h-10 rounded-md border border-ui-border">
|
||||
<ComboboxInput
|
||||
className="rounded-md"
|
||||
className="rounded-lg px-3"
|
||||
displayValue={(font: string) => font}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<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 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>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
anchor="bottom"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border empty:invisible"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border border-ui-border empty:invisible"
|
||||
>
|
||||
{filteredOptions.map((option, idx) => (
|
||||
<ComboboxOption
|
||||
key={idx}
|
||||
value={option}
|
||||
className="cursor-pointer bg-white p-2 data-[focus]:bg-primary-200 data-[selected]:font-medium"
|
||||
className="bg-ui-controls cursor-pointer px-3 py-2 data-[focus]:bg-gray-600 data-[selected]:font-medium"
|
||||
>
|
||||
{option}
|
||||
</ComboboxOption>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface SliderProps {
|
||||
showTooltip?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
suffix?: string;
|
||||
innerLabel?: string;
|
||||
}
|
||||
|
||||
export const Slider = ({
|
||||
@@ -24,6 +25,7 @@ export const Slider = ({
|
||||
showTooltip,
|
||||
tooltipContent,
|
||||
suffix,
|
||||
innerLabel,
|
||||
}: SliderProps) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -88,25 +90,40 @@ export const Slider = ({
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"relative h-2.5 w-full cursor-pointer rounded-full bg-gray-500",
|
||||
"glass group relative h-7 w-full cursor-pointer overflow-hidden rounded-lg border border-ui-border",
|
||||
isDragging && "!bg-ui-controls/90",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className="absolute h-2.5 rounded-full bg-primary"
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"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",
|
||||
"absolute h-7 bg-primary/30 transition-colors duration-300 group-hover:bg-primary/50",
|
||||
isDragging && "!bg-primary/50",
|
||||
)}
|
||||
style={{ left: `${percentage}%` }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
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>
|
||||
|
||||
{showTooltip && (
|
||||
<Transition
|
||||
as={"div"}
|
||||
|
||||
@@ -15,7 +15,7 @@ const resizeStyles = {
|
||||
horizontal: "resize-x",
|
||||
vertical: "resize-y",
|
||||
undefined: "",
|
||||
}
|
||||
};
|
||||
|
||||
export interface TextareaInterface
|
||||
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
@@ -27,7 +27,6 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
|
||||
{ className, label, resize = "vertical", id, ...rest }: TextareaInterface,
|
||||
ref,
|
||||
) => {
|
||||
|
||||
const resizeStyle = resizeStyles[resize];
|
||||
|
||||
return (
|
||||
@@ -38,17 +37,16 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
|
||||
ref={ref}
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"border-ui-panel-border bg-ui-controls rounded-lg border px-3 py-2",
|
||||
"rounded-lg border border-ui-border px-4 py-2.5 text-white outline-none focus:outline-none",
|
||||
className,
|
||||
resizeStyle,
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
transition: "outline-color 0.15s ease-in-out",
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
// disableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "#e66462";
|
||||
e.currentTarget.style.outlineColor = "#2d81ff";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// enableHotkeyInput(DomLevels.INPUT);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
faAlignCenter,
|
||||
faAlignJustify,
|
||||
// faAlignJustify,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faFont, faHashtag } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faFont,
|
||||
faHashtag,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
@@ -12,9 +16,15 @@ import { paperWrapperStyles } from "~/components/styles";
|
||||
export const ColorPicker = ({
|
||||
color: prevColor,
|
||||
onChange,
|
||||
faIcon = faFont,
|
||||
borderStyle,
|
||||
showBar = true,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (newColor: string) => void;
|
||||
faIcon?: IconDefinition;
|
||||
borderStyle?: string;
|
||||
showBar?: boolean;
|
||||
}) => {
|
||||
const [{ currColor, textInput }, setStates] = useState<{
|
||||
currColor: string;
|
||||
@@ -44,11 +54,19 @@ export const ColorPicker = ({
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<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
|
||||
className={twMerge(
|
||||
"bg-ui-controls flex size-10 flex-col items-center gap-1 rounded-md p-2",
|
||||
borderStyle,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faIcon} color={prevColor} />
|
||||
{showBar && (
|
||||
<span className="h-1 w-full" style={{ backgroundColor: prevColor }} />
|
||||
)}
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="bottom"
|
||||
|
||||
@@ -151,7 +151,11 @@ export const TextEditor = ({
|
||||
onChangeTextDecoration={onChangeTextDecoration}
|
||||
/>
|
||||
</div>
|
||||
<Textarea {...unionedTextAreaProps} ref={textAreaRef} />
|
||||
<Textarea
|
||||
className="bg-white"
|
||||
{...unionedTextAreaProps}
|
||||
ref={textAreaRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const buttonGroupButton =
|
||||
"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";
|
||||
"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";
|
||||
export const buttonGroupGrouper =
|
||||
"flex divide-x overflow-hidden rounded-md border";
|
||||
"flex divide-x overflow-hidden rounded-md border border-ui-border";
|
||||
|
||||
@@ -7,15 +7,18 @@ 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,
|
||||
toolTipStyles.base,
|
||||
toolTipStyles[position],
|
||||
forceShow && "after:block before:block",
|
||||
children.props.className,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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,5 +1,6 @@
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
|
||||
import { isToolbarMode } from "~/signals/uiEvents/toolbarMain/toolbarMain";
|
||||
|
||||
interface ToolbarMainSignalInterface {
|
||||
disabled: boolean;
|
||||
@@ -42,6 +43,17 @@ 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,12 +10,13 @@ type ButtonStates = {
|
||||
};
|
||||
};
|
||||
export interface ContextualToolbarProps extends ContextualUi {
|
||||
knodeIds: number[];
|
||||
knodeIds: string[];
|
||||
disabled: boolean;
|
||||
downloadUrl?: string;
|
||||
locked: boolean | "unknown";
|
||||
lockDisabled: boolean;
|
||||
buttonStates: ButtonStates;
|
||||
color: string;
|
||||
}
|
||||
interface PartialContextualToolbarProps
|
||||
extends Partial<Omit<ContextualToolbarProps, "buttonStates">> {
|
||||
@@ -32,6 +33,7 @@ const toolbarNodeSignal = signal<ContextualToolbarProps>({
|
||||
locked: false,
|
||||
lockDisabled: false,
|
||||
buttonStates: initButtonStates(),
|
||||
color: "#000000",
|
||||
});
|
||||
|
||||
export const toolbarNode = {
|
||||
@@ -77,6 +79,11 @@ export const toolbarNode = {
|
||||
hidden: false,
|
||||
active: buttonStates[ButtonNames.TRANSFORM].active,
|
||||
},
|
||||
[ButtonNames.COLOR]: {
|
||||
disabled: locked,
|
||||
hidden: false,
|
||||
active: buttonStates[ButtonNames.COLOR].active,
|
||||
},
|
||||
},
|
||||
locked: locked,
|
||||
};
|
||||
@@ -135,6 +142,12 @@ export const toolbarNode = {
|
||||
},
|
||||
};
|
||||
},
|
||||
setColor(color: string) {
|
||||
toolbarNodeSignal.value = {
|
||||
...toolbarNodeSignal.value,
|
||||
color,
|
||||
}
|
||||
},
|
||||
batchButtonStates({ buttonStates }: { buttonStates: Partial<ButtonStates> }) {
|
||||
const prevButtonStates = toolbarNodeSignal.value.buttonStates;
|
||||
toolbarNodeSignal.value = {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { signal, effect } from "@preact/signals-react";
|
||||
import { TextNodeData } from "~/KonvaApp/types";
|
||||
import { ShapeNodeData, 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) {
|
||||
@@ -48,15 +49,30 @@ 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,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ 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) => {
|
||||
@@ -25,22 +26,35 @@ 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>) => {
|
||||
if (effectsCleanups[buttonName]) {
|
||||
effectsCleanups[buttonName]();
|
||||
}
|
||||
effectsCleanups[buttonName] = effect(() => {
|
||||
if (events[buttonName].value) {
|
||||
callback(events[buttonName].value);
|
||||
events[buttonName].value = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
onClick: (callback: MouseEventHandler<HTMLButtonElement>) => { defaultHandler(buttonName, callback) },
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -44,6 +44,11 @@ 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 } =
|
||||
@@ -111,9 +116,27 @@ 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 = {
|
||||
@@ -123,5 +146,8 @@ export const eventsHandlers = {
|
||||
retry: {
|
||||
onClick: retryEventHandler,
|
||||
},
|
||||
color: {
|
||||
onConfirmChanged: onSelectedColorChanged,
|
||||
},
|
||||
...buttonEventsHandlers,
|
||||
};
|
||||
|
||||
@@ -44,16 +44,21 @@ export default {
|
||||
info: colors.sky[500],
|
||||
|
||||
// brand colors
|
||||
primary: storytellerColors.sunglo,
|
||||
primary: storytellerColors.azureRadiance,
|
||||
secondary: storytellerColors.gunpowder,
|
||||
tertiary: storytellerColors.aquamarineBlue,
|
||||
|
||||
ui: {
|
||||
background: colors.zinc[100],
|
||||
panel: colors.white,
|
||||
border: colors.zinc[200],
|
||||
background: "#101014",
|
||||
panel: "#3E3E41",
|
||||
border: "#FFFFFF0D",
|
||||
controls: "#3E3E41",
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
"radial-gradient":
|
||||
"radial-gradient(circle at left, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
export const storytellerColors = {
|
||||
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",
|
||||
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",
|
||||
},
|
||||
|
||||
gunpowder: {
|
||||
DEFAULT: "#39394c", //=950
|
||||
50: "#f5f6f9",
|
||||
|
||||
Reference in New Issue
Block a user