import { invoke } from "@tauri-apps/api"; import InitCanvasKit, { Canvas, CanvasKit, Surface } from "canvaskit-wasm"; import { AnimatedEntities } from "primitives/AnimatedEntities"; 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"; import drawStaggeredText, { StaggeredTextCache, StaggeredTextEntityCache, calculateLetters, } from "./staggered-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"; import { DependenciesService } from "services/dependencies.service"; import { RenderState } from "primitives/Timeline"; /** * * TODO Add more sophisticated dependency logic for e.g. dynamically loading fonts, images etc. */ export class Drawer { private didLoad: boolean; private entities: z.output | undefined; private ckDidLoad: boolean; drawCount: number; private CanvasKit: CanvasKit | undefined; 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.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; } async init(canvas: HTMLCanvasElement) { await this.loadCanvasKit(canvas); this.didLoad = this.ckDidLoad; } async loadCanvasKit(canvas: HTMLCanvasElement) { await InitCanvasKit({ locateFile: (file) => file, }).then((CanvasKit) => { if (canvas) { const CSurface = CanvasKit.MakeWebGLCanvasSurface(canvas); if (CSurface) { this.CanvasKit = CanvasKit; this.surface = CSurface; this.ckDidLoad = 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, prepareDependencies: boolean ) { // console.time("calculate"); if (this.didLoad) { const renderState = useRenderStateStore.getState().renderState; this.calculateAnimatedEntities(animatedEntities, renderState).then( (entities) => { this.entities = entities; if (prepareDependencies) { this.dependenciesService .prepareForEntities(this.entities) .then(() => { this.requestRedraw(!this.isCached); }); } else { this.requestRedraw(!this.isCached); } } ); } else { // console.timeEnd("calculate"); } } requestRedraw(rebuild: boolean) { if (this.didLoad && this.surface && !this.isLocked) { if (rebuild && this.raf !== undefined) { cancelAnimationFrame(this.raf); // this.surface.flush(); this.raf = this.surface.requestAnimationFrame((canvas) => this.draw(canvas) ); } else { // this.surface.flush(); this.raf = this.surface.requestAnimationFrame((canvas) => this.draw(canvas) ); } } } draw(canvas: Canvas) { if (this.CanvasKit && this.entities && !this.isLocked) { this.isLocked = true; //console.time("draw"); const CanvasKit = this.CanvasKit; canvas.clear(CanvasKit.WHITE); this.drawCount++; [...this.entities].reverse().forEach((entity) => { switch (entity.type) { case EntityType.Enum.Rect: drawRect(CanvasKit, canvas, entity); break; case EntityType.Enum.Ellipse: drawEllipse(CanvasKit, canvas, entity); break; case EntityType.Enum.Text: { const cache = handleEntityCache< z.output, TextCache, TextEntityCache >(entity, { build: () => { const cache = buildTextCache( CanvasKit, entity, this.dependenciesService.dependencies ); useEntitiesStore .getState() .updateEntityById(entity.id, { cache: { valid: true } }); return cache; }, 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, StaggeredTextCache, StaggeredTextEntityCache >(entity, { build: () => { const cache = calculateLetters( CanvasKit, entity, this.dependenciesService.dependencies ); useEntitiesStore .getState() .updateEntityById(entity.id, { cache: { valid: true } }); return cache; }, get: () => this.cache.staggeredText.get(entity.id), set: (id, cache) => this.cache.staggeredText.set(id, cache), cleanup: (cache) => { cache.font.delete(); cache.typeface.delete(); CanvasKit.Free(cache.glyphs); }, }); drawStaggeredText(CanvasKit, canvas, entity, cache); } break; default: break; } }); this.isLocked = false; //console.timeEnd("draw"); } } }