update logo
improve font resolution logic generate icons improve timeline
This commit is contained in:
@@ -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
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
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
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;
|
||||
Reference in New Issue
Block a user