update logo
improve font resolution logic generate icons improve timeline
BIN
app/app-icon.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 268 KiB |
@ -30,7 +30,7 @@ pub struct Paint {
|
|||||||
pub struct TextPaint {
|
pub struct TextPaint {
|
||||||
pub style: PaintStyle,
|
pub style: PaintStyle,
|
||||||
pub align: TextAlign,
|
pub align: TextAlign,
|
||||||
pub fontName: String,
|
pub font_name: String,
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use super::{
|
use super::{
|
||||||
entities::common::AnimationData,
|
entities::common::AnimationData,
|
||||||
values::{AnimatedFloatVec2, AnimatedValue},
|
values::{AnimatedFloatVec2, AnimatedFloatVec3, AnimatedValue},
|
||||||
};
|
};
|
||||||
use crate::animation::timeline::Timeline;
|
use crate::animation::timeline::Timeline;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -10,7 +10,7 @@ pub struct AnimatedTransform {
|
|||||||
pub translate: AnimatedFloatVec2,
|
pub translate: AnimatedFloatVec2,
|
||||||
pub scale: AnimatedFloatVec2,
|
pub scale: AnimatedFloatVec2,
|
||||||
pub skew: AnimatedFloatVec2,
|
pub skew: AnimatedFloatVec2,
|
||||||
pub rotate: AnimatedFloatVec2,
|
pub rotate: AnimatedFloatVec3,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
@ -18,7 +18,7 @@ pub struct Transform {
|
|||||||
pub translate: (f32, f32),
|
pub translate: (f32, f32),
|
||||||
pub scale: (f32, f32),
|
pub scale: (f32, f32),
|
||||||
pub skew: (f32, f32),
|
pub skew: (f32, f32),
|
||||||
pub rotate: (f32, f32),
|
pub rotate: (f32, f32, f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimatedTransform {
|
impl AnimatedTransform {
|
||||||
|
@ -154,7 +154,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
color: Color::new(0, 0, 0, 1.0),
|
color: Color::new(0, 0, 0, 1.0),
|
||||||
width: 10.0,
|
width: 10.0,
|
||||||
}),
|
}),
|
||||||
fontName: "Arial".to_string(),
|
font_name: "Arial".to_string(),
|
||||||
align: TextAlign::Center,
|
align: TextAlign::Center,
|
||||||
size: 20.0,
|
size: 20.0,
|
||||||
};
|
};
|
||||||
@ -163,7 +163,7 @@ pub fn test_timeline_entities_at_frame(
|
|||||||
style: PaintStyle::Fill(FillStyle {
|
style: PaintStyle::Fill(FillStyle {
|
||||||
color: Color::new(0, 0, 0, 1.0),
|
color: Color::new(0, 0, 0, 1.0),
|
||||||
}),
|
}),
|
||||||
fontName: "Arial".to_string(),
|
font_name: "Arial".to_string(),
|
||||||
align: TextAlign::Center,
|
align: TextAlign::Center,
|
||||||
size: 10.0,
|
size: 10.0,
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,27 @@
|
|||||||
use font_kit::source::SystemSource;
|
use font_kit::source::SystemSource;
|
||||||
|
|
||||||
pub struct Font {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_system_fonts() -> Option<Vec<String>> {
|
pub fn get_system_fonts() -> Option<Vec<String>> {
|
||||||
let source = SystemSource::new();
|
let source = SystemSource::new();
|
||||||
|
|
||||||
let found_families = source.all_families();
|
let found_fonts = source.all_fonts();
|
||||||
|
|
||||||
found_families.ok()
|
match found_fonts {
|
||||||
|
Ok(found_fonts) => {
|
||||||
|
let font_names: Vec<String> = found_fonts
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.load())
|
||||||
|
.filter(|f| f.is_ok())
|
||||||
|
.map(|f| f.unwrap())
|
||||||
|
.map(|f| f.postscript_name())
|
||||||
|
.filter(|f| f.is_some())
|
||||||
|
.map(|f| f.unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(font_names)
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
@ -102,10 +102,10 @@ export const TextProperties: FC<TextPropertiesProps> = ({
|
|||||||
onUpdate({
|
onUpdate({
|
||||||
...entity,
|
...entity,
|
||||||
cache: { valid: false },
|
cache: { valid: false },
|
||||||
paint: { ...entity.paint, fontName: e.target.value },
|
paint: { ...entity.paint, font_name: e.target.value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
value={entity.paint.fontName}
|
value={entity.paint.font_name}
|
||||||
>
|
>
|
||||||
{fonts.map((font) => (
|
{fonts.map((font) => (
|
||||||
<option value={font} key={font}>
|
<option value={font} key={font}>
|
||||||
@ -173,11 +173,11 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
|
|||||||
cache: { valid: false },
|
cache: { valid: false },
|
||||||
letter: {
|
letter: {
|
||||||
...entity.letter,
|
...entity.letter,
|
||||||
paint: { ...entity.letter.paint, fontName: e.target.value },
|
paint: { ...entity.letter.paint, font_name: e.target.value },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
value={entity.letter.paint.fontName}
|
value={entity.letter.paint.font_name}
|
||||||
>
|
>
|
||||||
{fonts.map((font) => (
|
{fonts.map((font) => (
|
||||||
<option value={font} key={font}>
|
<option value={font} key={font}>
|
||||||
|
27
app/src/components/Timeline/KeyframeIndicator.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ease } from "@unom/style";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { AnimationData } from "primitives/AnimatedEntities";
|
||||||
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { TIMELINE_SCALE } from "./common";
|
||||||
|
|
||||||
|
const KeyframeIndicator: FC<{
|
||||||
|
keyframe: z.input<typeof Keyframe>;
|
||||||
|
animationData: z.input<typeof AnimationData>;
|
||||||
|
}> = ({ keyframe, animationData }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 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%] select-none pointer-events-none"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyframeIndicator;
|
@ -1,7 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import * as Slider from "@radix-ui/react-slider";
|
import * as Slider from "@radix-ui/react-slider";
|
||||||
import { useRenderStateStore } from "stores/render-state.store";
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
import { TIMELINE_SCALE } from "./Timeline";
|
import { TIMELINE_SCALE } from "./common";
|
||||||
import { useTimelineStore } from "stores/timeline.store";
|
import { useTimelineStore } from "stores/timeline.store";
|
||||||
|
|
||||||
export type TimePickerProps = {};
|
export type TimePickerProps = {};
|
||||||
@ -12,19 +12,22 @@ const TimePicker: FC<TimePickerProps> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider.Root
|
<Slider.Root
|
||||||
className="relative flex select-none h-5 w-full items-center"
|
className="relative flex items-center select-none touch-none h-5 shrink-0"
|
||||||
defaultValue={[50]}
|
defaultValue={[50]}
|
||||||
style={{ width: TIMELINE_SCALE * timeline.duration }}
|
style={{ width: TIMELINE_SCALE * 10 }}
|
||||||
value={[renderState.curr_frame]}
|
value={[renderState.curr_frame]}
|
||||||
onValueChange={(val) => setCurrentFrame(val[0])}
|
onValueChange={(val) => setCurrentFrame(val[0])}
|
||||||
max={timeline.fps * timeline.duration}
|
max={timeline.fps * timeline.duration}
|
||||||
step={1}
|
step={1}
|
||||||
aria-label="Current Frame"
|
aria-label="Current Frame"
|
||||||
>
|
>
|
||||||
<Slider.Track className="SliderTrack">
|
<Slider.Track className="bg-blackA10 relative grow rounded-full h-[3px]">
|
||||||
<Slider.Range className="SliderRange" />
|
<Slider.Range className="absolute bg-white rounded-full h-full" />
|
||||||
</Slider.Track>
|
</Slider.Track>
|
||||||
<Slider.Thumb className="SliderThumb" />
|
<Slider.Thumb
|
||||||
|
className="block w-5 h-5 bg-white shadow-[0_2px_10px] shadow-blackA7 rounded-[10px] hover:bg-violet3 focus:outline-none focus:shadow-[0_0_0_5px] focus:shadow-blackA8"
|
||||||
|
aria-label="Volume"
|
||||||
|
/>
|
||||||
</Slider.Root>
|
</Slider.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,25 +1,13 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AnimatedEntity, AnimationData } from "primitives/AnimatedEntities";
|
|
||||||
import { Reorder, motion, useDragControls } from "framer-motion";
|
|
||||||
import TimePicker from "./TimePicker";
|
|
||||||
import { shallow } from "zustand/shallow";
|
|
||||||
import { useEntitiesStore } from "stores/entities.store";
|
|
||||||
import { ease } from "@unom/style";
|
import { ease } from "@unom/style";
|
||||||
import Timestamp from "./Timestamp";
|
import { useDragControls, Reorder, motion } from "framer-motion";
|
||||||
|
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
|
||||||
import { Keyframe } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { flattenedKeyframesByEntity } from "utils";
|
import { FC } from "react";
|
||||||
import { PauseIcon, PlayIcon } from "@radix-ui/react-icons";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { useRenderStateStore } from "stores/render-state.store";
|
import { z } from "zod";
|
||||||
|
import { shallow } from "zustand/shallow";
|
||||||
export type AnimationEntity = {
|
import KeyframeIndicator from "./KeyframeIndicator";
|
||||||
offset: number;
|
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TimelineProps = {};
|
|
||||||
|
|
||||||
export const TIMELINE_SCALE = 50;
|
|
||||||
|
|
||||||
type TrackProps = {
|
type TrackProps = {
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
@ -29,24 +17,6 @@ type TrackProps = {
|
|||||||
keyframes: Array<z.input<typeof Keyframe>>;
|
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) * TIMELINE_SCALE + 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%] select-none pointer-events-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Track: FC<TrackProps> = ({
|
const Track: FC<TrackProps> = ({
|
||||||
keyframes,
|
keyframes,
|
||||||
animationData,
|
animationData,
|
||||||
@ -72,7 +42,7 @@ const Track: FC<TrackProps> = ({
|
|||||||
value={entity}
|
value={entity}
|
||||||
dragListener={false}
|
dragListener={false}
|
||||||
dragControls={controls}
|
dragControls={controls}
|
||||||
className="h-8 w-96 flex flex-1 flex-row gap-1 select-none"
|
className="h-8 relative flex flex-1 flex-row gap-1 select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
@ -94,8 +64,8 @@ const Track: FC<TrackProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ width: "1000px" }}
|
style={{ width: TIMELINE_SCALE * 10 }}
|
||||||
className="flex h-full flex-row relative bg-gray-900 select-none"
|
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
|
||||||
>
|
>
|
||||||
{keyframes.map((keyframe, index) => (
|
{keyframes.map((keyframe, index) => (
|
||||||
<KeyframeIndicator
|
<KeyframeIndicator
|
||||||
@ -118,11 +88,11 @@ const Track: FC<TrackProps> = ({
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
transition={ease.circ(0.6).out}
|
transition={ease.circ(0.6).out}
|
||||||
dragElastic={false}
|
dragElastic={false}
|
||||||
dragConstraints={{ left: 0, right: 900 }}
|
dragConstraints={{ left: 0 }}
|
||||||
onDragEnd={(e, info) => {
|
onDragEnd={(e, info) => {
|
||||||
let offset = info.offset.x;
|
let offset = info.offset.x;
|
||||||
|
|
||||||
offset *= 0.01;
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
const animationOffset =
|
const animationOffset =
|
||||||
animationData.offset + offset < 0
|
animationData.offset + offset < 0
|
||||||
@ -156,11 +126,11 @@ const Track: FC<TrackProps> = ({
|
|||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
}}
|
}}
|
||||||
transition={ease.circ(0.6).out}
|
transition={ease.circ(0.6).out}
|
||||||
dragConstraints={{ left: 0, right: 900 }}
|
dragConstraints={{ left: 0 }}
|
||||||
onDragEnd={(e, info) => {
|
onDragEnd={(e, info) => {
|
||||||
let offset = info.offset.x;
|
let offset = info.offset.x;
|
||||||
|
|
||||||
offset *= 0.01;
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
const duration = animationData.duration + offset;
|
const duration = animationData.duration + offset;
|
||||||
|
|
||||||
@ -183,14 +153,16 @@ const Track: FC<TrackProps> = ({
|
|||||||
whileTap={{ scaleY: 0.9 }}
|
whileTap={{ scaleY: 0.9 }}
|
||||||
dragConstraints={{
|
dragConstraints={{
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 900,
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
transition={ease.circ(0.8).out}
|
transition={ease.circ(0.8).out}
|
||||||
onDragEnd={(_e, info) => {
|
onDragEnd={(_e, info) => {
|
||||||
let offset = info.offset.x;
|
let offset = info.offset.x;
|
||||||
offset *= 0.01;
|
|
||||||
|
offset = calculateOffset(offset);
|
||||||
|
|
||||||
offset += animationData.offset;
|
offset += animationData.offset;
|
||||||
|
|
||||||
updateEntity(index, {
|
updateEntity(index, {
|
||||||
animation_data: {
|
animation_data: {
|
||||||
...animationData,
|
...animationData,
|
||||||
@ -205,53 +177,4 @@ const Track: FC<TrackProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Timeline: FC<TimelineProps> = () => {
|
export default Track;
|
||||||
const { entities, setEntities } = useEntitiesStore((store) => ({
|
|
||||||
entities: store.entities,
|
|
||||||
setEntities: store.setEntities,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { setPlaying } = useRenderStateStore((store) => ({
|
|
||||||
setPlaying: store.setPlaying,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col p-4 w-full border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<button onClick={() => setPlaying(true)} className="w-8 h-8">
|
|
||||||
<PlayIcon color="white" width="100%" height="100%" />
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setPlaying(false)} className="w-8 h-8">
|
|
||||||
<PauseIcon color="white" width="100%" height="100%" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Timestamp />
|
|
||||||
</div>
|
|
||||||
<div className="gap-1 flex flex-col overflow-y-hidden">
|
|
||||||
<div className="z-20 flex flex-row gap-2">
|
|
||||||
<div className="flex-shrink-0 min-w-[200px]" />
|
|
||||||
<TimePicker />
|
|
||||||
</div>
|
|
||||||
<Reorder.Group
|
|
||||||
className="gap-1 flex-1 flex flex-col overflow-scroll"
|
|
||||||
values={entities}
|
|
||||||
onReorder={setEntities}
|
|
||||||
>
|
|
||||||
{entities.map((entity, index) => (
|
|
||||||
<Track
|
|
||||||
entity={entity}
|
|
||||||
key={entity.id}
|
|
||||||
name={entity.type}
|
|
||||||
index={index}
|
|
||||||
keyframes={flattenedKeyframesByEntity(entity)}
|
|
||||||
animationData={entity.animation_data}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Reorder.Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Timeline;
|
|
7
app/src/components/Timeline/common.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const TIMELINE_SCALE = 100;
|
||||||
|
|
||||||
|
export const calculateOffset = (offset: number) => {
|
||||||
|
let nextOffset = offset / TIMELINE_SCALE;
|
||||||
|
|
||||||
|
return nextOffset;
|
||||||
|
};
|
67
app/src/components/Timeline/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { Reorder } from "framer-motion";
|
||||||
|
import TimePicker from "./Timepicker";
|
||||||
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
|
import Timestamp from "./Timestamp";
|
||||||
|
import { flattenedKeyframesByEntity } from "utils";
|
||||||
|
import { PauseIcon, PlayIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
import Track from "./Track";
|
||||||
|
|
||||||
|
export type AnimationEntity = {
|
||||||
|
offset: number;
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TimelineProps = {};
|
||||||
|
|
||||||
|
const Timeline: FC<TimelineProps> = () => {
|
||||||
|
const { entities, setEntities } = useEntitiesStore((store) => ({
|
||||||
|
entities: store.entities,
|
||||||
|
setEntities: store.setEntities,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { setPlaying } = useRenderStateStore((store) => ({
|
||||||
|
setPlaying: store.setPlaying,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col p-4 w-full border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<button onClick={() => setPlaying(true)} className="w-8 h-8">
|
||||||
|
<PlayIcon color="white" width="100%" height="100%" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setPlaying(false)} className="w-8 h-8">
|
||||||
|
<PauseIcon color="white" width="100%" height="100%" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Timestamp />
|
||||||
|
</div>
|
||||||
|
<div className="gap-1 flex flex-col overflow-y-hidden">
|
||||||
|
<div className="z-20 flex flex-row gap-2">
|
||||||
|
<div className="flex-shrink-0 min-w-[200px]" />
|
||||||
|
<TimePicker />
|
||||||
|
</div>
|
||||||
|
<Reorder.Group
|
||||||
|
className="gap-1 flex-1 flex flex-col"
|
||||||
|
values={entities}
|
||||||
|
onReorder={setEntities}
|
||||||
|
>
|
||||||
|
{entities.map((entity, index) => (
|
||||||
|
<Track
|
||||||
|
entity={entity}
|
||||||
|
key={entity.id}
|
||||||
|
name={entity.type}
|
||||||
|
index={index}
|
||||||
|
keyframes={flattenedKeyframesByEntity(entity)}
|
||||||
|
animationData={entity.animation_data}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Reorder.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Timeline;
|
@ -1,4 +1,3 @@
|
|||||||
import { C } from "@tauri-apps/api/event-30ea0228";
|
|
||||||
import { BaseEntity } from "primitives/Entities";
|
import { BaseEntity } from "primitives/Entities";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ export function calculateLetters(
|
|||||||
dependencies: Dependencies
|
dependencies: Dependencies
|
||||||
): StaggeredTextCache {
|
): StaggeredTextCache {
|
||||||
const fontData = dependencies.fonts.get(
|
const fontData = dependencies.fonts.get(
|
||||||
entity.letter.paint.fontName
|
entity.letter.paint.font_name
|
||||||
) as ArrayBuffer;
|
) as ArrayBuffer;
|
||||||
|
|
||||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
||||||
@ -261,6 +261,12 @@ export default function drawStaggeredText(
|
|||||||
|
|
||||||
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
||||||
|
|
||||||
|
canvas.rotate(
|
||||||
|
letterTransform.rotate[0],
|
||||||
|
letterTransform.rotate[1],
|
||||||
|
letterTransform.rotate[2]
|
||||||
|
);
|
||||||
|
|
||||||
canvas.translate(
|
canvas.translate(
|
||||||
-origin[0] + measuredLetter.offset.x,
|
-origin[0] + measuredLetter.offset.x,
|
||||||
-origin[1] + lineOffset
|
-origin[1] + lineOffset
|
||||||
|
@ -17,7 +17,9 @@ export function buildTextCache(
|
|||||||
entity: z.output<typeof TextEntity>,
|
entity: z.output<typeof TextEntity>,
|
||||||
dependencies: Dependencies
|
dependencies: Dependencies
|
||||||
): TextCache {
|
): TextCache {
|
||||||
const fontData = dependencies.fonts.get(entity.paint.fontName) as ArrayBuffer;
|
const fontData = dependencies.fonts.get(
|
||||||
|
entity.paint.font_name
|
||||||
|
) as ArrayBuffer;
|
||||||
|
|
||||||
const fontManager = CanvasKit.FontMgr.FromData(fontData) as FontMgr;
|
const fontManager = CanvasKit.FontMgr.FromData(fontData) as FontMgr;
|
||||||
|
|
||||||
@ -43,7 +45,7 @@ export default function drawText(
|
|||||||
const pStyle = new CanvasKit.ParagraphStyle({
|
const pStyle = new CanvasKit.ParagraphStyle({
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: color,
|
color: color,
|
||||||
fontFamilies: [entity.paint.fontName],
|
fontFamilies: [entity.paint.font_name],
|
||||||
fontSize: entity.paint.size,
|
fontSize: entity.paint.size,
|
||||||
},
|
},
|
||||||
textDirection: CanvasKit.TextDirection.LTR,
|
textDirection: CanvasKit.TextDirection.LTR,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
||||||
import { Color } from "primitives/Paint";
|
import { Color } from "primitives/Paint";
|
||||||
import { Timeline } from "primitives/Timeline";
|
import { Timeline } from "primitives/Timeline";
|
||||||
import { staticAnimatedNumber, staticAnimatedVec2 } from "primitives/Values";
|
import {
|
||||||
|
staticAnimatedNumber,
|
||||||
|
staticAnimatedVec2,
|
||||||
|
staticAnimatedVec3,
|
||||||
|
} from "primitives/Values";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
@ -138,7 +142,7 @@ function buildText(
|
|||||||
type: "Fill",
|
type: "Fill",
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
fontName: "Arial",
|
font_name: "Arial",
|
||||||
size,
|
size,
|
||||||
align: "Center",
|
align: "Center",
|
||||||
},
|
},
|
||||||
@ -203,7 +207,7 @@ function buildStaggeredText(
|
|||||||
stagger: 0.05,
|
stagger: 0.05,
|
||||||
letter: {
|
letter: {
|
||||||
paint: {
|
paint: {
|
||||||
fontName: "Arial",
|
font_name: "Arial",
|
||||||
style: {
|
style: {
|
||||||
type: "Fill",
|
type: "Fill",
|
||||||
color,
|
color,
|
||||||
@ -213,7 +217,7 @@ function buildStaggeredText(
|
|||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
translate: staticAnimatedVec2(0, 0),
|
translate: staticAnimatedVec2(0, 0),
|
||||||
rotate: staticAnimatedVec2(0, 0),
|
rotate: staticAnimatedVec3(0, 0, 45),
|
||||||
skew: staticAnimatedVec2(0, 0),
|
skew: staticAnimatedVec2(0, 0),
|
||||||
scale: {
|
scale: {
|
||||||
keyframes: [
|
keyframes: [
|
||||||
@ -227,7 +231,7 @@ function buildStaggeredText(
|
|||||||
mass: 1,
|
mass: 1,
|
||||||
damping: 15,
|
damping: 15,
|
||||||
},
|
},
|
||||||
value: 0.0,
|
value: 5.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -246,11 +250,11 @@ function buildStaggeredText(
|
|||||||
{
|
{
|
||||||
interpolation: {
|
interpolation: {
|
||||||
type: "Spring",
|
type: "Spring",
|
||||||
stiffness: 200,
|
stiffness: 300,
|
||||||
mass: 1,
|
mass: 1,
|
||||||
damping: 15,
|
damping: 15,
|
||||||
},
|
},
|
||||||
value: 0.0,
|
value: -10.0,
|
||||||
offset: 0.0,
|
offset: 0.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
RectEntity,
|
RectEntity,
|
||||||
TextEntity,
|
TextEntity,
|
||||||
} from "./Entities";
|
} from "./Entities";
|
||||||
import { AnimatedVec2 } from "./Values";
|
import { AnimatedVec2, AnimatedVec3 } from "./Values";
|
||||||
import { TextPaint } from "./Paint";
|
import { TextPaint } from "./Paint";
|
||||||
|
|
||||||
export const AnimationData = z.object({
|
export const AnimationData = z.object({
|
||||||
@ -21,7 +21,7 @@ export const AnimatedTransform = z.object({
|
|||||||
/** Skews by the given animated vec2 */
|
/** Skews by the given animated vec2 */
|
||||||
skew: AnimatedVec2,
|
skew: AnimatedVec2,
|
||||||
/** Rotates by the given animated vec2 */
|
/** Rotates by the given animated vec2 */
|
||||||
rotate: AnimatedVec2,
|
rotate: AnimatedVec3,
|
||||||
/** Scales on the x and y axis by the given animated vec2 */
|
/** Scales on the x and y axis by the given animated vec2 */
|
||||||
scale: AnimatedVec2,
|
scale: AnimatedVec2,
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Vec2 } from "./Values";
|
import { Vec2, Vec3 } from "./Values";
|
||||||
import { Paint, TextPaint } from "./Paint";
|
import { Paint, TextPaint } from "./Paint";
|
||||||
|
|
||||||
const EntityTypeOptions = ["Text", "Ellipse", "Rect", "StaggeredText"] as const;
|
const EntityTypeOptions = ["Text", "Ellipse", "Rect", "StaggeredText"] as const;
|
||||||
@ -8,7 +8,7 @@ export const EntityType = z.enum(EntityTypeOptions);
|
|||||||
|
|
||||||
export const Transform = z.object({
|
export const Transform = z.object({
|
||||||
skew: Vec2,
|
skew: Vec2,
|
||||||
rotate: Vec2,
|
rotate: Vec3,
|
||||||
translate: Vec2,
|
translate: Vec2,
|
||||||
scale: Vec2,
|
scale: Vec2,
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,7 @@ export const Paint = z.object({
|
|||||||
export const TextPaint = z.object({
|
export const TextPaint = z.object({
|
||||||
style: PaintStyle,
|
style: PaintStyle,
|
||||||
align: TextAlign,
|
align: TextAlign,
|
||||||
fontName: z.string(),
|
font_name: z.string(),
|
||||||
size: z.number().min(0),
|
size: z.number().min(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { Keyframes } from "./Keyframe";
|
|||||||
import { Interpolation } from "./Interpolation";
|
import { Interpolation } from "./Interpolation";
|
||||||
|
|
||||||
export const Vec2 = z.array(z.number()).length(2);
|
export const Vec2 = z.array(z.number()).length(2);
|
||||||
|
export const Vec3 = z.array(z.number()).length(3);
|
||||||
|
|
||||||
export const AnimatedNumber = z.object({
|
export const AnimatedNumber = z.object({
|
||||||
keyframes: Keyframes,
|
keyframes: Keyframes,
|
||||||
@ -12,6 +13,10 @@ export const AnimatedVec2 = z.object({
|
|||||||
keyframes: z.array(AnimatedNumber).length(2),
|
keyframes: z.array(AnimatedNumber).length(2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AnimatedVec3 = z.object({
|
||||||
|
keyframes: z.array(AnimatedNumber).length(3),
|
||||||
|
});
|
||||||
|
|
||||||
export function staticAnimatedNumber(
|
export function staticAnimatedNumber(
|
||||||
number: number
|
number: number
|
||||||
): z.infer<typeof AnimatedNumber> {
|
): z.infer<typeof AnimatedNumber> {
|
||||||
@ -33,35 +38,22 @@ export function staticAnimatedNumber(
|
|||||||
export function staticAnimatedVec2(
|
export function staticAnimatedVec2(
|
||||||
x: number,
|
x: number,
|
||||||
y: number
|
y: number
|
||||||
|
): z.infer<typeof AnimatedVec2> {
|
||||||
|
return {
|
||||||
|
keyframes: [staticAnimatedNumber(x), staticAnimatedNumber(y)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function staticAnimatedVec3(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
z: number
|
||||||
): z.infer<typeof AnimatedVec2> {
|
): z.infer<typeof AnimatedVec2> {
|
||||||
return {
|
return {
|
||||||
keyframes: [
|
keyframes: [
|
||||||
{
|
staticAnimatedNumber(x),
|
||||||
keyframes: {
|
staticAnimatedNumber(y),
|
||||||
values: [
|
staticAnimatedNumber(z),
|
||||||
{
|
|
||||||
interpolation: {
|
|
||||||
type: "Linear",
|
|
||||||
},
|
|
||||||
value: x,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyframes: {
|
|
||||||
values: [
|
|
||||||
{
|
|
||||||
interpolation: {
|
|
||||||
type: "Linear",
|
|
||||||
},
|
|
||||||
value: y,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,10 @@ export class DependenciesService {
|
|||||||
entities.forEach((entity) => {
|
entities.forEach((entity) => {
|
||||||
switch (entity.type) {
|
switch (entity.type) {
|
||||||
case EntityType.Enum.Text:
|
case EntityType.Enum.Text:
|
||||||
fontNames.add(entity.paint.fontName);
|
fontNames.add(entity.paint.font_name);
|
||||||
break;
|
break;
|
||||||
case EntityType.Enum.StaggeredText:
|
case EntityType.Enum.StaggeredText:
|
||||||
fontNames.add(entity.letter.paint.fontName);
|
fontNames.add(entity.letter.paint.font_name);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -74,61 +74,3 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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::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;
|
|
||||||
}
|
|
||||||
|