add playback
This commit is contained in:
parent
60c8bb5877
commit
b671f9ee47
@ -1,51 +1,22 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api";
|
|
||||||
import { useTimelineStore } from "stores/timeline.store";
|
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 { useRenderStateStore } from "stores/render-state.store";
|
||||||
import { useEntitiesStore } from "stores/entities.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 { Drawer } from "drawers/draw";
|
||||||
|
import { PlaybackService } from "services/playback.service";
|
||||||
|
|
||||||
type CanvasProps = {};
|
type CanvasProps = {};
|
||||||
|
|
||||||
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
|
||||||
return array.buffer.slice(
|
|
||||||
array.byteOffset,
|
|
||||||
array.byteLength + array.byteOffset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CanvasComponent: FC<CanvasProps> = () => {
|
const CanvasComponent: FC<CanvasProps> = () => {
|
||||||
const canvas = useRef<HTMLCanvasElement>(null);
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
const [didInit, setDidInit] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (canvas.current && !didInit) {
|
if (canvas.current && !didInit) {
|
||||||
drawer
|
playbackService
|
||||||
.init(canvas.current)
|
.init(canvas.current)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setDidInit(true);
|
setDidInit(true);
|
||||||
@ -54,12 +25,6 @@ const CanvasComponent: FC<CanvasProps> = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (didInit) {
|
|
||||||
drawer.update(entities);
|
|
||||||
}
|
|
||||||
}, [entities, renderState.curr_frame, didInit]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import * as Slider from "@radix-ui/react-slider";
|
import * as Slider from "@radix-ui/react-slider";
|
||||||
import { useRenderStateStore } from "stores/render-state.store";
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
import { TIMELINE_SCALE } from "./Timeline";
|
||||||
|
import { useTimelineStore } from "stores/timeline.store";
|
||||||
|
|
||||||
export type TimePickerProps = {};
|
export type TimePickerProps = {};
|
||||||
|
|
||||||
const TimePicker: FC<TimePickerProps> = () => {
|
const TimePicker: FC<TimePickerProps> = () => {
|
||||||
const { renderState, setCurrentFrame } = useRenderStateStore();
|
const { renderState, setCurrentFrame } = useRenderStateStore();
|
||||||
|
const timeline = useTimelineStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider.Root
|
<Slider.Root
|
||||||
className="relative flex select-none h-5 w-full items-center"
|
className="relative flex select-none h-5 w-full items-center"
|
||||||
defaultValue={[50]}
|
defaultValue={[50]}
|
||||||
style={{ width: 100 * 10 }}
|
style={{ width: TIMELINE_SCALE * timeline.duration }}
|
||||||
value={[renderState.curr_frame]}
|
value={[renderState.curr_frame]}
|
||||||
onValueChange={(val) => setCurrentFrame(val[0])}
|
onValueChange={(val) => setCurrentFrame(val[0])}
|
||||||
max={60 * 10}
|
max={timeline.fps * timeline.duration}
|
||||||
step={1}
|
step={1}
|
||||||
aria-label="Current Frame"
|
aria-label="Current Frame"
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AnimationData } from "primitives/AnimatedEntities";
|
import { AnimatedEntity, AnimationData } from "primitives/AnimatedEntities";
|
||||||
import { motion } from "framer-motion";
|
import { Reorder, motion, useDragControls } from "framer-motion";
|
||||||
import TimePicker from "./TimePicker";
|
import TimePicker from "./TimePicker";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
@ -9,6 +9,8 @@ import { ease } from "@unom/style";
|
|||||||
import Timestamp from "./Timestamp";
|
import Timestamp from "./Timestamp";
|
||||||
import { Keyframe } from "primitives/Keyframe";
|
import { Keyframe } from "primitives/Keyframe";
|
||||||
import { flattenedKeyframesByEntity } from "utils";
|
import { flattenedKeyframesByEntity } from "utils";
|
||||||
|
import { PauseIcon, PlayIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
|
||||||
export type AnimationEntity = {
|
export type AnimationEntity = {
|
||||||
offset: number;
|
offset: number;
|
||||||
@ -17,10 +19,13 @@ export type AnimationEntity = {
|
|||||||
|
|
||||||
type TimelineProps = {};
|
type TimelineProps = {};
|
||||||
|
|
||||||
|
export const TIMELINE_SCALE = 50;
|
||||||
|
|
||||||
type TrackProps = {
|
type TrackProps = {
|
||||||
animationData: z.input<typeof AnimationData>;
|
animationData: z.input<typeof AnimationData>;
|
||||||
name: string;
|
name: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
entity: z.input<typeof AnimatedEntity>;
|
||||||
keyframes: Array<z.input<typeof Keyframe>>;
|
keyframes: Array<z.input<typeof Keyframe>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,7 +36,7 @@ const KeyframeIndicator: FC<{
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
x: (animationData.offset + keyframe.offset) * 100 + 4,
|
x: (animationData.offset + keyframe.offset) * TIMELINE_SCALE + 4,
|
||||||
}}
|
}}
|
||||||
transition={ease.quint(0.4).out}
|
transition={ease.quint(0.4).out}
|
||||||
style={{
|
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 } =
|
const { updateEntity, selectEntity, selectedEntity, deselectEntity } =
|
||||||
useEntitiesStore(
|
useEntitiesStore(
|
||||||
(store) => ({
|
(store) => ({
|
||||||
@ -55,22 +68,34 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
onClick={() =>
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
selectedEntity !== undefined && selectedEntity === index
|
onPointerDown={(e) => controls.start(e)}
|
||||||
? deselectEntity()
|
|
||||||
: selectEntity(index)
|
|
||||||
}
|
|
||||||
className={`h-full transition-all rounded-sm flex-shrink-0 w-96 p-1 px-2 flex flex-row ${
|
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"
|
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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ width: "1000px" }}
|
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) => (
|
{keyframes.map((keyframe, index) => (
|
||||||
<KeyframeIndicator
|
<KeyframeIndicator
|
||||||
@ -82,7 +107,7 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
drag="x"
|
drag="x"
|
||||||
animate={{
|
animate={{
|
||||||
x: animationData.offset * 100,
|
x: animationData.offset * TIMELINE_SCALE,
|
||||||
}}
|
}}
|
||||||
whileHover={{
|
whileHover={{
|
||||||
scale: 1.1,
|
scale: 1.1,
|
||||||
@ -120,7 +145,9 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
drag="x"
|
drag="x"
|
||||||
animate={{
|
animate={{
|
||||||
x: (animationData.duration + animationData.offset) * 100 - 16,
|
x:
|
||||||
|
(animationData.duration + animationData.offset) * TIMELINE_SCALE -
|
||||||
|
16,
|
||||||
}}
|
}}
|
||||||
whileHover={{
|
whileHover={{
|
||||||
scale: 1.1,
|
scale: 1.1,
|
||||||
@ -149,8 +176,8 @@ const Track: FC<TrackProps> = ({ keyframes, animationData, index, name }) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
drag="x"
|
drag="x"
|
||||||
animate={{
|
animate={{
|
||||||
width: animationData.duration * 100,
|
width: animationData.duration * TIMELINE_SCALE,
|
||||||
x: animationData.offset * 100,
|
x: animationData.offset * TIMELINE_SCALE,
|
||||||
}}
|
}}
|
||||||
whileHover={{ scaleY: 1.1 }}
|
whileHover={{ scaleY: 1.1 }}
|
||||||
whileTap={{ scaleY: 0.9 }}
|
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"
|
className="z-5 h-full absolute rounded-md transition-colors bg-gray-700 hover:bg-gray-600 select-none cursor-grab"
|
||||||
></motion.div>
|
></motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Reorder.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Timeline: FC<TimelineProps> = () => {
|
const Timeline: FC<TimelineProps> = () => {
|
||||||
const { entities } = useEntitiesStore((store) => ({
|
const { entities, setEntities } = useEntitiesStore((store) => ({
|
||||||
entities: store.entities,
|
entities: store.entities,
|
||||||
|
setEntities: store.setEntities,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { setPlaying } = useRenderStateStore((store) => ({
|
||||||
|
setPlaying: store.setPlaying,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-4 border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
<div className="flex flex-col p-4 w-full border transition-colors focus-within:border-gray-400 border-gray-600 rounded-md">
|
||||||
<Timestamp />
|
<div className="flex flex-row">
|
||||||
<div className="gap-1 flex flex-col overflow-hidden">
|
<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="z-20 flex flex-row gap-2">
|
||||||
<div className="flex-shrink-0 w-96" />
|
<div className="flex-shrink-0 w-96" />
|
||||||
<TimePicker />
|
<TimePicker />
|
||||||
</div>
|
</div>
|
||||||
|
<Reorder.Group
|
||||||
{entities.map((entity, index) => (
|
className="gap-1 flex flex-col"
|
||||||
<Track
|
values={entities}
|
||||||
name={entity.type}
|
onReorder={setEntities}
|
||||||
index={index}
|
>
|
||||||
key={index}
|
{entities.map((entity, index) => (
|
||||||
keyframes={flattenedKeyframesByEntity(entity)}
|
<Track
|
||||||
animationData={entity.animation_data}
|
entity={entity}
|
||||||
/>
|
key={entity.id}
|
||||||
))}
|
name={entity.type}
|
||||||
|
index={index}
|
||||||
|
keyframes={flattenedKeyframesByEntity(entity)}
|
||||||
|
animationData={entity.animation_data}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Reorder.Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,9 +7,12 @@ const Timestamp = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Frame {renderState.curr_frame}</h3>
|
<h3>
|
||||||
|
Frame {renderState.curr_frame} / {timeline.fps * timeline.duration}
|
||||||
|
</h3>
|
||||||
<h2 className="text-xl font-bold">
|
<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>
|
<span className="text-sm font-light">/ {timeline.fps}FPS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api";
|
||||||
import InitCanvasKit, { Canvas, CanvasKit, Surface } from "canvaskit-wasm";
|
import InitCanvasKit, { Canvas, CanvasKit, Surface } from "canvaskit-wasm";
|
||||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
import { Entities, EntityType, StaggeredText } from "primitives/Entities";
|
import {
|
||||||
|
Entities,
|
||||||
|
EntityType,
|
||||||
|
StaggeredTextEntity,
|
||||||
|
TextEntity,
|
||||||
|
} from "primitives/Entities";
|
||||||
import { useRenderStateStore } from "stores/render-state.store";
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
import { useTimelineStore } from "stores/timeline.store";
|
import { useTimelineStore } from "stores/timeline.store";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -10,18 +15,13 @@ import drawStaggeredText, {
|
|||||||
StaggeredTextEntityCache,
|
StaggeredTextEntityCache,
|
||||||
calculateLetters,
|
calculateLetters,
|
||||||
} from "./staggered-text";
|
} from "./staggered-text";
|
||||||
import drawText from "./text";
|
import drawText, { TextCache, TextEntityCache, buildTextCache } from "./text";
|
||||||
import drawEllipse from "./ellipse";
|
import drawEllipse from "./ellipse";
|
||||||
import drawRect from "./rect";
|
import drawRect from "./rect";
|
||||||
import { useEntitiesStore } from "stores/entities.store";
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
import { handleEntityCache } from "./cache";
|
import { handleEntityCache } from "./cache";
|
||||||
|
import { DependenciesService } from "services/dependencies.service";
|
||||||
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
import { RenderState } from "primitives/Timeline";
|
||||||
return array.buffer.slice(
|
|
||||||
array.byteOffset,
|
|
||||||
array.byteLength + array.byteOffset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -32,36 +32,39 @@ export class Drawer {
|
|||||||
private didLoad: boolean;
|
private didLoad: boolean;
|
||||||
private entities: z.output<typeof Entities> | undefined;
|
private entities: z.output<typeof Entities> | undefined;
|
||||||
private ckDidLoad: boolean;
|
private ckDidLoad: boolean;
|
||||||
private dependenciesDidLoad: boolean;
|
|
||||||
drawCount: number;
|
drawCount: number;
|
||||||
private CanvasKit: CanvasKit | undefined;
|
private CanvasKit: CanvasKit | undefined;
|
||||||
cache: { staggeredText: Map<string, StaggeredTextCache> };
|
cache: {
|
||||||
|
staggeredText: Map<string, StaggeredTextCache>;
|
||||||
|
text: Map<string, TextCache>;
|
||||||
|
};
|
||||||
surface: Surface | undefined;
|
surface: Surface | undefined;
|
||||||
fontData: ArrayBuffer | undefined;
|
fontData: ArrayBuffer | undefined;
|
||||||
raf: number | undefined;
|
raf: number | undefined;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
|
dependenciesService: DependenciesService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.entities = undefined;
|
this.entities = undefined;
|
||||||
this.CanvasKit = undefined;
|
this.CanvasKit = undefined;
|
||||||
this.ckDidLoad = false;
|
this.ckDidLoad = false;
|
||||||
this.dependenciesDidLoad = false;
|
|
||||||
this.drawCount = 0;
|
this.drawCount = 0;
|
||||||
this.surface = undefined;
|
this.surface = undefined;
|
||||||
this.fontData = undefined;
|
this.fontData = undefined;
|
||||||
this.cache = {
|
this.cache = {
|
||||||
staggeredText: new Map(),
|
staggeredText: new Map(),
|
||||||
|
text: new Map(),
|
||||||
};
|
};
|
||||||
|
this.dependenciesService = new DependenciesService();
|
||||||
this.isLocked = false;
|
this.isLocked = false;
|
||||||
this.raf = undefined;
|
this.raf = undefined;
|
||||||
this.didLoad = this.ckDidLoad && this.dependenciesDidLoad;
|
this.didLoad = this.ckDidLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(canvas: HTMLCanvasElement) {
|
async init(canvas: HTMLCanvasElement) {
|
||||||
await this.loadCanvasKit(canvas);
|
await this.loadCanvasKit(canvas);
|
||||||
await this.loadDependencies(false);
|
|
||||||
|
|
||||||
this.didLoad = this.ckDidLoad && this.dependenciesDidLoad;
|
this.didLoad = this.ckDidLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCanvasKit(canvas: HTMLCanvasElement) {
|
async loadCanvasKit(canvas: HTMLCanvasElement) {
|
||||||
@ -80,66 +83,69 @@ export class Drawer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDependencies(remote: boolean) {
|
async calculateAnimatedEntities(
|
||||||
if (remote) {
|
animatedEntities: z.input<typeof AnimatedEntities>,
|
||||||
await fetch(
|
renderState: z.output<typeof RenderState>
|
||||||
"https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf"
|
) {
|
||||||
)
|
const { fps, size, duration } = useTimelineStore.getState();
|
||||||
.then((response) => response.arrayBuffer())
|
|
||||||
.then((arrayBuffer) => {
|
const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities);
|
||||||
this.fontData = arrayBuffer;
|
|
||||||
this.dependenciesDidLoad = true;
|
const data = await invoke("calculate_timeline_entities_at_frame", {
|
||||||
});
|
timeline: {
|
||||||
} else {
|
entities: parsedAnimatedEntities,
|
||||||
await invoke("get_system_font", { fontName: "Helvetica-Bold" }).then(
|
render_state: renderState,
|
||||||
(data) => {
|
fps,
|
||||||
if (Array.isArray(data)) {
|
size,
|
||||||
const u8 = new Uint8Array(data as any);
|
duration,
|
||||||
const buffer = typedArrayToBuffer(u8);
|
},
|
||||||
this.fontData = buffer;
|
});
|
||||||
this.dependenciesDidLoad = true;
|
|
||||||
}
|
const parsedEntities = Entities.parse(data);
|
||||||
}
|
|
||||||
|
return parsedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCached(): boolean {
|
||||||
|
if (this.entities) {
|
||||||
|
return this.entities.reduce(
|
||||||
|
(prev, curr) => prev && curr.cache.valid,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the entities based on the input
|
* Updates the entities based on the input
|
||||||
*/
|
*/
|
||||||
update(animatedEntities: z.input<typeof AnimatedEntities>) {
|
update(
|
||||||
|
animatedEntities: z.input<typeof AnimatedEntities>,
|
||||||
|
prepareDependencies: boolean
|
||||||
|
) {
|
||||||
console.time("calculate");
|
console.time("calculate");
|
||||||
|
|
||||||
const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities);
|
|
||||||
|
|
||||||
if (this.didLoad) {
|
if (this.didLoad) {
|
||||||
const render_state = useRenderStateStore.getState().renderState;
|
const renderState = useRenderStateStore.getState().renderState;
|
||||||
const { fps, size, duration } = useTimelineStore.getState();
|
|
||||||
|
|
||||||
invoke("calculate_timeline_entities_at_frame", {
|
this.calculateAnimatedEntities(animatedEntities, renderState).then(
|
||||||
timeline: {
|
(entities) => {
|
||||||
entities: parsedAnimatedEntities,
|
this.entities = entities;
|
||||||
render_state,
|
|
||||||
fps,
|
|
||||||
size,
|
|
||||||
duration,
|
|
||||||
},
|
|
||||||
}).then((data) => {
|
|
||||||
console.timeEnd("calculate");
|
|
||||||
const parsedEntities = Entities.safeParse(data);
|
|
||||||
if (parsedEntities.success) {
|
|
||||||
this.entities = parsedEntities.data;
|
|
||||||
|
|
||||||
const isCached = this.entities.reduce(
|
if (prepareDependencies) {
|
||||||
(prev, curr) => prev && curr.cache.valid,
|
this.dependenciesService
|
||||||
true
|
.prepareForEntities(this.entities)
|
||||||
);
|
.then(() => {
|
||||||
|
this.requestRedraw(!this.isCached);
|
||||||
this.requestRedraw(!isCached);
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error(parsedEntities.error);
|
this.requestRedraw(!this.isCached);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
} else {
|
||||||
|
console.timeEnd("calculate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,11 +167,10 @@ export class Drawer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw(canvas: Canvas) {
|
draw(canvas: Canvas) {
|
||||||
if (this.CanvasKit && this.entities && this.fontData && !this.isLocked) {
|
if (this.CanvasKit && this.entities && !this.isLocked) {
|
||||||
this.isLocked = true;
|
this.isLocked = true;
|
||||||
console.time("draw");
|
console.time("draw");
|
||||||
const CanvasKit = this.CanvasKit;
|
const CanvasKit = this.CanvasKit;
|
||||||
const fontData = this.fontData;
|
|
||||||
|
|
||||||
canvas.clear(CanvasKit.WHITE);
|
canvas.clear(CanvasKit.WHITE);
|
||||||
|
|
||||||
@ -180,17 +185,42 @@ export class Drawer {
|
|||||||
drawEllipse(CanvasKit, canvas, entity);
|
drawEllipse(CanvasKit, canvas, entity);
|
||||||
break;
|
break;
|
||||||
case EntityType.Enum.Text:
|
case EntityType.Enum.Text:
|
||||||
drawText(CanvasKit, canvas, entity, fontData);
|
{
|
||||||
|
const cache = handleEntityCache<
|
||||||
|
z.output<typeof TextEntity>,
|
||||||
|
TextCache,
|
||||||
|
TextEntityCache
|
||||||
|
>(entity, {
|
||||||
|
build: () =>
|
||||||
|
buildTextCache(
|
||||||
|
CanvasKit,
|
||||||
|
entity,
|
||||||
|
this.dependenciesService.dependencies
|
||||||
|
),
|
||||||
|
get: () => this.cache.text.get(entity.id),
|
||||||
|
set: (id, cache) => this.cache.text.set(id, cache),
|
||||||
|
cleanup: (cache) => {
|
||||||
|
cache.fontManager.delete();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
drawText(CanvasKit, canvas, entity, cache);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case EntityType.Enum.StaggeredText:
|
case EntityType.Enum.StaggeredText:
|
||||||
{
|
{
|
||||||
const cache = handleEntityCache<
|
const cache = handleEntityCache<
|
||||||
z.output<typeof StaggeredText>,
|
z.output<typeof StaggeredTextEntity>,
|
||||||
StaggeredTextCache,
|
StaggeredTextCache,
|
||||||
StaggeredTextEntityCache
|
StaggeredTextEntityCache
|
||||||
>(entity, {
|
>(entity, {
|
||||||
build: () => {
|
build: () => {
|
||||||
const cache = calculateLetters(CanvasKit, entity, fontData);
|
const cache = calculateLetters(
|
||||||
|
CanvasKit,
|
||||||
|
entity,
|
||||||
|
this.dependenciesService.dependencies
|
||||||
|
);
|
||||||
useEntitiesStore
|
useEntitiesStore
|
||||||
.getState()
|
.getState()
|
||||||
.updateEntityById(entity.id, { cache: { valid: true } });
|
.updateEntityById(entity.id, { cache: { valid: true } });
|
||||||
|
0
app/src/drawers/paragraph.ts
Normal file
0
app/src/drawers/paragraph.ts
Normal file
@ -7,10 +7,11 @@ import {
|
|||||||
TypedArray,
|
TypedArray,
|
||||||
Typeface,
|
Typeface,
|
||||||
} from "canvaskit-wasm";
|
} from "canvaskit-wasm";
|
||||||
import { StaggeredText } from "primitives/Entities";
|
import { StaggeredTextEntity } from "primitives/Entities";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { buildPaintStyle } from "./paint";
|
import { buildPaintStyle } from "./paint";
|
||||||
import { EntityCache } from "./cache";
|
import { EntityCache } from "./cache";
|
||||||
|
import { Dependencies } from "services/dependencies.service";
|
||||||
|
|
||||||
export type StaggeredTextCache = {
|
export type StaggeredTextCache = {
|
||||||
letterMeasures: Array<LetterMeasures>;
|
letterMeasures: Array<LetterMeasures>;
|
||||||
@ -94,9 +95,13 @@ type LetterMeasures = {
|
|||||||
|
|
||||||
export function calculateLetters(
|
export function calculateLetters(
|
||||||
CanvasKit: CanvasKit,
|
CanvasKit: CanvasKit,
|
||||||
entity: z.output<typeof StaggeredText>,
|
entity: z.output<typeof StaggeredTextEntity>,
|
||||||
fontData: ArrayBuffer
|
dependencies: Dependencies
|
||||||
): StaggeredTextCache {
|
): StaggeredTextCache {
|
||||||
|
const fontData = dependencies.fonts.get(
|
||||||
|
entity.letter.paint.fontName
|
||||||
|
) as ArrayBuffer;
|
||||||
|
|
||||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
||||||
fontData
|
fontData
|
||||||
) as Typeface;
|
) as Typeface;
|
||||||
@ -189,7 +194,7 @@ export function calculateLetters(
|
|||||||
export default function drawStaggeredText(
|
export default function drawStaggeredText(
|
||||||
CanvasKit: CanvasKit,
|
CanvasKit: CanvasKit,
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
entity: z.output<typeof StaggeredText>,
|
entity: z.output<typeof StaggeredTextEntity>,
|
||||||
cache: StaggeredTextCache
|
cache: StaggeredTextCache
|
||||||
) {
|
) {
|
||||||
const paint = new CanvasKit.Paint();
|
const paint = new CanvasKit.Paint();
|
||||||
|
@ -1,46 +1,56 @@
|
|||||||
import { Canvas, CanvasKit, Font } from "canvaskit-wasm";
|
import { Canvas, CanvasKit, Font, FontMgr, Typeface } from "canvaskit-wasm";
|
||||||
import { TextEntity } from "primitives/Entities";
|
import { TextEntity } from "primitives/Entities";
|
||||||
import { convertToFloat } from "@tempblade/common";
|
import { convertToFloat } from "@tempblade/common";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EntityCache } from "./cache";
|
import { EntityCache } from "./cache";
|
||||||
|
import { Dependencies } from "services/dependencies.service";
|
||||||
|
import { buildPaintStyle } from "./paint";
|
||||||
|
|
||||||
export type TextCache = {
|
export type TextCache = {
|
||||||
font: Font;
|
fontManager: FontMgr;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TextEntityCache = EntityCache<TextCache>;
|
export type TextEntityCache = EntityCache<TextCache>;
|
||||||
|
|
||||||
|
export function buildTextCache(
|
||||||
|
CanvasKit: CanvasKit,
|
||||||
|
entity: z.output<typeof TextEntity>,
|
||||||
|
dependencies: Dependencies
|
||||||
|
): TextCache {
|
||||||
|
const fontData = dependencies.fonts.get(entity.paint.fontName) as ArrayBuffer;
|
||||||
|
|
||||||
|
const fontManager = CanvasKit.FontMgr.FromData(fontData) as FontMgr;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fontManager,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function drawText(
|
export default function drawText(
|
||||||
CanvasKit: CanvasKit,
|
CanvasKit: CanvasKit,
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
entity: z.output<typeof TextEntity>,
|
entity: z.output<typeof TextEntity>,
|
||||||
fontData: ArrayBuffer
|
cache: TextCache
|
||||||
) {
|
) {
|
||||||
canvas.save();
|
canvas.save();
|
||||||
const fontMgr = CanvasKit.FontMgr.FromData(fontData);
|
|
||||||
|
|
||||||
if (!fontMgr) {
|
|
||||||
console.error("No FontMgr");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paint = new CanvasKit.Paint();
|
const paint = new CanvasKit.Paint();
|
||||||
|
|
||||||
const color = convertToFloat(entity.paint.style.color.value);
|
const color = convertToFloat(entity.paint.style.color.value);
|
||||||
|
|
||||||
paint.setColor(color);
|
buildPaintStyle(CanvasKit, paint, entity.paint);
|
||||||
|
|
||||||
const pStyle = new CanvasKit.ParagraphStyle({
|
const pStyle = new CanvasKit.ParagraphStyle({
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: color,
|
color: color,
|
||||||
fontFamilies: ["Helvetica"],
|
fontFamilies: [entity.paint.fontName],
|
||||||
fontSize: entity.paint.size,
|
fontSize: entity.paint.size,
|
||||||
},
|
},
|
||||||
textDirection: CanvasKit.TextDirection.LTR,
|
textDirection: CanvasKit.TextDirection.LTR,
|
||||||
textAlign: CanvasKit.TextAlign[entity.paint.align],
|
textAlign: CanvasKit.TextAlign[entity.paint.align],
|
||||||
});
|
});
|
||||||
|
|
||||||
const builder = CanvasKit.ParagraphBuilder.Make(pStyle, fontMgr);
|
const builder = CanvasKit.ParagraphBuilder.Make(pStyle, cache.fontManager);
|
||||||
builder.addText(entity.text);
|
builder.addText(entity.text);
|
||||||
const p = builder.build();
|
const p = builder.build();
|
||||||
p.layout(900);
|
p.layout(900);
|
||||||
@ -49,7 +59,7 @@ export default function drawText(
|
|||||||
|
|
||||||
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
|
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
|
||||||
|
|
||||||
paint.delete();
|
|
||||||
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
|
builder.delete();
|
||||||
}
|
}
|
||||||
|
@ -300,7 +300,7 @@ const ExampleTimeline: z.input<typeof Timeline> = {
|
|||||||
render_state: {
|
render_state: {
|
||||||
curr_frame: 20,
|
curr_frame: 20,
|
||||||
},
|
},
|
||||||
fps: 60,
|
fps: 120,
|
||||||
entities: EXAMPLE_ANIMATED_ENTITIES,
|
entities: EXAMPLE_ANIMATED_ENTITIES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export const GeometryEntity = BaseEntity.extend({
|
|||||||
paint: Paint,
|
paint: Paint,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StaggeredText = BaseEntity.extend({
|
export const StaggeredTextEntity = BaseEntity.extend({
|
||||||
letter: z.object({
|
letter: z.object({
|
||||||
transform: z.array(Transform).optional(),
|
transform: z.array(Transform).optional(),
|
||||||
paint: TextPaint,
|
paint: TextPaint,
|
||||||
@ -64,7 +64,7 @@ export const Entity = z.discriminatedUnion("type", [
|
|||||||
RectEntity,
|
RectEntity,
|
||||||
EllipseEntity,
|
EllipseEntity,
|
||||||
TextEntity,
|
TextEntity,
|
||||||
StaggeredText,
|
StaggeredTextEntity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const Entities = z.array(Entity);
|
export const Entities = z.array(Entity);
|
||||||
|
@ -35,6 +35,7 @@ export const Paint = z.object({
|
|||||||
export const TextPaint = z.object({
|
export const TextPaint = z.object({
|
||||||
style: PaintStyle,
|
style: PaintStyle,
|
||||||
align: TextAlign,
|
align: TextAlign,
|
||||||
|
fontName: z.string().default("Helvetica-Bold"),
|
||||||
size: z.number().min(0),
|
size: z.number().min(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
83
app/src/services/dependencies.service.ts
Normal file
83
app/src/services/dependencies.service.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
|
import { Entities, Entity, EntityType } from "primitives/Entities";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||||
|
return array.buffer.slice(
|
||||||
|
array.byteOffset,
|
||||||
|
array.byteLength + array.byteOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dependencies = {
|
||||||
|
fonts: Map<string, ArrayBuffer>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DependenciesService {
|
||||||
|
dependencies: Dependencies;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dependencies = {
|
||||||
|
fonts: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepare(
|
||||||
|
entities: z.output<typeof Entities> | z.output<typeof AnimatedEntities>
|
||||||
|
) {
|
||||||
|
const fontNames = new Set<string>();
|
||||||
|
|
||||||
|
entities.forEach((entity) => {
|
||||||
|
if (entity.type === EntityType.Enum.Text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (entity.type) {
|
||||||
|
case EntityType.Enum.Text:
|
||||||
|
fontNames.add(entity.paint.fontName);
|
||||||
|
break;
|
||||||
|
case EntityType.Enum.StaggeredText:
|
||||||
|
fontNames.add(entity.letter.paint.fontName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.loadFonts(fontNames);
|
||||||
|
|
||||||
|
return this.dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareForEntities(entities: z.output<typeof Entities>) {
|
||||||
|
await this.prepare(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareForAnimatedEntities(
|
||||||
|
animatedEntities: z.output<typeof AnimatedEntities>
|
||||||
|
) {
|
||||||
|
await this.prepare(animatedEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFonts(fontNames: Set<string>) {
|
||||||
|
const resolveFonts: Array<Promise<void>> = [];
|
||||||
|
|
||||||
|
fontNames.forEach((fontName) => {
|
||||||
|
if (!this.dependencies.fonts.has(fontName)) {
|
||||||
|
resolveFonts.push(
|
||||||
|
invoke("get_system_font", { fontName }).then((data) => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const u8 = new Uint8Array(data as any);
|
||||||
|
const buffer = typedArrayToBuffer(u8);
|
||||||
|
this.dependencies.fonts.set(fontName, buffer);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(resolveFonts);
|
||||||
|
|
||||||
|
console.log(this.dependencies);
|
||||||
|
}
|
||||||
|
}
|
100
app/src/services/playback.service.ts
Normal file
100
app/src/services/playback.service.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Drawer } from "drawers/draw";
|
||||||
|
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||||
|
import { useEntitiesStore } from "stores/entities.store";
|
||||||
|
import { useRenderStateStore } from "stores/render-state.store";
|
||||||
|
import { useTimelineStore } from "stores/timeline.store";
|
||||||
|
|
||||||
|
export class PlaybackService {
|
||||||
|
drawer: Drawer;
|
||||||
|
lastDrawTime: number | undefined;
|
||||||
|
raf: number | undefined;
|
||||||
|
playing: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.drawer = new Drawer();
|
||||||
|
this.lastDrawTime = undefined;
|
||||||
|
this.raf = undefined;
|
||||||
|
this.playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(canvas: HTMLCanvasElement) {
|
||||||
|
await this.drawer.init(canvas);
|
||||||
|
|
||||||
|
useRenderStateStore.subscribe((state) => {
|
||||||
|
if (!this.playing && state.playing) {
|
||||||
|
this.playing = true;
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.playing && !state.playing) {
|
||||||
|
this.playing = false;
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.playing && !state.playing) {
|
||||||
|
this.seek();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.seek();
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
this.drawer.dependenciesService.prepareForAnimatedEntities(
|
||||||
|
this.animatedEntities
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentTime = window.performance.now();
|
||||||
|
this.lastDrawTime = currentTime;
|
||||||
|
this.playLoop(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.raf !== undefined) {
|
||||||
|
cancelAnimationFrame(this.raf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seek() {
|
||||||
|
this.drawer.update(this.animatedEntities, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get animatedEntities() {
|
||||||
|
return AnimatedEntities.parse(useEntitiesStore.getState().entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineStore() {
|
||||||
|
return useTimelineStore.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
get fpsInterval() {
|
||||||
|
return 1000 / this.timelineStore.fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currFrame() {
|
||||||
|
return useRenderStateStore.getState().renderState.curr_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalFrameCount() {
|
||||||
|
return this.timelineStore.fps * this.timelineStore.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
playLoop(currentTime: number) {
|
||||||
|
this.raf = requestAnimationFrame(this.playLoop.bind(this));
|
||||||
|
|
||||||
|
if (this.lastDrawTime !== undefined) {
|
||||||
|
const elapsed = currentTime - this.lastDrawTime;
|
||||||
|
|
||||||
|
if (elapsed > this.fpsInterval) {
|
||||||
|
this.lastDrawTime = currentTime - (elapsed % this.fpsInterval);
|
||||||
|
|
||||||
|
const nextFrame =
|
||||||
|
this.currFrame + 1 < this.totalFrameCount ? this.currFrame + 1 : 0;
|
||||||
|
|
||||||
|
useRenderStateStore.getState().setCurrentFrame(nextFrame);
|
||||||
|
|
||||||
|
this.drawer.update(this.animatedEntities, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ interface EntitiesStore {
|
|||||||
selectedEntity: number | undefined;
|
selectedEntity: number | undefined;
|
||||||
selectEntity: (index: number) => void;
|
selectEntity: (index: number) => void;
|
||||||
deselectEntity: () => void;
|
deselectEntity: () => void;
|
||||||
|
setEntities: (entities: z.input<typeof AnimatedEntities>) => void;
|
||||||
updateEntity: (
|
updateEntity: (
|
||||||
index: number,
|
index: number,
|
||||||
entity: Partial<z.input<typeof AnimatedEntity>>
|
entity: Partial<z.input<typeof AnimatedEntity>>
|
||||||
@ -24,6 +25,7 @@ const useEntitiesStore = create<EntitiesStore>((set) => ({
|
|||||||
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
selectEntity: (index) => set(() => ({ selectedEntity: index })),
|
||||||
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
deselectEntity: () => set(() => ({ selectedEntity: undefined })),
|
||||||
selectedEntity: undefined,
|
selectedEntity: undefined,
|
||||||
|
setEntities: (entities) => set({ entities }),
|
||||||
updateEntityById: (id, entity) =>
|
updateEntityById: (id, entity) =>
|
||||||
set(({ entities }) => {
|
set(({ entities }) => {
|
||||||
const nextEntities = produce(entities, (draft) => {
|
const nextEntities = produce(entities, (draft) => {
|
||||||
|
@ -4,6 +4,8 @@ import { create } from "zustand";
|
|||||||
|
|
||||||
interface RenderStateStore {
|
interface RenderStateStore {
|
||||||
renderState: z.infer<typeof RenderState>;
|
renderState: z.infer<typeof RenderState>;
|
||||||
|
playing: boolean;
|
||||||
|
setPlaying: (playing: boolean) => void;
|
||||||
setCurrentFrame: (target: number) => void;
|
setCurrentFrame: (target: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,6 +13,8 @@ const useRenderStateStore = create<RenderStateStore>((set) => ({
|
|||||||
renderState: {
|
renderState: {
|
||||||
curr_frame: 20,
|
curr_frame: 20,
|
||||||
},
|
},
|
||||||
|
playing: false,
|
||||||
|
setPlaying: (playing) => set({ playing }),
|
||||||
setCurrentFrame: (target) =>
|
setCurrentFrame: (target) =>
|
||||||
set((store) => {
|
set((store) => {
|
||||||
store.renderState = {
|
store.renderState = {
|
||||||
|
@ -7,8 +7,8 @@ interface TimelineStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useTimelineStore = create<TimelineStore>((set) => ({
|
const useTimelineStore = create<TimelineStore>((set) => ({
|
||||||
fps: 60,
|
fps: 120,
|
||||||
size: [1920, 1080],
|
size: [1280, 720],
|
||||||
duration: 10.0,
|
duration: 10.0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user