lvgl plugin (zinc:lvgl)

LVGL 9.6 (C, MIT, plugins/lvgl/vendor/, pinned in vendor/PIN) from TypeScript: the interface of a small board without PSRAM. LVGL keeps the widgets, the flex layout, scrolling with inertia, the animations and the drawing in C; the program holds handles (i32), sets values and reads events. LVGL redraws only what changed. On the ESP32 it renders 32-row areas in RGB565 and sends them to the panel itself (zn_panel_flush, firmware/esp32-mquickjs/main/zn.h). Its objects live outside the 96 KB JavaScript heap. On the computer the same program runs on the interpreter, AOT or QuickJS, and the screen is one zinc:gfx image.

zinc:ui and the kits are the interface of the computer and the phone. On a microcontroller, zinc:lvgl is the one to take: a whole page of widgets costs about 60 KB of bytecode, where zinc:ui does not fit the board’s heap.

import * as lv from 'zinc:lvgl';

lv.start(240, 320, true, 0x3b82f6);                 // the screen, the dark default theme, its primary colour
const page = lv.screen();
lv.flex(page, lv.FLEX_COLUMN, lv.FLEX_START, lv.FLEX_CENTER);
const volume = lv.slider(page, 0, 100, 60);
const play = lv.button(page, 'Play');
lv.on(volume, lv.EVENT_VALUE_CHANGED, () => console.log('volume ' + lv.value(volume)));
lv.on(play, lv.EVENT_CLICKED, () => lv.setText(lv.buttonLabel(play), 'Pause'));
lv.run((dt: number) => { /* the program's frame, before LVGL's */ });
  • Start and run. start(w, h, dark, primary) once. run(each) takes over onFrame. Each frame it calls each(dt), then gives LVGL the frame’s time and the pointer, runs LVGL’s timers and redraw, calls the listeners, and shows the result. LVGL’s clock is the frame’s: a deterministic run (ZINC_DETERMINISTIC=1) gives the same pixels every time.
  • Objects. obj, label, button (with a centred label: buttonLabel), slider, toggle, checkbox, arc, bar, chart, spinner, buttonMatrix (one object for a keypad), led, roller. child(o, i), remove(o) and clean(o) also work on them.
  • Charts. chart(parent, type, points, min, max), chartSeries(chart, rgb), then chartPush shifts a series by one value (a line that scrolls) and chartSet(chart, series, index, value) sets a point in place (a spectrum’s bars).
  • Lottie. lottie(parent, json, w, h) plays a LottieFiles JSON in a loop, drawn by ThorVG (the copy LVGL carries) into a buffer of w * h * 4 bytes of LVGL’s memory; lottiePlay(anim, on) pauses and resumes it, lottieRestart(anim) goes back to its first frame. LVGL draws a frame only while the animation is visible. On a board without PSRAM keep it small: 64 x 64 is 16 KB.
  • Layout. size, pos, align, flex(o, flow, main, cross), grow, pct(v), SIZE_CONTENT. The theme grows a pressed button by 3 px, and LVGL redraws a child only inside its parent’s box (with FLAG_OVERFLOW_VISIBLE too): a row of buttons keeps 3 px of padding around them, or the grown edge is cut off.
  • Animations. animate(o, prop, from, to, ms, path, delay, repeat, back) hands a property to LVGL, which moves it in C at every turn: the program sets it once instead of every frame. The properties are X, Y, WIDTH, HEIGHT, TRANSLATE_X, TRANSLATE_Y, OPA, BG_OPA, RADIUS, ROTATION (tenths of a degree) and SCALE_X, SCALE_Y (256 is the object’s size). The paths are EASE_IN_OUT (the default), LINEAR, EASE_IN, EASE_OUT, OVERSHOOT, BOUNCE and STEP. A negative delay starts the animation that far in, so objects move out of step. repeat is the number of extra plays (FOREVER), and back is the time each play takes to come back to from. A new animation of a property replaces the running one. stopAnimation(o, prop) stops it where it is (without prop, all of the object’s), and deleting the object ends them. posX(o) and posY(o) give where an object is now.
    lv.animate(card, lv.Y, 320, 40, 400, lv.OVERSHOOT);                            // slides in
    lv.animate(dot, lv.OPA, 255, 60, 600, lv.EASE_IN_OUT, 0, lv.FOREVER, 600);   // pulses
  • Turning the display. rotate(quarter) (0 as started, 1 to 3 clockwise) and LVGL lays the screens out again at the turned size; width() and height() give it, so a program builds what depends on the size again. On the board the panel turns (MADCTL) and the touch with it; on the computer the picture is centred in the window, upright (a square window holds both).
  • Deleting. remove(o) and clean(o) also drop the handlers of the objects deleted and their queued events, so a program can build a page when it is shown and delete it when it is left: on a board without PSRAM, only the page shown then takes memory.
  • Style. style(o, prop, value, selector) for numbers (PAD_*, RADIUS, BORDER_WIDTH, BG_OPA…). color(o, prop, rgb, selector) for colours. bg, textColor, pad, gap, radius, border and font(o, px) are shortcuts; the fonts are Montserrat 12, 14, 20 and 28. A selector is a part (PART_INDICATOR…) or’ed with a state (STATE_CHECKED…).
  • Values and events. setText, setValue, value, checked, selected, buttonText, scrollTo, scrollY. on(o, EVENT_*, fn): the events wait in a queue the frame reads, so no C calls into JavaScript.
  • Figures. stats(): since the last call, the turns, LVGL’s microseconds (total and slowest), the areas and pixels sent; then the free heap and its lowest point (the board’s; -1 on the computer) and LVGL’s pool in use, its size and its peak (the computer’s; 0 on the board, where LVGL takes the heap).
  • Symbols. LVGL’s Font Awesome glyphs are in its fonts: '' (home), '' (play)… (LV_SYMBOL_* in vendor/lvgl/src/font/lv_symbol_def.h).

