update logo

improve font resolution logic
generate icons
improve timeline
This commit is contained in:
Enrico Bühler 2023-05-28 22:57:13 +02:00
parent 1baa3ae736
commit 28613c9214
38 changed files with 204 additions and 221 deletions

BIN
app/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 268 KiB

View File

@ -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,
}

View File

@ -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 {

View File

@ -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,
};

View File

@ -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]

View File

@ -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}>

View 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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -0,0 +1,7 @@
export const TIMELINE_SCALE = 100;
export const calculateOffset = (offset: number) => {
let nextOffset = offset / TIMELINE_SCALE;
return nextOffset;
};

View 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;

View File

@ -1,4 +1,3 @@
import { C } from "@tauri-apps/api/event-30ea0228";
import { BaseEntity } from "primitives/Entities";
import { z } from "zod";

View File

@ -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

View File

@ -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,

View File

@ -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,
},
{

View File

@ -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,
});

View File

@ -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,
});

View File

@ -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),
});

View File

@ -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),
],
};
}

View File

@ -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;

View File

@ -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;
}