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 style: PaintStyle,
|
||||
pub align: TextAlign,
|
||||
pub fontName: String,
|
||||
pub font_name: String,
|
||||
pub size: f32,
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
entities::common::AnimationData,
|
||||
values::{AnimatedFloatVec2, AnimatedValue},
|
||||
values::{AnimatedFloatVec2, AnimatedFloatVec3, AnimatedValue},
|
||||
};
|
||||
use crate::animation::timeline::Timeline;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -10,7 +10,7 @@ pub struct AnimatedTransform {
|
||||
pub translate: AnimatedFloatVec2,
|
||||
pub scale: AnimatedFloatVec2,
|
||||
pub skew: AnimatedFloatVec2,
|
||||
pub rotate: AnimatedFloatVec2,
|
||||
pub rotate: AnimatedFloatVec3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@ -18,7 +18,7 @@ pub struct Transform {
|
||||
pub translate: (f32, f32),
|
||||
pub scale: (f32, f32),
|
||||
pub skew: (f32, f32),
|
||||
pub rotate: (f32, f32),
|
||||
pub rotate: (f32, f32, f32),
|
||||
}
|
||||
|
||||
impl AnimatedTransform {
|
||||
|
@ -154,7 +154,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
color: Color::new(0, 0, 0, 1.0),
|
||||
width: 10.0,
|
||||
}),
|
||||
fontName: "Arial".to_string(),
|
||||
font_name: "Arial".to_string(),
|
||||
align: TextAlign::Center,
|
||||
size: 20.0,
|
||||
};
|
||||
@ -163,7 +163,7 @@ pub fn test_timeline_entities_at_frame(
|
||||
style: PaintStyle::Fill(FillStyle {
|
||||
color: Color::new(0, 0, 0, 1.0),
|
||||
}),
|
||||
fontName: "Arial".to_string(),
|
||||
font_name: "Arial".to_string(),
|
||||
align: TextAlign::Center,
|
||||
size: 10.0,
|
||||
};
|
||||
|
@ -1,16 +1,27 @@
|
||||
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();
|
||||
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]
|
||||
|
@ -102,10 +102,10 @@ export const TextProperties: FC<TextPropertiesProps> = ({
|
||||
onUpdate({
|
||||
...entity,
|
||||
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) => (
|
||||
<option value={font} key={font}>
|
||||
@ -173,11 +173,11 @@ export const StaggeredTextProperties: FC<StaggeredTextPropertiesProps> = ({
|
||||
cache: { valid: false },
|
||||
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) => (
|
||||
<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 * as Slider from "@radix-ui/react-slider";
|
||||
import { useRenderStateStore } from "stores/render-state.store";
|
||||
import { TIMELINE_SCALE } from "./Timeline";
|
||||
import { TIMELINE_SCALE } from "./common";
|
||||
import { useTimelineStore } from "stores/timeline.store";
|
||||
|
||||
export type TimePickerProps = {};
|
||||
@ -12,19 +12,22 @@ const TimePicker: FC<TimePickerProps> = () => {
|
||||
|
||||
return (
|
||||
<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]}
|
||||
style={{ width: TIMELINE_SCALE * timeline.duration }}
|
||||
style={{ width: TIMELINE_SCALE * 10 }}
|
||||
value={[renderState.curr_frame]}
|
||||
onValueChange={(val) => setCurrentFrame(val[0])}
|
||||
max={timeline.fps * timeline.duration}
|
||||
step={1}
|
||||
aria-label="Current Frame"
|
||||
>
|
||||
<Slider.Track className="SliderTrack">
|
||||
<Slider.Range className="SliderRange" />
|
||||
<Slider.Track className="bg-blackA10 relative grow rounded-full h-[3px]">
|
||||
<Slider.Range className="absolute bg-white rounded-full h-full" />
|
||||
</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>
|
||||
);
|
||||
};
|
@ -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 Timestamp from "./Timestamp";
|
||||
import { useDragControls, Reorder, motion } from "framer-motion";
|
||||
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
|
||||
import { Keyframe } from "primitives/Keyframe";
|
||||
import { flattenedKeyframesByEntity } from "utils";
|
||||
import { PauseIcon, PlayIcon } from "@radix-ui/react-icons";
|
||||
import { useRenderStateStore } from "stores/render-state.store";
|
||||
|
||||
export type AnimationEntity = {
|
||||
offset: number;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
type TimelineProps = {};
|
||||
|
||||
export const TIMELINE_SCALE = 50;
|
||||
import { FC } from "react";
|
||||
import { useEntitiesStore } from "stores/entities.store";
|
||||
import { z } from "zod";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import KeyframeIndicator from "./KeyframeIndicator";
|
||||
import { TIMELINE_SCALE, calculateOffset } from "./common";
|
||||
|
||||
type TrackProps = {
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
@ -29,24 +17,6 @@ type TrackProps = {
|
||||
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> = ({
|
||||
keyframes,
|
||||
animationData,
|
||||
@ -72,7 +42,7 @@ const Track: FC<TrackProps> = ({
|
||||
value={entity}
|
||||
dragListener={false}
|
||||
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
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
@ -94,8 +64,8 @@ const Track: FC<TrackProps> = ({
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ width: "1000px" }}
|
||||
className="flex h-full flex-row relative bg-gray-900 select-none"
|
||||
style={{ width: TIMELINE_SCALE * 10 }}
|
||||
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
|
||||
>
|
||||
{keyframes.map((keyframe, index) => (
|
||||
<KeyframeIndicator
|
||||
@ -118,11 +88,11 @@ const Track: FC<TrackProps> = ({
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
transition={ease.circ(0.6).out}
|
||||
dragElastic={false}
|
||||
dragConstraints={{ left: 0, right: 900 }}
|
||||
dragConstraints={{ left: 0 }}
|
||||
onDragEnd={(e, info) => {
|
||||
let offset = info.offset.x;
|
||||
|
||||
offset *= 0.01;
|
||||
offset = calculateOffset(offset);
|
||||
|
||||
const animationOffset =
|
||||
animationData.offset + offset < 0
|
||||
@ -156,11 +126,11 @@ const Track: FC<TrackProps> = ({
|
||||
scale: 0.9,
|
||||
}}
|
||||
transition={ease.circ(0.6).out}
|
||||
dragConstraints={{ left: 0, right: 900 }}
|
||||
dragConstraints={{ left: 0 }}
|
||||
onDragEnd={(e, info) => {
|
||||
let offset = info.offset.x;
|
||||
|
||||
offset *= 0.01;
|
||||
offset = calculateOffset(offset);
|
||||
|
||||
const duration = animationData.duration + offset;
|
||||
|
||||
@ -183,14 +153,16 @@ const Track: FC<TrackProps> = ({
|
||||
whileTap={{ scaleY: 0.9 }}
|
||||
dragConstraints={{
|
||||
left: 0,
|
||||
right: 900,
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
transition={ease.circ(0.8).out}
|
||||
onDragEnd={(_e, info) => {
|
||||
let offset = info.offset.x;
|
||||
offset *= 0.01;
|
||||
|
||||
offset = calculateOffset(offset);
|
||||
|
||||
offset += animationData.offset;
|
||||
|
||||
updateEntity(index, {
|
||||
animation_data: {
|
||||
...animationData,
|
||||
@ -205,53 +177,4 @@ const Track: FC<TrackProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
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 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;
|
||||
export default Track;
|
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 { z } from "zod";
|
||||
|
||||
|
@ -99,7 +99,7 @@ export function calculateLetters(
|
||||
dependencies: Dependencies
|
||||
): StaggeredTextCache {
|
||||
const fontData = dependencies.fonts.get(
|
||||
entity.letter.paint.fontName
|
||||
entity.letter.paint.font_name
|
||||
) as ArrayBuffer;
|
||||
|
||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
||||
@ -261,6 +261,12 @@ export default function drawStaggeredText(
|
||||
|
||||
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
||||
|
||||
canvas.rotate(
|
||||
letterTransform.rotate[0],
|
||||
letterTransform.rotate[1],
|
||||
letterTransform.rotate[2]
|
||||
);
|
||||
|
||||
canvas.translate(
|
||||
-origin[0] + measuredLetter.offset.x,
|
||||
-origin[1] + lineOffset
|
||||
|
@ -17,7 +17,9 @@ export function buildTextCache(
|
||||
entity: z.output<typeof TextEntity>,
|
||||
dependencies: Dependencies
|
||||
): 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;
|
||||
|
||||
@ -43,7 +45,7 @@ export default function drawText(
|
||||
const pStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: color,
|
||||
fontFamilies: [entity.paint.fontName],
|
||||
fontFamilies: [entity.paint.font_name],
|
||||
fontSize: entity.paint.size,
|
||||
},
|
||||
textDirection: CanvasKit.TextDirection.LTR,
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { AnimatedEntity } from "primitives/AnimatedEntities";
|
||||
import { Color } from "primitives/Paint";
|
||||
import { Timeline } from "primitives/Timeline";
|
||||
import { staticAnimatedNumber, staticAnimatedVec2 } from "primitives/Values";
|
||||
import {
|
||||
staticAnimatedNumber,
|
||||
staticAnimatedVec2,
|
||||
staticAnimatedVec3,
|
||||
} from "primitives/Values";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@ -138,7 +142,7 @@ function buildText(
|
||||
type: "Fill",
|
||||
color,
|
||||
},
|
||||
fontName: "Arial",
|
||||
font_name: "Arial",
|
||||
size,
|
||||
align: "Center",
|
||||
},
|
||||
@ -203,7 +207,7 @@ function buildStaggeredText(
|
||||
stagger: 0.05,
|
||||
letter: {
|
||||
paint: {
|
||||
fontName: "Arial",
|
||||
font_name: "Arial",
|
||||
style: {
|
||||
type: "Fill",
|
||||
color,
|
||||
@ -213,7 +217,7 @@ function buildStaggeredText(
|
||||
},
|
||||
transform: {
|
||||
translate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec2(0, 0),
|
||||
rotate: staticAnimatedVec3(0, 0, 45),
|
||||
skew: staticAnimatedVec2(0, 0),
|
||||
scale: {
|
||||
keyframes: [
|
||||
@ -227,7 +231,7 @@ function buildStaggeredText(
|
||||
mass: 1,
|
||||
damping: 15,
|
||||
},
|
||||
value: 0.0,
|
||||
value: 5.0,
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
@ -246,11 +250,11 @@ function buildStaggeredText(
|
||||
{
|
||||
interpolation: {
|
||||
type: "Spring",
|
||||
stiffness: 200,
|
||||
stiffness: 300,
|
||||
mass: 1,
|
||||
damping: 15,
|
||||
},
|
||||
value: 0.0,
|
||||
value: -10.0,
|
||||
offset: 0.0,
|
||||
},
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
RectEntity,
|
||||
TextEntity,
|
||||
} from "./Entities";
|
||||
import { AnimatedVec2 } from "./Values";
|
||||
import { AnimatedVec2, AnimatedVec3 } from "./Values";
|
||||
import { TextPaint } from "./Paint";
|
||||
|
||||
export const AnimationData = z.object({
|
||||
@ -21,7 +21,7 @@ export const AnimatedTransform = z.object({
|
||||
/** Skews by the given animated vec2 */
|
||||
skew: AnimatedVec2,
|
||||
/** Rotates by the given animated vec2 */
|
||||
rotate: AnimatedVec2,
|
||||
rotate: AnimatedVec3,
|
||||
/** Scales on the x and y axis by the given animated vec2 */
|
||||
scale: AnimatedVec2,
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { Vec2 } from "./Values";
|
||||
import { Vec2, Vec3 } from "./Values";
|
||||
import { Paint, TextPaint } from "./Paint";
|
||||
|
||||
const EntityTypeOptions = ["Text", "Ellipse", "Rect", "StaggeredText"] as const;
|
||||
@ -8,7 +8,7 @@ export const EntityType = z.enum(EntityTypeOptions);
|
||||
|
||||
export const Transform = z.object({
|
||||
skew: Vec2,
|
||||
rotate: Vec2,
|
||||
rotate: Vec3,
|
||||
translate: Vec2,
|
||||
scale: Vec2,
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ export const Paint = z.object({
|
||||
export const TextPaint = z.object({
|
||||
style: PaintStyle,
|
||||
align: TextAlign,
|
||||
fontName: z.string(),
|
||||
font_name: z.string(),
|
||||
size: z.number().min(0),
|
||||
});
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Keyframes } from "./Keyframe";
|
||||
import { Interpolation } from "./Interpolation";
|
||||
|
||||
export const Vec2 = z.array(z.number()).length(2);
|
||||
export const Vec3 = z.array(z.number()).length(3);
|
||||
|
||||
export const AnimatedNumber = z.object({
|
||||
keyframes: Keyframes,
|
||||
@ -12,6 +13,10 @@ export const AnimatedVec2 = z.object({
|
||||
keyframes: z.array(AnimatedNumber).length(2),
|
||||
});
|
||||
|
||||
export const AnimatedVec3 = z.object({
|
||||
keyframes: z.array(AnimatedNumber).length(3),
|
||||
});
|
||||
|
||||
export function staticAnimatedNumber(
|
||||
number: number
|
||||
): z.infer<typeof AnimatedNumber> {
|
||||
@ -33,35 +38,22 @@ export function staticAnimatedNumber(
|
||||
export function staticAnimatedVec2(
|
||||
x: 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> {
|
||||
return {
|
||||
keyframes: [
|
||||
{
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
value: x,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
keyframes: {
|
||||
values: [
|
||||
{
|
||||
interpolation: {
|
||||
type: "Linear",
|
||||
},
|
||||
value: y,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
staticAnimatedNumber(x),
|
||||
staticAnimatedNumber(y),
|
||||
staticAnimatedNumber(z),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -33,10 +33,10 @@ export class DependenciesService {
|
||||
entities.forEach((entity) => {
|
||||
switch (entity.type) {
|
||||
case EntityType.Enum.Text:
|
||||
fontNames.add(entity.paint.fontName);
|
||||
fontNames.add(entity.paint.font_name);
|
||||
break;
|
||||
case EntityType.Enum.StaggeredText:
|
||||
fontNames.add(entity.letter.paint.fontName);
|
||||
fontNames.add(entity.letter.paint.font_name);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -74,61 +74,3 @@ body,
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
|