display — scenes, panes and the render targets
One word covers a chart, a dashboard, a data visualization, a 3-D view and a game: a scene —
a pure function of the Model, and a value: a tree of vetted primitives you bind, return from
a function, and hand to a window. The host paints that value into an island — a <div> it
owns inside an ordinary HTML page — turning it into pixels; the language never touches one. That is not a stylistic choice — it is what lets a game in a pane replay
frame-exactly, a chart overlay golden without a screenshot, and an animation stay beautiful
without ever touching a value a verdict depends on.
This page specifies what a scene is made of: the closed primitive set, the composition
combinators, coordinates, style, the viz.* encoding grammar, panes and render targets, the 3-D
projection, transitions, input, and the output membrane — with the invariants that keep the whole
of it inside the replay oracle. Two things live elsewhere: the capability catalogue that every
render target and asset draws on is specified in host-services, and the
re-derivation contract a ranked score rests on is owned by server.
New here? Start with Guide §9 — A scene that moves →
The display pillar is fully designed and additive to the frozen core; its rollout follows the v1 core.
The presentation theorem
The geometry of a scene is a pure function of the Model. It is therefore deterministic, byte-identical, bounded, and inside the replay oracle. The painting of that geometry — the GPU, the compositor, per-frame signals — is non-deterministic with respect to the world, and is therefore host-only, outside the oracle, and firewalled from the Model.
Everything else in this page follows from that sentence. A pane may read a live exchange feed, an order book, a game’s state; an indicator can never be read back by any of it.
Figure — geometry is byte-identical; cosmetics are pretty. Neither contaminates the other.
The two strata
Every scene splits, structurally, into two layers:
| (a) retained geometry | (b) per-frame cosmetics | |
|---|---|---|
| What | position, shape, size, token colour, order — as a function of the Model | glow, pulse, parallax, shimmer, morph particles |
| Driven by | Model values, tween/spring settle values, seeded randomness |
wall-clock, screen space, unseeded randomness |
| Runs where | in the oracle — the same bytes on every engine | on the host compositor — zero JavaScript per frame |
| Replayable | yes | no, and it never needs to be |
The routing is decided by the kind of the signal, which the compiler already knows. A signal
derived from the Model is stratum (a); a signal touching now(), screen.* or unseeded
randomness is stratum (b) — and reading one of those from analysis is [ErrFirewall], as is
smuggling one into a Model field that a verdict reads.
The scene value
def overlayOf(m) = scene {
when volume > sma(volume, 20) * 2 :
dot { at: (bar.i, high), r: 4, fill: token.spike, glow: throb(0.4) }
when ema(close, 9) cross_up ema(close, 21) :
triangle { at: (bar.i, low), r: 6, fill: token.bull }
}
// …and, inside an `app` block, the view mounts it:
// view(m) = chartView(chartId: "main", overlay: overlayOf(m))A scene is a value of kind ui. Note what is not in that sample: no loop over bars (per-bar
signals are evaluated per bar, implicitly), no animation API (glow is a signal like any other),
and no way to write back into the analysis it reads.
The glow is stratum (b) — it goes to the compositor. The position is stratum (a) — it is
in the oracle. The boundary runs through the same primitive, and the compiler knows which side
each property is on.
Primitives, composition, layout
The primitive set is closed and vetted: dot, circle, ring, rect, square,
triangle, poly, line, path, text, image, svg, sparkline, backdrop. They share
one property model — at, size/r/w/h, rotate, fill, stroke, width, opacity,
glow, blend, z, life, color, trail, paintOrder.
Three composition combinators, all bounded:
| Meaning | Bound | |
|---|---|---|
group { … } |
transform / blend / clip a subtree — the universal internal node | — |
repeat n as i { … } |
instancing: n shapes parameterized by the index |
n is const-folded |
for x in coll -> child |
a comprehension over a bounded collection | the collection’s declared capacity |
There is no integer iterator: for i in range(n) does not exist, because n is not a
collection. The bound always comes from a declared capacity — which is what makes the instance
budget computable at compile time.
Layout reuses the frozen ui containers (col, row, grid, stack, tabs, scroll,
panel, the application rails) and adds wrap/flow, plus two data widgets: virtualList
(the windowed feed — the host materializes only the visible rows) and tableView (a data grid
rendering a Table’s columns directly, never materializing rows).
Style
Every visual property is a style prop — a closed, typed value, never a raw string — and every style prop is a signal, so there is no separate animation API. Colours come from two places:
- Tokens —
token.bull,token.bear,token.grid. Theme-aware, and the default for anything semantic. - Explicit colours —
rgb(r,g,b),rgba(r,g,b,a),hex("#RRGGBB"). Theme-blind, by the author’s choice; for a scientific palette use the pinned scale constructors (color.seq,color.div,color.cat).
Rich fills are a closed variant:
record Stop { color: color ; at: ratio }
variant paint {
Solid(c: color) | Linear(start: Stop, to: Stop, angle: angle)
| Radial(center: Stop, stops: vec(Stop, 8)) | Texture(assetRef: string, fit: fit)
}Texture takes an asset key, never bytes — the host resolves it under asset:load, with an
allowlist and a quota.
The one injection surface, closed structurally. A free CSS string (
url(…), an expression, markup) is not filtered — it is inexpressible. No constructor produces one, and the lexer carries no colour literal. A malformedhexyieldsnaand a diagnostic; an out-of-range channel is clamped by the sanitizer.
Coordinates
A coordinate derives its axis from its kind: price → the price axis, barindex → the
ordinal x axis, time → the time axis, screen.* → viewport pixels, world3D → the 3-D scene.
Mixing spaces inside one coordinate is [ErrDim] at compile time. There is no such thing as a
geometrically incoherent scene.
Two subtleties are worth stating precisely:
- The data anchor plus a pixel offset (
dataSignal + 8px) is a composite coordinate constructor, not an arithmetic addition across sorts. The pixel part must be a const, so the position stays in the oracle. A screen-derived offset (screen.h * 0.05) would tip the whole position into stratum (b) — forbidden for an anchor a verdict reads. paintOrderis notz.paintOrder : numis 2-D painting order (who is in front of whom in the plane).z : depthis the depth coordinate, auto-normalized and projected. The kind decides which is which, so they cannot be confused.
The retained diff
A scene is a tree of keyed vnodes. It compiles once; on each logical frame the
reconciler diffs f(Model) against the previous tree and re-emits only what changed. Keyed
children keep their identity across reorders; events are delegated at the root.
The oracle is defined on the absolute draw-list f(Model), never on the sequence of diffs.
So an engine that re-renders more, or less, or in a different order, cannot change what is
replayed.
Encoding data: viz.*
Kind-driven auto-presentation already is an encoding grammar — for a series. viz.* is its
sibling for arbitrary tabular data: a library of pure functions that consume a Table from
the compute pillar and return a ui value.
plot viz.chart(t, { x: t.time, y: t.close, color: t.sector }, mark: Line)
plot viz.histogram(t.returns, bins: 40)
plot viz.facet(t, by: t.sector, shared: yScale, child: tile) // small multiples on ONE shared scale
plot viz.legend(scale) // derived from the encoded channelsDomain inference is a bounded fold (extent(col)), the same auto-scale the host already
performs on the visible window — causal, with no look-ahead. Everything lowers to scene{},
for, primitives and arithmetic: no new grammar, no new sort.
Brushing is bounded and edge-committed: viz.brush journals a range at pointer-up, sibling
views read that range from the Model, and linked cross-filtering works without hand-wiring — with
no continuous stream of pointer samples anywhere near the Model.
Drawing tools: the three host bindings
A custom drawing tool is a contribution, and the gesture is host-driven through exactly three declarative bindings:
| Binding | What it does |
|---|---|
drawPreview: |
a shape template (line, rect, circle, poly, path) parameterized by the anchors placed so far and the live pointer. The host interpolates it as pure presentation — zero messages during the gesture — and emits one journaled message at the end (pointer-up, the terminating gesture, or the declared anchor cap, which commits rather than overflows). |
magnet: |
declared snap candidates (Ohlc, Anchors) plus a pixel radius. The host snaps before delivery: the (bar, price) your script receives is already snapped, correct on linear and logarithmic axes alike. Projection stays host-side; the script stays in data space — at the named cost below. |
cursor: |
a host-allowlisted cursor enum, applied on hover through the picking system. Cosmetic, zero messages, outside the oracle. |
Esc or a lost capture throws the template away with no message at all — nothing entered
the journal, so nothing has to be undone.
What magnet: costs, named. Which candidate wins depends on the pixel radius and the
viewport, so the selection is a host-side, device-dependent decision — and the (bar, price) it
snaps to is journaled, which puts it squarely inside the oracle. It therefore inherits exactly
the outcome-forging contract that picking carries: a verdict resting on a snapped anchor needs
the server to re-derive it, or the run stays out of the shared leaderboard. What bounds the
exposure is that the snap can only ever land on a value re-derivable from the Model — an OHLC of
the bar under the cursor, or an anchor that already exists. It cannot conjure a price that was never
there.
Panes, targets and windows
A render target is a host resource under a capability, addressed by an allowlisted string
key — never a handle the script holds. Every capability a target or an asset opens under is
named in the capability catalogue, its canonical home. Three windows project
a ui value into a target:
// a `ui` value: the three windows in a container — exactly what a `view` returns
col {
chartView(chartId: "main", asset: "BTC-USD", overlay: overlayOf(m), onClick: ClickAt)
paneView("rsi")
sceneView(target: "pane.game", tree: worldOf(m), space: World3D, onPick: Tapped)
}The scene’s graphics and text both render on the WebGPU renderer — GPU-accelerated,
sidestepping DOM layout cost — text as an SDF glyph atlas that stays crisp at any zoom and DPR.
Accessibility rides the host’s a11y membrane (describe:/a11y: annotations), not glyphs left in
the DOM. On top of that, one host renderer routes the reconciled tree to whatever compositing
substrate the pane’s state requires — and the script never knows which:
UiTree ─► reconcile ─► SOLID → in place (graphics + SDF text on WebGPU)
LIQUID → Canvas2D → texture (deterministic capture)
FLOATING → texture + chrome (shadow, refraction)
SPATIAL → a quad in the 3-D sceneDowngrading a substrate under GPU load emits no message and does not change the absolute draw-list. Spatial and liquid paint the same logical geometry.
The 3-D model
3-D is a projection, not a second language. The same scene{…} carries world3D
coordinates when its window declares that space; the primitives (mesh, camera, light,
material, billboard) are declarative and vetted, and shaders are host-held recipes or
allowlisted catalogue keys — never WGSL text, never a lambda.
Two additive sorts serve this pillar, both flat under ⊤ and opaque to match — the exact
status of clock:
ease— an interpolation curve (linear,inOutCubic,outBack(s),cubicBezier(…),springCurve(k,d)), consumed by transitions, never arithmetic. It is a sort, not a variant, precisely so a script cannot decompose and re-parameterize a curve.shader<υ>— a host shader parameterized by the kind of its uniform record. The schema is the kind: the compiler checks the uniforms you pass againstυwith an ordinary call rule.
The chart’s own 3-D mode is a client of this, not a special case — and at a camera angle of zero it is pixel-identical to plain 2-D, by construction.
The execution model
The scene compiles once, into a bounded, deterministic draw-list serialized in linear memory. The host decodes it, sanitizes it, and paints it on the WebGPU renderer: graphics and text alike — text as SDF glyphs — with 3-D riding the same WebGPU path. The module never touches a Web API.
Figure — three signal classes, three routes, and a per-frame cost of zero for the ones that move the most.
Signals are classified and routed:
| Class | Example | Route | Cost per frame |
|---|---|---|---|
| static | stroke: token.grid |
cached — never recomputed | 0 |
| per-bar | at: (bar.i, ema(close,20)) |
pre-allocated buffers; identical shapes instanced | O(Δ bars) |
| per-frame, time-only | glow: throb(0.4) |
the host compositor | 0 JavaScript per frame |
Three budgets, all const-folded and checked at compile time: the number of draw-list ops,
the number of instances (a repeat n emits one op and n instances; an emitter draws from a
host-fixed capped pool), and the worst-case GPU work of a shader recipe. Exceeding any of
them is [ErrSceneBudget] at compile time — never an out-of-memory or a device reset at
runtime.
[ObsDeterminism] — determinism in observation
Deterministic output is only half of it. The other half is the half everyone forgets: wherever a channel reads presentation state and routes it back as a message, a non-deterministic value could leak into the Model — and from there into a verdict.
[ObsDeterminism]. Any subscription or channel carrying a presentation signal into the APP plane must deliver a payload that is either (i) deterministic and replayable — an ordinal index, a discrete edge, a stable key, a const, or a pinned host measurement — or (ii) presentation-tagged, and therefore[ErrFirewall]if it feeds a Model field a verdict reads. No channel ever delivers a continuous wall-clock time, a transition’s progress, a transient spawn position, a continuous pick intersection, a held pointer/wheel/analog sample, or a device-variable measurement into a Model.
Its instances, each enforced where it lives:
| Invariant | What it guarantees |
|---|---|
[TransSettle] |
a transition exposes only its terminal edge, never its progress. The edge is scheduled at a deterministic journal rank derived from the declared duration — not at the real moment the animation ends. |
[TickOrdinal] |
OnTick journals a monotone integer index, never a wall time; dt is the subscription’s declared constant. A raw frame time is presentation-tagged. |
[SpawnGeom] |
emitter and morph particles are cosmetic by construction — a transient position is never readable by anything that feeds the Model. |
[TextMetric] |
any text measurement entering geometry goes through a pinned host metric routine, byte-identical across devices. |
[FocusMsg] |
every focus transition is a journaled message; a key event reaches the Model only if the journal attests focus at that rank. |
[HeldFromEdges] |
held inputs (keys, pointer, wheel, gamepad axes) are always derived from journaled edges, never sampled freely per frame. |
[PickKey] |
a pick delivers the key only — the continuous intersection coordinate is presentation and never reaches the Model. Gameplay decides on a key, by construction. |
[SlotGeom] |
slot geometry (a rectangle, a resize) is presentation: it sizes a view, it never decides a verdict. |
[HoverEdge] |
hover is reduced to discrete bar-crossing edges, or it is presentation-tagged. Its payload is fine; its cadence is not replayable. |
Together with [DiffAbsolute] (the oracle is the absolute draw-list, not the diff sequence),
this makes the logical draw-list replayable bit for bit: you replay
(init, messages) → Model → geometry, never the framebuffer.
The honest limit. Replay proves a journal is coherent; it does not prove it is truthful. A host-pushed payload journaled as data — a pick key, a pre-computed outcome — is re-folded verbatim, because replay does not re-run the ray-cast or the kernel to attest it. So a score that depends on such an outcome requires the server to re-derive it, or the run must be excluded from a shared leaderboard. The same is true of time: elapsed time in a ranked run is host-stamped and substituted at re-fold; the client’s journaled ticks are advisory.
The full re-derivation contract this leans on — what replay proves, and what a server must re-fold to attest a ranked outcome — is owned by server.
Transitions
A transition interpolates the rendering between two already-computed states. It is cosmetic by definition: it cannot change a value, so it cannot repaint. The transition descriptor and the morph boundary are specified in Transitions.
on switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 ; surplus: collapse }
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms, ease: outBack(1.2))A transition driven by time is stratum (b) throughout — non-replayable, and excluded from the oracle. A transition driven by a state change has its settle value in the oracle (the geometry it lands on) while its trajectory stays cosmetic.
prefers-reduced-motion is a host fact applied at the compositor: it jumps to the settle state.
Because the settle is in the oracle and the trajectory is not, the verdict is unchanged — and
the terminal edge still lands at the same journal rank, so two honest clients, one with reduced
motion and one without, produce the same trace.
Input
Every signal from the world enters through one ingestion point and becomes a message.
app snakePane {
capabilities: [ clock, input:keyboard ]
update(m, msg) = match msg {
Turned(c) -> { model: m with { dir: turn(m.dir, c) }, cmds: [] }
Tick(n) -> { model: advance(m), cmds: [] }
FocusChanged(f) -> { model: m with { focused: f }, cmds: [] }
}
subs(m) = [ OnKey(Turned), OnTick(120, Tick), OnFocus(FocusChanged) ]
}The doctrine of edges, which unifies half the invariants above: a discrete journaled edge
may enter the Model; a continuous presentation signal never may. Marking an item read, counting
impressions, tracking reading progress and lazily loading a list are all expressible — as
edges (OnVisible(itemKey, threshold, C)), not as geometry. A scroll position, as a readable
value, does not exist.
The output membrane
Everything the host paints passes one membrane: a sanitizer (text as text, an unknown node rejected, out-of-range values clamped), pinned text metrics, an accessibility layer that is annotated and inferred, picking that returns keys, assets resolved from allowlisted keys, and localization through a host-held catalogue.
There is no route from a script to raw markup, raw bytes, or a raw URL. Not because they are filtered — because they cannot be named.
Accessibility: you annotate, and the kind infers on top
A scene of pixels is, by default, invisible to a screen reader. Two mechanisms answer that, and they compose — the second is an addition to the first, never a replacement for it.
The annotation is in the language. Every ui primitive carries an optional
a11y: record{ role, label, desc } — token-localized, sanitizer-checked. A CANVAS scene{}
carries a describe:, its textual alternative. Both are ordinary bounded props (strings and
tokens): no new sort, no new grammar.
def overlayOf(m) = scene {
when volume > sma(volume, 20) * 2 :
dot { at: (bar.i, high), r: 4, fill: token.spike,
a11y: { role: token.roleMark, label: token.spikeLabel, desc: token.spikeDesc } }
}And the kind infers, for free. Beyond the manual a11y:/describe:, the dimensional kind
of a plot or a mark auto-derives an accessible name, a range and a sonification — a sixth
inferred output, standing beside the overlay, the pane, the scale, the reference lines and the
colour that presentation inference already derives from the kind. plot rsi(close, 14) has kind
osc(0,100), and that alone is enough for the host to announce “RSI oscillator, 0 to 100,
currently 72, above the 70 guide”, and to offer a sonification (pitch is the value, pan is time).
No author effort at all — exactly as the pane and the scale cost none.
Note what the inference reads: the kind, not the geometry. The descriptor lives host-side, outside the oracle, and is firewall-safe — it reads the scene and never writes back into it.
Four extensions complete the contract, every one a host prop rather than a language mechanism:
live regions (live: Polite | Assertive; a toast is Polite by default), widget states
(a11y widens from {role, label, desc} to expanded / checked / selected / disabled /
pressed, derived from the Model like any other prop), relations (controls:,
describedby:, activedescendant:, addressed by node key — the keyed vnodes are the identity),
and intra-pane focus (a roving tabindex inside a composite widget, plus the contractual focus
trap of a modal surface). Tab order stays centralized in the one component that touches the DOM,
which is also the one place prefers-reduced-motion is applied.
See also
- App plane — views, contributions, slots, and the PORT lifecycle.
- Canvas — signals, spaces, events and the frozen primitive set.
- host-services — the capability catalogue every render target opens under.
- server — the re-derivation contract behind a ranked, replay-proven score.
- compute — the
Tablethatviz.*consumes. - color — the
colorkind, OKLab interpolation and the pinned palettes.