This commit is contained in:
Enrico Bühler 2023-05-20 14:11:35 +02:00
parent 7f6b7f4695
commit 7576850ae0
109 changed files with 10720 additions and 0 deletions

24
app/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
app/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

7
app/README.md Normal file
View File

@ -0,0 +1,7 @@
# Tauri + React + Typescript
This template should help get you started developing with Tauri, React and Typescript in Vite.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

17
app/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1343
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
app/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-form": "^0.0.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-menubar": "^1.0.2",
"@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-toolbar": "^1.0.3",
"@tauri-apps/api": "^1.3.0",
"@tempblade/common": "^2.0.1",
"@unom/style": "^0.2.14",
"canvaskit-wasm": "^0.38.1",
"framer-motion": "^10.12.12",
"immer": "^10.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^3.21.4",
"zustand": "^4.3.8"
},
"devDependencies": {
"@tauri-apps/cli": "^1.3.0",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vite-tsconfig-paths": "^4.2.0"
}
}

6
app/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
app/public/tauri.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
app/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
app/src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4025
app/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

30
app/src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "tempblade-creator-app"
version = "0.0.0"
description = "An open motion design tool written in rust"
authors = ["enricobuehler"]
license = "BSD 3-Clause"
repository = "https://git.unom.io/tempblade/creator"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.3", features = [] }
[dependencies]
tauri = { version = "1.3", features = ["dialog-open", "dialog-save", "shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tint = "1.0.0"
simple-easing = "1.0.1"
logging_timer = "1.1.0"
rayon = "1.7"
font-kit = "0.11.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

3
app/src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,2 @@
pub mod primitives;
pub mod timeline;

View File

@ -0,0 +1,36 @@
impl Animateable for AnimatedCircularText<'_> {
fn draw(&mut self, mut canvas: &mut Canvas, timeline: &Timeline<'_>) {
self.prepare(&mut canvas, &self.animation_data);
self.sort_keyframes();
self.paint.set_anti_alias(true);
let default_text_typeface = &Typeface::default();
let default_text_font = &Font::from_typeface(default_text_typeface, 190.0);
let text_font: &Font = match self.font {
Some(font) => font,
None => default_text_font,
};
let radius: f32 = 0.35 * timeline.size.0.min(timeline.size.1) as f32;
let mut path = Path::new();
path.add_circle(
(timeline.size.0 / 2, timeline.size.1 / 2),
radius,
PathDirection::CW,
);
let text_width = text_font.measure_str(self.text, Some(&self.paint));
canvas.draw
}
fn sort_keyframes(&mut self) {
self.origin.sort_keyframes();
}
}

View File

@ -0,0 +1,241 @@
use serde::{Deserialize, Serialize};
use crate::animation::timeline::Timeline;
use super::{
paint::{Paint, TextPaint},
utils::timestamp_to_frame,
values::{AnimatedFloatVec2, AnimatedValue},
};
//#region Animateable Objects
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnimatedEntity {
Text(AnimatedTextEntity),
Ellipse(AnimatedEllipseEntity),
Box(AnimatedBoxEntity),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Entity {
Text(TextEntity),
Ellipse(EllipseEntity),
Box(BoxEntity),
}
impl AnimatedEntity {
pub fn calculate(&mut self, timeline: &Timeline) -> Option<Entity> {
match self {
Self::Text(text_entity) => text_entity.calculate(timeline),
Self::Box(box_entity) => box_entity.calculate(timeline),
Self::Ellipse(ellipse_entity) => ellipse_entity.calculate(timeline),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimatedTextEntity {
pub text: String,
pub origin: AnimatedFloatVec2,
pub paint: TextPaint,
pub animation_data: AnimationData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextEntity {
pub text: String,
pub origin: (f32, f32),
pub paint: TextPaint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimatedBoxEntity {
pub position: AnimatedFloatVec2,
pub size: AnimatedFloatVec2,
pub origin: AnimatedFloatVec2,
pub paint: Paint,
pub animation_data: AnimationData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BoxEntity {
pub position: (f32, f32),
pub size: (f32, f32),
pub origin: (f32, f32),
pub paint: Paint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimatedEllipseEntity {
pub paint: Paint,
pub radius: AnimatedFloatVec2,
pub origin: AnimatedFloatVec2,
pub position: AnimatedFloatVec2,
pub animation_data: AnimationData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EllipseEntity {
pub radius: (f32, f32),
pub position: (f32, f32),
pub origin: (f32, f32),
pub paint: Paint,
}
pub trait Animateable {
fn sort_keyframes(&mut self);
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity>;
// Checks if the Box is visible and should be drawn
fn should_draw(&self, animation_data: &AnimationData, timeline: &Timeline) -> bool {
let start_frame = timestamp_to_frame(animation_data.offset, timeline.fps);
let end_frame = timestamp_to_frame(
animation_data.offset + animation_data.duration,
timeline.fps,
);
// println!("start {0} end {1}", start_frame, end_frame);
let is_before = timeline.render_state.curr_frame < start_frame;
let is_after = timeline.render_state.curr_frame > end_frame;
let is_between = !is_after && !is_before;
if is_between {
return true;
} else {
return false;
}
}
}
impl AnimatedTextEntity {
fn into_static(&mut self, timeline: &Timeline) -> TextEntity {
self.sort_keyframes();
let origin = self.origin.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
TextEntity {
text: self.text.clone(),
origin,
paint: self.paint.clone(),
}
}
}
impl Animateable for AnimatedTextEntity {
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity> {
let should_draw = self.should_draw(&self.animation_data, timeline);
if should_draw {
self.sort_keyframes();
Some(Entity::Text(self.into_static(timeline)))
} else {
None
}
}
fn sort_keyframes(&mut self) {
self.origin.sort_keyframes();
}
}
impl Animateable for AnimatedBoxEntity {
fn sort_keyframes(&mut self) {
self.position.sort_keyframes();
self.size.sort_keyframes();
}
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity> {
let should_draw = self.should_draw(&self.animation_data, timeline);
if should_draw {
self.sort_keyframes();
let position = self.position.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
let size = self.size.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
let origin = self.origin.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
Some(Entity::Box(BoxEntity {
position,
size,
origin,
paint: self.paint.clone(),
}))
} else {
None
}
}
}
impl Animateable for AnimatedEllipseEntity {
fn calculate(&mut self, timeline: &Timeline) -> Option<Entity> {
let should_draw = self.should_draw(&self.animation_data, timeline);
if should_draw {
self.sort_keyframes();
let radius = self.radius.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
let position = self.position.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
let origin = self.origin.get_value_at_frame(
timeline.render_state.curr_frame,
&self.animation_data,
timeline.fps,
);
Some(Entity::Ellipse(EllipseEntity {
radius,
position,
origin,
paint: self.paint.clone(),
}))
} else {
None
}
}
fn sort_keyframes(&mut self) {
self.position.sort_keyframes();
self.radius.sort_keyframes();
}
}
//#endregion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimationData {
pub offset: f32,
pub duration: f32,
pub visible: bool,
}

View File

@ -0,0 +1,170 @@
use std::cmp::Ordering;
use super::keyframe::RenderedKeyframe;
use serde::{Deserialize, Serialize};
use simple_easing::{
circ_in, circ_in_out, circ_out, cubic_in, cubic_in_out, cubic_out, expo_in, expo_in_out,
expo_out, quad_in, quad_in_out, quad_out, quart_in, quart_in_out, quart_out, quint_in,
quint_in_out, quint_out,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SpringProperties {
pub mass: f32,
pub damping: f32,
pub stiffness: f32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SpringState {
pub velocity: f32,
pub last_val: f32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(tag = "easing_function")]
pub enum EasingFunction {
QuintOut,
QuintIn,
QuintInOut,
CircOut,
CircIn,
CircInOut,
CubicOut,
CubicIn,
CubicInOut,
ExpoOut,
ExpoIn,
ExpoInOut,
QuadOut,
QuadIn,
QuadInOut,
QuartOut,
QuartIn,
QuartInOut,
}
impl EasingFunction {
fn ease(self: &Self, t: f32) -> f32 {
match self {
EasingFunction::QuintOut => quint_out(t),
EasingFunction::QuintIn => quint_in(t),
EasingFunction::QuintInOut => quint_in_out(t),
EasingFunction::CircOut => circ_out(t),
EasingFunction::CircIn => circ_in(t),
EasingFunction::CircInOut => circ_in_out(t),
EasingFunction::CubicOut => cubic_out(t),
EasingFunction::CubicIn => cubic_in(t),
EasingFunction::CubicInOut => cubic_in_out(t),
EasingFunction::ExpoOut => expo_out(t),
EasingFunction::ExpoIn => expo_in(t),
EasingFunction::ExpoInOut => expo_in_out(t),
EasingFunction::QuadOut => quad_out(t),
EasingFunction::QuadIn => quad_in(t),
EasingFunction::QuadInOut => quad_in_out(t),
EasingFunction::QuartOut => quart_out(t),
EasingFunction::QuartIn => quart_in(t),
EasingFunction::QuartInOut => quart_in_out(t),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum InterpolationType {
Linear,
Spring(SpringProperties),
EasingFunction(EasingFunction),
}
pub fn calculate_spring_value(
curr_frame: i32,
start_value: f32,
target_value: f32,
start_frame: i32,
_end_frame: i32,
spring_props: &SpringProperties,
) -> f32 {
const PRECISION: f32 = 0.01;
const STEP: f32 = 10.0;
const REST_VELOCITY: f32 = PRECISION / 10.0;
let _is_growing = match start_value.total_cmp(&target_value) {
Ordering::Equal => false,
Ordering::Less => false,
Ordering::Greater => true,
};
let mut _is_moving = false;
let mut spring_state = SpringState {
last_val: start_value,
velocity: 0.0,
};
let mut position = start_value;
let relative_curr_frame = curr_frame - start_frame;
// println!("target_value {target_value} start_value {start_value}");
// println!("start_frame {start_frame} end_frame {end_frame}");
for _ in 0..relative_curr_frame {
let _is_moving = spring_state.velocity.abs() > REST_VELOCITY;
let spring_force = -spring_props.stiffness * 0.000001 * (position - target_value);
let damping_force = -spring_props.damping * 0.001 * spring_state.velocity;
let acceleration = (spring_force + damping_force) / spring_props.mass; // pt/ms^2
spring_state.velocity = spring_state.velocity + acceleration * STEP; // pt/ms
position = position + spring_state.velocity * STEP;
// println!("{position}")
}
position
}
pub fn interpolate_rendered_keyframes(
first_ren_keyframe: &RenderedKeyframe,
second_ren_keyframe: &RenderedKeyframe,
curr_frame: i32,
interpolation_type: InterpolationType,
_fps: i16,
) -> f32 {
let frame_range = second_ren_keyframe.absolute_frame - first_ren_keyframe.absolute_frame;
let position_in_range = curr_frame - first_ren_keyframe.absolute_frame;
let progress: f32 = (1.0 / frame_range as f32) * position_in_range as f32;
/* println!(
"Progress:{0} Frame_Range: {1} Position_In_Range: {2}",
progress, frame_range, position_in_range
); */
let value_diff = second_ren_keyframe.keyframe.value - first_ren_keyframe.keyframe.value;
match interpolation_type {
InterpolationType::Linear => {
let interpolated_val =
first_ren_keyframe.keyframe.value + (value_diff * progress as f32);
return interpolated_val;
}
InterpolationType::EasingFunction(easing_function) => {
let eased_progress = easing_function.ease(progress);
let interpolated_val =
first_ren_keyframe.keyframe.value + (value_diff * eased_progress as f32);
return interpolated_val;
}
InterpolationType::Spring(spring_properties) => {
let interpolated_value = calculate_spring_value(
curr_frame,
first_ren_keyframe.keyframe.value,
second_ren_keyframe.keyframe.value,
first_ren_keyframe.absolute_frame,
second_ren_keyframe.absolute_frame,
&spring_properties,
);
return interpolated_value;
}
};
}

View File

@ -0,0 +1,140 @@
use std::cmp::Ordering;
use serde::{Deserialize, Serialize};
use super::{
entities::AnimationData,
interpolations::{interpolate_rendered_keyframes, InterpolationType},
utils::render_keyframe,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Keyframe {
pub value: f32,
pub offset: f32,
pub interpolation: Option<InterpolationType>,
}
#[derive(Debug, Clone)]
pub struct RenderedKeyframe {
pub absolute_frame: i32,
pub keyframe: Keyframe,
pub index: usize,
pub distance_from_curr: i32,
pub abs_distance_from_curr: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keyframes {
pub values: Vec<Keyframe>,
}
impl Keyframes {
pub fn get_value_at_frame(
&self,
curr_frame: i32,
animation_data: &AnimationData,
fps: i16,
) -> f32 {
let keyframe_count = self.values.len();
if keyframe_count > 0 {
let mut rendered_keyframes: Vec<RenderedKeyframe> = self
.values
.to_vec()
.into_iter()
.enumerate()
.map(|(index, keyframe)| {
render_keyframe(keyframe, animation_data, index, curr_frame, fps)
})
.collect();
rendered_keyframes
.sort_by(|a, b| a.abs_distance_from_curr.cmp(&b.abs_distance_from_curr));
let closest_keyframe = rendered_keyframes.get(0).unwrap();
let result = match (closest_keyframe.distance_from_curr).cmp(&0) {
Ordering::Equal => closest_keyframe.keyframe.value,
Ordering::Greater => {
if closest_keyframe.absolute_frame == curr_frame {
return closest_keyframe.keyframe.value;
} else {
let previous_keyframe =
rendered_keyframes.to_vec().into_iter().find(|keyframe| {
if closest_keyframe.index > 0 {
keyframe.index == closest_keyframe.index - 1
} else {
false
}
});
if let Some(previous_keyframe) = previous_keyframe {
let interpolation = match previous_keyframe.keyframe.interpolation {
Some(val) => val,
None => InterpolationType::Linear,
};
let interpolated_value = interpolate_rendered_keyframes(
&previous_keyframe,
closest_keyframe,
curr_frame,
interpolation,
fps,
);
return interpolated_value;
} else {
if closest_keyframe.absolute_frame > curr_frame {
return closest_keyframe.keyframe.value;
} else {
return 0.0;
}
}
}
}
Ordering::Less => {
if closest_keyframe.absolute_frame == curr_frame {
return closest_keyframe.keyframe.value;
} else {
let next_keyframe = rendered_keyframes
.to_vec()
.into_iter()
.find(|keyframe| keyframe.index == closest_keyframe.index + 1);
if let Some(next_keyframe) = next_keyframe {
let interpolation = match closest_keyframe.keyframe.interpolation {
Some(val) => val,
None => InterpolationType::Linear,
};
let interpolated_value = interpolate_rendered_keyframes(
closest_keyframe,
&next_keyframe,
curr_frame,
interpolation,
fps,
);
return interpolated_value;
} else {
if closest_keyframe.absolute_frame < curr_frame {
return closest_keyframe.keyframe.value;
} else {
return 0.0;
}
}
}
}
};
result
} else {
0.0
}
}
pub fn sort(&mut self) {
self.values.sort_by(|a, b| a.offset.total_cmp(&b.offset));
}
}

View File

@ -0,0 +1,7 @@
pub mod entities;
pub mod interpolations;
pub mod keyframe;
pub mod paint;
pub mod tests;
pub mod utils;
pub mod values;

View File

@ -0,0 +1,69 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Color {
value: (u8, u8, u8, f32),
}
impl Color {
pub fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Color {
Color {
value: (red, green, blue, alpha),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PaintStyle {
Fill(FillStyle),
Stroke(StrokeStyle),
StrokeAndFill(StrokeAndFillStyle),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Paint {
pub style: PaintStyle,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextPaint {
pub style: PaintStyle,
pub align: TextAlign,
pub size: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StrokeStyle {
pub color: Color,
pub width: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StrokeAndFillStyle {
pub stroke: StrokeStyle,
pub fill: FillStyle,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FillStyle {
pub color: Color,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FontDefinition {
pub family_name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Font {
pub glyph_count: i32,
pub weight: i32,
pub style: String,
}

View File

@ -0,0 +1,175 @@
#[cfg(test)]
use crate::animation::primitives::{
entities::AnimationData,
interpolations::{calculate_spring_value, SpringProperties},
keyframe::{Keyframe, Keyframes},
utils::timestamp_to_frame,
};
#[test]
fn interpolates_the_input() {
use crate::animation::primitives::{
interpolations::{interpolate_rendered_keyframes, InterpolationType},
keyframe::{Keyframe, Keyframes, RenderedKeyframe},
utils::render_keyframe,
};
let animation_data = AnimationData {
offset: 0.0,
duration: 3.0,
visible: true,
};
let fps = 60;
let keyframes1 = Keyframes {
values: vec![
Keyframe {
value: 0.0,
offset: 0.0,
interpolation: None,
},
Keyframe {
value: 100.0,
offset: 1.0,
interpolation: None,
},
Keyframe {
value: 300.0,
offset: 3.0,
interpolation: None,
},
],
};
let keyframes2 = Keyframes {
values: vec![
Keyframe {
value: -100.0,
offset: 0.0,
interpolation: None,
},
Keyframe {
value: 0.0,
offset: 1.0,
interpolation: None,
},
],
};
let rendered_keyframes1: Vec<RenderedKeyframe> = keyframes1
.values
.into_iter()
.enumerate()
.map(|(index, keyframe)| {
let rendered_keyframe = render_keyframe(keyframe, &animation_data, index, 120, 60);
rendered_keyframe
})
.collect();
let rendered_keyframes2: Vec<RenderedKeyframe> = keyframes2
.values
.into_iter()
.enumerate()
.map(|(index, keyframe)| {
let rendered_keyframe = render_keyframe(keyframe, &animation_data, index, 120, 60);
rendered_keyframe
})
.collect();
let val1 = interpolate_rendered_keyframes(
rendered_keyframes1.get(1).unwrap(),
rendered_keyframes1.get(2).unwrap(),
120,
InterpolationType::Linear,
fps,
);
let _val2 = interpolate_rendered_keyframes(
rendered_keyframes2.get(0).unwrap(),
rendered_keyframes2.get(1).unwrap(),
30,
InterpolationType::Linear,
fps,
);
assert_eq!(val1, 200.0);
//println!("{0}", val2);
}
#[test]
fn calculates_the_spring_value() {
let _fps = 60;
let previous_value = 0.0;
let next_value = 500.0;
let mut spring_props = SpringProperties {
mass: 1.0, // Mass of the object attached to the spring
stiffness: 100.0, // Stiffness of the spring
damping: 10.0, // Damping factor of the spring
};
let value1 =
calculate_spring_value(100, previous_value, next_value, 100, 300, &mut spring_props);
let value2 =
calculate_spring_value(150, previous_value, next_value, 100, 300, &mut spring_props);
let value3 =
calculate_spring_value(200, previous_value, next_value, 100, 300, &mut spring_props);
println!("{value1}");
println!("{value2}");
println!("{value3}");
}
#[test]
fn converts_timestamp_to_frame() {
let frame1 = timestamp_to_frame(0.0, 60);
let frame2 = timestamp_to_frame(1.0, 60);
let frame3 = timestamp_to_frame(1.5, 60);
assert_eq!(frame1, 0);
assert_eq!(frame2, 60);
assert_eq!(frame3, 90);
}
#[test]
fn gets_value_at_frame() {
let animation_data = AnimationData {
offset: 0.0,
duration: 5.0,
visible: true,
};
let fps = 60;
let keyframes = Keyframes {
values: vec![
Keyframe {
value: 0.0,
offset: 0.0,
interpolation: None,
},
Keyframe {
value: 100.0,
offset: 1.0,
interpolation: None,
},
Keyframe {
value: 300.0,
offset: 3.0,
interpolation: None,
},
],
};
let value1 = keyframes.get_value_at_frame(50, &animation_data, fps);
let value2 = keyframes.get_value_at_frame(90, &animation_data, fps);
let value3 = keyframes.get_value_at_frame(120, &animation_data, fps);
let value4 = keyframes.get_value_at_frame(180, &animation_data, fps);
let value5 = keyframes.get_value_at_frame(220, &animation_data, fps);
println!("value1: {0}", value1);
println!("value2: {0}", value2);
println!("value3: {0}", value3);
println!("value4: {0}", value4);
println!("value5: {0}", value5);
}

View File

@ -0,0 +1,29 @@
use super::{
entities::AnimationData,
keyframe::{Keyframe, RenderedKeyframe},
};
pub fn timestamp_to_frame(timestamp: f32, fps: i16) -> i32 {
return (timestamp * fps as f32).round() as i32;
}
pub fn render_keyframe(
keyframe: Keyframe,
animation_data: &AnimationData,
index: usize,
curr_frame: i32,
fps: i16,
) -> RenderedKeyframe {
let animation_start_frame = timestamp_to_frame(animation_data.offset, fps);
let frame_offset = timestamp_to_frame(keyframe.offset, fps);
let absolute_frame = animation_start_frame + frame_offset;
let distance_from_curr = absolute_frame - curr_frame;
RenderedKeyframe {
absolute_frame,
keyframe,
index,
distance_from_curr,
abs_distance_from_curr: distance_from_curr.abs(),
}
}

View File

@ -0,0 +1,79 @@
use serde::{Deserialize, Serialize};
use super::{
entities::AnimationData,
keyframe::{Keyframe, Keyframes},
};
pub trait AnimatedValue<T> {
fn sort_keyframes(&mut self);
fn get_value_at_frame(&self, curr_frame: i32, animation_data: &AnimationData, fps: i16) -> T;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimatedFloat {
pub keyframes: Keyframes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimatedFloatVec2 {
pub keyframes: (AnimatedFloat, AnimatedFloat),
}
impl AnimatedFloat {
pub fn new(val: f32) -> AnimatedFloat {
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
value: val,
offset: 0.0,
interpolation: None,
}],
},
}
}
}
impl AnimatedFloatVec2 {
pub fn new(x: f32, y: f32) -> AnimatedFloatVec2 {
AnimatedFloatVec2 {
keyframes: (AnimatedFloat::new(x), AnimatedFloat::new(y)),
}
}
}
impl AnimatedValue<f32> for AnimatedFloat {
fn sort_keyframes(&mut self) {
self.keyframes.sort();
}
fn get_value_at_frame(&self, curr_frame: i32, animation_data: &AnimationData, fps: i16) -> f32 {
self.keyframes
.get_value_at_frame(curr_frame, &animation_data, fps)
}
}
impl AnimatedValue<(f32, f32)> for AnimatedFloatVec2 {
fn sort_keyframes(&mut self) {
self.keyframes.0.sort_keyframes();
self.keyframes.1.sort_keyframes();
}
fn get_value_at_frame(
&self,
curr_frame: i32,
animation_data: &AnimationData,
fps: i16,
) -> (f32, f32) {
let x = self
.keyframes
.0
.get_value_at_frame(curr_frame, animation_data, fps);
let y = self
.keyframes
.1
.get_value_at_frame(curr_frame, animation_data, fps);
return (x, y);
}
}

View File

@ -0,0 +1,262 @@
use crate::animation::primitives::{
entities::{AnimatedBoxEntity, AnimatedEntity, AnimatedTextEntity, AnimationData},
interpolations::{EasingFunction, InterpolationType, SpringProperties},
keyframe::{Keyframe, Keyframes},
};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use super::primitives::{
entities::Entity,
paint::{Color, FillStyle, Paint, PaintStyle, StrokeStyle, TextAlign, TextPaint},
values::{AnimatedFloat, AnimatedFloatVec2},
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Input {
pub title: String,
pub sub_title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
entities: Vec<AnimatedEntity>,
pub render_state: RenderState,
pub duration: f32,
pub fps: i16,
pub size: (i32, i32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenderState {
pub curr_frame: i32,
}
impl Timeline {
fn calculate(&self) -> Vec<Entity> {
let mut entities = self.entities.clone();
let entities = entities
.par_iter_mut()
.map(|entity| entity.calculate(self))
.filter(|entity| entity.is_some())
.map(|entity| entity.unwrap())
.collect();
return entities;
}
}
fn build_bg(offset: f32, paint: Paint, size: (i32, i32)) -> AnimatedBoxEntity {
let bg_box = AnimatedBoxEntity {
paint,
animation_data: AnimationData {
offset: 0.0 + offset,
duration: 5.0,
visible: true,
},
origin: AnimatedFloatVec2::new(1280.0 / 2.0, 720.0 / 2.0),
position: AnimatedFloatVec2 {
keyframes: (
AnimatedFloat {
keyframes: Keyframes {
values: vec![
Keyframe {
value: (size.0 * -1) as f32,
offset: 0.0,
interpolation: Some(InterpolationType::EasingFunction(
EasingFunction::QuintOut,
)),
},
Keyframe {
value: 0.0,
offset: 5.0,
interpolation: None,
},
],
},
},
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
value: 0.0,
offset: 0.0,
interpolation: None,
}],
},
},
),
},
size: AnimatedFloatVec2 {
keyframes: (
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
interpolation: None,
value: size.0 as f32,
offset: 0.0,
}],
},
},
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
value: size.1 as f32,
offset: 0.0,
interpolation: None,
}],
},
},
),
},
};
return bg_box;
}
#[tauri::command]
pub fn calculate_timeline_entities_at_frame(timeline: Timeline) -> Vec<Entity> {
timeline.calculate()
}
pub fn test_timeline_entities_at_frame(
render_state: RenderState,
size: (i32, i32),
input: Input,
) -> Vec<Entity> {
let box1_paint = Paint {
style: PaintStyle::Fill(FillStyle {
color: Color::new(34, 189, 58, 1.0),
}),
};
let box2_paint = Paint {
style: PaintStyle::Fill(FillStyle {
color: Color::new(23, 178, 28, 1.0),
}),
};
let box3_paint = Paint {
style: PaintStyle::Fill(FillStyle {
color: Color::new(43, 128, 98, 1.0),
}),
};
let title_paint = TextPaint {
style: PaintStyle::Stroke(StrokeStyle {
color: Color::new(0, 0, 0, 1.0),
width: 10.0,
}),
align: TextAlign::Center,
size: 20.0,
};
let sub_title_paint = TextPaint {
style: PaintStyle::Fill(FillStyle {
color: Color::new(0, 0, 0, 1.0),
}),
align: TextAlign::Center,
size: 10.0,
};
let timeline = Timeline {
fps: 60,
duration: 5.0,
size,
entities: vec![
AnimatedEntity::Box(build_bg(0.0, box1_paint, size)),
AnimatedEntity::Box(build_bg(0.5, box2_paint, size)),
AnimatedEntity::Box(build_bg(1.0, box3_paint, size)),
AnimatedEntity::Text(AnimatedTextEntity {
paint: title_paint,
text: input.title,
animation_data: AnimationData {
offset: 0.0,
duration: 6.0,
visible: true,
},
origin: AnimatedFloatVec2 {
keyframes: (
AnimatedFloat {
keyframes: Keyframes {
values: vec![
Keyframe {
value: 0.0,
offset: 0.0,
interpolation: Some(InterpolationType::Spring(
SpringProperties {
mass: 1.0,
damping: 20.0,
stiffness: 200.0,
},
)),
},
Keyframe {
value: (size.0 / 2) as f32,
offset: 2.0,
interpolation: None,
},
],
},
},
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
value: (size.1 / 2) as f32,
offset: 0.0,
interpolation: None,
}],
},
},
),
},
}),
AnimatedEntity::Text(AnimatedTextEntity {
paint: sub_title_paint,
text: input.sub_title,
animation_data: AnimationData {
offset: 0.5,
duration: 6.0,
visible: true,
},
origin: AnimatedFloatVec2 {
keyframes: (
AnimatedFloat {
keyframes: Keyframes {
values: vec![
Keyframe {
value: 0.0,
offset: 0.0,
interpolation: Some(InterpolationType::Spring(
SpringProperties {
mass: 1.0,
damping: 20.0,
stiffness: 200.0,
},
)),
},
Keyframe {
value: (size.0 / 2) as f32,
offset: 2.0,
interpolation: None,
},
],
},
},
AnimatedFloat {
keyframes: Keyframes {
values: vec![Keyframe {
value: ((size.1 / 2) as f32) + 80.0,
offset: 0.0,
interpolation: None,
}],
},
},
),
},
}),
],
render_state: render_state,
};
timeline.calculate()
}

View File

@ -0,0 +1,14 @@
use font_kit::source::SystemSource;
pub struct Font {
pub path: String,
}
#[tauri::command]
pub fn get_system_fonts() -> Option<Vec<String>> {
let source = SystemSource::new();
let found_families = source.all_families();
found_families.ok()
}

17
app/src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,17 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use crate::{animation::timeline::calculate_timeline_entities_at_frame, fonts::get_system_fonts};
pub mod animation;
pub mod fonts;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
calculate_timeline_entities_at_frame,
get_system_fonts
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,53 @@
{
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "tempblade-creator",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"dialog": {
"open": true,
"save": true
},
"shell": {
"all": false,
"open": true
}
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tempblade.creator",
"targets": "all"
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"title": "tempblade Creator",
"width": 1300,
"height": 900
}
]
}
}

7
app/src/App.css Normal file
View File

@ -0,0 +1,7 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}

39
app/src/App.tsx Normal file
View File

@ -0,0 +1,39 @@
import "./App.css";
import Timeline from "./components/Timeline";
import Canvas from "./components/Canvas";
import Properties, { PropertiesContainer } from "components/Properties";
import MenuBar from "components/MenuBar";
import ToolBar from "components/ToolBar";
import { useEffect } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { useFontsStore } from "stores/fonts.store";
export default function App() {
const { setFonts } = useFontsStore();
useEffect(() => {
invoke("get_system_fonts").then((data) => {
if (data && Array.isArray(data)) {
setFonts(data);
}
});
}, []);
return (
<div className="bg-gray-950 h-full w-full flex flex-col">
<MenuBar />
<div className="flex flex-row w-full h-full">
<ToolBar />
<div className="flex flex-col ml-4 w-full h-full">
<div className="flex gap-4 flex-row mb-4 justify-center items-center">
<Canvas />
<PropertiesContainer>
<Properties />
</PropertiesContainer>
</div>
<Timeline />
</div>
</div>
</div>
);
}

80
app/src/Old.tsx Normal file
View File

@ -0,0 +1,80 @@
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>
);
}

1
app/src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,125 @@
import { FC } from "react";
import { useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api";
import { useTimelineStore } from "stores/timeline.store";
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
import { Surface } from "canvaskit-wasm";
import drawText from "drawers/text";
import drawBox from "drawers/box";
import { Entities, EntityType } from "primitives/Entities";
import drawEllipse from "drawers/ellipse";
import { useRenderStateStore } from "stores/render-state.store";
import { useEntitiesStore } from "stores/entities.store";
import { AnimatedEntities } from "primitives/AnimatedEntities";
type CanvasProps = {};
const CanvasComponent: FC<CanvasProps> = () => {
const canvas = useRef<HTMLCanvasElement>(null);
const [loading, setLoading] = useState(true);
const [canvasKit, setCanvasKit] = useState<CanvasKit>();
const [fontData, setFontData] = useState<ArrayBuffer>();
const surface = useRef<Surface>();
const renderState = useRenderStateStore((store) => store.renderState);
const { fps, size, duration } = useTimelineStore((store) => ({
fps: store.fps,
size: store.size,
duration: store.duration,
}));
const { entities } = useEntitiesStore((store) => ({
entities: store.entities,
}));
useEffect(() => {
InitCanvasKit({
locateFile: (file) =>
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
}).then((CanvasKit) => {
setLoading(false);
setCanvasKit(CanvasKit);
fetch("https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf")
.then((response) => response.arrayBuffer())
.then((arrayBuffer) => {
setFontData(arrayBuffer);
});
if (canvas.current) {
const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas.current);
if (CSurface) {
surface.current = CSurface;
}
}
});
}, []);
useEffect(() => {
// console.time("calculation");
const parsedEntities = AnimatedEntities.parse(entities);
invoke("calculate_timeline_entities_at_frame", {
timeline: {
entities: parsedEntities,
render_state: renderState,
fps,
size,
duration,
},
}).then((data) => {
// console.timeEnd("calculation");
// console.log(data);
const entitiesResult = Entities.safeParse(data);
//console.time("draw");
if (canvasKit && canvas.current && surface.current && fontData) {
surface.current.flush();
surface.current.requestAnimationFrame((skCanvas) => {
skCanvas.clear(canvasKit.WHITE);
if (entitiesResult.success) {
const entities = entitiesResult.data;
entities.reverse().forEach((entity) => {
switch (entity.type) {
case EntityType.Enum.Box:
drawBox(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Ellipse:
drawEllipse(canvasKit, skCanvas, entity);
break;
case EntityType.Enum.Text:
drawText(canvasKit, skCanvas, entity, fontData);
break;
default:
break;
}
});
} else {
console.log(entitiesResult.error);
}
});
}
// console.timeEnd("draw");
});
});
return (
<div>
<div
className="flex items-center justify-center"
style={{ width: "100%", height: "500px" }}
>
<canvas
className="aspect-video h-full"
height={720}
width={1280}
ref={canvas}
></canvas>
</div>
</div>
);
};
export default CanvasComponent;

View File

@ -0,0 +1,9 @@
const Loading = () => {
return (
<div>
<h2>Lädt Skia...</h2>
</div>
);
};
export default Loading;

View File

@ -0,0 +1,129 @@
import { FC } from "react";
import * as Menubar from "@radix-ui/react-menubar";
import { ChevronRightIcon } from "@radix-ui/react-icons";
import { open, save } from "@tauri-apps/api/dialog";
const MenuBarTrigger: FC<{ label: string }> = ({ label }) => {
return (
<Menubar.Trigger className="py-2 dark:text-gray-300 px-3 transition-colors hover:bg-indigo-700 outline-none select-none font-medium leading-none rounded text-[13px] flex items-center justify-between gap-[2px]">
{label}
</Menubar.Trigger>
);
};
const MenuBarSubTrigger: FC<{ label: string }> = ({ label }) => {
return (
<Menubar.SubTrigger
className="group dark:text-gray-300 text-[13px] hover:bg-indigo-800 transition-colors leading-none
text-indigo11 rounded flex items-center h-[25px] px-[10px] relative select-none outline-none
data-[state=open]:bg-indigo data-[state=open]:text-white data-[highlighted]:bg-gradient-to-br
data-[highlighted]:from-indigo9 data-[highlighted]:to-indigo10 data-[highlighted]:text-indigo1
data-[highlighted]:data-[state=open]:text-indigo1 data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none"
>
{label}
<div className="ml-auto pl-5 text-mauve9 group-data-[highlighted]:text-white group-data-[disabled]:text-mauve8">
<ChevronRightIcon />
</div>
</Menubar.SubTrigger>
);
};
const MenuBarItem: FC<{ label: string; onClick?: () => void }> = ({
label,
onClick,
}) => {
return (
<Menubar.Item
onClick={onClick}
className="group dark:text-white text-[13px] leading-none
rounded flex items-center h-[25px] px-[10px]
relative select-none outline-none hover:bg-indigo-800
data-[disabled]:pointer-events-none transition-colors"
>
{label}
</Menubar.Item>
);
};
const MenuBarSeperator = () => {
return <Menubar.Separator className="h-[1px] bg-slate-500 m-[5px]" />;
};
const MenuBar = () => {
const menuBarContentClassName =
"min-w-[220px] bg-gray-800 rounded-md p-[5px]";
const menuBarSubContentClassName =
"min-w-[220px] bg-gray-800 rounded-md p-[5px]";
return (
<Menubar.Root className="flex bg-gray-900 p-[3px] ">
<Menubar.Menu>
<MenuBarTrigger label="File" />
<Menubar.Portal>
<Menubar.Content
className={menuBarContentClassName}
align="start"
sideOffset={5}
alignOffset={-3}
>
<MenuBarItem label="New File" />
<MenuBarItem
onClick={() => open({ multiple: false })}
label="Open File"
/>
<MenuBarItem
onClick={() =>
save({
title: "Save Project",
defaultPath: "project.tbcp",
}).then((val) => {
console.log(val);
})
}
label="Save"
/>
<MenuBarItem onClick={() => save()} label="Save as" />
<MenuBarSeperator />
<Menubar.Sub>
<MenuBarSubTrigger label="Export as ..." />
<Menubar.Portal>
<Menubar.SubContent
className={menuBarSubContentClassName}
alignOffset={-5}
>
<MenuBarItem label=".mp4" />
<MenuBarItem label=".gif" />
<MenuBarItem label=".mov" />
<MenuBarItem label=".webm" />
<MenuBarItem label=".webp" />
</Menubar.SubContent>
</Menubar.Portal>
</Menubar.Sub>
</Menubar.Content>
</Menubar.Portal>
</Menubar.Menu>
<Menubar.Menu>
<MenuBarTrigger label="Edit" />
<Menubar.Portal>
<Menubar.Content
className={menuBarContentClassName}
align="start"
sideOffset={5}
alignOffset={-3}
>
<MenuBarItem label="Undo" />
<MenuBarItem label="Redo" />
<MenuBarItem label="Copy" />
<MenuBarItem label="Paste" />
</Menubar.Content>
</Menubar.Portal>
</Menubar.Menu>
</Menubar.Root>
);
};
export default MenuBar;

View File

@ -0,0 +1,148 @@
import { ease } from "@unom/style";
import { motion } from "framer-motion";
import {
AnimatedTextEntity,
AnimatedBoxEntity,
AnimatedEllipseEntity,
} from "primitives/AnimatedEntities";
import { Paint, PaintStyle, PaintStyleType } from "primitives/Paint";
import { FC } from "react";
import { z } from "zod";
import { AnimatedVec2Properties } from "./Values";
import { PropertiesProps } from "./common";
type TextPropertiesProps = PropertiesProps<z.input<typeof AnimatedTextEntity>>;
type PaintPropertiesProps = PropertiesProps<z.input<typeof Paint>>;
type BoxPropertiesProps = PropertiesProps<z.input<typeof AnimatedBoxEntity>>;
type EllipsePropertiesProps = PropertiesProps<
z.input<typeof AnimatedEllipseEntity>
>;
export const PaintProperties: FC<PaintPropertiesProps> = ({
entity,
onUpdate,
}) => {
return (
<div>
<label className="flex flex-col items-start">
<span className="label">PaintStyle</span>
<select
value={entity.style.type}
onChange={(e) => {
if (entity.style.type !== e.target.value) {
const paintStyle = { type: e.target.value };
const parsedPaintStyle = PaintStyle.parse(paintStyle);
onUpdate({ style: parsedPaintStyle });
}
}}
>
{Object.keys(PaintStyleType.Values).map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
</label>
</div>
);
};
export const TextProperties: FC<TextPropertiesProps> = ({
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 })}
/>
</label>
<label className="flex flex-col items-start">
<span className="label">Size</span>
<input
value={entity.paint.size}
onChange={(e) =>
onUpdate({
...entity,
paint: { ...entity.paint, size: Number(e.target.value) },
})
}
></input>
</label>
<AnimatedVec2Properties
onUpdate={(updatedEntity) =>
onUpdate({ ...entity, origin: updatedEntity })
}
label="Origin"
entity={entity.origin}
/>
</motion.div>
);
};
export const BoxProperties: FC<BoxPropertiesProps> = ({ entity, onUpdate }) => {
return (
<div className="dark:text-white">
<PaintProperties
entity={entity.paint}
onUpdate={(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>
);
};
export const EllipseProperties: FC<EllipsePropertiesProps> = ({
entity,
onUpdate,
}) => {
return (
<div className="dark:text-white">
<PaintProperties
entity={entity.paint}
onUpdate={(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>
);
};

View File

@ -0,0 +1,176 @@
import { AnimatedNumber, AnimatedVec2 } from "primitives/Values";
import { PropertiesProps } from "./common";
import { FC } from "react";
import { z } from "zod";
import { produce } from "immer";
import { Interpolation } from "primitives/Interpolation";
import { Color } from "primitives/Paint";
const InterpolationProperties: FC<
PropertiesProps<z.input<typeof Interpolation>>
> = ({ entity, onUpdate }) => {
return <div>Interpolation: {entity.type}</div>;
};
const AnimatedNumberProperties: FC<
PropertiesProps<z.input<typeof AnimatedNumber>> & { label: string }
> = ({ entity, onUpdate, label }) => {
return (
<div>
<span>{label}</span>
{entity.keyframes.values.map((keyframe, index) => {
return (
<div key={index}>
<div className="flex flex-row gap-3">
<label className="flex flex-col items-start w-16">
<span className="label text-sm opacity-70">Offset</span>
<input
value={keyframe.offset}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].offset = Number(
e.target.value
);
})
)
}
/>
</label>
<label className="flex flex-col items-start">
<span className="label text-sm opacity-70">Value</span>
<input
value={keyframe.value}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].value = Number(
e.target.value
);
})
)
}
/>
</label>
</div>
{keyframe.interpolation && (
<InterpolationProperties
onUpdate={(updatedEntity) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes.values[index].interpolation =
updatedEntity;
})
)
}
entity={keyframe.interpolation}
/>
)}
</div>
);
})}
</div>
);
};
export const ColorProperties: FC<
PropertiesProps<z.input<typeof Color>> & {
label: string;
}
> = ({ entity, onUpdate }) => {
return (
<label className="flex flex-col items-start">
<span className="label">Color</span>
<div className="flex flex-row gap-3">
<input
value={entity.value[0]}
type="number"
max={255}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.value[0] = Number(e.target.value);
})
)
}
/>
<input
value={entity.value[1]}
type="number"
max={255}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.value[1] = Number(e.target.value);
})
)
}
/>
<input
value={entity.value[2]}
type="number"
max={255}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.value[2] = Number(e.target.value);
})
)
}
/>
<input
value={entity.value[3]}
type="number"
max={1}
onChange={(e) =>
onUpdate(
produce(entity, (draft) => {
draft.value[3] = Number(e.target.value);
})
)
}
/>
</div>
</label>
);
};
export const AnimatedVec2Properties: FC<
PropertiesProps<z.input<typeof AnimatedVec2>> & { label: string }
> = ({ entity, onUpdate, label }) => {
return (
<div>
<label className="flex flex-col items-start">
<span className="label">{label}</span>
<AnimatedNumberProperties
entity={entity.keyframes[0]}
label="X"
onUpdate={(updatedEntity) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes[0] = {
...draft.keyframes[0],
...updatedEntity,
};
})
)
}
/>
<AnimatedNumberProperties
entity={entity.keyframes[1]}
label="Y"
onUpdate={(updatedEntity) =>
onUpdate(
produce(entity, (draft) => {
draft.keyframes[1] = {
...draft.keyframes[1],
...updatedEntity,
};
})
)
}
/>
</label>
</div>
);
};

