add system font loading
improve drawing implement staggered text begin refactor of drawing code
This commit is contained in:
parent
8523e44029
commit
330fa6a7f0
5
app/.vscode/extensions.json
vendored
5
app/.vscode/extensions.json
vendored
@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
"recommendations": [
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
}
|
}
|
Binary file not shown.
@ -24,6 +24,7 @@
|
|||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"zod": "^3.21.4",
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"@types/node": "^18.7.10",
|
"@types/node": "^18.7.10",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/uuid": "^9",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
|
@ -37,6 +37,11 @@ pub trait Animateable {
|
|||||||
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity>;
|
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Cache {
|
||||||
|
pub valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum AnimatedEntity {
|
pub enum AnimatedEntity {
|
||||||
|
@ -9,11 +9,13 @@ use crate::animation::{
|
|||||||
timeline::Timeline,
|
timeline::Timeline,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::common::{Animateable, AnimationData, Drawable, Entity};
|
use super::common::{Animateable, AnimationData, Cache, Drawable, Entity};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AnimatedEllipseEntity {
|
pub struct AnimatedEllipseEntity {
|
||||||
pub paint: Paint,
|
pub paint: Paint,
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub radius: AnimatedFloatVec2,
|
pub radius: AnimatedFloatVec2,
|
||||||
pub origin: AnimatedFloatVec2,
|
pub origin: AnimatedFloatVec2,
|
||||||
pub position: AnimatedFloatVec2,
|
pub position: AnimatedFloatVec2,
|
||||||
@ -24,6 +26,8 @@ pub struct AnimatedEllipseEntity {
|
|||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct EllipseEntity {
|
pub struct EllipseEntity {
|
||||||
pub radius: (f32, f32),
|
pub radius: (f32, f32),
|
||||||
|
pub cache: Cache,
|
||||||
|
pub id: String,
|
||||||
pub position: (f32, f32),
|
pub position: (f32, f32),
|
||||||
pub origin: (f32, f32),
|
pub origin: (f32, f32),
|
||||||
pub paint: Paint,
|
pub paint: Paint,
|
||||||
@ -62,9 +66,11 @@ impl Animateable for AnimatedEllipseEntity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Some(Entity::Ellipse(EllipseEntity {
|
Some(Entity::Ellipse(EllipseEntity {
|
||||||
|
id: self.id.clone(),
|
||||||
radius,
|
radius,
|
||||||
position,
|
position,
|
||||||
origin,
|
origin,
|
||||||
|
cache: self.cache.clone(),
|
||||||
paint: self.paint.clone(),
|
paint: self.paint.clone(),
|
||||||
transform,
|
transform,
|
||||||
}))
|
}))
|
||||||
|
@ -9,10 +9,12 @@ use crate::animation::{
|
|||||||
timeline::Timeline,
|
timeline::Timeline,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::common::{Animateable, AnimationData, Drawable, Entity};
|
use super::common::{Animateable, AnimationData, Cache, Drawable, Entity};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AnimatedRectEntity {
|
pub struct AnimatedRectEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub position: AnimatedFloatVec2,
|
pub position: AnimatedFloatVec2,
|
||||||
pub size: AnimatedFloatVec2,
|
pub size: AnimatedFloatVec2,
|
||||||
pub origin: AnimatedFloatVec2,
|
pub origin: AnimatedFloatVec2,
|
||||||
@ -23,6 +25,8 @@ pub struct AnimatedRectEntity {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RectEntity {
|
pub struct RectEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub position: (f32, f32),
|
pub position: (f32, f32),
|
||||||
pub size: (f32, f32),
|
pub size: (f32, f32),
|
||||||
pub origin: (f32, f32),
|
pub origin: (f32, f32),
|
||||||
@ -71,6 +75,8 @@ impl Animateable for AnimatedRectEntity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Some(Entity::Rect(RectEntity {
|
Some(Entity::Rect(RectEntity {
|
||||||
|
id: self.id.clone(),
|
||||||
|
cache: self.cache.clone(),
|
||||||
position,
|
position,
|
||||||
size,
|
size,
|
||||||
origin,
|
origin,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use super::common::{Animateable, AnimationData, Drawable, Entity};
|
use super::common::{Animateable, AnimationData, Cache, Drawable, Entity};
|
||||||
use crate::animation::{
|
use crate::animation::{
|
||||||
primitives::{
|
primitives::{
|
||||||
paint::TextPaint,
|
paint::TextPaint,
|
||||||
transform::{AnimatedTransform, Transform},
|
transform::{AnimatedTransform, Transform},
|
||||||
|
values::{AnimatedFloatVec2, AnimatedValue},
|
||||||
},
|
},
|
||||||
timeline::Timeline,
|
timeline::Timeline,
|
||||||
};
|
};
|
||||||
@ -16,14 +17,17 @@ pub struct AnimatedStaggeredTextLetter {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StaggeredTextLetter {
|
pub struct StaggeredTextLetter {
|
||||||
pub transform: Option<Transform>,
|
pub transform: Option<Vec<Transform>>,
|
||||||
pub paint: TextPaint,
|
pub paint: TextPaint,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AnimatedStaggeredTextEntity {
|
pub struct AnimatedStaggeredTextEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub stagger: f32,
|
pub stagger: f32,
|
||||||
|
pub origin: AnimatedFloatVec2,
|
||||||
pub animation_data: AnimationData,
|
pub animation_data: AnimationData,
|
||||||
pub transform: Option<AnimatedTransform>,
|
pub transform: Option<AnimatedTransform>,
|
||||||
pub letter: AnimatedStaggeredTextLetter,
|
pub letter: AnimatedStaggeredTextLetter,
|
||||||
@ -31,8 +35,11 @@ pub struct AnimatedStaggeredTextEntity {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StaggeredTextEntity {
|
pub struct StaggeredTextEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub stagger: f32,
|
pub stagger: f32,
|
||||||
|
pub origin: (f32, f32),
|
||||||
pub transform: Option<Transform>,
|
pub transform: Option<Transform>,
|
||||||
pub animation_data: AnimationData,
|
pub animation_data: AnimationData,
|
||||||
pub letter: StaggeredTextLetter,
|
pub letter: StaggeredTextLetter,
|
||||||
@ -51,14 +58,36 @@ impl Animateable for AnimatedStaggeredTextEntity {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let letter_transform: Option<Transform> = match self.letter.transform.clone() {
|
// Iterate over the chars of the string and calculate the animation with the staggered offset
|
||||||
Some(mut val) => Some(val.calculate(timeline, &self.animation_data)),
|
let letter_transform: Option<Vec<Transform>> = match self.letter.transform.clone() {
|
||||||
|
Some(mut val) => {
|
||||||
|
let mut transforms: Vec<Transform> = Vec::new();
|
||||||
|
|
||||||
|
for c in self.text.chars().enumerate() {
|
||||||
|
let mut animation_data = self.animation_data.clone();
|
||||||
|
animation_data.offset += self.stagger * c.0 as f32;
|
||||||
|
|
||||||
|
let transform = val.calculate(timeline, &animation_data);
|
||||||
|
transforms.push(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(transforms)
|
||||||
|
}
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let origin = self.origin.get_value_at_frame(
|
||||||
|
timeline.render_state.curr_frame,
|
||||||
|
&self.animation_data,
|
||||||
|
timeline.fps,
|
||||||
|
);
|
||||||
|
|
||||||
Some(Entity::StaggeredText(StaggeredTextEntity {
|
Some(Entity::StaggeredText(StaggeredTextEntity {
|
||||||
|
id: self.id.clone(),
|
||||||
transform,
|
transform,
|
||||||
|
cache: self.cache.clone(),
|
||||||
stagger: self.stagger,
|
stagger: self.stagger,
|
||||||
|
origin,
|
||||||
text: self.text.clone(),
|
text: self.text.clone(),
|
||||||
animation_data: self.animation_data.clone(),
|
animation_data: self.animation_data.clone(),
|
||||||
letter: StaggeredTextLetter {
|
letter: StaggeredTextLetter {
|
||||||
|
@ -8,10 +8,12 @@ use crate::animation::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::common::{Animateable, AnimationData, Drawable, Entity};
|
use super::common::{Animateable, AnimationData, Cache, Drawable, Entity};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct TextEntity {
|
pub struct TextEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub origin: (f32, f32),
|
pub origin: (f32, f32),
|
||||||
pub paint: TextPaint,
|
pub paint: TextPaint,
|
||||||
@ -20,6 +22,8 @@ pub struct TextEntity {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AnimatedTextEntity {
|
pub struct AnimatedTextEntity {
|
||||||
|
pub id: String,
|
||||||
|
pub cache: Cache,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub origin: AnimatedFloatVec2,
|
pub origin: AnimatedFloatVec2,
|
||||||
pub paint: TextPaint,
|
pub paint: TextPaint,
|
||||||
@ -45,6 +49,8 @@ impl AnimatedTextEntity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TextEntity {
|
TextEntity {
|
||||||
|
id: self.id.clone(),
|
||||||
|
cache: self.cache.clone(),
|
||||||
transform,
|
transform,
|
||||||
text: self.text.clone(),
|
text: self.text.clone(),
|
||||||
origin,
|
origin,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::animation::primitives::{
|
use crate::animation::primitives::{
|
||||||
interpolations::{EasingFunction, InterpolationType, SpringProperties},
|
interpolations::{EasingFunction, InterpolationType, SpringProperties},
|
||||||
keyframe::{Keyframe, Keyframes},
|
keyframe::{Keyframe, Keyframes},
|
||||||
@ -7,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::primitives::{
|
use super::primitives::{
|
||||||
entities::{
|
entities::{
|
||||||
common::{AnimatedEntity, AnimationData, Entity},
|
common::{AnimatedEntity, AnimationData, Cache, Entity},
|
||||||
rect::AnimatedRectEntity,
|
rect::AnimatedRectEntity,
|
||||||
text::AnimatedTextEntity,
|
text::AnimatedTextEntity,
|
||||||
},
|
},
|
||||||
@ -52,12 +54,14 @@ impl Timeline {
|
|||||||
|
|
||||||
fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedRectEntity {
|
||||||
let bg_box = AnimatedRectEntity {
|
let bg_box = AnimatedRectEntity {
|
||||||
|
id: String::from_str("1").unwrap(),
|
||||||
paint,
|
paint,
|
||||||
animation_data: AnimationData {
|
animation_data: AnimationData {
|
||||||
offset: 0.0 + offset,
|
offset: 0.0 + offset,
|
||||||
duration: 5.0,
|
duration: 5.0,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
|
cache: Cache { valid: false },
|
||||||
transform: None,
|
transform: None,
|
||||||
origin: AnimatedFloatVec2::new(1280.0 / 2.0, 720.0 / 2.0),
|
origin: AnimatedFloatVec2::new(1280.0 / 2.0, 720.0 / 2.0),
|
||||||
position: AnimatedFloatVec2 {
|
position: AnimatedFloatVec2 {
|
||||||
@ -171,7 +175,9 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
AnimatedEntity::Rect(build_bg(0.5, rect2_paint, size)),
|
AnimatedEntity::Rect(build_bg(0.5, rect2_paint, size)),
|
||||||
AnimatedEntity::Rect(build_bg(1.0, rect3_paint, size)),
|
AnimatedEntity::Rect(build_bg(1.0, rect3_paint, size)),
|
||||||
AnimatedEntity::Text(AnimatedTextEntity {
|
AnimatedEntity::Text(AnimatedTextEntity {
|
||||||
|
id: String::from_str("2").unwrap(),
|
||||||
paint: title_paint,
|
paint: title_paint,
|
||||||
|
cache: Cache { valid: false },
|
||||||
text: input.title,
|
text: input.title,
|
||||||
animation_data: AnimationData {
|
animation_data: AnimationData {
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
@ -216,8 +222,10 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
AnimatedEntity::Text(AnimatedTextEntity {
|
AnimatedEntity::Text(AnimatedTextEntity {
|
||||||
|
id: String::from_str("3").unwrap(),
|
||||||
paint: sub_title_paint,
|
paint: sub_title_paint,
|
||||||
text: input.sub_title,
|
text: input.sub_title,
|
||||||
|
cache: Cache { valid: false },
|
||||||
animation_data: AnimationData {
|
animation_data: AnimationData {
|
||||||
offset: 0.5,
|
offset: 0.5,
|
||||||
duration: 6.0,
|
duration: 6.0,
|
||||||
|
@ -12,3 +12,27 @@ pub fn get_system_fonts() -> Option<Vec<String>> {
|
|||||||
|
|
||||||
found_families.ok()
|
found_families.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_system_font(font_name: String) -> Option<Vec<u8>> {
|
||||||
|
let source = SystemSource::new();
|
||||||
|
|
||||||
|
let font = source.select_by_postscript_name(font_name.as_str());
|
||||||
|
|
||||||
|
match font {
|
||||||
|
Ok(font) => {
|
||||||
|
let font = font.load();
|
||||||
|
|
||||||
|
if let Ok(font) = font {
|
||||||
|
if let Some(font_data) = font.copy_font_data() {
|
||||||
|
Some(font_data.as_slice().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use crate::{animation::timeline::calculate_timeline_entities_at_frame, fonts::get_system_fonts};
|
use crate::{
|
||||||
|
animation::timeline::calculate_timeline_entities_at_frame,
|
||||||
|
fonts::{get_system_font, get_system_fonts},
|
||||||
|
};
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
@ -10,6 +13,7 @@ fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.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_fonts
|
get_system_fonts
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import "./App.css";
|
|
||||||
import { invoke } from "@tauri-apps/api";
|
|
||||||
import { useTimelineStore } from "./stores/timeline.store";
|
|
||||||
import Timeline, { AnimationEntity } from "./components/Timeline";
|
|
||||||
import InitCanvasKit from "canvaskit-wasm";
|
|
||||||
import CanvasKitWasm from "canvaskit-wasm/bin/canvaskit.wasm?url";
|
|
||||||
|
|
||||||
const WIDTH = 1280 / 2;
|
|
||||||
const HEIGHT = 720 / 2;
|
|
||||||
|
|
||||||
const ENTITIES: Array<AnimationEntity> = [
|
|
||||||
{
|
|
||||||
offset: 0.2,
|
|
||||||
duration: 1.0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const [title, setTitle] = useState("Kleine");
|
|
||||||
const [subTitle, setSubTitle] = useState("Dumpfkopf");
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const { currentFrame } = useTimelineStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.time("render");
|
|
||||||
invoke("render_timeline_frame_cpu", {
|
|
||||||
currFrame: currentFrame,
|
|
||||||
title,
|
|
||||||
subTitle,
|
|
||||||
width: WIDTH,
|
|
||||||
height: HEIGHT,
|
|
||||||
}).then((data) => {
|
|
||||||
console.timeEnd("render");
|
|
||||||
if (canvas.current) {
|
|
||||||
const canvasElement = canvas.current;
|
|
||||||
|
|
||||||
canvasElement.width = WIDTH;
|
|
||||||
canvasElement.height = HEIGHT;
|
|
||||||
// console.time("draw");
|
|
||||||
const img = document.createElement("img");
|
|
||||||
|
|
||||||
const ctx = canvasElement.getContext("2d");
|
|
||||||
|
|
||||||
const arr = new Uint8ClampedArray(data as any);
|
|
||||||
|
|
||||||
const image = new Blob([arr], { type: "image/webp" });
|
|
||||||
img.src = URL.createObjectURL(image);
|
|
||||||
|
|
||||||
if (ctx) {
|
|
||||||
// ctx.fillStyle = "red";
|
|
||||||
// ctx.fillRect(0, 0, 1920, 1080);
|
|
||||||
}
|
|
||||||
|
|
||||||
img.onload = () => {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
// console.timeEnd("draw");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [currentFrame, title, subTitle]);
|
|
||||||
|
|
||||||
if (loading) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<div style={{ width: "600px" }}>
|
|
||||||
<canvas style={{ width: "100%", height: "100%" }} ref={canvas}></canvas>
|
|
||||||
</div>
|
|
||||||
<input value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
||||||
<input value={subTitle} onChange={(e) => setSubTitle(e.target.value)} />
|
|
||||||
<Timeline entities={ENTITIES} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -5,15 +5,27 @@ import { useTimelineStore } from "stores/timeline.store";
|
|||||||
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
|
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
|
||||||
import { Surface } from "canvaskit-wasm";
|
import { Surface } from "canvaskit-wasm";
|
||||||
import drawText from "drawers/text";
|
import drawText from "drawers/text";
|
||||||
import drawBox from "drawers/box";
|
import drawRect from "drawers/rect";
|
||||||
import { Entities, EntityType } from "primitives/Entities";
|
import { Entities, EntityType } from "primitives/Entities";
|
||||||
import drawEllipse from "drawers/ellipse";
|
import drawEllipse from "drawers/ellipse";
|
||||||
import { useRenderStateStore } from "stores/render-state.store";
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
|
import drawStaggeredText, {
|
||||||
|
StaggeredTextCache,
|
||||||
|
calculateLetters,
|
||||||
|
} from "drawers/staggered-text";
|
||||||
|
import useMap from "hooks/useMap";
|
||||||
|
|
||||||
type CanvasProps = {};
|
type CanvasProps = {};
|
||||||
|
|
||||||
|
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||||
|
return array.buffer.slice(
|
||||||
|
array.byteOffset,
|
||||||
|
array.byteLength + array.byteOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const CanvasComponent: FC<CanvasProps> = () => {
|
const CanvasComponent: FC<CanvasProps> = () => {
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
@ -21,15 +33,17 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
const [canvasKit, setCanvasKit] = useState<CanvasKit>();
|
const [canvasKit, setCanvasKit] = useState<CanvasKit>();
|
||||||
const [fontData, setFontData] = useState<ArrayBuffer>();
|
const [fontData, setFontData] = useState<ArrayBuffer>();
|
||||||
const surface = useRef<Surface>();
|
const surface = useRef<Surface>();
|
||||||
|
const staggeredTextCache = useMap<string, StaggeredTextCache>();
|
||||||
|
const isLocked = useRef<boolean>(false);
|
||||||
const renderState = useRenderStateStore((store) => store.renderState);
|
const renderState = useRenderStateStore((store) => store.renderState);
|
||||||
const { fps, size, duration } = useTimelineStore((store) => ({
|
const { fps, size, duration } = useTimelineStore((store) => ({
|
||||||
fps: store.fps,
|
fps: store.fps,
|
||||||
size: store.size,
|
size: store.size,
|
||||||
duration: store.duration,
|
duration: store.duration,
|
||||||
}));
|
}));
|
||||||
const { entities } = useEntitiesStore((store) => ({
|
const { entities, updateEntityById } = useEntitiesStore((store) => ({
|
||||||
entities: store.entities,
|
entities: store.entities,
|
||||||
|
updateEntityById: store.updateEntityById,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,14 +51,23 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
locateFile: (file) =>
|
locateFile: (file) =>
|
||||||
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
||||||
}).then((CanvasKit) => {
|
}).then((CanvasKit) => {
|
||||||
setLoading(false);
|
|
||||||
setCanvasKit(CanvasKit);
|
setCanvasKit(CanvasKit);
|
||||||
|
|
||||||
fetch("https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf")
|
/* fetch("https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf")
|
||||||
.then((response) => response.arrayBuffer())
|
.then((response) => response.arrayBuffer())
|
||||||
.then((arrayBuffer) => {
|
.then((arrayBuffer) => {
|
||||||
|
setLoading(false);
|
||||||
setFontData(arrayBuffer);
|
setFontData(arrayBuffer);
|
||||||
});
|
}); */
|
||||||
|
|
||||||
|
invoke("get_system_font", { fontName: "Helvetica-Bold" }).then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const u8 = new Uint8Array(data as any);
|
||||||
|
const buffer = typedArrayToBuffer(u8);
|
||||||
|
setFontData(buffer);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
if (canvas.current) {
|
if (canvas.current) {
|
||||||
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas.current);
|
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas.current);
|
||||||
@ -52,13 +75,17 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
surface.current = CSurface;
|
surface.current = CSurface;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.time("calculation");
|
//console.time("calculation");
|
||||||
const parsedEntities = AnimatedEntities.parse(entities);
|
const parsedEntities = AnimatedEntities.parse(entities);
|
||||||
|
|
||||||
|
if (!loading && !isLocked.current) {
|
||||||
|
isLocked.current = true;
|
||||||
invoke("calculate_timeline_entities_at_frame", {
|
invoke("calculate_timeline_entities_at_frame", {
|
||||||
timeline: {
|
timeline: {
|
||||||
entities: parsedEntities,
|
entities: parsedEntities,
|
||||||
@ -68,14 +95,11 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
duration,
|
duration,
|
||||||
},
|
},
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
console.timeEnd("calculation");
|
|
||||||
// console.log(data);
|
|
||||||
|
|
||||||
const entitiesResult = Entities.safeParse(data);
|
const entitiesResult = Entities.safeParse(data);
|
||||||
console.time("draw");
|
|
||||||
|
|
||||||
if (canvasKit && canvas.current && surface.current && fontData) {
|
if (canvasKit && canvas.current && surface.current && fontData) {
|
||||||
surface.current.flush();
|
surface.current.flush();
|
||||||
|
|
||||||
surface.current.requestAnimationFrame((skCanvas) => {
|
surface.current.requestAnimationFrame((skCanvas) => {
|
||||||
skCanvas.clear(canvasKit.WHITE);
|
skCanvas.clear(canvasKit.WHITE);
|
||||||
if (entitiesResult.success) {
|
if (entitiesResult.success) {
|
||||||
@ -83,27 +107,65 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
|
|
||||||
entities.reverse().forEach((entity) => {
|
entities.reverse().forEach((entity) => {
|
||||||
switch (entity.type) {
|
switch (entity.type) {
|
||||||
case EntityType.Enum.Box:
|
case EntityType.Enum.Rect:
|
||||||
drawBox(canvasKit, skCanvas, entity);
|
drawRect(canvasKit, skCanvas, entity);
|
||||||
break;
|
break;
|
||||||
case EntityType.Enum.Ellipse:
|
case EntityType.Enum.Ellipse:
|
||||||
drawEllipse(canvasKit, skCanvas, entity);
|
drawEllipse(canvasKit, skCanvas, entity);
|
||||||
break;
|
break;
|
||||||
case EntityType.Enum.Text:
|
case EntityType.Enum.Text:
|
||||||
drawText(canvasKit, skCanvas, entity, fontData);
|
drawText(canvasKit, skCanvas, entity, fontData);
|
||||||
|
break;
|
||||||
|
case EntityType.Enum.StaggeredText:
|
||||||
|
{
|
||||||
|
let cache: StaggeredTextCache;
|
||||||
|
if (!entity.cache.valid) {
|
||||||
|
const _cache = staggeredTextCache[0].get(entity.id);
|
||||||
|
|
||||||
|
if (_cache !== undefined) {
|
||||||
|
canvasKit.Free(_cache.glyphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = calculateLetters(canvasKit, entity, fontData);
|
||||||
|
|
||||||
|
staggeredTextCache[1].set(entity.id, cache);
|
||||||
|
updateEntityById(entity.id, { cache: { valid: true } });
|
||||||
|
} else {
|
||||||
|
const _cache = staggeredTextCache[0].get(entity.id);
|
||||||
|
if (_cache) {
|
||||||
|
cache = _cache;
|
||||||
|
} else {
|
||||||
|
cache = calculateLetters(canvasKit, entity, fontData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawStaggeredText(
|
||||||
|
canvasKit,
|
||||||
|
skCanvas,
|
||||||
|
entity,
|
||||||
|
cache.font,
|
||||||
|
cache.letterMeasures,
|
||||||
|
cache.metrics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLocked.current = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
isLocked.current = false;
|
||||||
console.log(entitiesResult.error);
|
console.log(entitiesResult.error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.timeEnd("draw");
|
//console.timeEnd("draw");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}, [entities, loading, renderState.curr_frame]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,18 +2,22 @@ import { ease } from "@unom/style";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
AnimatedTextEntity,
|
AnimatedTextEntity,
|
||||||
AnimatedBoxEntity,
|
AnimatedRectEntity,
|
||||||
|
AnimatedStaggeredTextEntity,
|
||||||
AnimatedEllipseEntity,
|
AnimatedEllipseEntity,
|
||||||
} from "primitives/AnimatedEntities";
|
} from "primitives/AnimatedEntities";
|
||||||
import { Paint, PaintStyle, PaintStyleType } from "primitives/Paint";
|
import { Paint, PaintStyle, PaintStyleType } from "primitives/Paint";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AnimatedVec2Properties } from "./Values";
|
import { AnimatedVec2Properties, ColorProperties } from "./Values";
|
||||||
import { PropertiesProps } from "./common";
|
import { PropertiesProps } from "./common";
|
||||||
|
|
||||||
type TextPropertiesProps = PropertiesProps<z.input<typeof AnimatedTextEntity>>;
|
type TextPropertiesProps = PropertiesProps<z.input<typeof AnimatedTextEntity>>;
|
||||||
|
type StaggeredTextPropertiesProps = PropertiesProps<
|
||||||
|
z.input<typeof AnimatedStaggeredTextEntity>
|
||||||
|
>;
|
||||||
type PaintPropertiesProps = PropertiesProps<z.input<typeof Paint>>;
|
type PaintPropertiesProps = PropertiesProps<z.input<typeof Paint>>;
|
||||||
type BoxPropertiesProps = PropertiesProps<z.input<typeof AnimatedBoxEntity>>;
|
type RectPropertiesProps = PropertiesProps<z.input<typeof AnimatedRectEntity>>;
|
||||||
type EllipsePropertiesProps = PropertiesProps<
|
type EllipsePropertiesProps = PropertiesProps<
|
||||||
z.input<typeof AnimatedEllipseEntity>
|
z.input<typeof AnimatedEllipseEntity>
|
||||||
>;
|
>;
|
||||||
@ -45,6 +49,15 @@ export const PaintProperties: FC<PaintPropertiesProps> = ({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{entity.style.color && (
|
||||||
|
<ColorProperties
|
||||||
|
label="Color"
|
||||||
|
onUpdate={(color) =>
|
||||||
|
onUpdate({ ...entity, style: { ...entity.style, color } })
|
||||||
|
}
|
||||||
|
entity={entity.style.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -90,7 +103,75 @@ export const TextProperties: FC<TextPropertiesProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BoxProperties: FC<BoxPropertiesProps> = ({ entity, onUpdate }) => {
|
export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
|
||||||
|
entity,
|
||||||
|
onUpdate,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
variants={{ enter: { opacity: 1, y: 0 }, from: { opacity: 0, y: 50 } }}
|
||||||
|
animate="enter"
|
||||||
|
initial="from"
|
||||||
|
transition={ease.quint(0.9).out}
|
||||||
|
>
|
||||||
|
<label className="flex flex-col items-start">
|
||||||
|
<span className="label">Text</span>
|
||||||
|
<input
|
||||||
|
value={entity.text}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate({
|
||||||
|
...entity,
|
||||||
|
text: e.target.value,
|
||||||
|
cache: { valid: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="flex flex-col items-start">
|
||||||
|
<span className="label">Size</span>
|
||||||
|
<input
|
||||||
|
value={entity.letter.paint.size}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate({
|
||||||
|
...entity,
|
||||||
|
letter: {
|
||||||
|
...entity.letter,
|
||||||
|
paint: {
|
||||||
|
...entity.letter.paint,
|
||||||
|
size: Number(e.target.value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</label>
|
||||||
|
<PaintProperties
|
||||||
|
entity={entity.letter.paint}
|
||||||
|
onUpdate={(paint) =>
|
||||||
|
onUpdate({
|
||||||
|
...entity,
|
||||||
|
letter: {
|
||||||
|
...entity.letter,
|
||||||
|
paint: { ...entity.letter.paint, ...paint },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AnimatedVec2Properties
|
||||||
|
onUpdate={(updatedEntity) =>
|
||||||
|
onUpdate({ ...entity, origin: updatedEntity })
|
||||||
|
}
|
||||||
|
label="Origin"
|
||||||
|
entity={entity.origin}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RectProperties: FC<RectPropertiesProps> = ({
|
||||||
|
entity,
|
||||||
|
onUpdate,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="dark:text-white">
|
<div className="dark:text-white">
|
||||||
<PaintProperties
|
<PaintProperties
|
||||||
|
@ -2,7 +2,12 @@ import { FC, ReactNode } from "react";
|
|||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
|
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import { BoxProperties, EllipseProperties, TextProperties } from "./Primitives";
|
import {
|
||||||
|
RectProperties,
|
||||||
|
EllipseProperties,
|
||||||
|
TextProperties,
|
||||||
|
StaggeredTextProperties,
|
||||||
|
} from "./Primitives";
|
||||||
|
|
||||||
const PropertiesContainer: FC<{ children: ReactNode }> = ({ children }) => {
|
const PropertiesContainer: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
@ -26,6 +31,15 @@ const Properties = () => {
|
|||||||
|
|
||||||
if (entity) {
|
if (entity) {
|
||||||
switch (entity.type) {
|
switch (entity.type) {
|
||||||
|
case "StaggeredText":
|
||||||
|
return (
|
||||||
|
<StaggeredTextProperties
|
||||||
|
key={selectedEntity}
|
||||||
|
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
|
||||||
|
entity={entity}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case "Text":
|
case "Text":
|
||||||
return (
|
return (
|
||||||
<TextProperties
|
<TextProperties
|
||||||
@ -35,9 +49,9 @@ const Properties = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "Box":
|
case "Rect":
|
||||||
return (
|
return (
|
||||||
<BoxProperties
|
<RectProperties
|
||||||
key={selectedEntity}
|
key={selectedEntity}
|
||||||
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
|
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
@ -7,7 +7,7 @@ import { shallow } from "zustand/shallow";
|
|||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { ease } from "@unom/style";
|
import { ease } from "@unom/style";
|
||||||
import Timestamp from "./Timestamp";
|
import Timestamp from "./Timestamp";
|
||||||
import { Keyframe, Keyframes } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { flattenedKeyframesByEntity } from "utils";
|
import { flattenedKeyframesByEntity } from "utils";
|
||||||
|
|
||||||
export type AnimationEntity = {
|
export type AnimationEntity = {
|
||||||
@ -37,8 +37,8 @@ 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"
|
className="bg-indigo-300 absolute w-2 h-2 z-30 top-[39%] select-none pointer-events-none"
|
||||||
></motion.div>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-8 w-100 flex flex-row gap-1">
|
<div className="h-8 w-100 flex flex-row gap-1 select-none">
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
selectedEntity !== undefined && selectedEntity === index
|
selectedEntity !== undefined && selectedEntity === index
|
||||||
@ -90,6 +90,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
whileTap={{
|
whileTap={{
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
transition={ease.circ(0.6).out}
|
transition={ease.circ(0.6).out}
|
||||||
dragElastic={false}
|
dragElastic={false}
|
||||||
dragConstraints={{ left: 0, right: 900 }}
|
dragConstraints={{ left: 0, right: 900 }}
|
||||||
@ -113,9 +114,10 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none"
|
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-w-resize"
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
drag="x"
|
drag="x"
|
||||||
animate={{
|
animate={{
|
||||||
x: (animationData.duration + animationData.offset) * 100 - 16,
|
x: (animationData.duration + animationData.offset) * 100 - 16,
|
||||||
@ -142,7 +144,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none"
|
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md select-none cursor-e-resize"
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
drag="x"
|
drag="x"
|
||||||
@ -156,16 +158,12 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 900,
|
right: 900,
|
||||||
}}
|
}}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
transition={ease.circ(0.8).out}
|
transition={ease.circ(0.8).out}
|
||||||
onDragEnd={(e, info) => {
|
onDragEnd={(_e, info) => {
|
||||||
let offset = info.offset.x;
|
let offset = info.offset.x;
|
||||||
|
|
||||||
offset *= 0.01;
|
offset *= 0.01;
|
||||||
|
|
||||||
offset += animationData.offset;
|
offset += animationData.offset;
|
||||||
|
|
||||||
console.log(offset);
|
|
||||||
|
|
||||||
updateEntity(index, {
|
updateEntity(index, {
|
||||||
animation_data: {
|
animation_data: {
|
||||||
...animationData,
|
...animationData,
|
||||||
@ -173,7 +171,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none"
|
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||||
></motion.div>
|
></motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
76
app/src/drawers/draw.ts
Normal file
76
app/src/drawers/draw.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
|
||||||
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
|
import { Entities } from "primitives/Entities";
|
||||||
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
import { useTimelineStore } from "stores/timeline.store";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { StaggeredTextCache } from "./staggered-text";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* TODO Add dependency logic for e.g. dynamically loading fonts, images etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Drawer {
|
||||||
|
private readonly didLoad: boolean;
|
||||||
|
private entities: z.output<typeof Entities> | undefined;
|
||||||
|
private ckDidLoad: boolean;
|
||||||
|
private dependenciesDidLoad: boolean;
|
||||||
|
private CanvasKit: CanvasKit | undefined;
|
||||||
|
cache: { staggeredText: Map<string, StaggeredTextCache> };
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.entities = undefined;
|
||||||
|
this.CanvasKit = undefined;
|
||||||
|
this.ckDidLoad = false;
|
||||||
|
this.dependenciesDidLoad = false;
|
||||||
|
this.cache = {
|
||||||
|
staggeredText: new Map(),
|
||||||
|
};
|
||||||
|
this.didLoad = this.ckDidLoad && this.dependenciesDidLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
InitCanvasKit({
|
||||||
|
locateFile: (file) =>
|
||||||
|
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
||||||
|
}).then((CanvasKit) => {
|
||||||
|
this.CanvasKit = CanvasKit;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the entities based on the input
|
||||||
|
*/
|
||||||
|
update(animatedEntities: z.input<typeof AnimatedEntities>) {
|
||||||
|
const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities);
|
||||||
|
|
||||||
|
if (this.didLoad) {
|
||||||
|
const render_state = useRenderStateStore.getState().renderState;
|
||||||
|
const { fps, size, duration } = useTimelineStore.getState();
|
||||||
|
|
||||||
|
invoke("calculate_timeline_entities_at_frame", {
|
||||||
|
timeline: {
|
||||||
|
entities: parsedAnimatedEntities,
|
||||||
|
render_state,
|
||||||
|
fps,
|
||||||
|
size,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
}).then((data) => {
|
||||||
|
const parsedEntities = Entities.safeParse(data);
|
||||||
|
if (parsedEntities.success) {
|
||||||
|
this.entities = parsedEntities.data;
|
||||||
|
} else {
|
||||||
|
console.error(parsedEntities.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
if (this.didLoad) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +1,278 @@
|
|||||||
import { convertToFloat } from "@tempblade/common";
|
import {
|
||||||
import { Canvas, CanvasKit } from "canvaskit-wasm";
|
Canvas,
|
||||||
|
CanvasKit,
|
||||||
|
Font,
|
||||||
|
FontMetrics,
|
||||||
|
MallocObj,
|
||||||
|
TypedArray,
|
||||||
|
Typeface,
|
||||||
|
} from "canvaskit-wasm";
|
||||||
import { StaggeredText } from "primitives/Entities";
|
import { StaggeredText } from "primitives/Entities";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { buildPaintStyle } from "./paint";
|
||||||
|
|
||||||
export default function drawStaggeredText(
|
export type StaggeredTextCache = {
|
||||||
|
letterMeasures: Array<LetterMeasures>;
|
||||||
|
metrics: FontMetrics;
|
||||||
|
typeface: Typeface;
|
||||||
|
font: Font;
|
||||||
|
glyphs: MallocObj;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUniqueCharacters(str: string): string {
|
||||||
|
const uniqueCharacters: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const character = str[i];
|
||||||
|
|
||||||
|
if (!uniqueCharacters.includes(character)) {
|
||||||
|
uniqueCharacters.push(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueCharacters.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureLetters(
|
||||||
|
glyphArr: TypedArray,
|
||||||
|
boundsById: Record<number, LetterBounds>,
|
||||||
|
maxWidth: number
|
||||||
|
): Array<LetterMeasures> {
|
||||||
|
const measuredLetters: Array<LetterMeasures> = [];
|
||||||
|
|
||||||
|
let currentWidth = 0;
|
||||||
|
let currentLine = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < glyphArr.length; i++) {
|
||||||
|
const nextGlyph = boundsById[glyphArr[i]];
|
||||||
|
|
||||||
|
const nextGlyphWidth = nextGlyph.x_advance;
|
||||||
|
|
||||||
|
currentWidth += nextGlyphWidth;
|
||||||
|
|
||||||
|
if (currentWidth > maxWidth) {
|
||||||
|
currentLine += 1;
|
||||||
|
currentWidth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glyph = glyphArr.subarray(i, i + 1) as unknown as MallocObj;
|
||||||
|
|
||||||
|
measuredLetters.push({
|
||||||
|
bounds: nextGlyph,
|
||||||
|
line: currentLine,
|
||||||
|
glyph,
|
||||||
|
offset: {
|
||||||
|
x: currentWidth - nextGlyphWidth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return measuredLetters;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterBounds = {
|
||||||
|
x: {
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
y: {
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x_advance: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LetterMeasures = {
|
||||||
|
offset: {
|
||||||
|
x: number;
|
||||||
|
};
|
||||||
|
line: number;
|
||||||
|
glyph: MallocObj;
|
||||||
|
bounds: LetterBounds;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function calculateLetters(
|
||||||
CanvasKit: CanvasKit,
|
CanvasKit: CanvasKit,
|
||||||
canvs: Canvas,
|
|
||||||
entity: z.output<typeof StaggeredText>,
|
entity: z.output<typeof StaggeredText>,
|
||||||
fontData: ArrayBuffer
|
fontData: ArrayBuffer
|
||||||
) {
|
): StaggeredTextCache {
|
||||||
const paint = new CanvasKit.Paint();
|
console.log("Called");
|
||||||
|
|
||||||
const color = convertToFloat(entity.letter.paint.style.color.value);
|
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
||||||
|
fontData
|
||||||
paint.setColor(color);
|
) as Typeface;
|
||||||
|
|
||||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontData);
|
|
||||||
|
|
||||||
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);
|
||||||
font.setSubpixel(true);
|
font.setSubpixel(true);
|
||||||
font.setHinting(CanvasKit.FontHinting.Slight);
|
font.setHinting(CanvasKit.FontHinting.None);
|
||||||
|
|
||||||
const bounds = font.getGlyphBounds(glyphIDs, paint);
|
const alphabet = getUniqueCharacters(entity.text);
|
||||||
const widths = font.getGlyphWidths(glyphIDs, paint);
|
const ids = font.getGlyphIDs(alphabet);
|
||||||
|
const unknownCharacterGlyphID = ids[0];
|
||||||
|
|
||||||
console.log(bounds);
|
const charsToGlyphIDs: Record<string, any> = {};
|
||||||
console.log(widths);
|
|
||||||
|
let glyphIdx = 0;
|
||||||
|
for (let i = 0; i < alphabet.length; i++) {
|
||||||
|
charsToGlyphIDs[alphabet[i]] = ids[glyphIdx];
|
||||||
|
if ((alphabet.codePointAt(i) as number) > 65535) {
|
||||||
|
i++; // skip the next index because that will be the second half of the code point.
|
||||||
|
}
|
||||||
|
glyphIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = font.getMetrics();
|
||||||
|
const bounds = font.getGlyphBounds(glyphIDs);
|
||||||
|
const widths = font.getGlyphWidths(glyphIDs);
|
||||||
|
|
||||||
|
const glyphMetricsByGlyphID: Record<number, LetterBounds> = {};
|
||||||
|
for (let i = 0; i < glyphIDs.length; i++) {
|
||||||
|
const id = glyphIDs[i];
|
||||||
|
|
||||||
|
const x_min = bounds[i * 4];
|
||||||
|
const x_max = bounds[i * 4 + 2];
|
||||||
|
const y_min = bounds[i * 4 + 3];
|
||||||
|
const y_max = bounds[i * 4 + 1];
|
||||||
|
const width = x_max - x_min;
|
||||||
|
const height = Math.abs(y_max - y_min);
|
||||||
|
|
||||||
|
glyphMetricsByGlyphID[id] = {
|
||||||
|
x: {
|
||||||
|
min: x_min,
|
||||||
|
max: x_max,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: y_min,
|
||||||
|
max: y_max,
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x_advance: widths[i],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const glyphs = CanvasKit.MallocGlyphIDs(entity.text.length);
|
||||||
|
let glyphArr = glyphs.toTypedArray();
|
||||||
|
|
||||||
|
const MAX_WIDTH = 900;
|
||||||
|
|
||||||
|
// Turn the code points into glyphs, accounting for up to 2 ligatures.
|
||||||
|
let shapedGlyphIdx = -1;
|
||||||
|
for (let i = 0; i < entity.text.length; i++) {
|
||||||
|
const char = entity.text[i];
|
||||||
|
shapedGlyphIdx++;
|
||||||
|
glyphArr[shapedGlyphIdx] = charsToGlyphIDs[char] || unknownCharacterGlyphID;
|
||||||
|
if ((entity.text.codePointAt(i) as number) > 65535) {
|
||||||
|
i++; // skip the next index because that will be the second half of the code point.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim down our array of glyphs to only the amount we have after ligatures and code points
|
||||||
|
// that are > 16 bits.
|
||||||
|
glyphArr = glyphs.subarray(0, shapedGlyphIdx + 1);
|
||||||
|
|
||||||
|
// Break our glyphs into runs based on the maxWidth and the xAdvance.
|
||||||
|
|
||||||
|
const letterMeasures = measureLetters(
|
||||||
|
glyphArr,
|
||||||
|
glyphMetricsByGlyphID,
|
||||||
|
MAX_WIDTH
|
||||||
|
);
|
||||||
|
|
||||||
|
return { letterMeasures, metrics, font, typeface, glyphs };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function drawStaggeredText(
|
||||||
|
CanvasKit: CanvasKit,
|
||||||
|
canvas: Canvas,
|
||||||
|
entity: z.output<typeof StaggeredText>,
|
||||||
|
font: Font,
|
||||||
|
measuredLetters: Array<LetterMeasures>,
|
||||||
|
metrics: FontMetrics
|
||||||
|
) {
|
||||||
|
const paint = new CanvasKit.Paint();
|
||||||
|
|
||||||
|
buildPaintStyle(CanvasKit, paint, entity.letter.paint);
|
||||||
|
|
||||||
|
// Draw all those runs.
|
||||||
|
for (let i = 0; i < measuredLetters.length; i++) {
|
||||||
|
const measuredLetter = measuredLetters[i];
|
||||||
|
|
||||||
|
const blob = CanvasKit.TextBlob.MakeFromGlyphs(
|
||||||
|
measuredLetters[i].glyph as unknown as Array<number>,
|
||||||
|
font
|
||||||
|
);
|
||||||
|
if (blob) {
|
||||||
|
canvas.save();
|
||||||
|
|
||||||
|
const width = measuredLetters
|
||||||
|
.filter((letter) => letter.line === 0)
|
||||||
|
.reduce((prev, curr) => curr.bounds.x_advance + prev, 0);
|
||||||
|
|
||||||
|
const lineOffset = (entity.letter.paint.size / 2) * measuredLetter.line;
|
||||||
|
|
||||||
|
const entityOrigin = [
|
||||||
|
entity.origin[0] - width / 2,
|
||||||
|
entity.origin[1] + lineOffset,
|
||||||
|
];
|
||||||
|
|
||||||
|
const lineCount = measuredLetters
|
||||||
|
.map((e) => e.line)
|
||||||
|
.sort((a, b) => a - b)[measuredLetters.length - 1];
|
||||||
|
|
||||||
|
if (entity.letter.transform && entity.letter.transform[i]) {
|
||||||
|
const letterTransform = entity.letter.transform[i];
|
||||||
|
const letterOrigin = [0, 0];
|
||||||
|
|
||||||
|
let origin = letterOrigin.map(
|
||||||
|
(val, index) => val + entityOrigin[index]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the spacing
|
||||||
|
|
||||||
|
const spacing =
|
||||||
|
measuredLetter.bounds.x_advance - measuredLetter.bounds.width;
|
||||||
|
|
||||||
|
//console.log(spacing);
|
||||||
|
|
||||||
|
// Center the origin
|
||||||
|
|
||||||
|
origin[0] =
|
||||||
|
origin[0] + measuredLetter.bounds.width / 2 + measuredLetter.offset.x;
|
||||||
|
origin[1] = origin[1] - metrics.descent + lineOffset;
|
||||||
|
|
||||||
|
//console.log(measuredLetter.bounds);
|
||||||
|
|
||||||
|
canvas.translate(origin[0], origin[1]);
|
||||||
|
|
||||||
|
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
||||||
|
|
||||||
|
canvas.translate(
|
||||||
|
-origin[0] + measuredLetter.offset.x,
|
||||||
|
-origin[1] + lineOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
/* canvas.translate(
|
||||||
|
measuredLetter.offset.x + measuredLetter.bounds.width / 2,
|
||||||
|
0
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* canvas.translate(
|
||||||
|
width * -0.5,
|
||||||
|
lineCount * (-entity.letter.paint.size / 2)
|
||||||
|
); */
|
||||||
|
|
||||||
|
canvas.drawTextBlob(blob, entityOrigin[0], entityOrigin[1], paint);
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ export default function drawText(
|
|||||||
entity: z.output<typeof TextEntity>,
|
entity: z.output<typeof TextEntity>,
|
||||||
fontData: ArrayBuffer
|
fontData: ArrayBuffer
|
||||||
) {
|
) {
|
||||||
|
canvas.save();
|
||||||
const fontMgr = CanvasKit.FontMgr.FromData(fontData);
|
const fontMgr = CanvasKit.FontMgr.FromData(fontData);
|
||||||
|
|
||||||
if (!fontMgr) {
|
if (!fontMgr) {
|
||||||
@ -25,7 +26,7 @@ export default function drawText(
|
|||||||
const pStyle = new CanvasKit.ParagraphStyle({
|
const pStyle = new CanvasKit.ParagraphStyle({
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: color,
|
color: color,
|
||||||
fontFamilies: ["Roboto"],
|
fontFamilies: ["Helvetica"],
|
||||||
fontSize: entity.paint.size,
|
fontSize: entity.paint.size,
|
||||||
},
|
},
|
||||||
textDirection: CanvasKit.TextDirection.LTR,
|
textDirection: CanvasKit.TextDirection.LTR,
|
||||||
@ -40,4 +41,8 @@ export default function drawText(
|
|||||||
const width = p.getMaxWidth() / 2;
|
const width = p.getMaxWidth() / 2;
|
||||||
|
|
||||||
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
|
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
|
||||||
|
|
||||||
|
paint.delete();
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,15 @@ import { Color } from "primitives/Paint";
|
|||||||
import { Timeline } from "primitives/Timeline";
|
import { Timeline } from "primitives/Timeline";
|
||||||
import { staticAnimatedNumber, staticAnimatedVec2 } from "primitives/Values";
|
import { staticAnimatedNumber, staticAnimatedVec2 } from "primitives/Values";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
function buildRect1(
|
function buildRect1(
|
||||||
offset: number,
|
offset: number,
|
||||||
color: z.infer<typeof Color>
|
color: z.infer<typeof Color>
|
||||||
): z.input<typeof AnimatedEntity> {
|
): z.input<typeof AnimatedEntity> {
|
||||||
return {
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
cache: {},
|
||||||
type: "Rect",
|
type: "Rect",
|
||||||
paint: {
|
paint: {
|
||||||
style: {
|
style: {
|
||||||
@ -58,6 +61,8 @@ function buildRect(
|
|||||||
): z.input<typeof AnimatedEntity> {
|
): z.input<typeof AnimatedEntity> {
|
||||||
return {
|
return {
|
||||||
type: "Rect",
|
type: "Rect",
|
||||||
|
id: uuid(),
|
||||||
|
cache: {},
|
||||||
paint: {
|
paint: {
|
||||||
style: {
|
style: {
|
||||||
type: "Fill",
|
type: "Fill",
|
||||||
@ -126,6 +131,8 @@ function buildText(
|
|||||||
): z.input<typeof AnimatedEntity> {
|
): z.input<typeof AnimatedEntity> {
|
||||||
return {
|
return {
|
||||||
type: "Text",
|
type: "Text",
|
||||||
|
id: uuid(),
|
||||||
|
cache: {},
|
||||||
paint: {
|
paint: {
|
||||||
style: {
|
style: {
|
||||||
type: "Fill",
|
type: "Fill",
|
||||||
@ -179,6 +186,9 @@ function buildStaggeredText(
|
|||||||
return {
|
return {
|
||||||
type: "StaggeredText",
|
type: "StaggeredText",
|
||||||
text,
|
text,
|
||||||
|
cache: {},
|
||||||
|
id: uuid(),
|
||||||
|
origin: staticAnimatedVec2(1280 / 2, 720 / 2),
|
||||||
transform: {
|
transform: {
|
||||||
translate: staticAnimatedVec2(0, 0),
|
translate: staticAnimatedVec2(0, 0),
|
||||||
rotate: staticAnimatedVec2(0, 0),
|
rotate: staticAnimatedVec2(0, 0),
|
||||||
@ -186,17 +196,17 @@ function buildStaggeredText(
|
|||||||
scale: staticAnimatedVec2(1, 1),
|
scale: staticAnimatedVec2(1, 1),
|
||||||
},
|
},
|
||||||
animation_data: {
|
animation_data: {
|
||||||
offset: 0,
|
offset,
|
||||||
duration: 2,
|
duration: 5.0,
|
||||||
},
|
},
|
||||||
stagger: 2.0,
|
stagger: 0.05,
|
||||||
letter: {
|
letter: {
|
||||||
paint: {
|
paint: {
|
||||||
style: {
|
style: {
|
||||||
type: "Fill",
|
type: "Fill",
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
size: 30,
|
size: 90,
|
||||||
align: "Center",
|
align: "Center",
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
@ -208,12 +218,22 @@ function buildStaggeredText(
|
|||||||
{
|
{
|
||||||
keyframes: {
|
keyframes: {
|
||||||
values: [
|
values: [
|
||||||
|
{
|
||||||
|
interpolation: {
|
||||||
|
type: "Spring",
|
||||||
|
stiffness: 200,
|
||||||
|
mass: 1,
|
||||||
|
damping: 15,
|
||||||
|
},
|
||||||
|
value: 0.0,
|
||||||
|
offset: 0.0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Linear",
|
type: "Linear",
|
||||||
},
|
},
|
||||||
value: 1.0,
|
value: 1.0,
|
||||||
offset: 0.0,
|
offset: 4.0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -223,8 +243,10 @@ function buildStaggeredText(
|
|||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "EasingFunction",
|
type: "Spring",
|
||||||
easing_function: "CircOut",
|
stiffness: 200,
|
||||||
|
mass: 1,
|
||||||
|
damping: 15,
|
||||||
},
|
},
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
@ -248,7 +270,13 @@ function buildStaggeredText(
|
|||||||
|
|
||||||
export const EXAMPLE_ANIMATED_ENTITIES: Array<z.input<typeof AnimatedEntity>> =
|
export const EXAMPLE_ANIMATED_ENTITIES: Array<z.input<typeof AnimatedEntity>> =
|
||||||
[
|
[
|
||||||
buildStaggeredText("Hallo", 0.0, { value: [30, 30, 30, 1.0] }),
|
buildStaggeredText("Ehrenmann?", 2.0, {
|
||||||
|
value: [255, 255, 255, 1.0],
|
||||||
|
}),
|
||||||
|
buildText("Wie gehts?", 2.5, 40, 40, { value: [200, 200, 200, 1.0] }),
|
||||||
|
buildRect(0.6, { value: [30, 30, 30, 1.0] }),
|
||||||
|
buildRect(0.4, { value: [20, 20, 20, 1.0] }),
|
||||||
|
buildRect(0.2, { value: [10, 10, 10, 1.0] }),
|
||||||
buildRect(0, { value: [0, 0, 0, 1.0] }),
|
buildRect(0, { value: [0, 0, 0, 1.0] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -259,6 +287,7 @@ export const EXAMPLE_ANIMATED_ENTITIES_2: Array<
|
|||||||
value: [255, 255, 255, 1.0],
|
value: [255, 255, 255, 1.0],
|
||||||
}),
|
}),
|
||||||
buildText("Wie gehts?", 1.5, 40, 30, { value: [255, 255, 255, 1.0] }),
|
buildText("Wie gehts?", 1.5, 40, 30, { value: [255, 255, 255, 1.0] }),
|
||||||
|
buildRect(0.8, { value: [40, 40, 40, 1.0] }),
|
||||||
buildRect(0.6, { value: [30, 30, 30, 1.0] }),
|
buildRect(0.6, { value: [30, 30, 30, 1.0] }),
|
||||||
buildRect(0.4, { value: [20, 20, 20, 1.0] }),
|
buildRect(0.4, { value: [20, 20, 20, 1.0] }),
|
||||||
buildRect(0.2, { value: [10, 10, 10, 1.0] }),
|
buildRect(0.2, { value: [10, 10, 10, 1.0] }),
|
||||||
|
53
app/src/hooks/useMap.ts
Normal file
53
app/src/hooks/useMap.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export type MapOrEntries<K, V> = Map<K, V> | [K, V][];
|
||||||
|
|
||||||
|
// Public interface
|
||||||
|
export interface Actions<K, V> {
|
||||||
|
set: (key: K, value: V) => void;
|
||||||
|
setAll: (entries: MapOrEntries<K, V>) => void;
|
||||||
|
remove: (key: K) => void;
|
||||||
|
reset: Map<K, V>["clear"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// We hide some setters from the returned map to disable autocompletion
|
||||||
|
type Return<K, V> = [
|
||||||
|
Omit<Map<K, V>, "set" | "clear" | "delete">,
|
||||||
|
Actions<K, V>
|
||||||
|
];
|
||||||
|
|
||||||
|
function useMap<K, V>(
|
||||||
|
initialState: MapOrEntries<K, V> = new Map()
|
||||||
|
): Return<K, V> {
|
||||||
|
const [map, setMap] = useState(new Map(initialState));
|
||||||
|
|
||||||
|
const actions: Actions<K, V> = {
|
||||||
|
set: useCallback((key, value) => {
|
||||||
|
setMap((prev) => {
|
||||||
|
const copy = new Map(prev);
|
||||||
|
copy.set(key, value);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
setAll: useCallback((entries) => {
|
||||||
|
setMap(() => new Map(entries));
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
remove: useCallback((key) => {
|
||||||
|
setMap((prev) => {
|
||||||
|
const copy = new Map(prev);
|
||||||
|
copy.delete(key);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []),
|
||||||
|
|
||||||
|
reset: useCallback(() => {
|
||||||
|
setMap(() => new Map());
|
||||||
|
}, []),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [map, actions];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMap;
|
@ -1,5 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EllipseEntity, EntityType, RectEntity, TextEntity } from "./Entities";
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
EllipseEntity,
|
||||||
|
EntityType,
|
||||||
|
RectEntity,
|
||||||
|
TextEntity,
|
||||||
|
} from "./Entities";
|
||||||
import { AnimatedVec2 } from "./Values";
|
import { AnimatedVec2 } from "./Values";
|
||||||
import { TextPaint } from "./Paint";
|
import { TextPaint } from "./Paint";
|
||||||
|
|
||||||
@ -20,7 +26,7 @@ export const AnimatedTransform = z.object({
|
|||||||
scale: AnimatedVec2,
|
scale: AnimatedVec2,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AnimatedStaggeredText = z.object({
|
export const AnimatedStaggeredTextEntity = BaseEntity.extend({
|
||||||
/** Transform applied to the whole layer. */
|
/** Transform applied to the whole layer. */
|
||||||
transform: AnimatedTransform,
|
transform: AnimatedTransform,
|
||||||
/** The staggered delay that is applied for each letter. Gets multiplied by the index of the letter. */
|
/** The staggered delay that is applied for each letter. Gets multiplied by the index of the letter. */
|
||||||
@ -31,6 +37,7 @@ export const AnimatedStaggeredText = z.object({
|
|||||||
paint: TextPaint,
|
paint: TextPaint,
|
||||||
}),
|
}),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
|
origin: AnimatedVec2,
|
||||||
animation_data: AnimationData,
|
animation_data: AnimationData,
|
||||||
type: z.literal(EntityType.Enum.StaggeredText),
|
type: z.literal(EntityType.Enum.StaggeredText),
|
||||||
});
|
});
|
||||||
@ -60,7 +67,7 @@ export const AnimatedEllipseEntity = EllipseEntity.extend({
|
|||||||
export const AnimatedEntity = z.discriminatedUnion("type", [
|
export const AnimatedEntity = z.discriminatedUnion("type", [
|
||||||
AnimatedRectEntity,
|
AnimatedRectEntity,
|
||||||
AnimatedTextEntity,
|
AnimatedTextEntity,
|
||||||
AnimatedStaggeredText,
|
AnimatedStaggeredTextEntity,
|
||||||
AnimatedEllipseEntity,
|
AnimatedEllipseEntity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -6,10 +6,6 @@ const EntityTypeOptions = ["Text", "Ellipse", "Rect", "StaggeredText"] as const;
|
|||||||
|
|
||||||
export const EntityType = z.enum(EntityTypeOptions);
|
export const EntityType = z.enum(EntityTypeOptions);
|
||||||
|
|
||||||
export const GeometryEntity = z.object({
|
|
||||||
paint: Paint,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Transform = z.object({
|
export const Transform = z.object({
|
||||||
skew: Vec2,
|
skew: Vec2,
|
||||||
rotate: Vec2,
|
rotate: Vec2,
|
||||||
@ -17,12 +13,25 @@ export const Transform = z.object({
|
|||||||
scale: Vec2,
|
scale: Vec2,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StaggeredText = z.object({
|
export const Cache = z.object({
|
||||||
|
valid: z.boolean().optional().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseEntity = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
cache: Cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GeometryEntity = BaseEntity.extend({
|
||||||
|
paint: Paint,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StaggeredText = BaseEntity.extend({
|
||||||
letter: z.object({
|
letter: z.object({
|
||||||
position: Vec2,
|
transform: z.array(Transform).optional(),
|
||||||
transform: Transform,
|
|
||||||
paint: TextPaint,
|
paint: TextPaint,
|
||||||
}),
|
}),
|
||||||
|
origin: Vec2,
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
type: z.literal(EntityType.Enum.StaggeredText),
|
type: z.literal(EntityType.Enum.StaggeredText),
|
||||||
});
|
});
|
||||||
@ -43,7 +52,7 @@ export const EllipseEntity = GeometryEntity.extend({
|
|||||||
transform: z.nullable(Transform),
|
transform: z.nullable(Transform),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TextEntity = z.object({
|
export const TextEntity = BaseEntity.extend({
|
||||||
type: z.literal(EntityType.Enum.Text),
|
type: z.literal(EntityType.Enum.Text),
|
||||||
paint: TextPaint,
|
paint: TextPaint,
|
||||||
origin: Vec2,
|
origin: Vec2,
|
||||||
@ -55,6 +64,7 @@ export const Entity = z.discriminatedUnion("type", [
|
|||||||
RectEntity,
|
RectEntity,
|
||||||
EllipseEntity,
|
EllipseEntity,
|
||||||
TextEntity,
|
TextEntity,
|
||||||
|
StaggeredText,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Entities = z.array(Entity);
|
export const Entities = z.array(Entity);
|
||||||
|
@ -13,6 +13,10 @@ interface EntitiesStore {
|
|||||||
index: number,
|
index: number,
|
||||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||||
) => void;
|
) => void;
|
||||||
|
updateEntityById: (
|
||||||
|
id: string,
|
||||||
|
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useEntitiesStore = create<EntitiesStore>((set) => ({
|
const useEntitiesStore = create<EntitiesStore>((set) => ({
|
||||||
@ -20,6 +24,18 @@ const useEntitiesStore = create<EntitiesStore>((set) => ({
|
|||||||
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
||||||
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
||||||
selectedEntity: undefined,
|
selectedEntity: undefined,
|
||||||
|
updateEntityById: (id, entity) =>
|
||||||
|
set(({ entities }) => {
|
||||||
|
const nextEntities = produce(entities, (draft) => {
|
||||||
|
const index = draft.findIndex((e) => e.id === id);
|
||||||
|
|
||||||
|
draft[index] = { ...draft[index], ...entity } as z.infer<
|
||||||
|
typeof AnimatedEntity
|
||||||
|
>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { entities: nextEntities };
|
||||||
|
}),
|
||||||
updateEntity: (index, entity) =>
|
updateEntity: (index, entity) =>
|
||||||
set(({ entities }) => {
|
set(({ entities }) => {
|
||||||
const nextEntities = produce(entities, (draft) => {
|
const nextEntities = produce(entities, (draft) => {
|
||||||
|
@ -105,10 +105,6 @@ body,
|
|||||||
transition: opacity 0.1s linear, filter 0.1s linear;
|
transition: opacity 0.1s linear, filter 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SliderThumb:hover {
|
|
||||||
filter: drop-shadow(0px 10px 10px white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.SliderThumb::before {
|
.SliderThumb::before {
|
||||||
content: "";
|
content: "";
|
||||||
background-color: var(--indigo-400);
|
background-color: var(--indigo-400);
|
||||||
|
@ -29,7 +29,7 @@ export function flattenedKeyframesByEntity(
|
|||||||
case "Text":
|
case "Text":
|
||||||
keyframes.push(...flattenAnimatedVec2Keyframes(entity.origin));
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.origin));
|
||||||
break;
|
break;
|
||||||
case "Box":
|
case "Rect":
|
||||||
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
|
||||||
keyframes.push(...flattenAnimatedVec2Keyframes(entity.size));
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.size));
|
||||||
break;
|
break;
|
||||||
@ -37,6 +37,21 @@ export function flattenedKeyframesByEntity(
|
|||||||
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
|
||||||
keyframes.push(...flattenAnimatedVec2Keyframes(entity.radius));
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.radius));
|
||||||
break;
|
break;
|
||||||
|
case "StaggeredText":
|
||||||
|
keyframes.push(
|
||||||
|
...flattenAnimatedVec2Keyframes(entity.letter.transform.rotate)
|
||||||
|
);
|
||||||
|
keyframes.push(
|
||||||
|
...flattenAnimatedVec2Keyframes(entity.letter.transform.translate)
|
||||||
|
);
|
||||||
|
keyframes.push(
|
||||||
|
...flattenAnimatedVec2Keyframes(entity.letter.transform.skew)
|
||||||
|
);
|
||||||
|
keyframes.push(
|
||||||
|
...flattenAnimatedVec2Keyframes(entity.letter.transform.scale)
|
||||||
|
);
|
||||||
|
keyframes.push(...flattenAnimatedVec2Keyframes(entity.origin));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"resolvePackageJsonExports": true,
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
@ -1266,6 +1266,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/uuid@npm:^9":
|
||||||
|
version: 9.0.1
|
||||||
|
resolution: "@types/uuid@npm:9.0.1"
|
||||||
|
checksum: ca96225b4d47b8146380c0f60921b2a0b62acf3a8def03e8e7b369ed3b0180989c12f8305eb71ffc0cecb944102e18eec1260a3e84040d9a48de987d3aa00148
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@unom/style@npm:^0.2.14":
|
"@unom/style@npm:^0.2.14":
|
||||||
version: 0.2.14
|
version: 0.2.14
|
||||||
resolution: "@unom/style@npm:0.2.14"
|
resolution: "@unom/style@npm:0.2.14"
|
||||||
@ -1379,6 +1386,7 @@ __metadata:
|
|||||||
"@types/node": "npm:^18.7.10"
|
"@types/node": "npm:^18.7.10"
|
||||||
"@types/react": "npm:^18.0.15"
|
"@types/react": "npm:^18.0.15"
|
||||||
"@types/react-dom": "npm:^18.0.6"
|
"@types/react-dom": "npm:^18.0.6"
|
||||||
|
"@types/uuid": "npm:^9"
|
||||||
"@unom/style": "npm:^0.2.14"
|
"@unom/style": "npm:^0.2.14"
|
||||||
"@vitejs/plugin-react": "npm:^3.0.0"
|
"@vitejs/plugin-react": "npm:^3.0.0"
|
||||||
autoprefixer: "npm:^10.4.14"
|
autoprefixer: "npm:^10.4.14"
|
||||||
@ -1390,6 +1398,7 @@ __metadata:
|
|||||||
react-dom: "npm:^18.2.0"
|
react-dom: "npm:^18.2.0"
|
||||||
tailwindcss: "npm:^3.3.2"
|
tailwindcss: "npm:^3.3.2"
|
||||||
typescript: "npm:^4.9.5"
|
typescript: "npm:^4.9.5"
|
||||||
|
uuid: "npm:^9.0.0"
|
||||||
vite: "npm:^4.2.1"
|
vite: "npm:^4.2.1"
|
||||||
vite-tsconfig-paths: "npm:^4.2.0"
|
vite-tsconfig-paths: "npm:^4.2.0"
|
||||||
zod: "npm:^3.21.4"
|
zod: "npm:^3.21.4"
|
||||||
@ -3378,6 +3387,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"uuid@npm:^9.0.0":
|
||||||
|
version: 9.0.0
|
||||||
|
resolution: "uuid@npm:9.0.0"
|
||||||
|
bin:
|
||||||
|
uuid: dist/bin/uuid
|
||||||
|
checksum: e1f76aff372e430bb129157360fd4cd3b393411b245604ea78e178a822ab7874ec2ebecec03ce94422b0b80bbd24733c6fa1df166c9cbad5086ef4b4843140bb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"vite-tsconfig-paths@npm:^4.2.0":
|
"vite-tsconfig-paths@npm:^4.2.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "vite-tsconfig-paths@npm:4.2.0"
|
resolution: "vite-tsconfig-paths@npm:4.2.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user