Build apps
Quickstart
Create an app, see it on a simulated panel as you edit, and render it to a file.
An app is a small TypeScript module that draws what a display shows. It runs on LEDABLE's servers, not on the display: when a display asks for the app, the platform runs it, turns the frames it returns into an image and sends the image to the display, which plays it.
You need the ledable command, installed as in Install and sign in. Nothing
on this page needs an account or a display.
Create a project
ledable developer init hello-led
cd hello-leddeveloper init takes one argument, the app's id, which also names the new directory: lowercase
letters, digits and dashes. It refuses a directory that already exists. Into the new directory it
writes:
| File | What it is |
|---|---|
ledable.json | The app's id, version, store listing and the hosts it may reach |
index.tsx | The app: a white square whose size is a setting |
README.md | The text of the app's page in the store |
package.json | The SDK as a dependency, the CLI as a development dependency, and check, dev and build scripts |
tsconfig.json | Compiler settings, taken from what the CLI generates |
.gitignore | The files that stay on your machine |
Then it runs npm install and type-checks the project once, so a new project already builds.
The project explains every file.
See it as you work
ledable developer devdeveloper dev builds the app and serves a preview page on your machine, on port 4400 unless you
pass --port. It prints the address to open:
{"preview":"http://127.0.0.1:4400/"}Open that address in a browser. The page runs the build as the platform runs it and shows what a display would show, on a simulated 64×32 panel, with the store preview below it and a form with the app's settings beside it. The command keeps running until you stop it with Ctrl-C.
Change it
Open index.tsx, change the colour of the square, and save. developer dev rebuilds on every
save and the page draws the new image. A type error shows above the panel while the last good
image stays; an error your code throws shows under the panel with the line it came from.
Local preview covers everything the page and the command do.
This is a complete app with two settings, a message and a colour. It scrolls the message across
the panel, and its preview draws the picture the store shows:
import { colorRgb, defineApp, marquee, string } from "@ledable/sdk/app";
import {
Frame, Text, measureText, renderFrame, type BitmapFont,
} from "@ledable/sdk/graphics";
import { MODERNDOS, loadFont } from "@ledable/sdk/fonts";
const WIDTH = 64;
const HEIGHT = 32;
/** Every frame of the message, from entering on the right to gone. */
function scroll(font: BitmapFont, message: string, color: string) {
const width = Math.ceil(measureText(font, message).width);
const distance = width + WIDTH;
// 30 ms a pixel at 25 frames a second: smooth on the panel, and a
// few hundred kilobytes at most.
const motion = marquee({
distancePx: distance,
durationMs: distance * 30,
fps: 25,
});
const image = Array.from({ length: motion.frameCount }, (_, frame) =>
renderFrame(
<Frame width={WIDTH} height={HEIGHT} background="#000000">
<Text
x={WIDTH - motion.offsetAt(frame)}
y={HEIGHT / 2}
anchor="lm"
text={message}
font={font}
color={color}
/>
</Frame>,
),
);
return { image, frameMs: motion.frameMs };
}
type Rgb = readonly [number, number, number];
function hex(rgb: Rgb): string {
const parts = rgb.map(part => part.toString(16).padStart(2, "0"));
return `#${parts.join("")}`;
}
export default defineApp({
settings: () => ({
message: string({
name: "Message",
description: "What scrolls across the panel",
default: "Hello, LEDABLE",
max_length: 80,
}),
color: colorRgb({
name: "Colour",
description: "The colour of the letters",
default: [246, 205, 109],
}),
}),
async render(ctx, params) {
const font = await loadFont(ctx.assets, MODERNDOS);
return scroll(font, params.message, hex(params.color));
},
async preview(ctx) {
const font = await loadFont(ctx.assets, MODERNDOS);
return scroll(font, "Hello, LEDABLE", "#f6cd6d");
},
});
Three things to notice:
- Settings are declared in code with builders such as
stringandcolorRgb, andrenderreceives their values typed:params.messageis astring. See Settings. - What an app cannot do for itself comes through
ctx: reading its own files, such as a font withloadFont(ctx.assets, …), the time, and the display's time zone. See How apps work. - A frame is JSX drawn with
renderFrame, pixel for pixel. See Drawing.
Render it once
To get the image a display would get, without a preview page:
ledable developer render --out frame.webpIt builds the app, renders it with its default settings and writes the WebP. It prints where the file went and how the display is told to play it:
{
"output": "/Users/you/hello-led/frame.webp",
"bytes": 800,
"behavior": "loop",
"realtime": false,
"softTtlMs": 300000,
"hardTtlMs": 600000,
"cacheId": null
}--preview writes the store preview instead, and --values takes settings from a file; see
Local preview.
Next
- How apps work: where your code runs, when it runs, and what it may not do.
- Drawing and Text and fonts: what you can put in a frame.
- From upload to the store: when the app is ready for other people.