Text Rendering
Everything about the process of getting text to the user’s screen.
Overview
Text rendering is based on SDF (Signed Distance Field) font atlas approach. This means that for every font to be rendered, the TTF file must be parsed, glyph information extracted and then the font atlas needs to be generated. The basic and default approach is to use HTML Canvas API to prepare it.
Font lookups
All the information about text that is later needed is bundled in what I call Lookups
:
type Lookups = {
atlas: {
fontSize: number;
height: number;
positions: Array<Vec2>;
sizes: Array<Vec2>;
width: number;
};
fonts: Array<{
ascender: number;
buffer: ArrayBuffer;
capHeight: number;
glyphs: Map<number, Glyph>;
kern: KerningFunction;
name: string;
ttf: TTF;
unitsPerEm: number;
}>;
uvs: Map<string, Vec4>;
};
And this is how it is generated:
// Alphabet is a string containing all characters that should be included in
// the atlas.
const alphabet = "AaBbCc…";
const [interTTF, interBoldTTF] = await Promise.all(
["/Inter.ttf", "/Inter-SemiBold.ttf"].map((url) =>
fetch(url).then((response) => response.arrayBuffer()),
),
);
const lookups = prepareLookups(
// Declare all fonts that should be included.
[
{
buffer: interTTF,
name: "Inter",
ttf: parseTTF(interTTF),
},
{
buffer: interBoldTTF,
name: "InterBold",
ttf: parseTTF(interBoldTTF),
},
],
// Render at 150px size. The size of the font atlas will be determined by
// packShelves() algorithm.
{ alphabet, fontSize: 150 },
);
// Generate _one_ font atlas texture for all fonts. In future it would make
// sense to allow splitting for projects that use many fonts.
const fontAtlas = await renderFontAtlas(lookups, { useSDF: true });
API
calculateGlyphQuads
/font/calculateGlyphQuads.ts ↗
Calculates glyph information for a given font file and optional alphabet.
Type declaration
(ttf: TTF, alphabet?: string) => Glyph[]
generateGlyphToClassMap
/font/generateGlyphToClassMap.ts ↗
Type declaration
(classDef: ClassDefFormat1 | ClassDefFormat2) => Map<number, number>;
generateKerningFunction
/font/generateKerningFunction.ts ↗
Generates a kerning function used by shapeText()
.
Type declaration
(ttf: TTF) => KerningFunction;
parseTTF
/font/parseTTF.ts ↗
Main function for parsing TTF files.
Type declaration
(data: ArrayBuffer) => TTF;
prepareLookups
/font/prepareLookups.ts ↗
This is generally extension of the font parsing process.
Type declaration
(
fontFiles: { buffer: ArrayBuffer; name: string; ttf: TTF }[],
options?: { alphabet?: string; fontSize?: number },
) => Lookups;
renderFontAtlas
/font/renderFontAtlas.ts ↗
Type declaration
(lookups: Lookups, options?: { alphabet?: string; useSDF?: boolean }) =>
Promise<ImageBitmap>;
fontSizeToGap
/font/renderFontAtlas.ts ↗
Helper function for placing glyphs in the font atlas.
Type declaration
(fontSize: number) => number;
shapeText
/font/shapeText.ts ↗
The most important function for getting text on the screen. Given a string and font data, finds the positions and sizes of each character.
Type declaration
(
lookups: Lookups,
fontName: string,
fontSize: number,
lineHeight: number,
text: string,
textAlign: TextAlign,
maxWidth?: number,
noWrap?: boolean,
) => Shape;
toSDF
/font/toSDF.ts ↗
Takes ImageData
and returns a new ImageData()
with the SDF applied.
Type declaration
(imageData: ImageData, width: number, height: number, radius: number) =>
ImageData;
BinaryReader
/font/BinaryReader.ts ↗
A module for reading binary data. Used internally by parseTTF()
. Keeps track of the current
position, assuming sequential reads.
getUint16
Read two bytes as an unsigned integer and advance the position by two bytes.
Type declaration
() => number;
getInt16
Read two bytes as a signed integer and advance the position by two bytes.
Type declaration
() => number;
getUint32
Read four bytes as an unsigned integer and advance the position by four bytes.
Type declaration
() => number;
getInt32
Read four bytes as a signed integer and advance the position by four bytes.
Type declaration
() => number;
getFixed
Read four bytes as a fixed-point number (2 bytes integer and 2 byte fraction) and advance the position by four bytes.
Type declaration
() => number;
getDate
Read eight bytes as a date (seconds since 1904-01-01 00:00:00) without advancing the position.
Type declaration
() => Date;
getString
Read a string of the given length and advance the position by that length.
Type declaration
(length: number) => string;
getDataSlice
Look up array slice of the given length at the current position without advancing it.
Type declaration
(offset: number, length: number) => Uint8Array;
runAt
Run the given action at the given position, restoring the original position afterwards.
Type declaration
<T,>(position: number, action: () => T) => T;