add app
24
app/.gitignore
vendored
Normal 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
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||||
|
}
|
7
app/README.md
Normal 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
@ -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
43
app/package.json
Normal 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
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
6
app/public/tauri.svg
Normal 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
@ -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
@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
4025
app/src-tauri/Cargo.lock
generated
Normal file
30
app/src-tauri/Cargo.toml
Normal 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
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
BIN
app/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
app/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 974 B |
BIN
app/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
app/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 903 B |
BIN
app/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src-tauri/icons/icon.icns
Normal file
BIN
app/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
app/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
2
app/src-tauri/src/animation/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod primitives;
|
||||||
|
pub mod timeline;
|
36
app/src-tauri/src/animation/primitives/circular_text.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
241
app/src-tauri/src/animation/primitives/entities.rs
Normal 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,
|
||||||
|
}
|
170
app/src-tauri/src/animation/primitives/interpolations.rs
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
140
app/src-tauri/src/animation/primitives/keyframe.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
7
app/src-tauri/src/animation/primitives/mod.rs
Normal 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;
|
69
app/src-tauri/src/animation/primitives/paint.rs
Normal 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,
|
||||||
|
}
|
175
app/src-tauri/src/animation/primitives/tests.rs
Normal 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);
|
||||||
|
}
|
29
app/src-tauri/src/animation/primitives/utils.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
79
app/src-tauri/src/animation/primitives/values.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
262
app/src-tauri/src/animation/timeline.rs
Normal 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()
|
||||||
|
}
|
14
app/src-tauri/src/fonts.rs
Normal 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
@ -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");
|
||||||
|
}
|
53
app/src-tauri/tauri.conf.json
Normal 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
@ -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
@ -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
@ -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
@ -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 |
125
app/src/components/Canvas/index.tsx
Normal 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;
|
9
app/src/components/Loading.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const Loading = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Lädt Skia...</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
129
app/src/components/MenuBar/index.tsx
Normal 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;
|
148
app/src/components/Properties/Primitives.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
176
app/src/components/Properties/Values.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
4
app/src/components/Properties/common.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type PropertiesProps<E> = {
|
||||||
|
entity: E;
|
||||||
|
onUpdate: (entity: E) => void;
|
||||||
|
};
|
69
app/src/components/Properties/index.tsx
Normal 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;
|
29
app/src/components/TimePicker.tsx
Normal 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;
|
211
app/src/components/Timeline.tsx
Normal 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;
|
19
app/src/components/Timestamp.tsx
Normal 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;
|
58
app/src/components/ToolBar/index.tsx
Normal 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
@ -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);
|
||||||
|
}
|
24
app/src/drawers/ellipse.ts
Normal 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
@ -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
@ -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
@ -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
@ -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>
|
||||||
|
);
|
37
app/src/primitives/AnimatedEntities.ts
Normal 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);
|
40
app/src/primitives/Entities.ts
Normal 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);
|
53
app/src/primitives/Interpolation.ts
Normal 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,
|
||||||
|
]);
|
12
app/src/primitives/Keyframe.ts
Normal 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),
|
||||||
|
});
|
49
app/src/primitives/Paint.ts
Normal 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,
|
||||||
|
}); */
|
14
app/src/primitives/Timeline.ts
Normal 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),
|
||||||
|
});
|
67
app/src/primitives/Values.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
27
app/src/services/project.service.ts
Normal 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() {}
|
||||||
|
}
|
35
app/src/stores/entities.store.ts
Normal 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 };
|
13
app/src/stores/fonts.store.ts
Normal 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 };
|
24
app/src/stores/render-state.store.ts
Normal 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 };
|
15
app/src/stores/timeline.store.ts
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
8
app/tailwind.config.js
Normal 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
@ -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
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
28
app/vite.config.ts
Normal 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
0
.gitignore → web/.gitignore
vendored
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 749 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |