···11export * from "./ops.ts";
22+export * from "./ops-transitions.ts";
23export * from "./term.ts";
34export * from "./input.ts";
45export * from "./settings.ts";
···11+import type { Transition } from "./ops-transitions.ts";
22+import { easingByte, propertyMask } from "./ops-transitions.ts";
33+14/* Command buffer opcodes — mirrors ops.h */
25const OP_OPEN_ELEMENT = 0x02;
36const OP_TEXT = 0x03;
···1013const PROP_BORDER = 0x08;
1114const PROP_CLIP = 0x10;
1215const PROP_FLOATING = 0x20;
1616+const PROP_TRANSITION = 0x40;
13171418const encoder = new TextEncoder();
1519···9195 if (op.border) mask |= PROP_BORDER;
9296 if (op.clip) mask |= PROP_CLIP;
9397 if (op.floating) mask |= PROP_FLOATING;
9898+ if (op.transition) mask |= PROP_TRANSITION;
9499 view.setUint32(o, mask, true);
95100 o += 4;
96101···173178 );
174179 o += 4;
175180 }
181181+182182+ if (op.transition) {
183183+ let t = op.transition;
184184+ let pmask = 0;
185185+ for (let name of t.properties) pmask |= propertyMask(name);
186186+187187+ view.setFloat32(o, t.duration, true);
188188+ o += 4;
189189+ view.setUint16(o, pmask, true);
190190+ o += 2;
191191+ view.setUint8(o, easingByte(t.easing ?? "linear"));
192192+ o += 1;
193193+ view.setUint8(o, t.interactive ? 1 : 0);
194194+ o += 1;
195195+ }
176196 break;
177197 }
178198···269289 attachPoints?: number;
270290 zIndex?: number;
271291 };
292292+ transition?: Transition;
272293}
273294274295export interface Text {
+3
specs/renderer-spec.md
···2525Input parsing is specified separately in the
2626[Clayterm Input Specification](input-spec.md).
27272828+Transitions are specified separately in the
2929+[Clayterm Transitions Specification](transitions-spec.md).
3030+2831---
29323033## 2. Scope
+575
specs/transitions-spec.md
···11+# Clayterm Transitions Specification
22+33+**Version:** 0.1 (draft) **Status:** Design specification for a work-in-progress
44+feature. Normative where it establishes invariants and contract. Descriptive
55+where surfaces may settle during implementation.
66+77+---
88+99+## 1. Purpose
1010+1111+A transition smoothly interpolates an element's visual properties over time when
1212+they change between frames. This specification defines how transitions integrate
1313+with Clayterm's frame-snapshot rendering model: how they are declared, how time
1414+is supplied, and how callers observe in-flight animation so they can drive the
1515+render loop.
1616+1717+Transitions are a first-class extension of the rendering contract defined in the
1818+[Clayterm Renderer Specification](renderer-spec.md). They do not change the
1919+architectural model, do not introduce a component tree, and do not require
2020+callers to hold cross-frame identity beyond the stable element identifiers they
2121+already use.
2222+2323+This specification covers what clayterm ships against the current upstream Clay
2424+layout engine. Several capabilities that the rendering model naturally invites —
2525+per-property easing, per-element enter/exit behaviors, custom bezier easings —
2626+are intentionally excluded from v1 because the underlying Clay API cannot
2727+express them without upstream changes that are still in flight. Section 13
2828+records these deferrals and the upstream dependencies that unblock them.
2929+3030+---
3131+3232+## 2. Scope
3333+3434+### In scope (normative)
3535+3636+- The transition model and its relationship to the frame-snapshot rendering
3737+ contract
3838+- Time handling and the `deltaTime` convention
3939+- The animating signal returned from `render()`
4040+- Element identity requirements for transitions
4141+- Cancellation semantics (as a consequence of the frame-snapshot model)
4242+4343+### In scope (non-normative, descriptive)
4444+4545+- The shape of the `transition` field on the `open()` directive
4646+- The set of easing functions exposed in v1
4747+- The set of transition properties exposed in v1
4848+- The wire encoding of transition data in the directive buffer
4949+- Interaction with line mode
5050+- Testing strategy
5151+5252+### Out of scope (v1)
5353+5454+See Section 13 for the deferred features and their upstream unblockers.
5555+5656+### Out of scope (indefinitely)
5757+5858+- Physics-based animation, spring interpolation, keyframe sequences
5959+- Framework-level concepts of "animation groups" or cross-element choreography
6060+ (orchestration is a caller concern)
6161+- Input parsing (see [Input Specification](input-spec.md))
6262+6363+---
6464+6565+## 3. Terminology
6666+6767+**Transition.** A time-based interpolation of one or more of an element's visual
6868+properties between an initial value and a target value.
6969+7070+**Transition property.** A specific visual attribute of an element that can be
7171+interpolated: position (x, y), size (width, height), background color, overlay
7272+color, border color, or border width.
7373+7474+**Easing.** A function mapping normalized progress in [0, 1] to an eased value
7575+in [0, 1]. Clayterm exposes a fixed set of built-in easings.
7676+7777+**Delta time (`deltaTime`).** The number of seconds elapsed since the previous
7878+render transaction. Used by the renderer to advance interpolation.
7979+8080+**Animating signal.** A boolean flag in the render result indicating whether any
8181+transition is currently in progress. Callers use it to decide whether to
8282+schedule another frame.
8383+8484+---
8585+8686+## 4. Architectural Model
8787+8888+_This section is normative._
8989+9090+### 4.1 Relationship to the frame-snapshot model
9191+9292+Transitions do not alter the frame-snapshot contract defined in INV-3 of the
9393+renderer specification. The directive array still fully describes the desired
9494+state for its frame. Transitions interpolate between the previous frame's state
9595+and the current frame's target state; they do not reintroduce a persistent
9696+component tree on the caller side.
9797+9898+What transitions add is the requirement that element identifiers remain stable
9999+across frames for any element on which animation is desired. This is not a new
100100+invariant — the existing pointer-event subsystem already relies on stable
101101+identifiers — but it becomes load-bearing for transitions.
102102+103103+### 4.2 Time ownership
104104+105105+The `Term` instance is the sole source of frame-to-frame time. On each
106106+`render()` call, the Term reads a monotonic clock and computes the elapsed
107107+seconds since the previous render. That value is passed to the layout engine to
108108+advance any in-flight transitions.
109109+110110+If the previous render reported `animating=false`, the Term passes `deltaTime=0`
111111+to the layout engine on the current render, regardless of wall-clock time
112112+elapsed. The rationale: Clay is delta-based and has no concept of when a
113113+transition began. Idle time between renders must not count toward any subsequent
114114+transition's elapsed clock, otherwise a long idle gap followed by a mutation
115115+would cause the transition to complete instantly. Passing `deltaTime=0` on the
116116+first frame of any new transition gives it a clean elapsed=0 starting point;
117117+real deltas resume once the previous render signals `animating=true`.
118118+119119+The caller MAY override the computed delta via an explicit `deltaTime` option on
120120+`render()`. Use cases include deterministic testing, snapshot rendering, and
121121+compute-only renders where the caller is querying bounds without displaying
122122+output.
123123+124124+The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). Wall-clock
125125+time can move backward under NTP adjustments or DST, which would produce
126126+negative deltas and corrupt interpolation.
127127+128128+### 4.3 Delta clamping
129129+130130+Clayterm does not clamp `deltaTime`. Long gaps between frames (process
131131+suspension, backgrounded terminal, debugger pause) produce large deltas. The
132132+underlying interpolation is duration-based and naturally clamps at 1.0 of
133133+progress, so a large delta causes in-flight transitions to complete rather than
134134+to overshoot or become unstable.
135135+136136+### 4.4 Animation-loop signaling
137137+138138+The render result MUST surface whether any transition is currently active.
139139+Callers use this signal to schedule the next frame. When no transition is
140140+active, callers may stop rendering until the next external event (input, resize,
141141+application state change).
142142+143143+This requirement exists because terminal applications typically render on-demand
144144+rather than at a fixed refresh rate. Without an explicit animating signal, a
145145+caller has no way to know that a transition it triggered is still in progress.
146146+147147+### 4.5 Boundary preservation
148148+149149+Transition configuration MUST be fully serializable. No function pointers,
150150+closures, or callback registries cross the TS→WASM boundary during a render
151151+transaction.
152152+153153+This preserves INV-2 (single transaction per frame): one binary buffer in, one
154154+result struct out. On the C side, a fixed set of easing handlers is
155155+pre-registered; the directive selects one by enum value.
156156+157157+---
158158+159159+## 5. Core Invariants
160160+161161+_This section is normative._
162162+163163+**INV-T1. Time is driven by delta, not wall clock.** All transition
164164+interpolation advances by `deltaTime`, a per-frame seconds value. The renderer
165165+does not subscribe to an internal timer or schedule work of its own.
166166+167167+**INV-T2. Render remains pure under time override.** When the caller supplies an
168168+explicit `deltaTime`, the render result depends only on the directive array, the
169169+previous frame's cell buffer, and the supplied `deltaTime`. This makes
170170+deterministic rendering possible for tests and snapshots.
171171+172172+**INV-T3. No callbacks across the boundary.** Transition configuration MUST be
173173+fully serializable. No function pointers, closures, or callback registries cross
174174+the TS→WASM boundary during a render transaction.
175175+176176+**INV-T4. Identity is drawn from element IDs.** Transition state is associated
177177+with elements by their declared `id`. Callers using transitions on an element
178178+MUST assign it a stable, unique `id` across frames. Reusing an `id` for a
179179+different logical element in a later frame is a caller error; behavior is
180180+unspecified.
181181+182182+**INV-T5. Animating signal is accurate per transaction.** The `animating` flag
183183+returned by `render()` reflects the state of transitions as of the end of that
184184+transaction. If it is `true`, at least one transition has non-zero remaining
185185+progress and calling `render()` again with positive `deltaTime` will advance it.
186186+187187+**INV-T6. Cancellation is structural.** There is no imperative `cancel()` API.
188188+Transitions are cancelled by re-describing the previous target in a later frame;
189189+the transition infrastructure re-anchors the interpolation from the current
190190+visible value to the new target.
191191+192192+---
193193+194194+## 6. Rendering Contract Additions
195195+196196+_This section is normative._
197197+198198+### 6.1 `render()` signature
199199+200200+The `render()` method accepts an optional `deltaTime` field in its options
201201+argument:
202202+203203+```
204204+render(ops: Op[], options?: RenderOptions): RenderResult
205205+206206+interface RenderOptions {
207207+ mode?: "line";
208208+ row?: number;
209209+ pointer?: { x, y, down };
210210+ deltaTime?: number;
211211+}
212212+```
213213+214214+Each `render()` call advances transitions by its `deltaTime`:
215215+216216+- If `deltaTime` is provided explicitly, it is used verbatim.
217217+- Otherwise, if the previous render reported `animating=false`, `deltaTime=0`
218218+ (see §4.2 for rationale).
219219+- Otherwise, `deltaTime` is the monotonic wall-clock time elapsed since the
220220+ previous `render()` call.
221221+222222+On every `render()` call, Term captures the current monotonic timestamp as the
223223+reference point for the next implicit delta. The two modes can be freely mixed,
224224+but mixing within a single session is primarily useful for tests that step time
225225+manually and should otherwise be avoided.
226226+227227+### 6.2 `RenderResult` addition
228228+229229+The render result gains one field:
230230+231231+```
232232+interface RenderResult {
233233+ output: Uint8Array;
234234+ events: PointerEvent[];
235235+ info: RenderInfo;
236236+ errors: ClayError[];
237237+ animating: boolean;
238238+}
239239+```
240240+241241+`animating` is `true` if and only if at least one element has an in-flight
242242+transition at the end of the transaction.
243243+244244+### 6.3 The `transition` field on `open()`
245245+246246+An element may declare a transition by adding a `transition` field to its
247247+open-element directive. The field is optional. Its absence means the element has
248248+no transitions, which is the default.
249249+250250+See Section 7 for the shape.
251251+252252+---
253253+254254+## 7. Declarative Transition Surface
255255+256256+_This section is descriptive._
257257+258258+### 7.1 The `transition` field
259259+260260+All listed properties share a single duration and a single easing.
261261+262262+```ts
263263+open("sidebar", {
264264+ layout: { width: fixed(20) },
265265+ bg: rgba(30, 30, 30, 255),
266266+ transition: {
267267+ duration: 0.2,
268268+ easing: "easeOut",
269269+ properties: ["x", "width", "bg"],
270270+ interactive: false,
271271+ },
272272+});
273273+```
274274+275275+**`duration`** — seconds. Must be non-negative.
276276+277277+**`easing`** — a string naming one of the built-in easing curves (Section 7.2).
278278+Defaults to `"linear"` when omitted.
279279+280280+**`properties`** — list of property names to interpolate. Group names
281281+(`position`, `size`, `all`) expand to the union of the underlying properties.
282282+283283+**`interactive`** (default `false`) — when `false`, pointer interactions with
284284+the element are disabled while a position transition is in progress. When
285285+`true`, pointer interactions remain enabled throughout.
286286+287287+### 7.2 Easing values
288288+289289+The `easing` field takes one of four string values:
290290+291291+```ts
292292+type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut";
293293+```
294294+295295+Each value maps to a wire byte (see Section 8). The byte space is deliberately
296296+larger than this set so additional easings can be added later without breaking
297297+serialized frames. A future parametric easing (e.g., cubic bezier) would extend
298298+the type to a discriminated union:
299299+`"linear" | "easeIn" | ... | { cubicBezier: [number, number, number, number] }`.
300300+Today all values are non-parametric, so the type is a plain string union.
301301+302302+### 7.3 Property names
303303+304304+```ts
305305+type TransitionProperty =
306306+ | "x"
307307+ | "y"
308308+ | "position"
309309+ | "width"
310310+ | "height"
311311+ | "size"
312312+ | "bg"
313313+ | "overlay"
314314+ | "borderColor"
315315+ | "borderWidth"
316316+ | "all";
317317+```
318318+319319+Group names expand as follows:
320320+321321+- `position` → `x`, `y`
322322+- `size` → `width`, `height`
323323+- `all` → every individual property above
324324+325325+---
326326+327327+## 8. Wire Encoding
328328+329329+_This section is descriptive._
330330+331331+The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. Its
332332+presence is indicated by a bit in the open-element property mask. When present,
333333+the block is a fixed 8-byte record:
334334+335335+```
336336+transition_block {
337337+ duration: f32 // seconds, non-negative
338338+ properties: u16 // Clay-native bitmask (see below)
339339+ easing: u8 // easing kind (0 = linear, 1 = easeIn, 2 = easeOut, 3 = easeInOut)
340340+ flags: u8 // bit 0: interactive (0 = disable, 1 = allow)
341341+}
342342+```
343343+344344+The `properties` value is the Clay transition property bitmask:
345345+346346+```
347347+CLAY_TRANSITION_PROPERTY_X = 1
348348+CLAY_TRANSITION_PROPERTY_Y = 2
349349+CLAY_TRANSITION_PROPERTY_WIDTH = 4
350350+CLAY_TRANSITION_PROPERTY_HEIGHT = 8
351351+CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR = 16
352352+CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR = 32
353353+CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128
354354+CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256
355355+```
356356+357357+(Value 64, `CLAY_TRANSITION_PROPERTY_CORNER_RADIUS`, is defined upstream but has
358358+no field in `Clay_TransitionData` and is not emitted by clayterm.)
359359+360360+The property-name helpers on the TS side expand to this bitmask during packing.
361361+362362+### 8.1 Validation
363363+364364+`validate()` checks:
365365+366366+- `duration >= 0`.
367367+- `easing` is one of the defined enum values (0-3).
368368+- Property names are from the defined set (Section 7.3).
369369+370370+---
371371+372372+## 9. Cancellation Semantics
373373+374374+_This section is normative._
375375+376376+A caller cancels an in-flight transition by emitting a new frame whose directive
377377+for that element describes a different target state. The transition
378378+infrastructure re-anchors the interpolation:
379379+380380+- The new `initial` value becomes the element's currently-visible value.
381381+- `elapsedTime` resets to zero.
382382+- The new `target` is the value declared in the current frame.
383383+384384+The transition duration is unchanged. A cancelled-and-reversed transition takes
385385+its full configured duration regardless of how far it had progressed at the time
386386+of cancellation.
387387+388388+There is no `term.cancelTransition(id)` call. The frame-snapshot model makes
389389+cancellation a structural consequence of re-describing the desired state rather
390390+than an imperative operation.
391391+392392+---
393393+394394+## 10. Interaction with Line Mode
395395+396396+_This section is descriptive._
397397+398398+Line mode emits cells as newline-separated rows without absolute cursor
399399+positioning. Position transitions (`x`, `y`) have no meaningful effect in this
400400+mode: rows are placed at the current cursor, not at absolute coordinates.
401401+402402+Expected behavior in line mode:
403403+404404+- Color and size transitions proceed normally.
405405+- Position transitions are silently skipped (the property bits for x and y are
406406+ cleared before the configuration reaches Clay).
407407+- The `animating` signal reports accurately regardless of mode.
408408+409409+---
410410+411411+## 11. Testing Strategy
412412+413413+_This section is descriptive._
414414+415415+The `deltaTime` override enables deterministic, snapshot-friendly tests. A test
416416+sequence looks like:
417417+418418+```ts
419419+term.render(opsA, { deltaTime: 0 });
420420+term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed
421421+term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition
422422+term.render(opsB, { deltaTime: 0.1 }); // 100%, completed
423423+```
424424+425425+Test coverage should include, at minimum:
426426+427427+- Property change mid-stream interpolates and completes.
428428+- `animating` is false on static frames, true during interpolation, false again
429429+ when the transition completes.
430430+- Mid-transition target change re-anchors initial to current value.
431431+- Multiple concurrent transitions on multiple elements.
432432+- Line mode: color and size transitions apply, position transitions are silently
433433+ skipped.
434434+- Each easing enum produces distinct progression (linear, easeIn, easeOut,
435435+ easeInOut).
436436+437437+---
438438+439439+## 12. Implementation Notes
440440+441441+_This section is descriptive and may change without affecting contract._
442442+443443+### 12.1 Clay submodule pin
444444+445445+clayterm pins Clay at a specific commit that includes the transition API
446446+introduced upstream in commit `ee192f4`. The pin is recorded in the `clay`
447447+submodule pointer. Advancing the pin is a prerequisite when upstream adds
448448+capabilities clayterm depends on (Section 13).
449449+450450+### 12.2 Handler architecture
451451+452452+Each `Term` registers one C-side transition handler per easing kind (four total
453453+for v1: linear, easeIn, easeOut, easeInOut). At element-configuration time the
454454+decoder selects the handler matching the element's easing enum and stores it on
455455+the `Clay_TransitionElementConfig`.
456456+457457+Each handler:
458458+459459+1. Computes progress as `clamp(elapsedTime / duration, 0, 1)`.
460460+2. Applies its easing curve to progress.
461461+3. Lerps each property named in the `properties` bitmask from `initial` to
462462+ `target`.
463463+4. Increments the Term context's `animating_count` unless progress is 1.0.
464464+5. Returns `true` if progress is 1.0 (transition complete), `false` otherwise.
465465+466466+At the start of each `render()`, the Term resets `animating_count` to zero. At
467467+the end, the value is copied into the result struct as the `animating` flag
468468+(`true` if count > 0).
469469+470470+### 12.3 Per-Term isolation
471471+472472+The `animating_count` lives on the Term's C-side context, not as module-level
473473+state. Multiple Terms created in the same process remain isolated.
474474+475475+### 12.4 Resolving the active Term inside the handler
476476+477477+Clay's transition-handler signature does not carry a `userData` pointer or
478478+element ID. Each `reduce()` call records the currently-active Term pointer in a
479479+module-level variable (`ct_active_context`) and clears it at the end. The
480480+handler reads this variable to reach the Term's `animating_count`. A single
481481+render pass cannot overlap with another (renders are synchronous), so there is
482482+no concurrency concern.
483483+484484+---
485485+486486+## 13. Deferred Until Upstream Clay
487487+488488+These capabilities are intentionally not in v1 because the required Clay
489489+primitives are either missing or in flight upstream. The absence is motivated;
490490+re-adding them is straightforward once Clay lands the pieces.
491491+492492+### 13.1 Per-property easing and duration
493493+494494+The directive API could allow each property to have its own duration and easing
495495+(e.g., "fade bg in 150ms, slide x in 300ms"). Clay's
496496+`Clay_TransitionElementConfig` carries a single `duration`, a single `handler`,
497497+and a single `properties` bitmask per element, so the handler has no way to
498498+distinguish per-property timing. Working around this requires per-element
499499+metadata addressable from inside the handler.
500500+501501+**Unblocked by:** Clay adding `void* userData` to the transition arguments
502502+(upstream PR [nicbarker/clay#603](https://github.com/nicbarker/clay/pull/603)).
503503+504504+### 13.2 Enter and exit transitions
505505+506506+Elements mounted or removed between frames cannot express per-element initial or
507507+final state deltas. Clay exposes `setInitialState` and `setFinalState` callbacks
508508+with signatures that take no element identifier or user pointer, so there is no
509509+way to look up per-element deltas from inside the callbacks. Additionally, exit
510510+transitions require their configuration to survive past the frame on which the
511511+element was last declared, which requires a lifetime signal.
512512+513513+**Unblocked by:**
514514+515515+- Clay `userData` on transition arguments (PR #603, above).
516516+- An exit-completion callback or an `exiting` flag on the render command, both
517517+ of which have been discussed upstream with Clay's maintainer as forthcoming.
518518+519519+### 13.3 `cubicBezier` easing
520520+521521+Custom cubic-bezier curves need per-element control-point parameters, and Clay's
522522+fixed handler signature has no mechanism to thread parameters to a shared
523523+handler.
524524+525525+**Unblocked by:** the same Clay `userData` addition as 13.1.
526526+527527+### 13.4 Corner-radius transitions
528528+529529+`CLAY_TRANSITION_PROPERTY_CORNER_RADIUS` is defined in the Clay property enum,
530530+but `Clay_TransitionData` has no field carrying corner radius. Upstream
531531+`Clay_EaseOut` does not interpolate it. Clayterm cannot either.
532532+533533+**Unblocked by:** Clay adding a `cornerRadius` field to `Clay_TransitionData`
534534+and interpolating it in layout.
535535+536536+---
537537+538538+## 14. Demos
539539+540540+One demo accompanies v1:
541541+542542+**`demo/transitions.ts`** — exercises v1 transitions meaningfully in a terminal
543543+context (e.g., a collapsing sidebar or a colored highlight that fades between
544544+states). Purpose: surface real-world API sharp edges.
545545+546546+---
547547+548548+## Appendix A. Relationship to the Renderer Specification
549549+550550+This specification extends, but does not modify, the renderer specification.
551551+Specifically:
552552+553553+- **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock for
554554+ `deltaTime` computation. A clock read is not terminal IO and does not violate
555555+ this invariant. The renderer still produces bytes only; it does not read or
556556+ write terminals.
557557+558558+- **INV-2 (Single transaction per frame).** Transitions preserve this. All
559559+ transition configuration is serialized into the single directive buffer; no
560560+ additional boundary crossings occur during rendering.
561561+562562+- **INV-3 (Frame-snapshot independence).** Transitions preserve this at the API
563563+ level. Each directive array still fully describes the desired state. Element
564564+ IDs carry more weight (Section 4.1) but callers do not acquire new cross-frame
565565+ bookkeeping responsibilities.
566566+567567+- **INV-4 (ANSI byte output).** Unchanged.
568568+569569+- **INV-5 (Layout/render/diff ownership).** The renderer additionally owns
570570+ transition interpolation. Interpolated values feed into the existing layout
571571+ and diff pipeline at the same pipeline stage that resolved values would.
572572+573573+The "Deferred/Future Areas" section of the renderer specification should be
574574+updated to reference this specification rather than list transitions as a single
575575+bullet.
+37-3
src/clayterm.c
···1212 */
13131414#include "clayterm.h"
1515+#include "transitions.h"
1516#include "../clay/clay.h"
1617#include "buffer.h"
1718#include "cell.h"
···1920#include "utf8.h"
2021#include "wcwidth.h"
21222323+/* Module-level pointer to the Term currently executing reduce().
2424+ * Set/cleared around each render pass so transition handlers (which Clay
2525+ * invokes with no userData — see Clay_TransitionCallbackArguments) can
2626+ * report back to the right Term's animating_count. Revisit once
2727+ * nicbarker/clay#603 lands userData on transition callbacks; then the
2828+ * handler can resolve its Term from args directly and this can go away. */
2929+struct Clayterm *ct_active_context = NULL;
3030+2231/* ── Command buffer protocol ──────────────────────────────────────── */
23322433#define OP_BEGIN_LAYOUT 0x01
···3342#define PROP_BORDER 0x08
3443#define PROP_CLIP 0x10
3544#define PROP_FLOATING 0x20
4545+#define PROP_TRANSITION 0x40
36463747/* ── Instance state ───────────────────────────────────────────────── */
3848···5161 /* error collection */
5262 Clay_ErrorData errors[MAX_ERRORS];
5363 int error_count;
6464+ int animating_count;
5465};
55665667/* Memory layout inside the arena provided by the host:
···467478 return ct;
468479}
469480470470-void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
481481+void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
482482+ float deltaTime) {
471483 int i = 0;
484484+ ct_active_context = ct;
472485 ct->error_count = 0;
486486+ ct->animating_count = 0;
473487474488 Clay_BeginLayout();
475489···555569 decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff);
556570 }
557571572572+ if (mask & PROP_TRANSITION) {
573573+ float duration = rdf(buf, len, &i);
574574+ uint32_t props_and_flags = rd(buf, len, &i);
575575+ uint16_t props = props_and_flags & 0xFFFF;
576576+ uint8_t easing = (props_and_flags >> 16) & 0xFF;
577577+ uint8_t interactive = (props_and_flags >> 24) & 0xFF;
578578+579579+ decl.transition.handler = ct_handler_for(easing);
580580+ decl.transition.duration = duration;
581581+ decl.transition.properties = (Clay_TransitionProperty)props;
582582+ decl.transition.interactionHandling =
583583+ interactive
584584+ ? CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION
585585+ : CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION;
586586+ }
587587+558588 Clay__ConfigureOpenElement(decl);
559589 break;
560590 }
···577607 /* attrs byte -> alpha channel for render_text to extract */
578608 config.textColor.a = (float)((cfg >> 24) & 0xff);
579609580580- Clay__OpenTextElement(text, Clay__StoreTextElementConfig(config));
610610+ Clay__OpenTextElement(text, config);
581611 break;
582612 }
583613···590620 }
591621 }
592622593593- Clay_RenderCommandArray cmds = Clay_EndLayout();
623623+ Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);
594624595625 /* reset output state */
596626 ct->out.length = 0;
···638668 } else {
639669 present_cups(ct, row);
640670 }
671671+672672+ ct_active_context = NULL;
641673}
642674643675char *output(struct Clayterm *ct) { return ct->out.data; }
644676645677int length(struct Clayterm *ct) { return ct->out.length; }
678678+679679+int animating(struct Clayterm *ct) { return ct->animating_count; }
646680647681int get_element_bounds(const char *name, int name_len, float *out) {
648682 Clay_String str = {.length = name_len, .chars = name};
+3-1
src/clayterm.h
···1212/* WASM exports */
1313int clayterm_size(int w, int h);
1414struct Clayterm *init(void *mem, int w, int h);
1515-void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row);
1515+void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
1616+ float deltaTime);
1617char *output(struct Clayterm *ct);
1718int length(struct Clayterm *ct);
1919+int animating(struct Clayterm *ct);
1820void measure(int ret, int txt);
19212022int get_element_bounds(const char *name, int name_len, float *out);