The Canvas 2D API, without the browser
When layout isn't the point and you just want the pixels, you reach for <canvas>
and getContext('2d'). GEA gives you that API — fillRect, drawImage, fillText —
and compiles each call to a native draw. No browser, no Skia on the device. Here is how it works,
and what it costs.
There are two ways to put something on a screen with GeaStack. Describe it — write CSS and let the engine lay it out — or draw it yourself, one command at a time. The first is GEA's declarative layout engine. This is the other half: an imperative Canvas 2D API.
It is the web's API. canvas.getContext('2d'), then fillRect,
drawImage, fillText and path fills — the same calls you'd write in a
browser tab. GEA keeps the calls and drops everything beneath them. Each ctx.* lowers to
a native draw straight into the framebuffer. The call is identical; the layers under it are not.
drawImage, fillRect and
fillText.
We took the whole demo apart in a
deep-dive.The same calls, none of the browser
getContext('2d') returns a real, typed native context — not a boxed dynamic value. The
methods are the ones you already know, and code that reads like a browser canvas behaves like one.
Here is the shipped maps app drawing a tile and a label, unchanged from how you'd write it for the
web:
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'rgb(228, 230, 221)'
ctx.fillRect(0, 0, width, height)
ctx.drawImage(tile, x, y, w, h) // a raster map tile, scaled
ctx.font = '16px Inter'
ctx.fillText(label, lx, ly)
It is a subset, and worth being plain about. The calls you reach for, unchanged —
fillRect, clearRect, paths, drawImage,
fillText, globalAlpha — are here, and behave as on the web. The heavier
machinery is not: no gradients, no transform stack, no clip() or
getImageData on the context. It is the drawing API, not all of Canvas 2D.
Immediate mode: you own the frame
Canvas is immediate-mode. There is no retained node tree to diff — every call rasterises pixels that same frame, and when the canvas owns the whole screen the batch is pushed straight to the panel, bypassing the UI tree entirely. That is the right model for scenes with thousands of moving things, where the tree-walk would be the bottleneck. Five hundred circles are one batched call:
const ctx = Display.ctx
ctx.beginBatch()
ctx.clear()
ctx.fillCirclesRgb565(ballX, ballY, radius, colors) // 500 circles, one call
ctx.fillText(fps, 6, 23)
ctx.endBatch() // recorded, pushed to the panel
Colours fold at build time
A colour never travels as a string into a hot loop. When the value is a literal, the compiler folds
it to the panel's pixel format at build time — a fill becomes a single 16-bit constant written
straight to memory, with no per-draw #rrggbb → native conversion. A runtime colour pays
that conversion once.
ctx.fillStyle = '#5260e0'
ctx.fillRect(x, y, w, h)
// '#5260e0' folded to a panel-native RGB565 constant at build time
ctx.setFillStyleRgb565(0x531c);
ctx.fillRect(x, y, w, h);
One rasteriser, every target
Text is drawn from coverage atlases rasterised at build time — anti-aliased glyphs, no TTF parser on the device. Images decode into native-format slots and blit, scaled with a nearest or bilinear path. And it is one C++ rasteriser everywhere: the same code paints an RGB565 framebuffer on an ESP32, compiles to WASM for the browser simulator, and renders through CoreGraphics on iOS — no Skia, no Metal under the canvas. The same draw calls land on an RGB565 framebuffer, a WASM canvas or CoreGraphics — only the target underneath changes.
How fast is it?
The honest argument for the imperative path is a comparison. On the same panel, five hundred circles in immediate mode hold the panel's full 60 fps — the same frame rate a retained tree reaches managing sixty-four. That's roughly eight times the count, at the same frame rate. The cost is per-circle CPU rasterisation, not the full-screen DMA both pay every frame. The engine is compute-bound, not bus-bound.
GEA makes embedded development feel like the web — a declarative CSS engine for layout, this Canvas API for the rest — and compiles both to native code for the device.
What works, and what doesn't
The boundary is deliberate, and worth stating plainly. What the context gives you today:
fillRect,strokeRect,clearRect;fillCircleand even-odd path fills withmoveTo/lineTo/fill/strokedrawImage(placed and scaled),fillText,globalAlpha,lineWidth,font- Batched RGB565 fast-paths — thousands of circles in a single call
beginBatch/endBatchto push a whole frame straight to the panel
And what it deliberately leaves out:
- Gradients (
createLinearGradient/createRadialGradient) and patterns - The transform stack —
save/restore,translate/rotate/scale— andclip()on the context getImageData/putImageData,measureText, shadows and filters on the context
Canvas, compiled to native
Describe a screen in CSS, or draw it yourself one command at a time — both compile to the same native binary, for a microcontroller, embedded Linux, macOS and iOS. The code is the best place to see it for yourself.