add system font loading

improve drawing
implement staggered text
begin refactor of drawing code
This commit is contained in:
2023-05-24 00:24:16 +02:00
parent 8523e44029
commit 330fa6a7f0
28 changed files with 844 additions and 207 deletions

76
app/src/drawers/draw.ts Normal file
View 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) {
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}