limn

Retained-mode canvas engine

Same HTML + CSS.
Paint on canvas.
Only recompute what changed.

limn is a platform-agnostic scene graph: declare nodes, change a style, three orthogonal dirty bits drive layout · paint · hit. Frontends (React / imperative) and backends (Canvas2D / mini-game) plug in at the edges.

40/40
capability domains
3 bits
LAYOUT · PAINT · TRANSFORM
1 line
canvas ↔ DOM switch
2142
tests · parity 0 miss
01 · MODEL

Retained scene graph

Nodes hold style, box, transform. Mutations mark dirty bits — the frame loop recomputes only the cone that needs it.

02 · SURFACE

HTML/CSS isomorphic

Standard tags, className, style strings, DOM event names. Same markup on canvas or browser DOM.

03 · EDGES

Pluggable ends

React reconciler or imperative Scene API on the front; Canvas2D (and friends) on the back. Core stays pure.

Three dirty bits

Orthogonal invalidation — not “redraw everything.”

Immediate-mode canvas redraws the world every frame. limn marks PAINT, LAYOUT, or TRANSFORM and runs only the matching pass.

Interactive · which bit fires?

Left: full-frame paint · Right: limn dirty cone

Immediate canvas ops/write0
clearRect
Header
Sidebar
Card A
Card B
Chart
Footer
limn dirty bits ops/write0
bitPAINT
bitLAYOUT
bitXFORM
passdirty rect
passsubtree
passmatrix
repaint 1 rect
relayout subtree
world matrix
rest cold

Click a mutation. Immediate mode lights the whole tree; limn pulses only the dirty bit and its pass.

HTML / CSS isomorphic

Write web. Render canvas. Zero rewrite.

Tags, className, CSS cascade, pointer events — the web surface you already know. Swap createRoot(canvas) for the DOM root when you need it.

Interactive · one tree, two backends

Target: Canvas2D

canvas Same JSX · Canvas2DBackend createRoot(createCanvas2DBackend(el), css)
<div className>
CSS cascade
onClick / pointer

            
Architecture

Edges are pluggable. Core is pure.

Frontends feed the graph. The core runs dirty → layout → paint → hit. Backends consume draw ops. No DOM required in the middle.

Pipeline · click a stage

See what each layer owns

React reconciler and imperative Scene API both call into the same Node graph.

Declare the tree. Mark the bit. Never repaint what you did not touch.

Why limn

Charts, games, mini-programs — still need layout.

Browsers have a real DOM. Everywhere else you still want flex, cascade, and hit-test without hand-rolling every frame.

Immediate canvas

  • Clear + redraw every tick
  • Layout is ad-hoc math
  • Hit-test is custom geometry
  • Web components do not transfer

limn retained mode

  • Dirty bits + dirty rects
  • Flex / grid / table / CSS units
  • Scene graph hit-test built in
  • Same JSX + CSS as the web

Capability snapshot

DomainStatusWhat you get
Layout 40/40 domains flex · grid · table · float · sticky · multi-column
Paint ✅ main path box model · gradients · filter · clip/mask · 2D/3D projection
Text ✅ main path wrap · ellipsis · RTL · writing-mode · @font-face
CSS ✅ subset selectors · @media · var() · @container · cascade
Interaction ✅ main path hit · scroll · focus · forms · transition/animation

Full table: Capability handbook · in-repo docs/CAPABILITY.md

Ship UI on canvas
without abandoning the web.

One scene graph. Three dirty bits. Pluggable edges.