View File

@ -0,0 +1,4 @@
export type PropertiesProps<E> = {
entity: E;
onUpdate: (entity: E) => void;
};

View File

@ -0,0 +1,69 @@
import { FC, ReactNode } from "react";
import { useEntitiesStore } from "stores/entities.store";
import { shallow } from "zustand/shallow";
import { BoxProperties, EllipseProperties, TextProperties } from "./Primitives";
const PropertiesContainer: FC<{ children: ReactNode }> = ({ children }) => {
return (
<div className="w-full rounded-md h-[500px] overflow-auto border transition-colors focus-within:border-gray-400 border-gray-600 flex flex-col items-start p-4">
{children}
</div>
);
};
const Properties = () => {
const { selectedEntity, entities, updateEntity } = useEntitiesStore(
(store) => ({
updateEntity: store.updateEntity,
selectedEntity: store.selectedEntity,
entities: store.entities,
}),
shallow
);
const entity = selectedEntity !== undefined && entities[selectedEntity];
if (entity) {
switch (entity.type) {
case "Text":
return (
<TextProperties
key={selectedEntity}
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
entity={entity}
/>
);
case "Box":
return (
<BoxProperties
key={selectedEntity}
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
entity={entity}
/>
);
case "Ellipse":
return (
<EllipseProperties
key={selectedEntity}
onUpdate={(entity) => updateEntity(selectedEntity, entity)}
entity={entity}
/>
);
default:
return null;
}
}
return (
<div>
<h3>Wähle ein Element aus</h3>
</div>
);
};
export { PropertiesContainer };
export default Properties;

