258 lines
7.2 KiB
TypeScript
258 lines
7.2 KiB
TypeScript
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<typeof Entities> | undefined;
|
|
private ckDidLoad: boolean;
|
|
drawCount: number;
|
|
private CanvasKit: CanvasKit | undefined;
|
|
cache: {
|
|
staggeredText: Map<string, StaggeredTextCache>;
|
|
text: Map<string, TextCache>;
|
|
};
|
|
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<typeof AnimatedEntities>,
|
|
renderState: z.output<typeof RenderState>
|
|
) {
|
|
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<typeof AnimatedEntities>,
|
|
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<typeof TextEntity>,
|
|
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<typeof StaggeredTextEntity>,
|
|
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");
|
|
}
|
|
}
|
|
}
|