diff --git a/app/src/components/Canvas/index.tsx b/app/src/components/Canvas/index.tsx index 578e1ca..15fc0ab 100644 --- a/app/src/components/Canvas/index.tsx +++ b/app/src/components/Canvas/index.tsx @@ -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 = () => { const canvas = useRef(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 = () => { } }, []); - useEffect(() => { - if (didInit) { - drawer.update(entities); - } - }, [entities, renderState.curr_frame, didInit]); - return (
= () => { const { renderState, setCurrentFrame } = useRenderStateStore(); + const timeline = useTimelineStore(); return ( setCurrentFrame(val[0])} - max={60 * 10} + max={timeline.fps * timeline.duration} step={1} aria-label="Current Frame" > diff --git a/app/src/components/Timeline.tsx b/app/src/components/Timeline.tsx index 7dc98a3..7c23ba5 100644 --- a/app/src/components/Timeline.tsx +++ b/app/src/components/Timeline.tsx @@ -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; name: string; index: number; + entity: z.input; keyframes: Array>; }; @@ -31,7 +36,7 @@ const KeyframeIndicator: FC<{ return ( = ({ keyframes, animationData, index, name }) => { +const Track: FC = ({ + keyframes, + animationData, + index, + name, + entity, +}) => { + const controls = useDragControls(); + const { updateEntity, selectEntity, selectedEntity, deselectEntity } = useEntitiesStore( (store) => ({ @@ -55,22 +68,34 @@ const Track: FC = ({ keyframes, animationData, index, name }) => { ); return ( -
+
- 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" }`} > -

{name}

+

+ selectedEntity !== undefined && selectedEntity === index + ? deselectEntity() + : selectEntity(index) + } + className="text-white-800 select-none pointer-events-none" + > + {name} +

+
{keyframes.map((keyframe, index) => ( = ({ keyframes, animationData, index, name }) => { = ({ 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 = ({ keyframes, animationData, index, name }) => { = ({ 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" >
-
+ ); }; const Timeline: FC = () => { - const { entities } = useEntitiesStore((store) => ({ + const { entities, setEntities } = useEntitiesStore((store) => ({ entities: store.entities, + setEntities: store.setEntities, + })); + + const { setPlaying } = useRenderStateStore((store) => ({ + setPlaying: store.setPlaying, })); return ( -
- -
+
+
+
+ + +
+ +
+
- - {entities.map((entity, index) => ( - - ))} + + {entities.map((entity, index) => ( + + ))} +
); diff --git a/app/src/components/Timestamp.tsx b/app/src/components/Timestamp.tsx index 1825af3..4f484d8 100644 --- a/app/src/components/Timestamp.tsx +++ b/app/src/components/Timestamp.tsx @@ -7,9 +7,12 @@ const Timestamp = () => { return (
-

Frame {renderState.curr_frame}

+

+ Frame {renderState.curr_frame} / {timeline.fps * timeline.duration} +

- {((renderState.curr_frame * timeline.fps) / 60 / 60).toPrecision(3)}{" "} + {(renderState.curr_frame / timeline.fps).toPrecision(3)} /{" "} + {timeline.duration.toPrecision(3)} / {timeline.fps}FPS

diff --git a/app/src/drawers/draw.ts b/app/src/drawers/draw.ts index 54bf8f8..c67eb6b 100644 --- a/app/src/drawers/draw.ts +++ b/app/src/drawers/draw.ts @@ -1,7 +1,12 @@ import { invoke } from "@tauri-apps/api"; import InitCanvasKit, { Canvas, CanvasKit, Surface } from "canvaskit-wasm"; 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 { useTimelineStore } from "stores/timeline.store"; import { z } from "zod"; @@ -10,18 +15,13 @@ import drawStaggeredText, { StaggeredTextEntityCache, calculateLetters, } from "./staggered-text"; -import drawText from "./text"; +import drawText, { TextCache, TextEntityCache, buildTextCache } from "./text"; import drawEllipse from "./ellipse"; import drawRect from "./rect"; import { useEntitiesStore } from "stores/entities.store"; import { handleEntityCache } from "./cache"; - -function typedArrayToBuffer(array: Uint8Array): ArrayBuffer { - return array.buffer.slice( - array.byteOffset, - array.byteLength + array.byteOffset - ); -} +import { DependenciesService } from "services/dependencies.service"; +import { RenderState } from "primitives/Timeline"; /** * @@ -32,36 +32,39 @@ export class Drawer { private didLoad: boolean; private entities: z.output | undefined; private ckDidLoad: boolean; - private dependenciesDidLoad: boolean; drawCount: number; private CanvasKit: CanvasKit | undefined; - cache: { staggeredText: Map }; + cache: { + staggeredText: Map; + text: Map; + }; surface: Surface | undefined; fontData: ArrayBuffer | undefined; raf: number | undefined; isLocked: boolean; + dependenciesService: DependenciesService; constructor() { this.entities = undefined; this.CanvasKit = undefined; this.ckDidLoad = false; - this.dependenciesDidLoad = false; this.drawCount = 0; this.surface = undefined; this.fontData = undefined; this.cache = { staggeredText: new Map(), + text: new Map(), }; + this.dependenciesService = new DependenciesService(); this.isLocked = false; this.raf = undefined; - this.didLoad = this.ckDidLoad && this.dependenciesDidLoad; + this.didLoad = this.ckDidLoad; } async init(canvas: HTMLCanvasElement) { await this.loadCanvasKit(canvas); - await this.loadDependencies(false); - this.didLoad = this.ckDidLoad && this.dependenciesDidLoad; + this.didLoad = this.ckDidLoad; } async loadCanvasKit(canvas: HTMLCanvasElement) { @@ -80,66 +83,69 @@ export class Drawer { }); } - async loadDependencies(remote: boolean) { - if (remote) { - await fetch( - "https://storage.googleapis.com/skia-cdn/misc/Roboto-Regular.ttf" - ) - .then((response) => response.arrayBuffer()) - .then((arrayBuffer) => { - this.fontData = arrayBuffer; - this.dependenciesDidLoad = true; - }); - } else { - await invoke("get_system_font", { fontName: "Helvetica-Bold" }).then( - (data) => { - if (Array.isArray(data)) { - const u8 = new Uint8Array(data as any); - const buffer = typedArrayToBuffer(u8); - this.fontData = buffer; - this.dependenciesDidLoad = true; - } - } + async calculateAnimatedEntities( + animatedEntities: z.input, + renderState: z.output + ) { + const { fps, size, duration } = useTimelineStore.getState(); + + const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities); + + const data = await invoke("calculate_timeline_entities_at_frame", { + timeline: { + entities: parsedAnimatedEntities, + render_state: renderState, + fps, + size, + duration, + }, + }); + + 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 */ - update(animatedEntities: z.input) { + update( + animatedEntities: z.input, + prepareDependencies: boolean + ) { console.time("calculate"); - const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities); - if (this.didLoad) { - const render_state = useRenderStateStore.getState().renderState; - const { fps, size, duration } = useTimelineStore.getState(); + const renderState = useRenderStateStore.getState().renderState; - invoke("calculate_timeline_entities_at_frame", { - timeline: { - entities: parsedAnimatedEntities, - render_state, - fps, - size, - duration, - }, - }).then((data) => { - console.timeEnd("calculate"); - const parsedEntities = Entities.safeParse(data); - if (parsedEntities.success) { - this.entities = parsedEntities.data; + this.calculateAnimatedEntities(animatedEntities, renderState).then( + (entities) => { + this.entities = entities; - const isCached = this.entities.reduce( - (prev, curr) => prev && curr.cache.valid, - true - ); - - this.requestRedraw(!isCached); - } else { - console.error(parsedEntities.error); + if (prepareDependencies) { + this.dependenciesService + .prepareForEntities(this.entities) + .then(() => { + this.requestRedraw(!this.isCached); + }); + } else { + this.requestRedraw(!this.isCached); + } } - }); + ); + } else { + console.timeEnd("calculate"); } } @@ -161,11 +167,10 @@ export class Drawer { } draw(canvas: Canvas) { - if (this.CanvasKit && this.entities && this.fontData && !this.isLocked) { + if (this.CanvasKit && this.entities && !this.isLocked) { this.isLocked = true; console.time("draw"); const CanvasKit = this.CanvasKit; - const fontData = this.fontData; canvas.clear(CanvasKit.WHITE); @@ -180,17 +185,42 @@ export class Drawer { drawEllipse(CanvasKit, canvas, entity); break; case EntityType.Enum.Text: - drawText(CanvasKit, canvas, entity, fontData); + { + const cache = handleEntityCache< + z.output, + 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; case EntityType.Enum.StaggeredText: { const cache = handleEntityCache< - z.output, + z.output, StaggeredTextCache, StaggeredTextEntityCache >(entity, { build: () => { - const cache = calculateLetters(CanvasKit, entity, fontData); + const cache = calculateLetters( + CanvasKit, + entity, + this.dependenciesService.dependencies + ); useEntitiesStore .getState() .updateEntityById(entity.id, { cache: { valid: true } }); diff --git a/app/src/drawers/paragraph.ts b/app/src/drawers/paragraph.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/src/drawers/staggered-text.ts b/app/src/drawers/staggered-text.ts index 4841f90..5eb0d85 100644 --- a/app/src/drawers/staggered-text.ts +++ b/app/src/drawers/staggered-text.ts @@ -7,10 +7,11 @@ import { TypedArray, Typeface, } from "canvaskit-wasm"; -import { StaggeredText } from "primitives/Entities"; +import { StaggeredTextEntity } from "primitives/Entities"; import { z } from "zod"; import { buildPaintStyle } from "./paint"; import { EntityCache } from "./cache"; +import { Dependencies } from "services/dependencies.service"; export type StaggeredTextCache = { letterMeasures: Array; @@ -94,9 +95,13 @@ type LetterMeasures = { export function calculateLetters( CanvasKit: CanvasKit, - entity: z.output, - fontData: ArrayBuffer + entity: z.output, + dependencies: Dependencies ): StaggeredTextCache { + const fontData = dependencies.fonts.get( + entity.letter.paint.fontName + ) as ArrayBuffer; + const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData( fontData ) as Typeface; @@ -189,7 +194,7 @@ export function calculateLetters( export default function drawStaggeredText( CanvasKit: CanvasKit, canvas: Canvas, - entity: z.output, + entity: z.output, cache: StaggeredTextCache ) { const paint = new CanvasKit.Paint(); diff --git a/app/src/drawers/text.ts b/app/src/drawers/text.ts index fae9638..73e211d 100644 --- a/app/src/drawers/text.ts +++ b/app/src/drawers/text.ts @@ -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 { convertToFloat } from "@tempblade/common"; import { z } from "zod"; import { EntityCache } from "./cache"; +import { Dependencies } from "services/dependencies.service"; +import { buildPaintStyle } from "./paint"; export type TextCache = { - font: Font; + fontManager: FontMgr; }; export type TextEntityCache = EntityCache; +export function buildTextCache( + CanvasKit: CanvasKit, + entity: z.output, + 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( CanvasKit: CanvasKit, canvas: Canvas, entity: z.output, - fontData: ArrayBuffer + cache: TextCache ) { canvas.save(); - const fontMgr = CanvasKit.FontMgr.FromData(fontData); - - if (!fontMgr) { - console.error("No FontMgr"); - return; - } const paint = new CanvasKit.Paint(); const color = convertToFloat(entity.paint.style.color.value); - paint.setColor(color); + buildPaintStyle(CanvasKit, paint, entity.paint); const pStyle = new CanvasKit.ParagraphStyle({ textStyle: { color: color, - fontFamilies: ["Helvetica"], + fontFamilies: [entity.paint.fontName], fontSize: entity.paint.size, }, textDirection: CanvasKit.TextDirection.LTR, 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); const p = builder.build(); p.layout(900); @@ -49,7 +59,7 @@ export default function drawText( canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height); - paint.delete(); - canvas.restore(); + + builder.delete(); } diff --git a/app/src/example.ts b/app/src/example.ts index 01fc7ad..9f47466 100644 --- a/app/src/example.ts +++ b/app/src/example.ts @@ -300,7 +300,7 @@ const ExampleTimeline: z.input = { render_state: { curr_frame: 20, }, - fps: 60, + fps: 120, entities: EXAMPLE_ANIMATED_ENTITIES, }; diff --git a/app/src/primitives/Entities.ts b/app/src/primitives/Entities.ts index 64ff0f1..a5664b5 100644 --- a/app/src/primitives/Entities.ts +++ b/app/src/primitives/Entities.ts @@ -26,7 +26,7 @@ export const GeometryEntity = BaseEntity.extend({ paint: Paint, }); -export const StaggeredText = BaseEntity.extend({ +export const StaggeredTextEntity = BaseEntity.extend({ letter: z.object({ transform: z.array(Transform).optional(), paint: TextPaint, @@ -64,7 +64,7 @@ export const Entity = z.discriminatedUnion("type", [ RectEntity, EllipseEntity, TextEntity, - StaggeredText, + StaggeredTextEntity, ]); export const Entities = z.array(Entity); diff --git a/app/src/primitives/Paint.ts b/app/src/primitives/Paint.ts index c3f3740..476c9d2 100644 --- a/app/src/primitives/Paint.ts +++ b/app/src/primitives/Paint.ts @@ -35,6 +35,7 @@ export const Paint = z.object({ export const TextPaint = z.object({ style: PaintStyle, align: TextAlign, + fontName: z.string().default("Helvetica-Bold"), size: z.number().min(0), }); diff --git a/app/src/services/dependencies.service.ts b/app/src/services/dependencies.service.ts new file mode 100644 index 0000000..510d51f --- /dev/null +++ b/app/src/services/dependencies.service.ts @@ -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; +}; + +export class DependenciesService { + dependencies: Dependencies; + + constructor() { + this.dependencies = { + fonts: new Map(), + }; + } + + private async prepare( + entities: z.output | z.output + ) { + const fontNames = new Set(); + + 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) { + await this.prepare(entities); + } + + async prepareForAnimatedEntities( + animatedEntities: z.output + ) { + await this.prepare(animatedEntities); + } + + async loadFonts(fontNames: Set) { + const resolveFonts: Array> = []; + + 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); + } +} diff --git a/app/src/services/playback.service.ts b/app/src/services/playback.service.ts new file mode 100644 index 0000000..194105f --- /dev/null +++ b/app/src/services/playback.service.ts @@ -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); + } + } + } +} diff --git a/app/src/stores/entities.store.ts b/app/src/stores/entities.store.ts index 2ee67dd..81ff267 100644 --- a/app/src/stores/entities.store.ts +++ b/app/src/stores/entities.store.ts @@ -9,6 +9,7 @@ interface EntitiesStore { selectedEntity: number | undefined; selectEntity: (index: number) => void; deselectEntity: () => void; + setEntities: (entities: z.input) => void; updateEntity: ( index: number, entity: Partial> @@ -24,6 +25,7 @@ const useEntitiesStore = create((set) => ({ selectEntity: (index) => set(() => ({ selectedEntity: index })), deselectEntity: () => set(() => ({ selectedEntity: undefined })), selectedEntity: undefined, + setEntities: (entities) => set({ entities }), updateEntityById: (id, entity) => set(({ entities }) => { const nextEntities = produce(entities, (draft) => { diff --git a/app/src/stores/render-state.store.ts b/app/src/stores/render-state.store.ts index e20e06d..e1869b8 100644 --- a/app/src/stores/render-state.store.ts +++ b/app/src/stores/render-state.store.ts @@ -4,6 +4,8 @@ import { create } from "zustand"; interface RenderStateStore { renderState: z.infer; + playing: boolean; + setPlaying: (playing: boolean) => void; setCurrentFrame: (target: number) => void; } @@ -11,6 +13,8 @@ const useRenderStateStore = create((set) => ({ renderState: { curr_frame: 20, }, + playing: false, + setPlaying: (playing) => set({ playing }), setCurrentFrame: (target) => set((store) => { store.renderState = { diff --git a/app/src/stores/timeline.store.ts b/app/src/stores/timeline.store.ts index 670c517..56d8682 100644 --- a/app/src/stores/timeline.store.ts +++ b/app/src/stores/timeline.store.ts @@ -7,8 +7,8 @@ interface TimelineStore { } const useTimelineStore = create((set) => ({ - fps: 60, - size: [1920, 1080], + fps: 120, + size: [1280, 720], duration: 10.0, }));