View File

@ -0,0 +1,29 @@
import { FC } from "react";
import * as Slider from "@radix-ui/react-slider";
import { useRenderStateStore } from "stores/render-state.store";
export type TimePickerProps = {};
const TimePicker: FC<TimePickerProps> = () => {
const { renderState, setCurrentFrame } = useRenderStateStore();
return (
<Slider.Root
className="relative flex select-none h-5 w-full items-center"
defaultValue={[50]}
style={{ width: 100 * 10 }}
value={[renderState.curr_frame]}
onValueChange={(val) => setCurrentFrame(val[0])}
max={60 * 10}
step={1}
aria-label="Current Frame"
>
<Slider.Track className="SliderTrack">
<Slider.Range className="SliderRange" />
</Slider.Track>
<Slider.Thumb className="SliderThumb" />
</Slider.Root>
);
};
export default TimePicker;

View File

@ -0,0 +1,211 @@
import { FC } from "react";
import { z } from "zod";
import { AnimationData } from "primitives/AnimatedEntities";
import { motion } from "framer-motion";
import TimePicker from "./TimePicker";
import { shallow } from "zustand/shallow";
import { useEntitiesStore } from "stores/entities.store";
import { ease } from "@unom/style";
import Timestamp from "./Timestamp";
import { Keyframe, Keyframes } from "primitives/Keyframe";
import { flattenedKeyframesByEntity } from "utils";
export type AnimationEntity = {
offset: number;
duration: number;
};
type TimelineProps = {};
type TrackProps = {
animationData: z.input<typeof AnimationData>;
name: string;
index: number;
keyframes: Array<z.input<typeof Keyframe>>;
};
const KeyframeIndicator: FC<{
keyframe: z.input<typeof Keyframe>;
animationData: z.input<typeof AnimationData>;
}> = ({ keyframe, animationData }) => {
return (
<motion.div
animate={{
x: (animationData.offset + keyframe.offset) * 100 + 4,
}}
transition={ease.quint(0.4).out}
style={{
clipPath: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
}}
className="bg-indigo-300 absolute w-2 h-2 z-30 top-[39%]"
></motion.div>
);
};
const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
useEntitiesStore(
(store) => ({
updateEntity: store.updateEntity,
selectedEntity: store.selectedEntity,
selectEntity: store.selectEntity,
deselectEntity: store.deselectEntity,
}),
shallow
);
return (
<div className="h-8 w-100 flex flex-row gap-1">
<div
onClick={() =>
selectedEntity !== undefined && selectedEntity === index
? deselectEntity()
: selectEntity(index)
}
className={`h-full transition-all rounded-sm flex-shrink-0 w-96 p-1 px-2 flex flex-row ${
selectedEntity === index ? "bg-gray-800" : "bg-gray-900"
}`}
>
<h3 className="text-white-800">{name}</h3>
</div>
<div
style={{ width: "1000px" }}
className="flex w-full h-full flex-row relative bg-gray-900"
>
{keyframes.map((keyframe, index) => (
<KeyframeIndicator
animationData={animationData}
keyframe={keyframe}
key={index}
/>
))}
<motion.div
drag="x"
animate={{
x: animationData.offset * 100,
}}
whileHover={{
scale: 1.1,
}}
whileTap={{
scale: 0.9,
}}
transition={ease.circ(0.6).out}
dragElastic={false}
dragConstraints={{ left: 0, right: 900 }}
onDragEnd={(e, info) => {
let offset = info.offset.x;
offset *= 0.01;
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-full absolute rounded-md"
/>
<motion.div
drag="x"
animate={{
x: (animationData.duration + animationData.offset) * 100 - 16,
}}
whileHover={{
scale: 1.1,
}}
whileTap={{
scale: 0.9,
}}
transition={ease.circ(0.6).out}
dragConstraints={{ left: 0, right: 900 }}
onDragEnd={(e, info) => {
let offset = info.offset.x;
offset *= 0.01;
const duration = animationData.duration + offset;
updateEntity(index, {
animation_data: {
...animationData,
duration: duration < 0 ? 0 : duration,
},
});
}}
className="z-10 w-4 bg-slate-500 h-full absolute rounded-md"
/>
<motion.div
drag="x"
animate={{
width: animationData.duration * 100,
x: animationData.offset * 100,
}}
whileHover={{ scaleY: 1.1 }}
whileTap={{ scaleY: 0.9 }}
dragConstraints={{
left: 0,
right: 900,
}}
transition={ease.circ(0.8).out}
onDragEnd={(e, info) => {
let offset = info.offset.x;
offset *= 0.01;
offset += animationData.offset;
console.log(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"
></motion.div>
</div>
</div>
);
};
const Timeline: FC<TimelineProps> = () => {
const { entities } = useEntitiesStore((store) => ({
entities: store.entities,
}));
return (
<div className="flex flex-col p-4 border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
<Timestamp />
<div className="gap-1 flex flex-col overflow-hidden">
<div className="z-20 flex flex-row gap-2">
<div className="flex-shrink-0 w-96" />
<TimePicker />
</div>
{entities.map((entity, index) => (
<Track
name={entity.type}
index={index}
key={index}
keyframes={flattenedKeyframesByEntity(entity)}
animationData={entity.animation_data}
/>
))}
</div>
</div>
);
};
export default Timeline;

View File

@ -0,0 +1,19 @@
import { useRenderStateStore } from "stores/render-state.store";
import { useTimelineStore } from "stores/timeline.store";
const Timestamp = () => {
const { renderState } = useRenderStateStore();
const timeline = useTimelineStore();
return (
<div>
<h3>Frame {renderState.curr_frame}</h3>
<h2 className="text-xl font-bold">
{((renderState.curr_frame * timeline.fps) / 60 / 60).toPrecision(3)}{" "}
<span className="text-sm font-light">/ {timeline.fps}FPS</span>
</h2>
</div>
);
};
export default Timestamp;

View File

@ -0,0 +1,58 @@
import {
BoxIcon,
CircleIcon,
CursorArrowIcon,
MixIcon,
Pencil1Icon,
Pencil2Icon,
SymbolIcon,
TextIcon,
} from "@radix-ui/react-icons";
import * as Toolbar from "@radix-ui/react-toolbar";
import { FC, ReactNode } from "react";
const ToolBarButton: FC<{ children: ReactNode }> = ({ children }) => {
return (
<Toolbar.Button
className="text-white p-[10px] bg-gray-900 flex-shrink-0 flex-grow-0
basis-auto w-[40px] h-[40px] rounded inline-flex text-[13px] leading-none
items-center justify-center outline-none hover:bg-indigo-900
transition-colors
focus:relative focus:shadow-[0_0_0_2px] focus:shadow-indigo"
>
{children}
</Toolbar.Button>
);
};
const ToolBar = () => {
return (
<Toolbar.Root
className="bg-gray-800 flex flex-col gap-1 p-1 h-full"
orientation="vertical"
>
<ToolBarButton>
<CursorArrowIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton>
<BoxIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<CircleIcon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<Pencil1Icon width="100%" height="100%" />
</ToolBarButton>
<ToolBarButton>
<MixIcon width="100%" height="100%" />
</ToolBarButton>
<Toolbar.Separator />
<ToolBarButton>
<TextIcon width="100%" height="100%" />
</ToolBarButton>
</Toolbar.Root>
);
};
export default ToolBar;

47
app/src/drawers/box.ts Normal file
View File

@ -0,0 +1,47 @@
import { Canvas, CanvasKit } from "canvaskit-wasm";
import { z } from "zod";
import { BoxEntity } from "primitives/Entities";
import { buildPaintStyle } from "./paint";
export default function drawBox(
CanvasKit: CanvasKit,
canvas: Canvas,
entity: z.infer<typeof BoxEntity>
) {
const paint = new CanvasKit.Paint();
const debugPaint = new CanvasKit.Paint();
debugPaint.setColor(CanvasKit.RED);
buildPaintStyle(CanvasKit, paint, entity.paint);
let targetPosition = entity.position;
canvas.drawCircle(targetPosition[0], targetPosition[1], 10, debugPaint);
targetPosition = targetPosition.map((val, index) => {
let temp = val - entity.size[index] * 0.5;
return temp;
});
debugPaint.setColor(CanvasKit.BLUE);
canvas.drawCircle(targetPosition[0], targetPosition[1], 10, debugPaint);
debugPaint.setColor(CanvasKit.GREEN);
canvas.drawCircle(targetPosition[0], targetPosition[1], 10, debugPaint);
console.log(targetPosition[0], targetPosition[1]);
const rect = CanvasKit.XYWHRect(
targetPosition[0],
targetPosition[1],
entity.size[0],
entity.size[1]
);
canvas.drawRect(rect, paint);
canvas.drawCircle(targetPosition[0], targetPosition[1], 10, debugPaint);
}

View File

@ -0,0 +1,24 @@
import { convertToFloat } from "@tempblade/common";
import { Canvas, CanvasKit } from "canvaskit-wasm";
import { EllipseEntity } from "primitives/Entities";
import { z } from "zod";
import { buildPaintStyle } from "./paint";
export default function drawEllipse(
CanvasKit: CanvasKit,
canvas: Canvas,
entity: z.infer<typeof EllipseEntity>
) {
const paint = new CanvasKit.Paint();
buildPaintStyle(CanvasKit, paint, entity.paint);
const rect = CanvasKit.XYWHRect(
entity.position[0],
entity.position[1],
entity.radius[0],
entity.radius[1]
);
canvas.drawOval(rect, paint);
}

30
app/src/drawers/paint.ts Normal file
View File

@ -0,0 +1,30 @@
import { convertToFloat } from "@tempblade/common";
import { Paint as SkPaint, CanvasKit } from "canvaskit-wasm";
import { Paint } from "primitives/Paint";
import { z } from "zod";
export function buildPaintStyle(
CanvasKit: CanvasKit,
skPaint: SkPaint,
paint: z.output<typeof Paint>
) {
const color = convertToFloat(paint.style.color.value);
skPaint.setAntiAlias(true);
skPaint.setColor(color);
switch (paint.style.type) {
case "Fill":
skPaint.setStyle(CanvasKit.PaintStyle.Fill);
break;
case "Stroke":
skPaint.setStyle(CanvasKit.PaintStyle.Stroke);
skPaint.setStrokeWidth(paint.style.width);
break;
default:
console.error("Paint Style not supported!");
break;
}
}

42
app/src/drawers/text.ts Normal file
View File

@ -0,0 +1,42 @@
import { Canvas, CanvasKit } from "canvaskit-wasm";
import { TextEntity } from "primitives/Entities";
import { convertToFloat } from "@tempblade/common";
import { z } from "zod";
export default function drawText(
CanvasKit: CanvasKit,
canvas: Canvas,
entity: z.infer<typeof TextEntity>,
fontData: ArrayBuffer
) {
const fontMgr = CanvasKit.FontMgr.FromData(fontData);
if (!fontMgr) {
console.error("No FontMgr");
return;
}
const paint = new CanvasKit.Paint();
const color = convertToFloat(entity.paint.style.color.value);
paint.setColor(color);
const pStyle = new CanvasKit.ParagraphStyle({
textStyle: {
color: color,
fontFamilies: ["Roboto"],
fontSize: entity.paint.size,
},
textDirection: CanvasKit.TextDirection.LTR,
textAlign: CanvasKit.TextAlign[entity.paint.align],
});
const builder = CanvasKit.ParagraphBuilder.Make(pStyle, fontMgr);
builder.addText(entity.text);
const p = builder.build();
p.layout(900);
const height = p.getHeight() / 2;
const width = p.getMaxWidth() / 2;
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
}

190
app/src/example.ts Normal file
View File

@ -0,0 +1,190 @@
import { AnimatedEntity } from "primitives/AnimatedEntities";
import { Color } from "primitives/Paint";
import { Timeline } from "primitives/Timeline";
import { staticAnimatedNumber, staticAnimatedVec2 } from "primitives/Values";
import { z } from "zod";
function buildBox1(
offset: number,
color: z.infer<typeof Color>
): z.input<typeof AnimatedEntity> {
return {
type: "Box",
paint: {
style: {
type: "Stroke",
width: 50,
color,
},
},
size: {
keyframes: [
{
keyframes: {
values: [
{
interpolation: {
type: "EasingFunction",
easing_function: "CircOut",
},
value: 0.0,
offset: 0.0,
},
{
interpolation: {
type: "Linear",
},
value: 1280.0,
offset: 4.0,
},
],
},
},
staticAnimatedNumber(720),
],
},
origin: staticAnimatedVec2(1280 / 2, 720 / 2),
position: staticAnimatedVec2(0, 0),
animation_data: {
offset,
duration: 10.0,
},
};
}
function buildBox(
offset: number,
color: z.infer<typeof Color>
): z.input<typeof AnimatedEntity> {
return {
type: "Box",
paint: {
style: {
type: "Fill",
color,
},
},
size: {
keyframes: [
{
keyframes: {
values: [
{
interpolation: {
type: "Linear",
},
value: 1280.0,
offset: 0.0,
},
],
},
},
{
keyframes: {
values: [
{
interpolation: {
type: "EasingFunction",
easing_function: "CircOut",
},
value: 0.0,
offset: 0.0,
},
{
interpolation: {
type: "Linear",
},
value: 720.0,
offset: 4.0,
},
],
},
},
],
},
origin: staticAnimatedVec2(0, -720),
position: staticAnimatedVec2(1280 / 2, 720 / 2),
animation_data: {
offset,
duration: 10.0,
},
};
}
function buildText(
text: string,
offset: number,
size: number,
y_offset: number,
color: z.infer<typeof Color>
): z.input<typeof AnimatedEntity> {
return {
type: "Text",
paint: {
style: {
type: "Fill",
color,
},
size,
align: "Center",
},
text,
animation_data: {
offset,
duration: 5.0,
},
origin: {
keyframes: [
{
keyframes: {
values: [
{
interpolation: {
type: "Spring",
mass: 1,
stiffness: 100,
damping: 15,
},
value: (1280 / 2) * -1 - 300,
offset: 0.0,
},
{
interpolation: {
type: "EasingFunction",
easing_function: "QuartOut",
},
value: 1280 / 2,
offset: 5.0,
},
],
},
},
staticAnimatedNumber(720 / 2 + y_offset),
],
},
};
}
export const EXAMPLE_ANIMATED_ENTITIES: Array<z.input<typeof AnimatedEntity>> =
[
buildText("Kleine Dumpfkopf!", 1.0, 80, -30, {
value: [255, 255, 255, 1.0],
}),
buildText("Wie gehts?", 1.5, 40, 30, { value: [255, 255, 255, 1.0] }),
buildBox(0.6, { value: [30, 30, 30, 1.0] }),
buildBox(0.4, { value: [20, 20, 20, 1.0] }),
buildBox(0.2, { value: [10, 10, 10, 1.0] }),
buildBox(0, { value: [0, 0, 0, 1.0] }),
];
const ExampleTimeline: z.input<typeof Timeline> = {
size: [1920, 1080],
duration: 10.0,
render_state: {
curr_frame: 20,
},
fps: 60,
entities: EXAMPLE_ANIMATED_ENTITIES,
};
export { ExampleTimeline };

10
app/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,37 @@
import { z } from "zod";
import { BoxEntity, EllipseEntity, TextEntity } from "./Entities";
import { AnimatedVec2 } from "./Values";
export const AnimationData = z.object({
offset: z.number(),
duration: z.number(),
visible: z.boolean().optional().default(true),
});
export const AnimatedBoxEntity = BoxEntity.extend({
position: AnimatedVec2,
size: AnimatedVec2,
origin: AnimatedVec2,
animation_data: AnimationData,
});
export const AnimatedTextEntity = TextEntity.extend({
origin: AnimatedVec2,
animation_data: AnimationData,
});
export const AnimatedEllipseEntity = EllipseEntity.extend({
radius: AnimatedVec2,
position: AnimatedVec2,
origin: AnimatedVec2,
animation_data: AnimationData,
});
export const AnimatedEntity = z.discriminatedUnion("type", [
AnimatedBoxEntity,
AnimatedTextEntity,
AnimatedEllipseEntity,
]);
export const AnimatedEntities = z.array(AnimatedEntity);

View File

@ -0,0 +1,40 @@
import { z } from "zod";
import { Vec2 } from "./Values";
import { Paint, TextPaint } from "./Paint";
const EntityTypeOptions = ["Text", "Ellipse", "Box"] as const;
export const EntityType = z.enum(EntityTypeOptions);
export const GeometryEntity = z.object({
paint: Paint,
});
export const BoxEntity = GeometryEntity.extend({
type: z.literal(EntityType.Enum.Box),
size: Vec2,
position: Vec2,
origin: Vec2,
});
export const EllipseEntity = GeometryEntity.extend({
type: z.literal(EntityType.Enum.Ellipse),
radius: Vec2,
position: Vec2,
origin: Vec2,
});
export const TextEntity = z.object({
type: z.literal(EntityType.Enum.Text),
paint: TextPaint,
origin: Vec2,
text: z.string(),
});
export const Entity = z.discriminatedUnion("type", [
BoxEntity,
EllipseEntity,
TextEntity,
]);
export const Entities = z.array(Entity);

View File

@ -0,0 +1,53 @@
import { z } from "zod";
const InterpolationTypeOptions = [
"Linear",
"Spring",
"EasingFunction",
] as const;
const EasingFunctionOptions = [
"QuintOut",
"QuintIn",
"QuintInOut",
"CircOut",
"CircIn",
"CircInOut",
"CubicOut",
"CubicIn",
"CubicInOut",
"ExpoOut",
"ExpoIn",
"ExpoInOut",
"QuadOut",
"QuadIn",
"QuadInOut",
"QuartOut",
"QuartIn",
"QuartInOut",
] as const;
export const EasingFunction = z.enum(EasingFunctionOptions);
export const InterpolationType = z.enum(InterpolationTypeOptions);
export const LinearInterpolation = z.object({
type: z.literal(InterpolationType.Enum.Linear),
});
export const EasingFunctionInterpolation = z.object({
type: z.literal(InterpolationType.Enum.EasingFunction),
easing_function: EasingFunction,
});
export const SpringInterpolation = z.object({
mass: z.number(),
damping: z.number(),
stiffness: z.number(),
type: z.literal(InterpolationType.Enum.Spring),
});
export const Interpolation = z.discriminatedUnion("type", [
SpringInterpolation,
EasingFunctionInterpolation,
LinearInterpolation,
]);

View File

@ -0,0 +1,12 @@
import { z } from "zod";
import { Interpolation } from "./Interpolation";
export const Keyframe = z.object({
value: z.number(),
offset: z.number(),
interpolation: z.optional(Interpolation),
});
export const Keyframes = z.object({
values: z.array(Keyframe),
});

View File

@ -0,0 +1,49 @@
import { z } from "zod";
export const Color = z.object({
value: z.array(z.number().min(0).max(255)).max(4),
});
const PaintStyleTypeOptions = ["Fill", "Stroke"] as const;
export const PaintStyleType = z.enum(PaintStyleTypeOptions);
const ColorWithDefault = Color.optional().default({ value: [0, 0, 0, 1] });
export const StrokeStyle = z.object({
width: z.number().min(0).optional().default(10),
color: ColorWithDefault,
type: z.literal(PaintStyleType.Enum.Stroke),
});
export const FillStyle = z.object({
color: ColorWithDefault,
type: z.literal(PaintStyleType.Enum.Fill),
});
export const TextAlign = z.enum(["Left", "Center", "Right"]);
export const PaintStyle = z.discriminatedUnion("type", [
StrokeStyle,
FillStyle,
]);
export const Paint = z.object({
style: PaintStyle,
});
export const TextPaint = z.object({
style: PaintStyle,
align: TextAlign,
size: z.number().min(0),
});
/* const NestedFillStyle = FillStyle.omit({ type: true }).default({});
const NestedStrokeStyle = StrokeStyle.omit({ type: true }).default({});
export const StrokeAndFillStyle = z.object({
color: ColorWithDefault,
type: z.literal(PaintStyleType.Enum.StrokeAndFill),
fill: NestedFillStyle,
stroke: NestedStrokeStyle,
}); */

View File

@ -0,0 +1,14 @@
import { z } from "zod";
import { AnimatedEntities } from "./AnimatedEntities";
export const RenderState = z.object({
curr_frame: z.number(),
});
export const Timeline = z.object({
entities: AnimatedEntities,
render_state: RenderState,
duration: z.number(),
fps: z.number().int(),
size: z.array(z.number().int()).length(2),
});

View File

@ -0,0 +1,67 @@
import { z } from "zod";
import { Keyframes } from "./Keyframe";
import { Interpolation } from "./Interpolation";
export const Vec2 = z.array(z.number()).length(2);
export const AnimatedNumber = z.object({
keyframes: Keyframes,
});
export const AnimatedVec2 = z.object({
keyframes: z.array(AnimatedNumber).length(2),
});
export function staticAnimatedNumber(
number: number
): z.infer<typeof AnimatedNumber> {
return {
keyframes: {
values: [
{
interpolation: {
type: "Linear",
},
value: number,
offset: 0,
},
],
},
};
}
export function staticAnimatedVec2(
x: number,
y: number
): z.infer<typeof AnimatedVec2> {
return {
keyframes: [
{
keyframes: {
values: [
{
interpolation: {
type: "Linear",
},
value: x,
offset: 0,
},
],
},
},
{
keyframes: {
values: [
{
interpolation: {
type: "Linear",
},
value: y,
offset: 0,
},
],
},
},
],
};
}

View File

@ -0,0 +1,27 @@
import { useTimelineStore } from "stores/timeline.store";
import { useEntitiesStore } from "stores/entities.store";
import { useRenderStateStore } from "stores/render-state.store";
import { z } from "zod";
import { Timeline } from "primitives/Timeline";
export class ProjectService {
public saveProject() {
const timelineStore = useTimelineStore.getState();
const entitiesStore = useEntitiesStore.getState();
const renderStateStore = useRenderStateStore.getState();
const timeline: z.input<typeof Timeline> = {
...timelineStore,
entities: entitiesStore.entities,
render_state: renderStateStore.renderState,
};
const parsedTimeline = Timeline.parse(timeline);
const serializedTimeline = JSON.stringify(parsedTimeline);
return serializedTimeline;
}
public loadProject() {}
}

View File

@ -0,0 +1,35 @@
import { EXAMPLE_ANIMATED_ENTITIES } from "example";
import { produce } from "immer";
import { AnimatedEntities, AnimatedEntity } from "primitives/AnimatedEntities";
import { z } from "zod";
import { create } from "zustand";
interface EntitiesStore {
entities: z.input<typeof AnimatedEntities>;
selectedEntity: number | undefined;
selectEntity: (index: number) => void;
deselectEntity: () => void;
updateEntity: (
index: number,
entity: Partial<z.input<typeof AnimatedEntity>>
) => void;
}
const useEntitiesStore = create<EntitiesStore>((set) => ({
entities: EXAMPLE_ANIMATED_ENTITIES,
selectEntity: (index) => set(() => ({ selectedEntity: index })),
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
selectedEntity: undefined,
updateEntity: (index, entity) =>
set(({ entities }) => {
const nextEntities = produce(entities, (draft) => {
draft[index] = { ...draft[index], ...entity } as z.infer<
typeof AnimatedEntity
>;
});
return { entities: nextEntities };
}),
}));
export { useEntitiesStore };

View File

@ -0,0 +1,13 @@
import { create } from "zustand";
interface FontsStore {
fonts: Array<string>;
setFonts: (fonts: Array<string>) => void;
}
const useFontsStore = create<FontsStore>((set) => ({
fonts: [],
setFonts: (fonts) => ({ fonts }),
}));
export { useFontsStore };

View File

@ -0,0 +1,24 @@
import { RenderState } from "primitives/Timeline";
import { z } from "zod";
import { create } from "zustand";
interface RenderStateStore {
renderState: z.infer<typeof RenderState>;
setCurrentFrame: (target: number) => void;
}
const useRenderStateStore = create<RenderStateStore>((set) => ({
renderState: {
curr_frame: 20,
},
setCurrentFrame: (target) =>
set((store) => {
store.renderState = {
curr_frame: target,
};
return { renderState: store.renderState };
}),
}));
export { useRenderStateStore };

View File

@ -0,0 +1,15 @@
import { create } from "zustand";
interface TimelineStore {
fps: number;
duration: number;
size: [number, number];
}
const useTimelineStore = create<TimelineStore>((set) => ({
fps: 60,
size: [1920, 1080],
duration: 10.0,
}));
export { useTimelineStore };

137
app/src/styles.css Normal file
View File

@ -0,0 +1,137 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply text-2xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply text-lg;
}
h1,
h2,
h3,
h4,
h5,
h6,
a,
label {
@apply dark:text-white;
}
a {
@apply text-blue-600 underline;
}
select,
input {
@apply box-border bg-gray-900 shadow-indigo-100 hover:shadow-indigo-400
focus:shadow-indigo-600 selection:bg-indigo-400 selection:text-black
outline-none px-3 py-2 rounded-md shadow-[0_0_0_1px];
}
input {
@apply appearance-none items-center justify-center
w-full text-base leading-none
text-white transition-all;
}
select {
@apply appearance-none;
}
}
.label {
@apply mb-1;
}
label {
@apply mb-2;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
html,
body,
#root {
width: 100%;
height: 100%;
}
.SliderRoot {
position: relative;
display: flex;
align-items: center;
user-select: none;
touch-action: none;
width: 200px;
height: 20px;
}
.SliderTrack {
background-color: var(--black);
position: relative;
flex-grow: 1;
border-radius: 9999px;
height: 3px;
}
.SliderRange {
position: absolute;
background-color: #ddd;
border-radius: 9999px;
height: 100%;
}
.SliderThumb {
position: relative;
z-index: 100;
transition: opacity 0.1s linear, filter 0.1s linear;
}
.SliderThumb:hover {
filter: drop-shadow(0px 10px 10px white);
}
.SliderThumb::before {
content: "";
background-color: var(--indigo-400);
width: 20px;
height: 20px;
position: absolute;
left: -8.5px;
top: -10px;
clip-path: polygon(100% 0, 0 0, 50% 75%);
display: block;
@apply bg-indigo-300;
}
.SliderThumb::after {
content: "";
position: absolute;
top: 0;
width: 3px;
height: 1000px;
z-index: 200;
opacity: 1;
@apply bg-indigo-300;
}

45
app/src/utils/index.ts Normal file
View File

@ -0,0 +1,45 @@
import { AnimatedEntity } from "primitives/AnimatedEntities";
import { Keyframe } from "primitives/Keyframe";
import { AnimatedNumber, AnimatedVec2 } from "primitives/Values";
import { z } from "zod";
export function flattenAnimatedNumberKeyframes(
aNumber: z.input<typeof AnimatedNumber>
): Array<z.input<typeof Keyframe>> {
return aNumber.keyframes.values;
}
export function flattenAnimatedVec2Keyframes(
aVec2: z.input<typeof AnimatedVec2>
): Array<z.input<typeof Keyframe>> {
const keyframes: Array<z.input<typeof Keyframe>> = [
...flattenAnimatedNumberKeyframes(aVec2.keyframes[0]),
...flattenAnimatedNumberKeyframes(aVec2.keyframes[1]),
];
return keyframes;
}
export function flattenedKeyframesByEntity(
entity: z.input<typeof AnimatedEntity>
): Array<z.input<typeof Keyframe>> {
const keyframes: Array<z.input<typeof Keyframe>> = [];
switch (entity.type) {
case "Text":
keyframes.push(...flattenAnimatedVec2Keyframes(entity.origin));
break;
case "Box":
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
keyframes.push(...flattenAnimatedVec2Keyframes(entity.size));
break;
case "Ellipse":
keyframes.push(...flattenAnimatedVec2Keyframes(entity.position));
keyframes.push(...flattenAnimatedVec2Keyframes(entity.radius));
break;
default:
break;
}
return keyframes;
}

1
app/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

8
app/tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

33
app/tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"allowJs": false,
"baseUrl": "src",
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolvePackageJsonExports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

9
app/tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

28
app/vite.config.ts Normal file
View File

@ -0,0 +1,28 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [tsconfigPaths(), react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
},
}));

1820
app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

View File

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 749 B

View File

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

Some files were not shown because too many files have changed in this diff Show More