The TRANSITION plane
The transition plane is the fourth and smallest of Flux’s planes: it interpolates the rendering between two states the analysis plane has already computed, and it can do nothing else. This page specifies the five powers a script has here, the descriptor every transition compiles to, the settle-versus-trajectory rule that keeps animation out of the oracle, and the boundary with the native morph controller. The signal algebra a transition borrows is specified in Canvas; the compositor behaviour, the two strata and the transition invariants live in display.
That “already computed” is the entire safety argument. If both endpoints are values the analysis plane produced, interpolating between them cannot produce a new value — so a transition is cosmetic by definition, and no animation, however elaborate, can repaint a chart. The plane exists because the alternative is worse: animation bolted onto a rendering layer ends up reading state it should not read and re-entering computations it should not re-enter. Giving it its own plane, with its own clock and its own rule, keeps the guarantee where it belongs.
New here? Start with Guide §9 — A scene that moves →
What a script can do here
Five powers, and no more:
// ① parameterize a built-in morph
on switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 ; surplus: collapse }
// ② carry the transition of a custom representation (the `morph:` hook of its descriptor — a later rollout)
// ③ trigger a bounded effect on a signal
on close cross_up ema(close, 50) -> flash
// ④ animate the view
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms, ease: outBack(1.2))
// ⑤ replay history from the bar where a signal fired
replay from close cross_up ema(close, 200) over 8sswitch(asset) is a host event, not an analysis symbol: the current asset is a fact the host
owns, and the firewall forbids analysis from reading it. It is delivered like any other
presentation edge (hover, click, enter), which is why a transition can respond to it while
an indicator cannot.
Power ② — the per-representation morph: hook — is designed, but not yet an author key: the morph
controller is single-type (candle), and the descriptor the hook would hang from is reified in the
same rollout. Until then a custom representation animates through the built-in morph (①), which is
the path every built-in representation already takes.
replay from takes a signal, not a bar index. The replay begins at the bar where the condition
fires and runs forward over the declared duration, which is what lets you address a replay by what
happened rather than by how far back it was. The start point is therefore an analysis value the
oracle already contains — the transition plane discovers it, it never computes it.
The settle is in the oracle; the trajectory is not
This is the distinction that makes the plane work, and it is worth being exact about:
| In the byte-identity oracle? | Why | |
|---|---|---|
| the settle value — where the transition lands | yes | it is analysis data (or a Model value); it is what the scene is once the animation ends |
the trajectory — how it gets there, t ∈ (0,1) |
no | it is per-frame, device-dependent, and observable by nobody but the eye |
Figure — the endpoints and the terminal edge (
Done, at a deterministic rank) live inside the byte-identity oracle and the journal; the trajectory t ∈ (0,1) is drawn outside it — per-frame, device-dependent, the one thing the plane withholds.
Two consequences follow directly.
prefers-reduced-motion changes nothing that matters. The host applies it at the compositor:
it jumps to the settle state. Since the settle is in the oracle and the trajectory is not, the
verdict — the numbers, the golden, the replay — is identical either way. The compositor rule is
specified once in display; here we state only why the plane’s
guarantee is indifferent to it.
A transition never leaks its progress into a decision. The plane exposes only the terminal
edge (Done), never the continuous progress; and that edge is scheduled at a deterministic
journal rank derived from the declared duration, not at the wall-clock moment the animation
happened to finish. Two clients, one animating and one with motion reduced, journal the same
edge at the same rank — same numbers, same scene, on both machines. This is the [TransSettle]
invariant, whose canonical statement lives in
display.
Why the progress is withheld. If a script could read
t = 0.63mid-animation and store it in a model, then the model — and any verdict computed from it — would depend on the frame rate, the device, and the GPU’s mood. Withholding the trajectory is not a limitation of the animation; it is what makes the animation free.
The transition descriptor
Built-ins and scripts converge on one descriptor, so a custom representation animates exactly the way a candle does:
| Field | Meaning |
|---|---|
durationMs |
how long |
easing |
the curve — a value of the ease sort, opaque and host-vetted |
wave, staggerSpread |
the shape of the stagger across elements |
wickLead |
the lead-in of the thin parts, before the bodies |
surplusPolicy |
collapse | spawn | hold — what happens to elements that have no counterpart on the other side |
chromeFadeFrac, holdDeadlineMs, flipTiming |
the cosmetic timings around the morph |
Per-call overrides (over D, stagger, surplus:) refine it at the point of use.
ease is a sort, not a variant — you can pass inOutCubic or cubicBezier(a,b,c,d) around,
but you cannot match on a curve and take it apart. That opacity is deliberate: a decomposable
curve would let a script re-parameterize time in a data-dependent way, and the boundedness of the
animation would go with it.
The boundary with the native engine
The heavy per-candle morph stays native — it is a hot path, and it is not the language’s job to re-implement it. Flux orchestrates: it fills in the descriptor once, and the native controller runs it. The descriptor is a compilation target; how the host receives and drives it is specified in Host integration.
That split is why there is no performance cliff when you animate: the script does not run per frame, and the thing that does is the code that was already there.
What a transition may not do
| May | May not |
|---|---|
| interpolate the rendering between two computed states | compute a state |
| read analysis values (to know where to land) | write an analysis value |
respond to a host edge (switch(asset), a click) |
read the wall clock into a Model |
| expose its terminal edge | expose its progress |
See also
- The four planes & the firewall — where TRANSITION sits, and why it is separate.
- Canvas — the plane whose signal algebra transitions borrow.
- display — the two strata, the transition invariants,
prefers-reduced-motion. - Host integration — the transition descriptor as a compilation target.
- Time and state — why an interpolation can never be a repaint.