add system font loading
improve drawing implement staggered text begin refactor of drawing code
This commit is contained in:
76
app/src/drawers/draw.ts
Normal file
76
app/src/drawers/draw.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import InitCanvasKit, { CanvasKit } from "canvaskit-wasm";
|
||||
import { AnimatedEntities } from "primitives/AnimatedEntities";
|
||||
import { Entities } from "primitives/Entities";
|
||||
import { useRenderStateStore } from "stores/render-state.store";
|
||||
import { useTimelineStore } from "stores/timeline.store";
|
||||
import { z } from "zod";
|
||||
import { StaggeredTextCache } from "./staggered-text";
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO Add dependency logic for e.g. dynamically loading fonts, images etc.
|
||||
*/
|
||||
|
||||
export class Drawer {
|
||||
private readonly didLoad: boolean;
|
||||
private entities: z.output<typeof Entities> | undefined;
|
||||
private ckDidLoad: boolean;
|
||||
private dependenciesDidLoad: boolean;
|
||||
private CanvasKit: CanvasKit | undefined;
|
||||
cache: { staggeredText: Map<string, StaggeredTextCache> };
|
||||
|
||||
constructor() {
|
||||
this.entities = undefined;
|
||||
this.CanvasKit = undefined;
|
||||
this.ckDidLoad = false;
|
||||
this.dependenciesDidLoad = false;
|
||||
this.cache = {
|
||||
staggeredText: new Map(),
|
||||
};
|
||||
this.didLoad = this.ckDidLoad && this.dependenciesDidLoad;
|
||||
}
|
||||
|
||||
init() {
|
||||
InitCanvasKit({
|
||||
locateFile: (file) =>
|
||||
"https://unpkg.com/canvaskit-wasm@latest/bin/" + file,
|
||||
}).then((CanvasKit) => {
|
||||
this.CanvasKit = CanvasKit;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the entities based on the input
|
||||
*/
|
||||
update(animatedEntities: z.input<typeof AnimatedEntities>) {
|
||||
const parsedAnimatedEntities = AnimatedEntities.parse(animatedEntities);
|
||||
|
||||
if (this.didLoad) {
|
||||
const render_state = useRenderStateStore.getState().renderState;
|
||||
const { fps, size, duration } = useTimelineStore.getState();
|
||||
|
||||
invoke("calculate_timeline_entities_at_frame", {
|
||||
timeline: {
|
||||
entities: parsedAnimatedEntities,
|
||||
render_state,
|
||||
fps,
|
||||
size,
|
||||
duration,
|
||||
},
|
||||
}).then((data) => {
|
||||
const parsedEntities = Entities.safeParse(data);
|
||||
if (parsedEntities.success) {
|
||||
this.entities = parsedEntities.data;
|
||||
} else {
|
||||
console.error(parsedEntities.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.didLoad) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,278 @@
|
||||
import { convertToFloat } from "@tempblade/common";
|
||||
import { Canvas, CanvasKit } from "canvaskit-wasm";
|
||||
import {
|
||||
Canvas,
|
||||
CanvasKit,
|
||||
Font,
|
||||
FontMetrics,
|
||||
MallocObj,
|
||||
TypedArray,
|
||||
Typeface,
|
||||
} from "canvaskit-wasm";
|
||||
import { StaggeredText } from "primitives/Entities";
|
||||
import { z } from "zod";
|
||||
import { buildPaintStyle } from "./paint";
|
||||
|
||||
export default function drawStaggeredText(
|
||||
export type StaggeredTextCache = {
|
||||
letterMeasures: Array<LetterMeasures>;
|
||||
metrics: FontMetrics;
|
||||
typeface: Typeface;
|
||||
font: Font;
|
||||
glyphs: MallocObj;
|
||||
};
|
||||
|
||||
function getUniqueCharacters(str: string): string {
|
||||
const uniqueCharacters: string[] = [];
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const character = str[i];
|
||||
|
||||
if (!uniqueCharacters.includes(character)) {
|
||||
uniqueCharacters.push(character);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueCharacters.join("");
|
||||
}
|
||||
|
||||
function measureLetters(
|
||||
glyphArr: TypedArray,
|
||||
boundsById: Record<number, LetterBounds>,
|
||||
maxWidth: number
|
||||
): Array<LetterMeasures> {
|
||||
const measuredLetters: Array<LetterMeasures> = [];
|
||||
|
||||
let currentWidth = 0;
|
||||
let currentLine = 0;
|
||||
|
||||
for (let i = 0; i < glyphArr.length; i++) {
|
||||
const nextGlyph = boundsById[glyphArr[i]];
|
||||
|
||||
const nextGlyphWidth = nextGlyph.x_advance;
|
||||
|
||||
currentWidth += nextGlyphWidth;
|
||||
|
||||
if (currentWidth > maxWidth) {
|
||||
currentLine += 1;
|
||||
currentWidth = 0;
|
||||
}
|
||||
|
||||
const glyph = glyphArr.subarray(i, i + 1) as unknown as MallocObj;
|
||||
|
||||
measuredLetters.push({
|
||||
bounds: nextGlyph,
|
||||
line: currentLine,
|
||||
glyph,
|
||||
offset: {
|
||||
x: currentWidth - nextGlyphWidth,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return measuredLetters;
|
||||
}
|
||||
|
||||
type LetterBounds = {
|
||||
x: {
|
||||
max: number;
|
||||
min: number;
|
||||
};
|
||||
y: {
|
||||
max: number;
|
||||
min: number;
|
||||
};
|
||||
width: number;
|
||||
height: number;
|
||||
x_advance: number;
|
||||
};
|
||||
|
||||
type LetterMeasures = {
|
||||
offset: {
|
||||
x: number;
|
||||
};
|
||||
line: number;
|
||||
glyph: MallocObj;
|
||||
bounds: LetterBounds;
|
||||
};
|
||||
|
||||
export function calculateLetters(
|
||||
CanvasKit: CanvasKit,
|
||||
canvs: Canvas,
|
||||
entity: z.output<typeof StaggeredText>,
|
||||
fontData: ArrayBuffer
|
||||
) {
|
||||
const paint = new CanvasKit.Paint();
|
||||
): StaggeredTextCache {
|
||||
console.log("Called");
|
||||
|
||||
const color = convertToFloat(entity.letter.paint.style.color.value);
|
||||
|
||||
paint.setColor(color);
|
||||
|
||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontData);
|
||||
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(
|
||||
fontData
|
||||
) as Typeface;
|
||||
|
||||
const font = new CanvasKit.Font(typeface, entity.letter.paint.size);
|
||||
|
||||
console.log(font.isDeleted());
|
||||
|
||||
const glyphIDs = font.getGlyphIDs(entity.text);
|
||||
|
||||
font.setLinearMetrics(true);
|
||||
font.setSubpixel(true);
|
||||
font.setHinting(CanvasKit.FontHinting.Slight);
|
||||
font.setHinting(CanvasKit.FontHinting.None);
|
||||
|
||||
const bounds = font.getGlyphBounds(glyphIDs, paint);
|
||||
const widths = font.getGlyphWidths(glyphIDs, paint);
|
||||
const alphabet = getUniqueCharacters(entity.text);
|
||||
const ids = font.getGlyphIDs(alphabet);
|
||||
const unknownCharacterGlyphID = ids[0];
|
||||
|
||||
console.log(bounds);
|
||||
console.log(widths);
|
||||
const charsToGlyphIDs: Record<string, any> = {};
|
||||
|
||||
let glyphIdx = 0;
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
charsToGlyphIDs[alphabet[i]] = ids[glyphIdx];
|
||||
if ((alphabet.codePointAt(i) as number) > 65535) {
|
||||
i++; // skip the next index because that will be the second half of the code point.
|
||||
}
|
||||
glyphIdx++;
|
||||
}
|
||||
|
||||
const metrics = font.getMetrics();
|
||||
const bounds = font.getGlyphBounds(glyphIDs);
|
||||
const widths = font.getGlyphWidths(glyphIDs);
|
||||
|
||||
const glyphMetricsByGlyphID: Record<number, LetterBounds> = {};
|
||||
for (let i = 0; i < glyphIDs.length; i++) {
|
||||
const id = glyphIDs[i];
|
||||
|
||||
const x_min = bounds[i * 4];
|
||||
const x_max = bounds[i * 4 + 2];
|
||||
const y_min = bounds[i * 4 + 3];
|
||||
const y_max = bounds[i * 4 + 1];
|
||||
const width = x_max - x_min;
|
||||
const height = Math.abs(y_max - y_min);
|
||||
|
||||
glyphMetricsByGlyphID[id] = {
|
||||
x: {
|
||||
min: x_min,
|
||||
max: x_max,
|
||||
},
|
||||
y: {
|
||||
min: y_min,
|
||||
max: y_max,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
x_advance: widths[i],
|
||||
};
|
||||
}
|
||||
|
||||
const glyphs = CanvasKit.MallocGlyphIDs(entity.text.length);
|
||||
let glyphArr = glyphs.toTypedArray();
|
||||
|
||||
const MAX_WIDTH = 900;
|
||||
|
||||
// Turn the code points into glyphs, accounting for up to 2 ligatures.
|
||||
let shapedGlyphIdx = -1;
|
||||
for (let i = 0; i < entity.text.length; i++) {
|
||||
const char = entity.text[i];
|
||||
shapedGlyphIdx++;
|
||||
glyphArr[shapedGlyphIdx] = charsToGlyphIDs[char] || unknownCharacterGlyphID;
|
||||
if ((entity.text.codePointAt(i) as number) > 65535) {
|
||||
i++; // skip the next index because that will be the second half of the code point.
|
||||
}
|
||||
}
|
||||
// Trim down our array of glyphs to only the amount we have after ligatures and code points
|
||||
// that are > 16 bits.
|
||||
glyphArr = glyphs.subarray(0, shapedGlyphIdx + 1);
|
||||
|
||||
// Break our glyphs into runs based on the maxWidth and the xAdvance.
|
||||
|
||||
const letterMeasures = measureLetters(
|
||||
glyphArr,
|
||||
glyphMetricsByGlyphID,
|
||||
MAX_WIDTH
|
||||
);
|
||||
|
||||
return { letterMeasures, metrics, font, typeface, glyphs };
|
||||
}
|
||||
|
||||
export default function drawStaggeredText(
|
||||
CanvasKit: CanvasKit,
|
||||
canvas: Canvas,
|
||||
entity: z.output<typeof StaggeredText>,
|
||||
font: Font,
|
||||
measuredLetters: Array<LetterMeasures>,
|
||||
metrics: FontMetrics
|
||||
) {
|
||||
const paint = new CanvasKit.Paint();
|
||||
|
||||
buildPaintStyle(CanvasKit, paint, entity.letter.paint);
|
||||
|
||||
// Draw all those runs.
|
||||
for (let i = 0; i < measuredLetters.length; i++) {
|
||||
const measuredLetter = measuredLetters[i];
|
||||
|
||||
const blob = CanvasKit.TextBlob.MakeFromGlyphs(
|
||||
measuredLetters[i].glyph as unknown as Array<number>,
|
||||
font
|
||||
);
|
||||
if (blob) {
|
||||
canvas.save();
|
||||
|
||||
const width = measuredLetters
|
||||
.filter((letter) => letter.line === 0)
|
||||
.reduce((prev, curr) => curr.bounds.x_advance + prev, 0);
|
||||
|
||||
const lineOffset = (entity.letter.paint.size / 2) * measuredLetter.line;
|
||||
|
||||
const entityOrigin = [
|
||||
entity.origin[0] - width / 2,
|
||||
entity.origin[1] + lineOffset,
|
||||
];
|
||||
|
||||
const lineCount = measuredLetters
|
||||
.map((e) => e.line)
|
||||
.sort((a, b) => a - b)[measuredLetters.length - 1];
|
||||
|
||||
if (entity.letter.transform && entity.letter.transform[i]) {
|
||||
const letterTransform = entity.letter.transform[i];
|
||||
const letterOrigin = [0, 0];
|
||||
|
||||
let origin = letterOrigin.map(
|
||||
(val, index) => val + entityOrigin[index]
|
||||
);
|
||||
|
||||
// Calculate the spacing
|
||||
|
||||
const spacing =
|
||||
measuredLetter.bounds.x_advance - measuredLetter.bounds.width;
|
||||
|
||||
//console.log(spacing);
|
||||
|
||||
// Center the origin
|
||||
|
||||
origin[0] =
|
||||
origin[0] + measuredLetter.bounds.width / 2 + measuredLetter.offset.x;
|
||||
origin[1] = origin[1] - metrics.descent + lineOffset;
|
||||
|
||||
//console.log(measuredLetter.bounds);
|
||||
|
||||
canvas.translate(origin[0], origin[1]);
|
||||
|
||||
canvas.scale(letterTransform.scale[0], letterTransform.scale[1]);
|
||||
|
||||
canvas.translate(
|
||||
-origin[0] + measuredLetter.offset.x,
|
||||
-origin[1] + lineOffset
|
||||
);
|
||||
|
||||
/* canvas.translate(
|
||||
measuredLetter.offset.x + measuredLetter.bounds.width / 2,
|
||||
0
|
||||
); */
|
||||
}
|
||||
|
||||
/* canvas.translate(
|
||||
width * -0.5,
|
||||
lineCount * (-entity.letter.paint.size / 2)
|
||||
); */
|
||||
|
||||
canvas.drawTextBlob(blob, entityOrigin[0], entityOrigin[1], paint);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export default function drawText(
|
||||
entity: z.output<typeof TextEntity>,
|
||||
fontData: ArrayBuffer
|
||||
) {
|
||||
canvas.save();
|
||||
const fontMgr = CanvasKit.FontMgr.FromData(fontData);
|
||||
|
||||
if (!fontMgr) {
|
||||
@@ -25,7 +26,7 @@ export default function drawText(
|
||||
const pStyle = new CanvasKit.ParagraphStyle({
|
||||
textStyle: {
|
||||
color: color,
|
||||
fontFamilies: ["Roboto"],
|
||||
fontFamilies: ["Helvetica"],
|
||||
fontSize: entity.paint.size,
|
||||
},
|
||||
textDirection: CanvasKit.TextDirection.LTR,
|
||||
@@ -40,4 +41,8 @@ export default function drawText(
|
||||
const width = p.getMaxWidth() / 2;
|
||||
|
||||
canvas.drawParagraph(p, entity.origin[0] - width, entity.origin[1] - height);
|
||||
|
||||
paint.delete();
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user