Commands, settings, SDK functions, anything in the docs.

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.

Every bundled font, at its own size.
Every bundled font, at its own size.
font-specimen/index.tsx
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:

TSX
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

FontNative sizeLine heightLine advanceCharacters
TINY565ASCII and Latin‑1, € and typographic quotes
J7X5887ASCII, a few currency signs, ß and typographic quotes
NOKIA8108ASCII, most Latin‑1 letters, €
PIXOLLETTA10108ASCII, Latin‑1, Latin Extended‑A, €
FUSION887ASCII, Latin‑1, Latin Extended‑A, Chinese characters, kana and Hangul
SINS161310ASCII, Latin‑1 letters, Central European letters, Russian and Ukrainian Cyrillic
NOPE161511The same characters as SINS
MODERNDOS161612ASCII, Latin‑1, some Latin Extended‑A and Greek, box drawing and DOS symbols
HOMEVIDEO201816ASCII, 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

TSX
<Text x={32} y={16} anchor="mm" text="12:34" font={font} color="#f6cd6d" />

x, y, text, font and color are required. The others:

  • anchor says which point of the text sits at x, y (below). It is la, the top left, unless you set it.
  • size draws the font larger. It must be a whole multiple of the font's native size: NOPE draws 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, align and lineSpacing lay out several lines (below).
  • opacity and blendMode work 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:

LetterAt y
a or tThe top of the first line
sThe baseline of the first line, where letters without descenders end
mThe middle of the whole block of text
b or dThe bottom of the last line, below its descenders
iThe top of the highest pixel actually drawn
jThe 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:

current-temperature/index.tsx
/** `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:

TSX
const covered = (font: BitmapFont, text: string) =>
  Array.from(text).every(char => font.has(char.codePointAt(0) ?? 0));
const font = covered(nope, title) ? nope : fusion;