improve ui

add track properties editor
This commit is contained in:
Enrico Bühler 2023-05-30 23:58:36 +02:00
parent 28613c9214
commit 8d1f949280
33 changed files with 2777 additions and 3751 deletions

View File

@ -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

Binary file not shown.

View File

@ -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]]

View File

@ -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"]

View File

@ -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>,
} }

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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();

View File

@ -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!())

View File

@ -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);
} }

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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;

View File

@ -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>
); );
}; };

View 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;

View 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 };

View File

@ -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);
} }

View File

@ -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);

View File

@ -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(

View File

@ -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,
}, },
], ],
}, },

View 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]);
}

View File

@ -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));
});
}

View 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);

View File

@ -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),

View File

@ -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),

View File

@ -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);
} }
} }

View File

@ -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) =>

View 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 };

View File

@ -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) => {

View File

@ -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,
})); }));

View File

@ -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)

File diff suppressed because it is too large Load Diff