◆ Flux

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.

The two strata of a scene 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

FLUX
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:

Rich fills are a closed variant:

FLUX
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 malformed hex yields na and 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 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.

FLUX
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 channels

Domain 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:

FLUX
// 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 scene

Downgrading 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:

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.

The draw-list chain 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.

FLUX
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.

FLUX
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.

FLUX
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