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-menubar": "^1.0.2",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@radix-ui/react-toolbar": "^1.0.3",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"@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-build",
|
||||
"tint",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3376,11 +3377,24 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2"
|
||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||
dependencies = [
|
||||
"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]]
|
||||
|
@ -12,7 +12,10 @@ edition = "2021"
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.3", features = [] }
|
||||
|
||||
|
||||
[dependencies]
|
||||
|
||||
uuid = { version = "1.3.3", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
tauri = { version = "1.3", features = ["dialog-open", "dialog-save", "shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@ -22,9 +25,7 @@ logging_timer = "1.1.0"
|
||||
rayon = "1.7"
|
||||
font-kit = "0.11.0"
|
||||
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
|
@ -12,6 +12,7 @@ use super::{
|
||||
pub struct Keyframe {
|
||||
pub value: f32,
|
||||
pub offset: f32,
|
||||
pub id: String,
|
||||
pub interpolation: Option<InterpolationType>,
|
||||
}
|
||||
|
||||
|
@ -25,16 +25,19 @@ fn interpolates_the_input() {
|
||||
let keyframes1 = Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "1".to_string(),
|
||||
value: 0.0,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
},
|
||||
Keyframe {
|
||||
id: "2".to_string(),
|
||||
value: 100.0,
|
||||
offset: 1.0,
|
||||
interpolation: None,
|
||||
},
|
||||
Keyframe {
|
||||
id: "3".to_string(),
|
||||
value: 300.0,
|
||||
offset: 3.0,
|
||||
interpolation: None,
|
||||
@ -45,11 +48,13 @@ fn interpolates_the_input() {
|
||||
let keyframes2 = Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "4".to_string(),
|
||||
value: -100.0,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
},
|
||||
Keyframe {
|
||||
id: "5".to_string(),
|
||||
value: 0.0,
|
||||
offset: 1.0,
|
||||
interpolation: None,
|
||||
@ -145,16 +150,19 @@ fn gets_value_at_frame() {
|
||||
let keyframes = Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "1".to_string(),
|
||||
value: 0.0,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
},
|
||||
Keyframe {
|
||||
id: "2".to_string(),
|
||||
value: 100.0,
|
||||
offset: 1.0,
|
||||
interpolation: None,
|
||||
},
|
||||
Keyframe {
|
||||
id: "3".to_string(),
|
||||
value: 300.0,
|
||||
offset: 3.0,
|
||||
interpolation: None,
|
||||
|
@ -3,6 +3,7 @@ use super::{
|
||||
keyframe::{Keyframe, Keyframes},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub trait AnimatedValue<T> {
|
||||
fn sort_keyframes(&mut self);
|
||||
@ -28,6 +29,7 @@ impl AnimatedFloat {
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
value: val,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
@ -93,7 +95,7 @@ impl AnimatedValue<(f32, f32, f32)> for AnimatedFloatVec3 {
|
||||
|
||||
let z = self
|
||||
.keyframes
|
||||
.1
|
||||
.2
|
||||
.get_value_at_frame(curr_frame, animation_data, fps);
|
||||
|
||||
return (x, y, z);
|
||||
|
@ -70,6 +70,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||
keyframes: Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "1".to_string(),
|
||||
value: (size.0 * -1) as f32,
|
||||
offset: 0.0,
|
||||
interpolation: Some(InterpolationType::EasingFunction(
|
||||
@ -77,6 +78,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||
)),
|
||||
},
|
||||
Keyframe {
|
||||
id: "2".to_string(),
|
||||
value: 0.0,
|
||||
offset: 5.0,
|
||||
interpolation: None,
|
||||
@ -87,6 +89,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: "3".to_string(),
|
||||
value: 0.0,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
@ -100,6 +103,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: "4".to_string(),
|
||||
interpolation: None,
|
||||
value: size.0 as f32,
|
||||
offset: 0.0,
|
||||
@ -109,6 +113,7 @@ fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: "5".to_string(),
|
||||
value: size.1 as f32,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
@ -193,6 +198,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
keyframes: Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "1".to_string(),
|
||||
value: 0.0,
|
||||
offset: 0.0,
|
||||
interpolation: Some(InterpolationType::Spring(
|
||||
@ -204,6 +210,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
)),
|
||||
},
|
||||
Keyframe {
|
||||
id: "2".to_string(),
|
||||
value: (size.0 / 2) as f32,
|
||||
offset: 2.0,
|
||||
interpolation: None,
|
||||
@ -214,6 +221,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: "3".to_string(),
|
||||
value: (size.1 / 2) as f32,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
@ -240,6 +248,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
keyframes: Keyframes {
|
||||
values: vec![
|
||||
Keyframe {
|
||||
id: "5".to_string(),
|
||||
value: 0.0,
|
||||
offset: 0.0,
|
||||
interpolation: Some(InterpolationType::Spring(
|
||||
@ -251,6 +260,8 @@ pub fn test_timeline_entities_at_frame(
|
||||
)),
|
||||
},
|
||||
Keyframe {
|
||||
id: "6".to_string(),
|
||||
|
||||
value: (size.0 / 2) as f32,
|
||||
offset: 2.0,
|
||||
interpolation: None,
|
||||
@ -261,6 +272,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
AnimatedFloat {
|
||||
keyframes: Keyframes {
|
||||
values: vec![Keyframe {
|
||||
id: "7".to_string(),
|
||||
value: ((size.1 / 2) as f32) + 80.0,
|
||||
offset: 0.0,
|
||||
interpolation: None,
|
||||
|
@ -1,4 +1,5 @@
|
||||
use font_kit::source::SystemSource;
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_system_fonts() -> Option<Vec<String>> {
|
||||
@ -9,7 +10,7 @@ pub fn get_system_fonts() -> Option<Vec<String>> {
|
||||
match found_fonts {
|
||||
Ok(found_fonts) => {
|
||||
let font_names: Vec<String> = found_fonts
|
||||
.iter()
|
||||
.par_iter()
|
||||
.map(|f| f.load())
|
||||
.filter(|f| f.is_ok())
|
||||
.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]
|
||||
pub fn get_system_font(font_name: String) -> Option<Vec<u8>> {
|
||||
let source = SystemSource::new();
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
use crate::{
|
||||
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;
|
||||
@ -14,6 +14,7 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
calculate_timeline_entities_at_frame,
|
||||
get_system_font,
|
||||
get_system_families,
|
||||
get_system_fonts
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
@ -7,12 +7,15 @@ import ToolBar from "components/ToolBar";
|
||||
import { useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { useFontsStore } from "stores/fonts.store";
|
||||
import useKeyControls from "hooks/useKeyControls";
|
||||
|
||||
export default function App() {
|
||||
const { setFonts } = useFontsStore();
|
||||
|
||||
useKeyControls();
|
||||
|
||||
useEffect(() => {
|
||||
invoke("get_system_fonts").then((data) => {
|
||||
invoke("get_system_families").then((data) => {
|
||||
if (data && Array.isArray(data)) {
|
||||
setFonts(data);
|
||||
}
|
||||
|
@ -127,13 +127,6 @@ export const TextProperties: FC<TextPropertiesProps> = ({
|
||||
}
|
||||
></input>
|
||||
</label>
|
||||
<AnimatedVec2Properties
|
||||
onUpdate={(updatedEntity) =>
|
||||
onUpdate({ ...entity, origin: updatedEntity })
|
||||
}
|
||||
label="Origin"
|
||||
entity={entity.origin}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@ -240,20 +226,6 @@ export const RectProperties: FC<RectPropertiesProps> = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -270,20 +242,6 @@ export const EllipseProperties: FC<EllipsePropertiesProps> = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -5,6 +5,8 @@ import { z } from "zod";
|
||||
import { produce } from "immer";
|
||||
import { Interpolation } from "primitives/Interpolation";
|
||||
import { Color } from "primitives/Paint";
|
||||
import { colorToString, parseColor, parseCssColor } from "@tempblade/common";
|
||||
import { rgbToHex } from "utils";
|
||||
|
||||
const InterpolationProperties: FC<
|
||||
PropertiesProps<z.input<typeof Interpolation>>
|
||||
@ -76,8 +78,43 @@ const AnimatedNumberProperties: FC<
|
||||
export const ColorProperties: FC<
|
||||
PropertiesProps<z.input<typeof Color>> & {
|
||||
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 (
|
||||
<label className="flex flex-col items-start">
|
||||
<span className="label">Color</span>
|
||||
|
@ -5,13 +5,24 @@ import { Keyframe } from "primitives/Keyframe";
|
||||
import { FC } from "react";
|
||||
import { z } from "zod";
|
||||
import { TIMELINE_SCALE } from "./common";
|
||||
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
||||
import { useKeyframeStore } from "stores/keyframe.store";
|
||||
|
||||
const KeyframeIndicator: FC<{
|
||||
keyframe: z.input<typeof Keyframe>;
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
}> = ({ keyframe, animationData }) => {
|
||||
const { selectedKeyframe, selectKeyframe, deselectKeyframe } =
|
||||
useKeyframeStore();
|
||||
|
||||
const selected = selectedKeyframe === keyframe.id;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
drag="x"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
data-selected={selected}
|
||||
dragConstraints={{ left: 0 }}
|
||||
animate={{
|
||||
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||
}}
|
||||
@ -19,9 +30,105 @@ const KeyframeIndicator: FC<{
|
||||
style={{
|
||||
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;
|
||||
|
@ -2,12 +2,17 @@ import { ease } from "@unom/style";
|
||||
import { useDragControls, Reorder, motion } from "framer-motion";
|
||||
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
|
||||
import { Keyframe } from "primitives/Keyframe";
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { useEntitiesStore } from "stores/entities.store";
|
||||
import { z } from "zod";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import KeyframeIndicator from "./KeyframeIndicator";
|
||||
import KeyframeIndicator, {
|
||||
AnimatedVec2KeyframeIndicator,
|
||||
AnimatedVec3KeyframeIndicator,
|
||||
} from "./KeyframeIndicator";
|
||||
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||
import { TriangleDownIcon } from "@radix-ui/react-icons";
|
||||
import TrackPropertiesEditor from "./TrackPropertiesEditor";
|
||||
|
||||
type TrackProps = {
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
@ -26,6 +31,8 @@ const Track: FC<TrackProps> = ({
|
||||
}) => {
|
||||
const controls = useDragControls();
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
|
||||
useEntitiesStore(
|
||||
(store) => ({
|
||||
@ -42,15 +49,28 @@ const Track: FC<TrackProps> = ({
|
||||
value={entity}
|
||||
dragListener={false}
|
||||
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 className="flex flex-row gap-1 select-none">
|
||||
<div
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onPointerDown={(e) => controls.start(e)}
|
||||
className={`h-full transition-all rounded-sm min-w-[200px] p-1 px-2 flex flex-row ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<motion.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
|
||||
@ -62,18 +82,21 @@ const Track: FC<TrackProps> = ({
|
||||
{name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ width: TIMELINE_SCALE * 10 }}
|
||||
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
|
||||
>
|
||||
{keyframes.map((keyframe, index) => (
|
||||
{!isExpanded &&
|
||||
keyframes.map((keyframe, index) => (
|
||||
<KeyframeIndicator
|
||||
animationData={animationData}
|
||||
keyframe={keyframe}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
<motion.div
|
||||
drag="x"
|
||||
animate={{
|
||||
@ -109,14 +132,16 @@ const Track: FC<TrackProps> = ({
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-w-resize"
|
||||
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-w-resize"
|
||||
/>
|
||||
<motion.div
|
||||
className="z-10 w-4 bg-slate-500 h-8 absolute rounded-md select-none cursor-e-resize"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
drag="x"
|
||||
animate={{
|
||||
x:
|
||||
(animationData.duration + animationData.offset) * TIMELINE_SCALE -
|
||||
(animationData.duration + animationData.offset) *
|
||||
TIMELINE_SCALE -
|
||||
16,
|
||||
}}
|
||||
whileHover={{
|
||||
@ -141,7 +166,6 @@ const Track: FC<TrackProps> = ({
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-e-resize"
|
||||
/>
|
||||
<motion.div
|
||||
drag="x"
|
||||
@ -170,9 +194,11 @@ const Track: FC<TrackProps> = ({
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||
className="z-5 h-8 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>
|
||||
);
|
||||
};
|
||||
|
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();
|
||||
|
||||
if (!entity.cache.valid) {
|
||||
console.log("Invalid cache");
|
||||
// console.log("Invalid cache");
|
||||
if (cached) {
|
||||
cache.cleanup(cached);
|
||||
}
|
||||
|
@ -69,8 +69,7 @@ export class Drawer {
|
||||
|
||||
async loadCanvasKit(canvas: HTMLCanvasElement) {
|
||||
await InitCanvasKit({
|
||||
locateFile: (file) =>
|
||||
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
||||
locateFile: (file) => file,
|
||||
}).then((CanvasKit) => {
|
||||
if (canvas) {
|
||||
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas);
|
||||
|
@ -108,8 +108,6 @@ export function calculateLetters(
|
||||
|
||||
const font = new CanvasKit.Font(typeface, entity.letter.paint.size);
|
||||
|
||||
console.log(font.isDeleted());
|
||||
|
||||
const glyphIDs = font.getGlyphIDs(entity.text);
|
||||
|
||||
// font.setLinearMetrics(true);
|
||||
@ -252,19 +250,29 @@ export default function drawStaggeredText(
|
||||
origin[0] =
|
||||
origin[0] +
|
||||
measuredLetter.bounds.width / 2 +
|
||||
measuredLetter.offset.x;
|
||||
origin[1] = origin[1] - metrics.descent + lineOffset;
|
||||
measuredLetter.offset.x +
|
||||
letterTransform.translate[0];
|
||||
origin[1] =
|
||||
origin[1] -
|
||||
metrics.descent +
|
||||
lineOffset +
|
||||
letterTransform.translate[1];
|
||||
|
||||
//console.log(measuredLetter.bounds);
|
||||
|
||||
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.rotate(
|
||||
letterTransform.rotate[0],
|
||||
letterTransform.rotate[1],
|
||||
letterTransform.rotate[2]
|
||||
canvas.translate(
|
||||
letterTransform.translate[0],
|
||||
letterTransform.translate[1]
|
||||
);
|
||||
|
||||
canvas.translate(
|
||||
|
@ -25,11 +25,14 @@ function buildRect1(
|
||||
},
|
||||
},
|
||||
size: {
|
||||
type: "Vec2",
|
||||
keyframes: [
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
interpolation: {
|
||||
type: "EasingFunction",
|
||||
easing_function: "CircOut",
|
||||
@ -38,6 +41,7 @@ function buildRect1(
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
@ -77,15 +81,20 @@ function buildRect(
|
||||
origin: staticAnimatedVec2(0, -720),
|
||||
position: staticAnimatedVec2(1280 / 2, 720 / 2),
|
||||
transform: {
|
||||
type: "Transform",
|
||||
translate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec3(0, 0, 0),
|
||||
skew: staticAnimatedVec2(0, 0),
|
||||
scale: {
|
||||
type: "Vec2",
|
||||
keyframes: [
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
@ -96,9 +105,12 @@ function buildRect(
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "EasingFunction",
|
||||
easing_function: "CircOut",
|
||||
@ -107,6 +119,8 @@ function buildRect(
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
@ -152,21 +166,25 @@ function buildText(
|
||||
duration: 5.0,
|
||||
},
|
||||
origin: {
|
||||
type: "Vec2",
|
||||
keyframes: [
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Spring",
|
||||
mass: 1,
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
type: "EasingFunction",
|
||||
easing_function: "CircOut",
|
||||
},
|
||||
value: (1280 / 2) * -1 - 300,
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "EasingFunction",
|
||||
easing_function: "QuartOut",
|
||||
@ -195,8 +213,9 @@ function buildStaggeredText(
|
||||
id: uuid(),
|
||||
origin: staticAnimatedVec2(1280 / 2, 720 / 2),
|
||||
transform: {
|
||||
type: "Transform",
|
||||
translate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec3(0, 0, 0),
|
||||
skew: staticAnimatedVec2(0, 0),
|
||||
scale: staticAnimatedVec2(1, 1),
|
||||
},
|
||||
@ -204,7 +223,7 @@ function buildStaggeredText(
|
||||
offset,
|
||||
duration: 5.0,
|
||||
},
|
||||
stagger: 0.05,
|
||||
stagger: 0.1,
|
||||
letter: {
|
||||
paint: {
|
||||
font_name: "Arial",
|
||||
@ -216,53 +235,128 @@ function buildStaggeredText(
|
||||
align: "Center",
|
||||
},
|
||||
transform: {
|
||||
translate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec3(0, 0, 45),
|
||||
skew: staticAnimatedVec2(0, 0),
|
||||
scale: {
|
||||
type: "Transform",
|
||||
translate: {
|
||||
type: "Vec2",
|
||||
keyframes: [
|
||||
staticAnimatedNumber(0),
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Spring",
|
||||
stiffness: 200,
|
||||
mass: 1,
|
||||
damping: 15,
|
||||
stiffness: 350,
|
||||
mass: 1,
|
||||
},
|
||||
value: 5.0,
|
||||
value: 200.0,
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
value: 1.0,
|
||||
value: 0.0,
|
||||
offset: 4.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
rotate: {
|
||||
type: "Vec3",
|
||||
keyframes: [
|
||||
staticAnimatedNumber(0),
|
||||
staticAnimatedNumber(0),
|
||||
{
|
||||
type: "Number",
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
|
||||
interpolation: {
|
||||
type: "Spring",
|
||||
stiffness: 300,
|
||||
mass: 1,
|
||||
damping: 15,
|
||||
stiffness: 150,
|
||||
mass: 1,
|
||||
},
|
||||
value: -10.0,
|
||||
value: -180.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: {
|
||||
type: "Linear",
|
||||
},
|
||||
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,
|
||||
TextEntity,
|
||||
} from "./Entities";
|
||||
import { AnimatedVec2, AnimatedVec3 } from "./Values";
|
||||
import { AnimatedTransform, AnimatedVec2 } from "./Values";
|
||||
import { TextPaint } from "./Paint";
|
||||
import { AnimatedProperties } from "./AnimatedProperty";
|
||||
|
||||
export const AnimationData = z.object({
|
||||
offset: z.number(),
|
||||
@ -15,17 +16,6 @@ export const AnimationData = z.object({
|
||||
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({
|
||||
/** Transform applied to the whole layer. */
|
||||
transform: AnimatedTransform,
|
||||
@ -72,3 +62,133 @@ export const AnimatedEntity = z.discriminatedUnion("type", [
|
||||
]);
|
||||
|
||||
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 { Interpolation } from "./Interpolation";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
export const Keyframe = z.object({
|
||||
id: z.string().uuid(),
|
||||
value: z.number(),
|
||||
offset: z.number(),
|
||||
interpolation: z.optional(Interpolation),
|
||||
|
@ -1,29 +1,57 @@
|
||||
import { z } from "zod";
|
||||
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 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({
|
||||
keyframes: Keyframes,
|
||||
type: z.literal(ValueType.Enum.Number),
|
||||
});
|
||||
|
||||
export const AnimatedVec2 = z.object({
|
||||
keyframes: z.array(AnimatedNumber).length(2),
|
||||
type: z.literal(ValueType.Enum.Vec2),
|
||||
});
|
||||
|
||||
export const AnimatedVec3 = z.object({
|
||||
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(
|
||||
number: number
|
||||
): z.infer<typeof AnimatedNumber> {
|
||||
return {
|
||||
type: ValueType.Enum.Number,
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
id: uuid(),
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
@ -40,6 +68,7 @@ export function staticAnimatedVec2(
|
||||
y: number
|
||||
): z.infer<typeof AnimatedVec2> {
|
||||
return {
|
||||
type: ValueType.Enum.Vec2,
|
||||
keyframes: [staticAnimatedNumber(x), staticAnimatedNumber(y)],
|
||||
};
|
||||
}
|
||||
@ -48,8 +77,9 @@ export function staticAnimatedVec3(
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
): z.infer<typeof AnimatedVec2> {
|
||||
): z.infer<typeof AnimatedVec3> {
|
||||
return {
|
||||
type: ValueType.Enum.Vec3,
|
||||
keyframes: [
|
||||
staticAnimatedNumber(x),
|
||||
staticAnimatedNumber(y),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||
import { Entities, Entity, EntityType } from "primitives/Entities";
|
||||
import { Entities, EntityType } from "primitives/Entities";
|
||||
import { z } from "zod";
|
||||
|
||||
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||
@ -28,8 +28,6 @@ export class DependenciesService {
|
||||
) {
|
||||
const fontNames = new Set<string>();
|
||||
|
||||
console.log(entities);
|
||||
|
||||
entities.forEach((entity) => {
|
||||
switch (entity.type) {
|
||||
case EntityType.Enum.Text:
|
||||
@ -78,9 +76,5 @@ export class DependenciesService {
|
||||
});
|
||||
|
||||
await Promise.all(resolveFonts);
|
||||
|
||||
console.log(fontNames);
|
||||
|
||||
// console.log(this.dependencies);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { create } from "zustand";
|
||||
interface EntitiesStore {
|
||||
entities: z.input<typeof AnimatedEntities>;
|
||||
selectedEntity: number | undefined;
|
||||
selectedKeyframe: string | undefined;
|
||||
selectEntity: (index: number) => void;
|
||||
deselectEntity: () => void;
|
||||
setEntities: (entities: z.input<typeof AnimatedEntities>) => void;
|
||||
@ -14,6 +15,8 @@ interface EntitiesStore {
|
||||
index: number,
|
||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||
) => void;
|
||||
|
||||
deleteEntity: (index: number) => void;
|
||||
updateEntityById: (
|
||||
id: string,
|
||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||
@ -22,6 +25,7 @@ interface EntitiesStore {
|
||||
|
||||
const useEntitiesStore = create<EntitiesStore>((set) => ({
|
||||
entities: EXAMPLE_ANIMATED_ENTITIES,
|
||||
selectedKeyframe: undefined,
|
||||
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
||||
deselectEntity: () => set(() => ({ 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 };
|
||||
}),
|
||||
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>;
|
||||
playing: boolean;
|
||||
setPlaying: (playing: boolean) => void;
|
||||
togglePlaying: () => void;
|
||||
setCurrentFrame: (target: number) => void;
|
||||
}
|
||||
|
||||
const useRenderStateStore = create<RenderStateStore>((set) => ({
|
||||
const useRenderStateStore = create<RenderStateStore>((set, get) => ({
|
||||
renderState: {
|
||||
curr_frame: 20,
|
||||
},
|
||||
playing: false,
|
||||
togglePlaying: () => set({ playing: !get().playing }),
|
||||
setPlaying: (playing) => set({ playing }),
|
||||
setCurrentFrame: (target) =>
|
||||
set((store) => {
|
||||
|
@ -7,7 +7,7 @@ interface TimelineStore {
|
||||
}
|
||||
|
||||
const useTimelineStore = create<TimelineStore>((set) => ({
|
||||
fps: 30,
|
||||
fps: 60,
|
||||
size: [1280, 720],
|
||||
duration: 10.0,
|
||||
}));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
||||
import { Keyframe } from "primitives/Keyframe";
|
||||
import { AnimatedNumber, AnimatedVec2 } from "primitives/Values";
|
||||
import { AnimatedNumber, AnimatedVec2, AnimatedVec3 } from "primitives/Values";
|
||||
import { z } from "zod";
|
||||
|
||||
export function flattenAnimatedNumberKeyframes(
|
||||
@ -9,6 +9,15 @@ export function flattenAnimatedNumberKeyframes(
|
||||
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(
|
||||
aVec2: z.input<typeof AnimatedVec2>
|
||||
): Array<z.input<typeof Keyframe>> {
|
||||
@ -20,6 +29,18 @@ export function flattenAnimatedVec2Keyframes(
|
||||
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(
|
||||
entity: z.input<typeof AnimatedEntity>
|
||||
): Array<z.input<typeof Keyframe>> {
|
||||
@ -39,7 +60,7 @@ export function flattenedKeyframesByEntity(
|
||||
break;
|
||||
case "StaggeredText":
|
||||
keyframes.push(
|
||||
...flattenAnimatedVec2Keyframes(entity.letter.transform.rotate)
|
||||
...flattenAnimatedVec3Keyframes(entity.letter.transform.rotate)
|
||||
);
|
||||
keyframes.push(
|
||||
...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