update logo

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

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

@@ -0,0 +1,35 @@
import { FC } from "react";
import * as Slider from "@radix-ui/react-slider";
import { useRenderStateStore } from "stores/render-state.store";
import { TIMELINE_SCALE } from "./common";
import { useTimelineStore } from "stores/timeline.store";
export type TimePickerProps = {};
const TimePicker: FC<TimePickerProps> = () => {
const { renderState, setCurrentFrame } = useRenderStateStore();
const timeline = useTimelineStore();
return (
<Slider.Root
className="relative flex items-center select-none touch-none h-5 shrink-0"
defaultValue={[50]}
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="bg-blackA10 relative grow rounded-full h-[3px]">
<Slider.Range className="absolute bg-white rounded-full h-full" />
</Slider.Track>
<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>
);
};
export default TimePicker;

View File

@@ -0,0 +1,22 @@
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} / {timeline.fps * timeline.duration}
</h3>
<h2 className="text-xl font-bold">
{(renderState.curr_frame / timeline.fps).toPrecision(3)} /{" "}
{timeline.duration.toPrecision(3)}
<span className="text-sm font-light">/ {timeline.fps}FPS</span>
</h2>
</div>
);
};
export default Timestamp;

View File

@@ -0,0 +1,180 @@
import { ease } from "@unom/style";
import { useDragControls, Reorder, motion } from "framer-motion";
import { AnimationData, AnimatedEntity } from "primitives/AnimatedEntities";
import { Keyframe } from "primitives/Keyframe";
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>;
name: string;
index: number;
entity: z.input<typeof AnimatedEntity>;
keyframes: Array<z.input<typeof Keyframe>>;
};
const Track: FC<TrackProps> = ({
keyframes,
animationData,
index,
name,
entity,
}) => {
const controls = useDragControls();
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
useEntitiesStore(
(store) => ({
updateEntity: store.updateEntity,
selectedEntity: store.selectedEntity,
selectEntity: store.selectEntity,
deselectEntity: store.deselectEntity,
}),
shallow
);
return (
<Reorder.Item
value={entity}
dragListener={false}
dragControls={controls}
className="h-8 relative flex flex-1 flex-row gap-1 select-none"
>
<div
onMouseDown={(e) => e.preventDefault()}
onPointerDown={(e) => controls.start(e)}
className={`h-full transition-all rounded-sm min-w-[200px] p-1 px-2 flex flex-row ${
selectedEntity === index ? "bg-gray-800" : "bg-gray-900"
}`}
>
<h3
onClick={() =>
selectedEntity !== undefined && selectedEntity === index
? deselectEntity()
: selectEntity(index)
}
className="text-white-800 select-none cursor-pointer"
>
{name}
</h3>
</div>
<div
style={{ width: TIMELINE_SCALE * 10 }}
className="flex h-full flex-row relative bg-gray-900 select-none shrink-0"
>
{keyframes.map((keyframe, index) => (
<KeyframeIndicator
animationData={animationData}
keyframe={keyframe}
key={index}
/>
))}
<motion.div
drag="x"
animate={{
x: animationData.offset * TIMELINE_SCALE,
}}
whileHover={{
scale: 1.1,
}}
whileTap={{
scale: 0.9,
}}
onMouseDown={(e) => e.preventDefault()}
transition={ease.circ(0.6).out}
dragElastic={false}
dragConstraints={{ left: 0 }}
onDragEnd={(e, info) => {
let offset = info.offset.x;
offset = calculateOffset(offset);
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 select-none cursor-w-resize"
/>
<motion.div
onMouseDown={(e) => e.preventDefault()}
drag="x"
animate={{
x:
(animationData.duration + animationData.offset) * TIMELINE_SCALE -
16,
}}
whileHover={{
scale: 1.1,
}}
whileTap={{
scale: 0.9,
}}
transition={ease.circ(0.6).out}
dragConstraints={{ left: 0 }}
onDragEnd={(e, info) => {
let offset = info.offset.x;
offset = calculateOffset(offset);
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 select-none cursor-e-resize"
/>
<motion.div
drag="x"
animate={{
width: animationData.duration * TIMELINE_SCALE,
x: animationData.offset * TIMELINE_SCALE,
}}
whileHover={{ scaleY: 1.1 }}
whileTap={{ scaleY: 0.9 }}
dragConstraints={{
left: 0,
}}
onMouseDown={(e) => e.preventDefault()}
transition={ease.circ(0.8).out}
onDragEnd={(_e, info) => {
let offset = info.offset.x;
offset = calculateOffset(offset);
offset += animationData.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 select-none cursor-grab"
></motion.div>
</div>
</Reorder.Item>
);
};
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;