add playback
This commit is contained in:
@@ -1,51 +1,22 @@
|
||||
import { FC, useMemo } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { useTimelineStore } from "stores/timeline.store";
|
||||
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
|
||||
import { Surface } from "canvaskit-wasm";
|
||||
import drawText from "drawers/text";
|
||||
import drawRect from "drawers/rect";
|
||||
import { Entities, EntityType } from "primitives/Entities";
|
||||
import drawEllipse from "drawers/ellipse";
|
||||
import { useRenderStateStore } from "stores/render-state.store";
|
||||
import { useEntitiesStore } from "stores/entities.store";
|
||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||
import drawStaggeredText, {
|
||||
StaggeredTextCache,
|
||||
calculateLetters,
|
||||
} from "drawers/staggered-text";
|
||||
import useMap from "hooks/useMap";
|
||||
import { Drawer } from "drawers/draw";
|
||||
import { PlaybackService } from "services/playback.service";
|
||||
|
||||
type CanvasProps = {};
|
||||
|
||||
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||
return array.buffer.slice(
|
||||
array.byteOffset,
|
||||
array.byteLength + array.byteOffset
|
||||
);
|
||||
}
|
||||
|
||||
const CanvasComponent: FC<CanvasProps> = () => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const [didInit, setDidInit] = useState(false);
|
||||
const renderState = useRenderStateStore((store) => store.renderState);
|
||||
const { fps, size, duration } = useTimelineStore((store) => ({
|
||||
fps: store.fps,
|
||||
size: store.size,
|
||||
duration: store.duration,
|
||||
}));
|
||||
const { entities, updateEntityById } = useEntitiesStore((store) => ({
|
||||
entities: store.entities,
|
||||
updateEntityById: store.updateEntityById,
|
||||
}));
|
||||
|
||||
const drawer = useMemo(() => new Drawer(), []);
|
||||
const playbackService = useMemo(() => new PlaybackService(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (canvas.current && !didInit) {
|
||||
drawer
|
||||
playbackService
|
||||
.init(canvas.current)
|
||||
.then(() => {
|
||||
setDidInit(true);
|
||||
@@ -54,12 +25,6 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (didInit) {
|
||||
drawer.update(entities);
|
||||
}
|
||||
}, [entities, renderState.curr_frame, didInit]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
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 { 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 select-none h-5 w-full items-center"
|
||||
defaultValue={[50]}
|
||||
style={{ width: 100 * 10 }}
|
||||
style={{ width: TIMELINE_SCALE * timeline.duration }}
|
||||
value={[renderState.curr_frame]}
|
||||
onValueChange={(val) => setCurrentFrame(val[0])}
|
||||
max={60 * 10}
|
||||
max={timeline.fps * timeline.duration}
|
||||
step={1}
|
||||
aria-label="Current Frame"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from "react";
|
||||
import { z } from "zod";
|
||||
import { AnimationData } from "primitives/AnimatedEntities";
|
||||
import { motion } from "framer-motion";
|
||||
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";
|
||||
@@ -9,6 +9,8 @@ import { ease } from "@unom/style";
|
||||
import Timestamp from "./Timestamp";
|
||||
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;
|
||||
@@ -17,10 +19,13 @@ export type AnimationEntity = {
|
||||
|
||||
type TimelineProps = {};
|
||||
|
||||
export const TIMELINE_SCALE = 50;
|
||||
|
||||
type TrackProps = {
|
||||
animationData: z.input<typeof AnimationData>;
|
||||
name: string;
|
||||
index: number;
|
||||
entity: z.input<typeof AnimatedEntity>;
|
||||
keyframes: Array<z.input<typeof Keyframe>>;
|
||||
};
|
||||
|
||||
@@ -31,7 +36,7 @@ const KeyframeIndicator: FC<{
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
x: (animationData.offset + keyframe.offset) * 100 + 4,
|
||||
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||
}}
|
||||
transition={ease.quint(0.4).out}
|
||||
style={{
|
||||
@@ -42,7 +47,15 @@ const KeyframeIndicator: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
const Track: FC<TrackProps> = ({
|
||||
keyframes,
|
||||
animationData,
|
||||
index,
|
||||
name,
|
||||
entity,
|
||||
}) => {
|
||||
const controls = useDragControls();
|
||||
|
||||
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
|
||||
useEntitiesStore(
|
||||
(store) => ({
|
||||
@@ -55,22 +68,34 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-8 w-100 flex flex-row gap-1 select-none">
|
||||
<Reorder.Item
|
||||
value={entity}
|
||||
dragListener={false}
|
||||
dragControls={controls}
|
||||
className="h-8 w-full flex flex-row gap-1 select-none"
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
selectedEntity !== undefined && selectedEntity === index
|
||||
? deselectEntity()
|
||||
: selectEntity(index)
|
||||
}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onPointerDown={(e) => controls.start(e)}
|
||||
className={`h-full transition-all rounded-sm flex-shrink-0 w-96 p-1 px-2 flex flex-row ${
|
||||
selectedEntity === index ? "bg-gray-800" : "bg-gray-900"
|
||||
}`}
|
||||
>
|
||||
<h3 className="text-white-800">{name}</h3>
|
||||
<h3
|
||||
onClick={() =>
|
||||
selectedEntity !== undefined && selectedEntity === index
|
||||
? deselectEntity()
|
||||
: selectEntity(index)
|
||||
}
|
||||
className="text-white-800 select-none pointer-events-none"
|
||||
>
|
||||
{name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ width: "1000px" }}
|
||||
className="flex w-full h-full flex-row relative bg-gray-900 select-none"
|
||||
className="flex h-full flex-row relative bg-gray-900 select-none"
|
||||
>
|
||||
{keyframes.map((keyframe, index) => (
|
||||
<KeyframeIndicator
|
||||
@@ -82,7 +107,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
<motion.div
|
||||
drag="x"
|
||||
animate={{
|
||||
x: animationData.offset * 100,
|
||||
x: animationData.offset * TIMELINE_SCALE,
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
@@ -120,7 +145,9 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
drag="x"
|
||||
animate={{
|
||||
x: (animationData.duration + animationData.offset) * 100 - 16,
|
||||
x:
|
||||
(animationData.duration + animationData.offset) * TIMELINE_SCALE -
|
||||
16,
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
@@ -149,8 +176,8 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
<motion.div
|
||||
drag="x"
|
||||
animate={{
|
||||
width: animationData.duration * 100,
|
||||
x: animationData.offset * 100,
|
||||
width: animationData.duration * TIMELINE_SCALE,
|
||||
x: animationData.offset * TIMELINE_SCALE,
|
||||
}}
|
||||
whileHover={{ scaleY: 1.1 }}
|
||||
whileTap={{ scaleY: 0.9 }}
|
||||
@@ -174,33 +201,54 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
||||
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||
></motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const Timeline: FC<TimelineProps> = () => {
|
||||
const { entities } = useEntitiesStore((store) => ({
|
||||
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 border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
||||
<Timestamp />
|
||||
<div className="gap-1 flex flex-col overflow-hidden">
|
||||
<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 w-96" />
|
||||
<TimePicker />
|
||||
</div>
|
||||
|
||||
{entities.map((entity, index) => (
|
||||
<Track
|
||||
name={entity.type}
|
||||
index={index}
|
||||
key={index}
|
||||
keyframes={flattenedKeyframesByEntity(entity)}
|
||||
animationData={entity.animation_data}
|
||||
/>
|
||||
))}
|
||||
<Reorder.Group
|
||||
className="gap-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>
|
||||
);
|
||||
|
||||
@@ -7,9 +7,12 @@ const Timestamp = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Frame {renderState.curr_frame}</h3>
|
||||
<h3>
|
||||
Frame {renderState.curr_frame} / {timeline.fps * timeline.duration}
|
||||
</h3>
|
||||
<h2 className="text-xl font-bold">
|
||||
{((renderState.curr_frame * timeline.fps) / 60 / 60).toPrecision(3)}{" "}
|
||||
{(renderState.curr_frame / timeline.fps).toPrecision(3)} /{" "}
|
||||
{timeline.duration.toPrecision(3)}
|
||||
<span className="text-sm font-light">/ {timeline.fps}FPS</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user