Configuration. plugins/lvgl/lv_conf.h: RGB565, the software renderer with ThorVG, no logs, and the widgets above. Changing it rebuilds the library and the board’s firmware.

Memory. On the board LVGL takes the heap the JavaScript runtime leaves (malloc): there is no pool to fragment, and the heap is the real limit. On the computer (the window and the mquickjs check of --target esp32) LVGL draws from a pool of LV_MEM_SIZE: the board’s budget in 64-bit objects (96 KB for the board’s 138 KB), so a screen that would not fit the board does not fit the computer either, and create says “LVGL’s memory pool is full” instead of the board crashing later. The firmware’s main task gets a 28 KB stack when zinc:lvgl is in it (sdkconfig in plugin.json): ThorVG goes 21 KB deep to draw a Lottie animation, where 8 KB is the default.

Speed. On a board an area costs more than its pixels (a pass over the objects, a panel window). LVGL keeps 128 changed areas a frame apart (LV_INV_BUF_SIZE; past them it redraws the whole screen, and an object that moves makes two), and the plugin joins an area to a nearby one of the same frame when that draws at most 4,096 more pixels (JOIN_SLACK in lvgl.abi.c). The benchmark of esp32-widgets in esp32sim, 24 objects: 76,800 pixels a frame and 37 to 41 fps before, 22,000 and 60 fps now. Under mquickjs a program’s numbers are cheapest as integers: on the 32-bit ESP32 any other number is allocated in the JavaScript heap, and its FPU computes no doubles (a position kept in fixed point and shifted, (x * w) >> 12, rather than Math.round(x * w)). An object moved costs the same whether the program or an animation moves it: LVGL refreshes its style either way (and measures a label’s text again), so in the benchmark 64 objects run at the same rate both ways. An animation is for a few objects at a time: each takes about 110 bytes of the heap while it runs.

Example. examples/esp32-widgets: six pages (dashboard, controls, player, Lottie, benchmark, system). Test. tests/t0/mquickjs_esp32.sh checks it under mquickjs with LVGL’s C built for the computer.

Edit this page on GitHub ↗