improve ui
add track properties editor
This commit is contained in:
parent
28613c9214
commit
8d1f949280
@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-menubar": "^1.0.2",
|
"@radix-ui/react-menubar": "^1.0.2",
|
||||||
"@radix-ui/react-slider": "^1.1.1",
|
"@radix-ui/react-slider": "^1.1.1",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
"@radix-ui/react-toolbar": "^1.0.3",
|
"@radix-ui/react-toolbar": "^1.0.3",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
"@tempblade/common": "^2.0.1",
|
"@tempblade/common": "^2.0.1",
|
||||||
|
BIN
app/public/canvaskit.wasm
Normal file
BIN
app/public/canvaskit.wasm
Normal file
Binary file not shown.
18
app/src-tauri/Cargo.lock
generated
18
app/src-tauri/Cargo.lock
generated
@ -3077,6 +3077,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tint",
|
"tint",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3376,11 +3377,24 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.3.2"
|
version = "1.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.9",
|
"getrandom 0.2.9",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"uuid-macro-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid-macro-internal"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -12,7 +12,10 @@ edition = "2021"
|
|||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.3", features = [] }
|
tauri-build = { version = "1.3", features = [] }
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
uuid = { version = "1.3.3", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
tauri = { version = "1.3", features = ["dialog-open", "dialog-save", "shell-open"] }
|
tauri = { version = "1.3", features = ["dialog-open", "dialog-save", "shell-open"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@ -22,9 +25,7 @@ logging_timer = "1.1.0"
|
|||||||
rayon = "1.7"
|
rayon = "1.7"
|
||||||
font-kit = "0.11.0"
|
font-kit = "0.11.0"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
# DO NOT REMOVE!!
|
# DO NOT REMOVE!!
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ use super::{
|
|||||||
pub struct Keyframe {
|
pub struct Keyframe {
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
pub offset: f32,
|
pub offset: f32,
|
||||||
|
pub id: String,
|
||||||
pub interpolation: Option<InterpolationType>,
|
pub interpolation: Option<InterpolationType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,16 +25,19 @@ fn interpolates_the_input() {
|
|||||||
let keyframes1 = Keyframes {
|
let keyframes1 = Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "1".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "2".to_string(),
|
||||||
value: 100.0,
|
value: 100.0,
|
||||||
offset: 1.0,
|
offset: 1.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "3".to_string(),
|
||||||
value: 300.0,
|
value: 300.0,
|
||||||
offset: 3.0,
|
offset: 3.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -45,11 +48,13 @@ fn interpolates_the_input() {
|
|||||||
let keyframes2 = Keyframes {
|
let keyframes2 = Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "4".to_string(),
|
||||||
value: -100.0,
|
value: -100.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "5".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 1.0,
|
offset: 1.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -145,16 +150,19 @@ fn gets_value_at_frame() {
|
|||||||
let keyframes = Keyframes {
|
let keyframes = Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "1".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "2".to_string(),
|
||||||
value: 100.0,
|
value: 100.0,
|
||||||
offset: 1.0,
|
offset: 1.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "3".to_string(),
|
||||||
value: 300.0,
|
value: 300.0,
|
||||||
offset: 3.0,
|
offset: 3.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
|
@ -3,6 +3,7 @@ use super::{
|
|||||||
keyframe::{Keyframe, Keyframes},
|
keyframe::{Keyframe, Keyframes},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub trait AnimatedValue<T> {
|
pub trait AnimatedValue<T> {
|
||||||
fn sort_keyframes(&mut self);
|
fn sort_keyframes(&mut self);
|
||||||
@ -28,6 +29,7 @@ impl AnimatedFloat {
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: Uuid::new_v4().to_string(),
|
||||||
value: val,
|
value: val,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -93,7 +95,7 @@ impl AnimatedValue<(f32, f32, f32)> for AnimatedFloatVec3 {
|
|||||||
|
|
||||||
let z = self
|
let z = self
|
||||||
.keyframes
|
.keyframes
|
||||||
.1
|
.2
|
||||||
.get_value_at_frame(curr_frame, animation_data, fps);
|
.get_value_at_frame(curr_frame, animation_data, fps);
|
||||||
|
|
||||||
return (x, y, z);
|
return (x, y, z);
|
||||||
|
@ -70,6 +70,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
|||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "1".to_string(),
|
||||||
value: (size.0 * -1) as f32,
|
value: (size.0 * -1) as f32,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: Some(InterpolationType::EasingFunction(
|
interpolation: Some(InterpolationType::EasingFunction(
|
||||||
@ -77,6 +78,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
|||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "2".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 5.0,
|
offset: 5.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -87,6 +89,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: "3".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -100,6 +103,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: "4".to_string(),
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
value: size.0 as f32,
|
value: size.0 as f32,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
@ -109,6 +113,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: "5".to_string(),
|
||||||
value: size.1 as f32,
|
value: size.1 as f32,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -193,6 +198,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "1".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: Some(InterpolationType::Spring(
|
interpolation: Some(InterpolationType::Spring(
|
||||||
@ -204,6 +210,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "2".to_string(),
|
||||||
value: (size.0 / 2) as f32,
|
value: (size.0 / 2) as f32,
|
||||||
offset: 2.0,
|
offset: 2.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -214,6 +221,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: "3".to_string(),
|
||||||
value: (size.1 / 2) as f32,
|
value: (size.1 / 2) as f32,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -240,6 +248,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![
|
values: vec![
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "5".to_string(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: Some(InterpolationType::Spring(
|
interpolation: Some(InterpolationType::Spring(
|
||||||
@ -251,6 +260,8 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Keyframe {
|
Keyframe {
|
||||||
|
id: "6".to_string(),
|
||||||
|
|
||||||
value: (size.0 / 2) as f32,
|
value: (size.0 / 2) as f32,
|
||||||
offset: 2.0,
|
offset: 2.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
@ -261,6 +272,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
AnimatedFloat {
|
AnimatedFloat {
|
||||||
keyframes: Keyframes {
|
keyframes: Keyframes {
|
||||||
values: vec![Keyframe {
|
values: vec![Keyframe {
|
||||||
|
id: "7".to_string(),
|
||||||
value: ((size.1 / 2) as f32) + 80.0,
|
value: ((size.1 / 2) as f32) + 80.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
interpolation: None,
|
interpolation: None,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use font_kit::source::SystemSource;
|
use font_kit::source::SystemSource;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_system_fonts() -> Option<Vec<String>> {
|
pub fn get_system_fonts() -> Option<Vec<String>> {
|
||||||
@ -9,7 +10,7 @@ pub fn get_system_fonts() -> Option<Vec<String>> {
|
|||||||
match found_fonts {
|
match found_fonts {
|
||||||
Ok(found_fonts) => {
|
Ok(found_fonts) => {
|
||||||
let font_names: Vec<String> = found_fonts
|
let font_names: Vec<String> = found_fonts
|
||||||
.iter()
|
.par_iter()
|
||||||
.map(|f| f.load())
|
.map(|f| f.load())
|
||||||
.filter(|f| f.is_ok())
|
.filter(|f| f.is_ok())
|
||||||
.map(|f| f.unwrap())
|
.map(|f| f.unwrap())
|
||||||
@ -24,6 +25,15 @@ pub fn get_system_fonts() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_system_families() -> Option<Vec<String>> {
|
||||||
|
let source = SystemSource::new();
|
||||||
|
|
||||||
|
let found_families = source.all_families();
|
||||||
|
|
||||||
|
found_families.ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_system_font(font_name: String) -> Option<Vec<u8>> {
|
pub fn get_system_font(font_name: String) -> Option<Vec<u8>> {
|
||||||
let source = SystemSource::new();
|
let source = SystemSource::new();
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
animation::timeline::calculate_timeline_entities_at_frame,
|
animation::timeline::calculate_timeline_entities_at_frame,
|
||||||
fonts::{get_system_font, get_system_fonts},
|
fonts::{get_system_families, get_system_font, get_system_fonts},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
@ -14,6 +14,7 @@ fn main() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
calculate_timeline_entities_at_frame,
|
calculate_timeline_entities_at_frame,
|
||||||
get_system_font,
|
get_system_font,
|
||||||
|
get_system_families,
|
||||||
get_system_fonts
|
get_system_fonts
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
@ -7,12 +7,15 @@ import ToolBar from "components/ToolBar";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/tauri";
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
import { useFontsStore } from "stores/fonts.store";
|
import { useFontsStore } from "stores/fonts.store";
|
||||||
|
import useKeyControls from "hooks/useKeyControls";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { setFonts } = useFontsStore();
|
const { setFonts } = useFontsStore();
|
||||||
|
|
||||||
|
useKeyControls();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invoke("get_system_fonts").then((data) => {
|
invoke("get_system_families").then((data) => {
|
||||||
if (data && Array.isArray(data)) {
|
if (data && Array.isArray(data)) {
|
||||||
setFonts(data);
|
setFonts(data);
|
||||||
}
|
}
|
||||||
|
@ -127,13 +127,6 @@ export const TextProperties: FC<TextPropertiesProps> = ({
|
|||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</label>
|
</label>
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, origin: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Origin"
|
|
||||||
entity={entity.origin}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -217,13 +210,6 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, origin: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Origin"
|
|
||||||
entity={entity.origin}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -240,20 +226,6 @@ export const RectProperties: FC<RectPropertiesProps> = ({
|
|||||||
onUpdate({ ...entity, paint: { ...entity.paint, ...paint } })
|
onUpdate({ ...entity, paint: { ...entity.paint, ...paint } })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, position: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Position"
|
|
||||||
entity={entity.position}
|
|
||||||
/>
|
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, size: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Size"
|
|
||||||
entity={entity.size}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -270,20 +242,6 @@ export const EllipseProperties: FC<EllipsePropertiesProps> = ({
|
|||||||
onUpdate({ ...entity, paint: { ...entity.paint, ...paint } })
|
onUpdate({ ...entity, paint: { ...entity.paint, ...paint } })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, position: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Position"
|
|
||||||
entity={entity.position}
|
|
||||||
/>
|
|
||||||
<AnimatedVec2Properties
|
|
||||||
onUpdate={(updatedEntity) =>
|
|
||||||
onUpdate({ ...entity, radius: updatedEntity })
|
|
||||||
}
|
|
||||||
label="Size"
|
|
||||||
entity={entity.radius}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,8 @@ import { z } from "zod";
|
|||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { Interpolation } from "primitives/Interpolation";
|
import { Interpolation } from "primitives/Interpolation";
|
||||||
import { Color } from "primitives/Paint";
|
import { Color } from "primitives/Paint";
|
||||||
|
import { colorToString, parseColor, parseCssColor } from "@tempblade/common";
|
||||||
|
import { rgbToHex } from "utils";
|
||||||
|
|
||||||
const InterpolationProperties: FC<
|
const InterpolationProperties: FC<
|
||||||
PropertiesProps<z.input<typeof Interpolation>>
|
PropertiesProps<z.input<typeof Interpolation>>
|
||||||
@ -76,8 +78,43 @@ const AnimatedNumberProperties: FC<
|
|||||||
export const ColorProperties: FC<
|
export const ColorProperties: FC<
|
||||||
PropertiesProps<z.input<typeof Color>> & {
|
PropertiesProps<z.input<typeof Color>> & {
|
||||||
label: string;
|
label: string;
|
||||||
|
mode?: "RGB" | "Picker";
|
||||||
}
|
}
|
||||||
> = ({ entity, onUpdate }) => {
|
> = ({ entity, onUpdate, mode = "Picker" }) => {
|
||||||
|
if (mode === "Picker") {
|
||||||
|
return (
|
||||||
|
<label className="flex flex-col items-start">
|
||||||
|
<span className="label">Color</span>
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<input
|
||||||
|
value={rgbToHex(entity.value[0], entity.value[1], entity.value[2])}
|
||||||
|
type="color"
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
backgroundColor: rgbToHex(
|
||||||
|
entity.value[0],
|
||||||
|
entity.value[1],
|
||||||
|
entity.value[2]
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate(
|
||||||
|
produce(entity, (draft) => {
|
||||||
|
const color = parseCssColor(e.target.value);
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
draft.value = [...color, 1.0];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="flex flex-col items-start">
|
<label className="flex flex-col items-start">
|
||||||
<span className="label">Color</span>
|
<span className="label">Color</span>
|
||||||
|
@ -5,13 +5,24 @@ import { Keyframe } from "primitives/Keyframe";
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TIMELINE_SCALE } from "./common";
|
import { TIMELINE_SCALE } from "./common";
|
||||||
|
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
||||||
|
import { useKeyframeStore } from "stores/keyframe.store";
|
||||||
|
|
||||||
const KeyframeIndicator: FC<{
|
const KeyframeIndicator: FC<{
|
||||||
keyframe: z.input<typeof Keyframe>;
|
keyframe: z.input<typeof Keyframe>;
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
}> = ({ keyframe, animationData }) => {
|
}> = ({ keyframe, animationData }) => {
|
||||||
|
const { selectedKeyframe, selectKeyframe, deselectKeyframe } =
|
||||||
|
useKeyframeStore();
|
||||||
|
|
||||||
|
const selected = selectedKeyframe === keyframe.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
drag="x"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
data-selected={selected}
|
||||||
|
dragConstraints={{ left: 0 }}
|
||||||
animate={{
|
animate={{
|
||||||
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||||
}}
|
}}
|
||||||
@ -19,9 +30,105 @@ const KeyframeIndicator: FC<{
|
|||||||
style={{
|
style={{
|
||||||
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
|
||||||
}}
|
}}
|
||||||
className="bg-indigo-300 absolute w-2 h-2 z-30 top-[39%] select-none pointer-events-none"
|
onClick={() =>
|
||||||
|
selected ? deselectKeyframe() : selectKeyframe(keyframe.id)
|
||||||
|
}
|
||||||
|
className="bg-indigo-500 data-[selected=true]:bg-indigo-300 absolute w-2 h-2 z-30 select-none"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AnimatedNumberKeyframeIndicator: FC<{
|
||||||
|
animatedNumber: z.input<typeof AnimatedNumber>;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
}> = ({ animatedNumber, animationData }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{animatedNumber.keyframes.values.map((keyframe) => (
|
||||||
|
<KeyframeIndicator
|
||||||
|
key={keyframe.id}
|
||||||
|
keyframe={keyframe}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DimensionsVec2 = "x" | "y";
|
||||||
|
const VEC2_DIMENSION_INDEX_MAPPING: Record<DimensionsVec2, number> = {
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnimatedVec2KeyframeIndicator: FC<{
|
||||||
|
animatedVec2: z.input<typeof AnimatedVec2>;
|
||||||
|
dimension?: DimensionsVec2;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
}> = ({ animatedVec2, animationData, dimension }) => {
|
||||||
|
if (dimension) {
|
||||||
|
return (
|
||||||
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
animationData={animationData}
|
||||||
|
animatedNumber={
|
||||||
|
animatedVec2.keyframes[VEC2_DIMENSION_INDEX_MAPPING[dimension]]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{animatedVec2.keyframes.map((animatedNumber, index) => (
|
||||||
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
key={index}
|
||||||
|
animatedNumber={animatedNumber}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DimensionsVec3 = "x" | "y" | "z";
|
||||||
|
const VEC3_DIMENSION_INDEX_MAPPING: Record<DimensionsVec3, number> = {
|
||||||
|
x: 0,
|
||||||
|
y: 1,
|
||||||
|
z: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnimatedVec3KeyframeIndicator: FC<{
|
||||||
|
animatedVec3: z.input<typeof AnimatedVec3>;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
dimension?: DimensionsVec3;
|
||||||
|
}> = ({ animatedVec3, animationData, dimension }) => {
|
||||||
|
if (dimension) {
|
||||||
|
return (
|
||||||
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
animationData={animationData}
|
||||||
|
animatedNumber={
|
||||||
|
animatedVec3.keyframes[VEC3_DIMENSION_INDEX_MAPPING[dimension]]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{animatedVec3.keyframes.map((animatedNumber, index) => (
|
||||||
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
key={index}
|
||||||
|
animatedNumber={animatedNumber}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
AnimatedNumberKeyframeIndicator,
|
||||||
|
AnimatedVec3KeyframeIndicator,
|
||||||
|
AnimatedVec2KeyframeIndicator,
|
||||||
|
};
|
||||||
export default KeyframeIndicator;
|
export default KeyframeIndicator;
|
||||||
|
@ -2,12 +2,17 @@ import { ease } from "@unom/style";
|
|||||||
import { useDragControls, Reorder, motion } from "framer-motion";
|
import { useDragControls, Reorder, motion } from "framer-motion";
|
||||||
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
|
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
|
||||||
import { Keyframe } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import KeyframeIndicator from "./KeyframeIndicator";
|
import KeyframeIndicator, {
|
||||||
|
AnimatedVec2KeyframeIndicator,
|
||||||
|
AnimatedVec3KeyframeIndicator,
|
||||||
|
} from "./KeyframeIndicator";
|
||||||
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||||
|
import { TriangleDownIcon } from "@radix-ui/react-icons";
|
||||||
|
import TrackPropertiesEditor from "./TrackPropertiesEditor";
|
||||||
|
|
||||||
type TrackProps = {
|
type TrackProps = {
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
@ -26,6 +31,8 @@ const Track: FC<TrackProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const controls = useDragControls();
|
const controls = useDragControls();
|
||||||
|
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
|
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
|
||||||
useEntitiesStore(
|
useEntitiesStore(
|
||||||
(store) => ({
|
(store) => ({
|
||||||
@ -42,137 +49,156 @@ const Track: FC<TrackProps> = ({
|
|||||||
value={entity}
|
value={entity}
|
||||||
dragListener={false}
|
dragListener={false}
|
||||||
dragControls={controls}
|
dragControls={controls}
|
||||||
className="h-8 relative flex flex-1 flex-row gap-1 select-none"
|
className="min-h-8 relative flex flex-1 flex-col gap-1 select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex flex-row gap-1 select-none">
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
<div
|
||||||
onPointerDown={(e) => controls.start(e)}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
className={`h-full transition-all rounded-sm min-w-[200px] p-1 px-2 flex flex-row ${
|
onPointerDown={(e) => controls.start(e)}
|
||||||
selectedEntity === index ? "bg-gray-800" : "bg-gray-900"
|
className={`h-full transition-all rounded-sm min-w-[200px] p-1 px-2 flex flex-col ${
|
||||||
}`}
|
selectedEntity === index ? "bg-gray-800" : "bg-gray-900"
|
||||||
>
|
}`}
|
||||||
<h3
|
|
||||||
onClick={() =>
|
|
||||||
selectedEntity !== undefined && selectedEntity === index
|
|
||||||
? deselectEntity()
|
|
||||||
: selectEntity(index)
|
|
||||||
}
|
|
||||||
className="text-white-800 select-none cursor-pointer"
|
|
||||||
>
|
>
|
||||||
{name}
|
<div className="flex flex-row">
|
||||||
</h3>
|
<motion.div
|
||||||
</div>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="will-change-transform"
|
||||||
|
animate={{ rotate: isExpanded ? 0 : -90 }}
|
||||||
|
>
|
||||||
|
<TriangleDownIcon
|
||||||
|
width="32px"
|
||||||
|
height="32px"
|
||||||
|
className="text-white"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<h3
|
||||||
|
onClick={() =>
|
||||||
|
selectedEntity !== undefined && selectedEntity === index
|
||||||
|
? deselectEntity()
|
||||||
|
: selectEntity(index)
|
||||||
|
}
|
||||||
|
className="text-white-800 select-none cursor-pointer"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ width: TIMELINE_SCALE * 10 }}
|
style={{ width: TIMELINE_SCALE * 10 }}
|
||||||
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
|
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
|
||||||
>
|
>
|
||||||
{keyframes.map((keyframe, index) => (
|
{!isExpanded &&
|
||||||
<KeyframeIndicator
|
keyframes.map((keyframe, index) => (
|
||||||
animationData={animationData}
|
<KeyframeIndicator
|
||||||
keyframe={keyframe}
|
animationData={animationData}
|
||||||
key={index}
|
keyframe={keyframe}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
drag="x"
|
||||||
|
animate={{
|
||||||
|
x: animationData.offset * TIMELINE_SCALE,
|
||||||
|
}}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.1,
|
||||||
|
}}
|
||||||
|
whileTap={{
|
||||||
|
scale: 0.9,
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
transition={ease.circ(0.6).out}
|
||||||
|
dragElastic={false}
|
||||||
|
dragConstraints={{ left: 0 }}
|
||||||
|
onDragEnd={(e, info) => {
|
||||||
|
let offset = info.offset.x;
|
||||||
|
|
||||||
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
|
const animationOffset =
|
||||||
|
animationData.offset + offset < 0
|
||||||
|
? 0
|
||||||
|
: animationData.offset + offset;
|
||||||
|
|
||||||
|
const duration = animationData.duration - offset;
|
||||||
|
|
||||||
|
updateEntity(index, {
|
||||||
|
animation_data: {
|
||||||
|
...animationData,
|
||||||
|
offset: animationOffset < 0 ? 0 : animationOffset,
|
||||||
|
duration: duration < 0 ? 0 : duration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-w-resize"
|
||||||
/>
|
/>
|
||||||
))}
|
<motion.div
|
||||||
<motion.div
|
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-e-resize"
|
||||||
drag="x"
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
animate={{
|
drag="x"
|
||||||
x: animationData.offset * TIMELINE_SCALE,
|
animate={{
|
||||||
}}
|
x:
|
||||||
whileHover={{
|
(animationData.duration + animationData.offset) *
|
||||||
scale: 1.1,
|
TIMELINE_SCALE -
|
||||||
}}
|
16,
|
||||||
whileTap={{
|
}}
|
||||||
scale: 0.9,
|
whileHover={{
|
||||||
}}
|
scale: 1.1,
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
}}
|
||||||
transition={ease.circ(0.6).out}
|
whileTap={{
|
||||||
dragElastic={false}
|
scale: 0.9,
|
||||||
dragConstraints={{ left: 0 }}
|
}}
|
||||||
onDragEnd={(e, info) => {
|
transition={ease.circ(0.6).out}
|
||||||
let offset = info.offset.x;
|
dragConstraints={{ left: 0 }}
|
||||||
|
onDragEnd={(e, info) => {
|
||||||
|
let offset = info.offset.x;
|
||||||
|
|
||||||
offset = calculateOffset(offset);
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
const animationOffset =
|
const duration = animationData.duration + offset;
|
||||||
animationData.offset + offset < 0
|
|
||||||
? 0
|
|
||||||
: animationData.offset + offset;
|
|
||||||
|
|
||||||
const duration = animationData.duration - offset;
|
updateEntity(index, {
|
||||||
|
animation_data: {
|
||||||
|
...animationData,
|
||||||
|
duration: duration < 0 ? 0 : duration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
drag="x"
|
||||||
|
animate={{
|
||||||
|
width: animationData.duration * TIMELINE_SCALE,
|
||||||
|
x: animationData.offset * TIMELINE_SCALE,
|
||||||
|
}}
|
||||||
|
whileHover={{ scaleY: 1.1 }}
|
||||||
|
whileTap={{ scaleY: 0.9 }}
|
||||||
|
dragConstraints={{
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
transition={ease.circ(0.8).out}
|
||||||
|
onDragEnd={(_e, info) => {
|
||||||
|
let offset = info.offset.x;
|
||||||
|
|
||||||
updateEntity(index, {
|
offset = calculateOffset(offset);
|
||||||
animation_data: {
|
|
||||||
...animationData,
|
|
||||||
offset: animationOffset < 0 ? 0 : animationOffset,
|
|
||||||
duration: duration < 0 ? 0 : duration,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-w-resize"
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
drag="x"
|
|
||||||
animate={{
|
|
||||||
x:
|
|
||||||
(animationData.duration + animationData.offset) * TIMELINE_SCALE -
|
|
||||||
16,
|
|
||||||
}}
|
|
||||||
whileHover={{
|
|
||||||
scale: 1.1,
|
|
||||||
}}
|
|
||||||
whileTap={{
|
|
||||||
scale: 0.9,
|
|
||||||
}}
|
|
||||||
transition={ease.circ(0.6).out}
|
|
||||||
dragConstraints={{ left: 0 }}
|
|
||||||
onDragEnd={(e, info) => {
|
|
||||||
let offset = info.offset.x;
|
|
||||||
|
|
||||||
offset = calculateOffset(offset);
|
offset += animationData.offset;
|
||||||
|
|
||||||
const duration = animationData.duration + offset;
|
updateEntity(index, {
|
||||||
|
animation_data: {
|
||||||
updateEntity(index, {
|
...animationData,
|
||||||
animation_data: {
|
offset: offset < 0 ? 0 : offset,
|
||||||
...animationData,
|
},
|
||||||
duration: duration < 0 ? 0 : duration,
|
});
|
||||||
},
|
}}
|
||||||
});
|
className="z-5 h-8 absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||||
}}
|
></motion.div>
|
||||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-e-resize"
|
</div>
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
drag="x"
|
|
||||||
animate={{
|
|
||||||
width: animationData.duration * TIMELINE_SCALE,
|
|
||||||
x: animationData.offset * TIMELINE_SCALE,
|
|
||||||
}}
|
|
||||||
whileHover={{ scaleY: 1.1 }}
|
|
||||||
whileTap={{ scaleY: 0.9 }}
|
|
||||||
dragConstraints={{
|
|
||||||
left: 0,
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
transition={ease.circ(0.8).out}
|
|
||||||
onDragEnd={(_e, info) => {
|
|
||||||
let offset = info.offset.x;
|
|
||||||
|
|
||||||
offset = calculateOffset(offset);
|
|
||||||
|
|
||||||
offset += animationData.offset;
|
|
||||||
|
|
||||||
updateEntity(index, {
|
|
||||||
animation_data: {
|
|
||||||
...animationData,
|
|
||||||
offset: offset < 0 ? 0 : offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
|
||||||
></motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{isExpanded && <TrackPropertiesEditor entity={entity} />}
|
||||||
</Reorder.Item>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
125
app/src/components/Timeline/TrackPropertiesEditor.tsx
Normal file
125
app/src/components/Timeline/TrackPropertiesEditor.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
AnimatedEntity,
|
||||||
|
AnimationData,
|
||||||
|
getAnimatedPropertiesByAnimatedEntity,
|
||||||
|
} from "primitives/AnimatedEntities";
|
||||||
|
import { AnimatedProperty } from "primitives/AnimatedProperty";
|
||||||
|
import { AnimatedVec2, ValueType } from "primitives/Values";
|
||||||
|
import { FC, useMemo, useState } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
AnimatedNumberKeyframeIndicator,
|
||||||
|
AnimatedVec2KeyframeIndicator,
|
||||||
|
AnimatedVec3KeyframeIndicator,
|
||||||
|
} from "./KeyframeIndicator";
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from "components/ToggleGroup";
|
||||||
|
|
||||||
|
const TrackAnimatedPropertyKeyframes: FC<{
|
||||||
|
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
selectedDimension?: "x" | "y" | "z";
|
||||||
|
}> = ({ animatedProperty, animationData, selectedDimension }) => {
|
||||||
|
switch (animatedProperty.animatedValue.type) {
|
||||||
|
case "Number":
|
||||||
|
return (
|
||||||
|
<AnimatedNumberKeyframeIndicator
|
||||||
|
animatedNumber={animatedProperty.animatedValue}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "Vec2":
|
||||||
|
return (
|
||||||
|
<AnimatedVec2KeyframeIndicator
|
||||||
|
dimension={selectedDimension !== "z" ? selectedDimension : undefined}
|
||||||
|
animatedVec2={animatedProperty.animatedValue}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "Vec3":
|
||||||
|
return (
|
||||||
|
<AnimatedVec3KeyframeIndicator
|
||||||
|
dimension={selectedDimension}
|
||||||
|
animatedVec3={animatedProperty.animatedValue}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TrackAnimatedProperty: FC<{
|
||||||
|
animatedProperty: z.input<typeof AnimatedProperty>;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
trackIndex: number;
|
||||||
|
}> = ({ animatedProperty, animationData }) => {
|
||||||
|
const [selectedDimension, setSelectedDimension] = useState<"x" | "y" | "z">();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="min-w-[200px] flex flex-row justify-between">
|
||||||
|
<h3>{animatedProperty.label}</h3>
|
||||||
|
<ToggleGroup>
|
||||||
|
<ToggleGroupItem
|
||||||
|
onClick={() => setSelectedDimension("x")}
|
||||||
|
selected={selectedDimension === "x"}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
onClick={() => setSelectedDimension("y")}
|
||||||
|
selected={selectedDimension === "y"}
|
||||||
|
>
|
||||||
|
Y
|
||||||
|
</ToggleGroupItem>
|
||||||
|
{animatedProperty.animatedValue.type === ValueType.Enum.Vec3 && (
|
||||||
|
<ToggleGroupItem
|
||||||
|
onClick={() => setSelectedDimension("z")}
|
||||||
|
selected={selectedDimension === "z"}
|
||||||
|
>
|
||||||
|
Z
|
||||||
|
</ToggleGroupItem>
|
||||||
|
)}
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<TrackAnimatedPropertyKeyframes
|
||||||
|
selectedDimension={
|
||||||
|
animatedProperty.animatedValue.type !== "Number"
|
||||||
|
? selectedDimension
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
animatedProperty={animatedProperty}
|
||||||
|
animationData={animationData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TrackPropertiesEditor: FC<{ entity: z.input<typeof AnimatedEntity> }> = ({
|
||||||
|
entity,
|
||||||
|
}) => {
|
||||||
|
const animatedProperties = useMemo(
|
||||||
|
() => getAnimatedPropertiesByAnimatedEntity(entity),
|
||||||
|
[entity]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{animatedProperties.map((animatedProperty, index) => (
|
||||||
|
<TrackAnimatedProperty
|
||||||
|
trackIndex={index}
|
||||||
|
animationData={entity.animation_data}
|
||||||
|
key={index}
|
||||||
|
animatedProperty={animatedProperty}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrackPropertiesEditor;
|
||||||
|
|
||||||
|
AnimatedVec2._def.typeName;
|
37
app/src/components/ToggleGroup.tsx
Normal file
37
app/src/components/ToggleGroup.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
import * as ToggleGroupComponents from "@radix-ui/react-toggle-group";
|
||||||
|
|
||||||
|
const ToggleGroupItem: FC<{
|
||||||
|
children: ReactNode;
|
||||||
|
selected: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}> = ({ children, selected, onClick }) => {
|
||||||
|
return (
|
||||||
|
<ToggleGroupComponents.Item
|
||||||
|
data-selected={selected}
|
||||||
|
onClick={onClick}
|
||||||
|
className="hover:bg-indigo-400 text-white data-[selected=true]:bg-indigo-600
|
||||||
|
data-[selected=true]:text-indigo-200 flex h-6 w-6
|
||||||
|
items-center justify-center bg-slate-900 text-sm leading-4
|
||||||
|
first:rounded-l last:rounded-r focus:z-10 focus:shadow-[0_0_0_2px] focus:shadow-black
|
||||||
|
focus:outline-none"
|
||||||
|
value="left"
|
||||||
|
aria-label="Left aligned"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupComponents.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggleGroup: FC<{ children: ReactNode }> = ({ children }) => (
|
||||||
|
<ToggleGroupComponents.Root
|
||||||
|
className="inline-flex bg-slate-800 rounded shadow-[0_2px_10px] shadow-black space-x-px"
|
||||||
|
type="single"
|
||||||
|
defaultValue="center"
|
||||||
|
aria-label="Text alignment"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupComponents.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
@ -16,7 +16,7 @@ export function handleEntityCache<
|
|||||||
const cached = cache.get();
|
const cached = cache.get();
|
||||||
|
|
||||||
if (!entity.cache.valid) {
|
if (!entity.cache.valid) {
|
||||||
console.log("Invalid cache");
|
// console.log("Invalid cache");
|
||||||
if (cached) {
|
if (cached) {
|
||||||
cache.cleanup(cached);
|
cache.cleanup(cached);
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,7 @@ export class Drawer {
|
|||||||
|
|
||||||
async loadCanvasKit(canvas: HTMLCanvasElement) {
|
async loadCanvasKit(canvas: HTMLCanvasElement) {
|
||||||
await InitCanvasKit({
|
await InitCanvasKit({
|
||||||
locateFile: (file) =>
|
locateFile: (file) => file,
|
||||||
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
|
||||||
}).then((CanvasKit) => {
|
}).then((CanvasKit) => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas);
|
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas);
|
||||||
|
@ -108,8 +108,6 @@ export function calculateLetters(
|
|||||||
|
|
||||||
const font = new CanvasKit.Font(typeface, entity.letter.paint.size);
|
const font = new CanvasKit.Font(typeface, entity.letter.paint.size);
|
||||||
|
|
||||||
console.log(font.isDeleted());
|
|
||||||
|
|
||||||
const glyphIDs = font.getGlyphIDs(entity.text);
|
const glyphIDs = font.getGlyphIDs(entity.text);
|
||||||
|
|
||||||
// font.setLinearMetrics(true);
|
// font.setLinearMetrics(true);
|
||||||
@ -252,19 +250,29 @@ export default function drawStaggeredText(
|
|||||||
origin[0] =
|
origin[0] =
|
||||||
origin[0] +
|
origin[0] +
|
||||||
measuredLetter.bounds.width / 2 +
|
measuredLetter.bounds.width / 2 +
|
||||||
measuredLetter.offset.x;
|
measuredLetter.offset.x +
|
||||||
origin[1] = origin[1] - metrics.descent + lineOffset;
|
letterTransform.translate[0];
|
||||||
|
origin[1] =
|
||||||
|
origin[1] -
|
||||||
|
metrics.descent +
|
||||||
|
lineOffset +
|
||||||
|
letterTransform.translate[1];
|
||||||
|
|
||||||
//console.log(measuredLetter.bounds);
|
//console.log(measuredLetter.bounds);
|
||||||
|
|
||||||
canvas.translate(origin[0], origin[1]);
|
canvas.translate(origin[0], origin[1]);
|
||||||
|
|
||||||
|
canvas.rotate(
|
||||||
|
letterTransform.rotate[2],
|
||||||
|
letterTransform.rotate[0],
|
||||||
|
letterTransform.rotate[1]
|
||||||
|
);
|
||||||
|
|
||||||
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
||||||
|
|
||||||
canvas.rotate(
|
canvas.translate(
|
||||||
letterTransform.rotate[0],
|
letterTransform.translate[0],
|
||||||
letterTransform.rotate[1],
|
letterTransform.translate[1]
|
||||||
letterTransform.rotate[2]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
canvas.translate(
|
canvas.translate(
|
||||||
|
@ -25,11 +25,14 @@ function buildRect1(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
type: "Vec2",
|
||||||
keyframes: [
|
keyframes: [
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "EasingFunction",
|
type: "EasingFunction",
|
||||||
easing_function: "CircOut",
|
easing_function: "CircOut",
|
||||||
@ -38,6 +41,7 @@ function buildRect1(
|
|||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
@ -77,15 +81,20 @@ function buildRect(
|
|||||||
origin: staticAnimatedVec2(0, -720),
|
origin: staticAnimatedVec2(0, -720),
|
||||||
position: staticAnimatedVec2(1280 / 2, 720 / 2),
|
position: staticAnimatedVec2(1280 / 2, 720 / 2),
|
||||||
transform: {
|
transform: {
|
||||||
|
type: "Transform",
|
||||||
translate: staticAnimatedVec2(0, 0),
|
translate: staticAnimatedVec2(0, 0),
|
||||||
rotate: staticAnimatedVec2(0, 0),
|
rotate: staticAnimatedVec3(0, 0, 0),
|
||||||
skew: staticAnimatedVec2(0, 0),
|
skew: staticAnimatedVec2(0, 0),
|
||||||
scale: {
|
scale: {
|
||||||
|
type: "Vec2",
|
||||||
keyframes: [
|
keyframes: [
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
@ -96,9 +105,12 @@ function buildRect(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "EasingFunction",
|
type: "EasingFunction",
|
||||||
easing_function: "CircOut",
|
easing_function: "CircOut",
|
||||||
@ -107,6 +119,8 @@ function buildRect(
|
|||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
@ -152,21 +166,25 @@ function buildText(
|
|||||||
duration: 5.0,
|
duration: 5.0,
|
||||||
},
|
},
|
||||||
origin: {
|
origin: {
|
||||||
|
type: "Vec2",
|
||||||
keyframes: [
|
keyframes: [
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Spring",
|
type: "EasingFunction",
|
||||||
mass: 1,
|
easing_function: "CircOut",
|
||||||
stiffness: 100,
|
|
||||||
damping: 15,
|
|
||||||
},
|
},
|
||||||
value: (1280 / 2) * -1 - 300,
|
value: (1280 / 2) * -1 - 300,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "EasingFunction",
|
type: "EasingFunction",
|
||||||
easing_function: "QuartOut",
|
easing_function: "QuartOut",
|
||||||
@ -195,8 +213,9 @@ function buildStaggeredText(
|
|||||||
id: uuid(),
|
id: uuid(),
|
||||||
origin: staticAnimatedVec2(1280 / 2, 720 / 2),
|
origin: staticAnimatedVec2(1280 / 2, 720 / 2),
|
||||||
transform: {
|
transform: {
|
||||||
|
type: "Transform",
|
||||||
translate: staticAnimatedVec2(0, 0),
|
translate: staticAnimatedVec2(0, 0),
|
||||||
rotate: staticAnimatedVec2(0, 0),
|
rotate: staticAnimatedVec3(0, 0, 0),
|
||||||
skew: staticAnimatedVec2(0, 0),
|
skew: staticAnimatedVec2(0, 0),
|
||||||
scale: staticAnimatedVec2(1, 1),
|
scale: staticAnimatedVec2(1, 1),
|
||||||
},
|
},
|
||||||
@ -204,7 +223,7 @@ function buildStaggeredText(
|
|||||||
offset,
|
offset,
|
||||||
duration: 5.0,
|
duration: 5.0,
|
||||||
},
|
},
|
||||||
stagger: 0.05,
|
stagger: 0.1,
|
||||||
letter: {
|
letter: {
|
||||||
paint: {
|
paint: {
|
||||||
font_name: "Arial",
|
font_name: "Arial",
|
||||||
@ -216,53 +235,128 @@ function buildStaggeredText(
|
|||||||
align: "Center",
|
align: "Center",
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
translate: staticAnimatedVec2(0, 0),
|
type: "Transform",
|
||||||
rotate: staticAnimatedVec3(0, 0, 45),
|
translate: {
|
||||||
skew: staticAnimatedVec2(0, 0),
|
type: "Vec2",
|
||||||
scale: {
|
|
||||||
keyframes: [
|
keyframes: [
|
||||||
|
staticAnimatedNumber(0),
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Spring",
|
type: "Spring",
|
||||||
stiffness: 200,
|
|
||||||
mass: 1,
|
|
||||||
damping: 15,
|
damping: 15,
|
||||||
|
stiffness: 350,
|
||||||
|
mass: 1,
|
||||||
},
|
},
|
||||||
value: 5.0,
|
value: 200.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
value: 1.0,
|
value: 0.0,
|
||||||
offset: 4.0,
|
offset: 4.0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rotate: {
|
||||||
|
type: "Vec3",
|
||||||
|
keyframes: [
|
||||||
|
staticAnimatedNumber(0),
|
||||||
|
staticAnimatedNumber(0),
|
||||||
{
|
{
|
||||||
|
type: "Number",
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Spring",
|
type: "Spring",
|
||||||
stiffness: 300,
|
|
||||||
mass: 1,
|
|
||||||
damping: 15,
|
damping: 15,
|
||||||
|
stiffness: 150,
|
||||||
|
mass: 1,
|
||||||
},
|
},
|
||||||
value: -10.0,
|
value: -180.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
type: "Linear",
|
||||||
|
},
|
||||||
|
value: 0.0,
|
||||||
|
offset: 4.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
skew: staticAnimatedVec2(0, 0),
|
||||||
|
scale: {
|
||||||
|
type: "Vec2",
|
||||||
|
keyframes: [
|
||||||
|
{
|
||||||
|
type: "Number",
|
||||||
|
keyframes: {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
type: "EasingFunction",
|
||||||
|
easing_function: "CircOut",
|
||||||
|
},
|
||||||
|
value: 0.0,
|
||||||
|
offset: 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
value: 1.0,
|
value: 1.0,
|
||||||
offset: 4.0,
|
offset: 2.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "Number",
|
||||||
|
keyframes: {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
type: "EasingFunction",
|
||||||
|
easing_function: "CircOut",
|
||||||
|
},
|
||||||
|
value: 0.0,
|
||||||
|
offset: 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
type: "Linear",
|
||||||
|
},
|
||||||
|
value: 1.0,
|
||||||
|
offset: 2.0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
27
app/src/hooks/useKeyControls.ts
Normal file
27
app/src/hooks/useKeyControls.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
|
||||||
|
export default function useKeyControls() {
|
||||||
|
const handleKeyPress = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (e.code === "Space") {
|
||||||
|
useRenderStateStore.getState().togglePlaying();
|
||||||
|
}
|
||||||
|
if (e.code === "Backspace") {
|
||||||
|
const selectedEntity = useEntitiesStore.getState().selectedEntity;
|
||||||
|
if (selectedEntity !== undefined) {
|
||||||
|
useEntitiesStore.getState().deleteEntity(selectedEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// attach the event listener
|
||||||
|
document.addEventListener("keydown", handleKeyPress);
|
||||||
|
|
||||||
|
// remove the event listener
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress);
|
||||||
|
};
|
||||||
|
}, [handleKeyPress]);
|
||||||
|
}
|
@ -6,8 +6,9 @@ import {
|
|||||||
RectEntity,
|
RectEntity,
|
||||||
TextEntity,
|
TextEntity,
|
||||||
} from "./Entities";
|
} from "./Entities";
|
||||||
import { AnimatedVec2, AnimatedVec3 } from "./Values";
|
import { AnimatedTransform, AnimatedVec2 } from "./Values";
|
||||||
import { TextPaint } from "./Paint";
|
import { TextPaint } from "./Paint";
|
||||||
|
import { AnimatedProperties } from "./AnimatedProperty";
|
||||||
|
|
||||||
export const AnimationData = z.object({
|
export const AnimationData = z.object({
|
||||||
offset: z.number(),
|
offset: z.number(),
|
||||||
@ -15,17 +16,6 @@ export const AnimationData = z.object({
|
|||||||
visible: z.boolean().optional().default(true),
|
visible: z.boolean().optional().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AnimatedTransform = z.object({
|
|
||||||
/** Translates by the given animated vec2 */
|
|
||||||
translate: AnimatedVec2,
|
|
||||||
/** Skews by the given animated vec2 */
|
|
||||||
skew: AnimatedVec2,
|
|
||||||
/** Rotates by the given animated vec2 */
|
|
||||||
rotate: AnimatedVec3,
|
|
||||||
/** Scales on the x and y axis by the given animated vec2 */
|
|
||||||
scale: AnimatedVec2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AnimatedStaggeredTextEntity = BaseEntity.extend({
|
export const AnimatedStaggeredTextEntity = BaseEntity.extend({
|
||||||
/** Transform applied to the whole layer. */
|
/** Transform applied to the whole layer. */
|
||||||
transform: AnimatedTransform,
|
transform: AnimatedTransform,
|
||||||
@ -72,3 +62,133 @@ export const AnimatedEntity = z.discriminatedUnion("type", [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const AnimatedEntities = z.array(AnimatedEntity);
|
export const AnimatedEntities = z.array(AnimatedEntity);
|
||||||
|
|
||||||
|
export function animatedTransformToAnimatedProperties(
|
||||||
|
animatedTransform: z.input<typeof AnimatedTransform>,
|
||||||
|
basePath?: string
|
||||||
|
): z.input<typeof AnimatedProperties> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
animatedValue: animatedTransform.translate,
|
||||||
|
label: "Translation",
|
||||||
|
propertyPath: basePath
|
||||||
|
? basePath + ".transform.translate"
|
||||||
|
: "transform.translate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
animatedValue: animatedTransform.rotate,
|
||||||
|
label: "Rotation",
|
||||||
|
propertyPath: basePath
|
||||||
|
? basePath + ".transform.rotate"
|
||||||
|
: "transform.rotate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
animatedValue: animatedTransform.scale,
|
||||||
|
label: "Scale",
|
||||||
|
propertyPath: basePath
|
||||||
|
? basePath + ".transform.scale"
|
||||||
|
: "transform.scale",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
animatedValue: animatedTransform.skew,
|
||||||
|
label: "Skew",
|
||||||
|
propertyPath: basePath ? basePath + ".transform.skew" : "transform.skew",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnimatedPropertiesByAnimatedEntity(
|
||||||
|
animatedEntity: z.input<typeof AnimatedEntity>
|
||||||
|
) {
|
||||||
|
const animatedProperties: z.input<typeof AnimatedProperties> = [];
|
||||||
|
|
||||||
|
switch (animatedEntity.type) {
|
||||||
|
case "Ellipse":
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "origin",
|
||||||
|
animatedValue: animatedEntity.origin,
|
||||||
|
label: "Origin",
|
||||||
|
});
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "radius",
|
||||||
|
animatedValue: animatedEntity.radius,
|
||||||
|
label: "Radius",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (animatedEntity.transform) {
|
||||||
|
animatedProperties.push(
|
||||||
|
...animatedTransformToAnimatedProperties(animatedEntity.transform)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Rect":
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "origin",
|
||||||
|
animatedValue: animatedEntity.origin,
|
||||||
|
label: "Origin",
|
||||||
|
});
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "radius",
|
||||||
|
animatedValue: animatedEntity.size,
|
||||||
|
label: "Radius",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (animatedEntity.transform) {
|
||||||
|
animatedProperties.push(
|
||||||
|
...animatedTransformToAnimatedProperties(animatedEntity.transform)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "StaggeredText":
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "origin",
|
||||||
|
animatedValue: animatedEntity.origin,
|
||||||
|
label: "Origin",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (animatedEntity.transform) {
|
||||||
|
animatedProperties.push(
|
||||||
|
...animatedTransformToAnimatedProperties(animatedEntity.transform)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animatedEntity.letter.transform) {
|
||||||
|
animatedProperties.push(
|
||||||
|
...animatedTransformToAnimatedProperties(
|
||||||
|
animatedEntity.letter.transform,
|
||||||
|
"letter"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Text":
|
||||||
|
animatedProperties.push({
|
||||||
|
propertyPath: "origin",
|
||||||
|
animatedValue: animatedEntity.origin,
|
||||||
|
label: "Origin",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (animatedEntity.transform) {
|
||||||
|
animatedProperties.push(
|
||||||
|
...animatedTransformToAnimatedProperties(animatedEntity.transform)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return animatedProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnimatedPropertiesByAnimatedEnties(
|
||||||
|
animatedEntities: z.input<typeof AnimatedEntities>
|
||||||
|
) {
|
||||||
|
const animatedProperties: z.input<typeof AnimatedProperties> = [];
|
||||||
|
|
||||||
|
animatedEntities.forEach((aEnt) => {
|
||||||
|
animatedProperties.push(...getAnimatedPropertiesByAnimatedEntity(aEnt));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
10
app/src/primitives/AnimatedProperty.ts
Normal file
10
app/src/primitives/AnimatedProperty.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { AnimatedValue } from "./Values";
|
||||||
|
|
||||||
|
export const AnimatedProperty = z.object({
|
||||||
|
propertyPath: z.string(),
|
||||||
|
animatedValue: AnimatedValue,
|
||||||
|
label: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AnimatedProperties = z.array(AnimatedProperty);
|
@ -1,7 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Interpolation } from "./Interpolation";
|
import { Interpolation } from "./Interpolation";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
export const Keyframe = z.object({
|
export const Keyframe = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
value: z.number(),
|
value: z.number(),
|
||||||
offset: z.number(),
|
offset: z.number(),
|
||||||
interpolation: z.optional(Interpolation),
|
interpolation: z.optional(Interpolation),
|
||||||
|
@ -1,29 +1,57 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Keyframes } from "./Keyframe";
|
import { Keyframes } from "./Keyframe";
|
||||||
import { Interpolation } from "./Interpolation";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
export const Vec2 = z.array(z.number()).length(2);
|
export const Vec2 = z.array(z.number()).length(2);
|
||||||
export const Vec3 = z.array(z.number()).length(3);
|
export const Vec3 = z.array(z.number()).length(3);
|
||||||
|
|
||||||
|
const ValueTypeOptions = ["Vec2", "Vec3", "Number"] as const;
|
||||||
|
|
||||||
|
export const ValueType = z.enum(ValueTypeOptions);
|
||||||
|
|
||||||
export const AnimatedNumber = z.object({
|
export const AnimatedNumber = z.object({
|
||||||
keyframes: Keyframes,
|
keyframes: Keyframes,
|
||||||
|
type: z.literal(ValueType.Enum.Number),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AnimatedVec2 = z.object({
|
export const AnimatedVec2 = z.object({
|
||||||
keyframes: z.array(AnimatedNumber).length(2),
|
keyframes: z.array(AnimatedNumber).length(2),
|
||||||
|
type: z.literal(ValueType.Enum.Vec2),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AnimatedVec3 = z.object({
|
export const AnimatedVec3 = z.object({
|
||||||
keyframes: z.array(AnimatedNumber).length(3),
|
keyframes: z.array(AnimatedNumber).length(3),
|
||||||
|
type: z.literal(ValueType.Enum.Vec3),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AnimatedTransform = z.object({
|
||||||
|
type: z.literal("Transform"),
|
||||||
|
/** Translates by the given animated vec2 */
|
||||||
|
translate: AnimatedVec2,
|
||||||
|
/** Skews by the given animated vec2 */
|
||||||
|
skew: AnimatedVec2,
|
||||||
|
/** Rotates by the given animated vec3 */
|
||||||
|
rotate: AnimatedVec3,
|
||||||
|
/** Scales on the x and y axis by the given animated vec2 */
|
||||||
|
scale: AnimatedVec2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AnimatedValue = z.discriminatedUnion("type", [
|
||||||
|
AnimatedNumber,
|
||||||
|
AnimatedVec2,
|
||||||
|
AnimatedVec3,
|
||||||
|
AnimatedTransform,
|
||||||
|
]);
|
||||||
|
|
||||||
export function staticAnimatedNumber(
|
export function staticAnimatedNumber(
|
||||||
number: number
|
number: number
|
||||||
): z.infer<typeof AnimatedNumber> {
|
): z.infer<typeof AnimatedNumber> {
|
||||||
return {
|
return {
|
||||||
|
type: ValueType.Enum.Number,
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
id: uuid(),
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
@ -40,6 +68,7 @@ export function staticAnimatedVec2(
|
|||||||
y: number
|
y: number
|
||||||
): z.infer<typeof AnimatedVec2> {
|
): z.infer<typeof AnimatedVec2> {
|
||||||
return {
|
return {
|
||||||
|
type: ValueType.Enum.Vec2,
|
||||||
keyframes: [staticAnimatedNumber(x), staticAnimatedNumber(y)],
|
keyframes: [staticAnimatedNumber(x), staticAnimatedNumber(y)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -48,8 +77,9 @@ export function staticAnimatedVec3(
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
z: number
|
z: number
|
||||||
): z.infer<typeof AnimatedVec2> {
|
): z.infer<typeof AnimatedVec3> {
|
||||||
return {
|
return {
|
||||||
|
type: ValueType.Enum.Vec3,
|
||||||
keyframes: [
|
keyframes: [
|
||||||
staticAnimatedNumber(x),
|
staticAnimatedNumber(x),
|
||||||
staticAnimatedNumber(y),
|
staticAnimatedNumber(y),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api";
|
||||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
import { Entities, Entity, EntityType } from "primitives/Entities";
|
import { Entities, EntityType } from "primitives/Entities";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||||
@ -28,8 +28,6 @@ export class DependenciesService {
|
|||||||
) {
|
) {
|
||||||
const fontNames = new Set<string>();
|
const fontNames = new Set<string>();
|
||||||
|
|
||||||
console.log(entities);
|
|
||||||
|
|
||||||
entities.forEach((entity) => {
|
entities.forEach((entity) => {
|
||||||
switch (entity.type) {
|
switch (entity.type) {
|
||||||
case EntityType.Enum.Text:
|
case EntityType.Enum.Text:
|
||||||
@ -78,9 +76,5 @@ export class DependenciesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(resolveFonts);
|
await Promise.all(resolveFonts);
|
||||||
|
|
||||||
console.log(fontNames);
|
|
||||||
|
|
||||||
// console.log(this.dependencies);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { create } from "zustand";
|
|||||||
interface EntitiesStore {
|
interface EntitiesStore {
|
||||||
entities: z.input<typeof AnimatedEntities>;
|
entities: z.input<typeof AnimatedEntities>;
|
||||||
selectedEntity: number | undefined;
|
selectedEntity: number | undefined;
|
||||||
|
selectedKeyframe: string | undefined;
|
||||||
selectEntity: (index: number) => void;
|
selectEntity: (index: number) => void;
|
||||||
deselectEntity: () => void;
|
deselectEntity: () => void;
|
||||||
setEntities: (entities: z.input<typeof AnimatedEntities>) => void;
|
setEntities: (entities: z.input<typeof AnimatedEntities>) => void;
|
||||||
@ -14,6 +15,8 @@ interface EntitiesStore {
|
|||||||
index: number,
|
index: number,
|
||||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
deleteEntity: (index: number) => void;
|
||||||
updateEntityById: (
|
updateEntityById: (
|
||||||
id: string,
|
id: string,
|
||||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||||
@ -22,6 +25,7 @@ interface EntitiesStore {
|
|||||||
|
|
||||||
const useEntitiesStore = create<EntitiesStore>((set) => ({
|
const useEntitiesStore = create<EntitiesStore>((set) => ({
|
||||||
entities: EXAMPLE_ANIMATED_ENTITIES,
|
entities: EXAMPLE_ANIMATED_ENTITIES,
|
||||||
|
selectedKeyframe: undefined,
|
||||||
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
||||||
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
||||||
selectedEntity: undefined,
|
selectedEntity: undefined,
|
||||||
@ -36,6 +40,14 @@ const useEntitiesStore = create<EntitiesStore>((set) => ({
|
|||||||
>;
|
>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { entities: nextEntities };
|
||||||
|
}),
|
||||||
|
deleteEntity: (index) =>
|
||||||
|
set(({ entities }) => {
|
||||||
|
const nextEntities = produce(entities, (draft) => {
|
||||||
|
draft.splice(index, 1);
|
||||||
|
});
|
||||||
|
|
||||||
return { entities: nextEntities };
|
return { entities: nextEntities };
|
||||||
}),
|
}),
|
||||||
updateEntity: (index, entity) =>
|
updateEntity: (index, entity) =>
|
||||||
|
15
app/src/stores/keyframe.store.ts
Normal file
15
app/src/stores/keyframe.store.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface KeyframeStore {
|
||||||
|
selectedKeyframe: string | undefined;
|
||||||
|
selectKeyframe: (id: string) => void;
|
||||||
|
deselectKeyframe: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useKeyframeStore = create<KeyframeStore>((set) => ({
|
||||||
|
selectKeyframe: (id) => set({ selectedKeyframe: id }),
|
||||||
|
deselectKeyframe: () => set({ selectedKeyframe: undefined }),
|
||||||
|
selectedKeyframe: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export { useKeyframeStore };
|
@ -6,14 +6,16 @@ interface RenderStateStore {
|
|||||||
renderState: z.infer<typeof RenderState>;
|
renderState: z.infer<typeof RenderState>;
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
setPlaying: (playing: boolean) => void;
|
setPlaying: (playing: boolean) => void;
|
||||||
|
togglePlaying: () => void;
|
||||||
setCurrentFrame: (target: number) => void;
|
setCurrentFrame: (target: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRenderStateStore = create<RenderStateStore>((set) => ({
|
const useRenderStateStore = create<RenderStateStore>((set, get) => ({
|
||||||
renderState: {
|
renderState: {
|
||||||
curr_frame: 20,
|
curr_frame: 20,
|
||||||
},
|
},
|
||||||
playing: false,
|
playing: false,
|
||||||
|
togglePlaying: () => set({ playing: !get().playing }),
|
||||||
setPlaying: (playing) => set({ playing }),
|
setPlaying: (playing) => set({ playing }),
|
||||||
setCurrentFrame: (target) =>
|
setCurrentFrame: (target) =>
|
||||||
set((store) => {
|
set((store) => {
|
||||||
|
@ -7,7 +7,7 @@ interface TimelineStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useTimelineStore = create<TimelineStore>((set) => ({
|
const useTimelineStore = create<TimelineStore>((set) => ({
|
||||||
fps: 30,
|
fps: 60,
|
||||||
size: [1280, 720],
|
size: [1280, 720],
|
||||||
duration: 10.0,
|
duration: 10.0,
|
||||||
}));
|
}));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
||||||
import { Keyframe } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { AnimatedNumber, AnimatedVec2 } from "primitives/Values";
|
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export function flattenAnimatedNumberKeyframes(
|
export function flattenAnimatedNumberKeyframes(
|
||||||
@ -9,6 +9,15 @@ export function flattenAnimatedNumberKeyframes(
|
|||||||
return aNumber.keyframes.values;
|
return aNumber.keyframes.values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function componentToHex(c: number) {
|
||||||
|
var hex = c.toString(16);
|
||||||
|
return hex.length == 1 ? "0" + hex : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToHex(r: number, g: number, b: number) {
|
||||||
|
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||||
|
}
|
||||||
|
|
||||||
export function flattenAnimatedVec2Keyframes(
|
export function flattenAnimatedVec2Keyframes(
|
||||||
aVec2: z.input<typeof AnimatedVec2>
|
aVec2: z.input<typeof AnimatedVec2>
|
||||||
): Array<z.input<typeof Keyframe>> {
|
): Array<z.input<typeof Keyframe>> {
|
||||||
@ -20,6 +29,18 @@ export function flattenAnimatedVec2Keyframes(
|
|||||||
return keyframes;
|
return keyframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenAnimatedVec3Keyframes(
|
||||||
|
aVec3: z.input<typeof AnimatedVec3>
|
||||||
|
): Array<z.input<typeof Keyframe>> {
|
||||||
|
const keyframes: Array<z.input<typeof Keyframe>> = [
|
||||||
|
...flattenAnimatedNumberKeyframes(aVec3.keyframes[0]),
|
||||||
|
...flattenAnimatedNumberKeyframes(aVec3.keyframes[1]),
|
||||||
|
...flattenAnimatedNumberKeyframes(aVec3.keyframes[2]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return keyframes;
|
||||||
|
}
|
||||||
|
|
||||||
export function flattenedKeyframesByEntity(
|
export function flattenedKeyframesByEntity(
|
||||||
entity: z.input<typeof AnimatedEntity>
|
entity: z.input<typeof AnimatedEntity>
|
||||||
): Array<z.input<typeof Keyframe>> {
|
): Array<z.input<typeof Keyframe>> {
|
||||||
@ -39,7 +60,7 @@ export function flattenedKeyframesByEntity(
|
|||||||
break;
|
break;
|
||||||
case "StaggeredText":
|
case "StaggeredText":
|
||||||
keyframes.push(
|
keyframes.push(
|
||||||
...flattenAnimatedVec2Keyframes(entity.letter.transform.rotate)
|
...flattenAnimatedVec3Keyframes(entity.letter.transform.rotate)
|
||||||
);
|
);
|
||||||
keyframes.push(
|
keyframes.push(
|
||||||
...flattenAnimatedVec2Keyframes(entity.letter.transform.translate)
|
...flattenAnimatedVec2Keyframes(entity.letter.transform.translate)
|
||||||
|
5393
app/yarn.lock
5393
app/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user