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 overonFrame. Each frame it callseach(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)andclean(o)also work on them. - Charts.
chart(parent, type, points, min, max),chartSeries(chart, rgb), thenchartPushshifts a series by one value (a line that scrolls) andchartSet(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 ofw * h * 4bytes 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 (withFLAG_OVERFLOW_VISIBLEtoo): 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 areX,Y,WIDTH,HEIGHT,TRANSLATE_X,TRANSLATE_Y,OPA,BG_OPA,RADIUS,ROTATION(tenths of a degree) andSCALE_X,SCALE_Y(256 is the object’s size). The paths areEASE_IN_OUT(the default),LINEAR,EASE_IN,EASE_OUT,OVERSHOOT,BOUNCEandSTEP. A negativedelaystarts the animation that far in, so objects move out of step.repeatis the number of extra plays (FOREVER), andbackis the time each play takes to come back tofrom. A new animation of a property replaces the running one.stopAnimation(o, prop)stops it where it is (withoutprop, all of the object’s), and deleting the object ends them.posX(o)andposY(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()andheight()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)andclean(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,borderandfont(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_*invendor/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.