Build apps
Text and fonts
Set text in the bitmap fonts made for the panel, and measure it.
On this page
Text on the panel is set in bitmap fonts: every letter is a fixed grid of pixels, drawn exactly,
never scaled smoothly. The fonts come with @ledable/sdk/fonts.

import { defineApp, type AppAssets, type AssetRef } from "@ledable/sdk/app";
import {
Frame,
Text,
renderFrame,
type BitmapFont,
} from "@ledable/sdk/graphics";
import {
FUSION,
HOMEVIDEO,
J7X5,
MODERNDOS,
NOKIA,
NOPE,
PIXOLLETTA,
SINS,
TINY,
loadFont,
} from "@ledable/sdk/fonts";
/** Every bundled font, by the name it is exported under, with a sample. */
const SPECIMENS: readonly (readonly [string, AssetRef, string])[] = [
["TINY", TINY, "Ag 12"],
["J7X5", J7X5, "Ag 12"],
["NOKIA", NOKIA, "Ag 12"],
["PIXOLLETTA", PIXOLLETTA, "Ag 12"],
["FUSION", FUSION, "Ag 汉字"],
["SINS", SINS, "Ag 12"],
["NOPE", NOPE, "Ag 12"],
["MODERNDOS", MODERNDOS, "Ag 12"],
["HOMEVIDEO", HOMEVIDEO, "Ag 12"],
];
function page(
label: BitmapFont,
name: string,
font: BitmapFont,
sample: string,
) {
return renderFrame(
<Frame width={64} height={32} background="#000000">
<Text
x={1}
y={1}
text={`${name} ${font.size}`}
font={label}
color="#8a96a3"
/>
{/* "mm" centres the sample's box: its line height, not its ink. */}
<Text
x={32}
y={19}
anchor="mm"
text={sample}
font={font}
color="#f6cd6d"
/>
</Frame>,
);
}
async function specimen(assets: AppAssets) {
const label = await loadFont(assets, TINY);
const image = await Promise.all(
SPECIMENS.map(async ([name, ref, sample]) =>
page(label, name, await loadFont(assets, ref), sample),
),
);
return { image, frameMs: 1500 };
}
export default defineApp({
settings: () => ({}),
render: (ctx) => specimen(ctx.assets),
preview: (ctx) => specimen(ctx.assets),
});Load a font
Each font is exported as a reference to its font file. Importing it publishes the file with your
app; loadFont(ctx.assets, FONT) reads and decodes it and gives you the font to draw with:
import { MODERNDOS, loadFont } from "@ledable/sdk/fonts";
const font = await loadFont(ctx.assets, MODERNDOS);loadFonts(ctx.assets, { title: NOPE, small: TINY }) loads several at once and returns them under
the same keys. A font is decoded once and kept in memory, so loading it on every render costs
little after the first time. preview loads fonts the same way, from its own ctx.assets.
The fonts
| Font | Native size | Line height | Line advance | Characters |
|---|---|---|---|---|
TINY | 5 | 6 | 5 | ASCII and Latin‑1, € and typographic quotes |
J7X5 | 8 | 8 | 7 | ASCII, a few currency signs, ß and typographic quotes |
NOKIA | 8 | 10 | 8 | ASCII, most Latin‑1 letters, € |
PIXOLLETTA | 10 | 10 | 8 | ASCII, Latin‑1, Latin Extended‑A, € |
FUSION | 8 | 8 | 7 | ASCII, Latin‑1, Latin Extended‑A, Chinese characters, kana and Hangul |
SINS | 16 | 13 | 10 | ASCII, Latin‑1 letters, Central European letters, Russian and Ukrainian Cyrillic |
NOPE | 16 | 15 | 11 | The same characters as SINS |
MODERNDOS | 16 | 16 | 12 | ASCII, Latin‑1, some Latin Extended‑A and Greek, box drawing and DOS symbols |
HOMEVIDEO | 20 | 18 | 16 | ASCII, Latin‑1, Latin Extended‑A, Cyrillic; capital letters only |
All three numbers are pixels at the native size. The line height is the height of the box one line
of text fills, from the font's ascent down to the bottom of its descenders; the line advance is how
far the next line starts below it. HOMEVIDEO has no lower-case letters of its own: it draws a
as A. FUSION holds over 22,000 characters, which makes its file far larger than the others;
import it only where you need its characters. Every font file you import counts toward your app's
asset limits.
Draw text
<Text x={32} y={16} anchor="mm" text="12:34" font={font} color="#f6cd6d" />x, y, text, font and color are required. The others:
anchorsays which point of the text sits atx,y(below). It isla, the top left, unless you set it.sizedraws the font larger. It must be a whole multiple of the font's native size:NOPEdraws at 16, 32 or 48, and any other size throws an error that lists the valid ones. Each pixel of the font becomes a square of pixels.maxWidth,alignandlineSpacinglay out several lines (below).opacityandblendModework as for every other element (see Drawing).
A Text holds at most 4096 characters, counted as JavaScript counts a string's
length.
Anchors
An anchor is two letters. The first places the text horizontally: l puts its left edge at x,
m its middle, r its right edge. The second places it vertically:
| Letter | At y |
|---|---|
a or t | The top of the first line |
s | The baseline of the first line, where letters without descenders end |
m | The middle of the whole block of text |
b or d | The bottom of the last line, below its descenders |
i | The top of the highest pixel actually drawn |
j | The bottom of the lowest pixel actually drawn |
m, b and the others place the line's full box, which leaves room for accents above and
descenders below. Digits and capitals have neither, so with mm they can sit a pixel or two off
the centre. i and j place the text by its drawn pixels instead: y={2} with anchor="li"
puts the top of the letters exactly on row 2, whatever the font.
Several lines
A \n in text starts a new line. With maxWidth, lines also wrap to that many pixels: at spaces,
after hyphens, and between Chinese, Japanese and Korean characters, never just before closing
punctuation or just after opening punctuation. A single word longer than maxWidth is broken
between letters.
align (left, center or right) aligns the lines within the width of the longest. Each line
starts the font's line advance plus lineSpacing pixels below the one before. lineSpacing is 4
unless you set it, which is a lot on a 32-pixel panel; 0 or 1 sets text tightly.
Measure text
measureText(font, text) returns the width and height of the block the text would fill, in
pixels, and the lines it would be broken into. It takes the same size, maxWidth and
lineSpacing as <Text>, so what you measure is what you draw. Measure to centre something next
to the text, to find how far a message must scroll (as the
scrolling message does), or to shorten text that does not fit.
measureTextBounds gives the top, bottom and height of the pixels actually drawn instead.
The SDK does not shorten text for you. This function from the network example drops characters until the text fits:
/** `text`, shortened until it fits in `maxWidth` pixels. */
function fit(font: BitmapFont, text: string, maxWidth: number): string {
// Whole characters, so a character made of two UTF-16 units stays whole.
const characters = Array.from(text);
const width = () => measureText(font, characters.join("")).width;
while (characters.length > 1 && width() > maxWidth) {
characters.pop();
}
return characters.join("");
}Characters a font does not have
A font draws a character it does not have with its placeholder: a box in most fonts, and an empty
gap in SINS, NOKIA and NOPE. There is no fallback to another font. When text comes from
people or from the network, pick a font that covers the languages you expect, such as FUSION for
Chinese, Japanese and Korean. font.has(codePoint) tells you whether a font has a character, so
you can choose a font per text:
const covered = (font: BitmapFont, text: string) =>
Array.from(text).every(char => font.has(char.codePointAt(0) ?? 0));
const font = covered(nope, title) ? nope : fusion;