Skip to content

Add transitions to clayterm#27

Draft
cowboyd wants to merge 28 commits intomainfrom
transitions
Draft

Add transitions to clayterm#27
cowboyd wants to merge 28 commits intomainfrom
transitions

Conversation

@cowboyd
Copy link
Copy Markdown
Member

@cowboyd cowboyd commented Apr 23, 2026

Motivation

The renderer has no way to express time-based interpolation of visual properties. Every frame is a hard cut, which makes the terminal UI feel brittle for anything beyond static layouts — a collapsing sidebar jumps, a focus change flashes, a list reorder teleports. Transitions fill that gap: they let callers describe the desired state each frame and let the renderer smoothly interpolate between frames, without introducing a component tree or violating the frame-snapshot contract.

This branch also needs to live within what the upstream Clay engine can currently do. Clay ships a transition API but its callbacks carry no per-element userData, which means several features the rendering model naturally invites — per-property easing, per-element enter/exit deltas, custom bezier curves — can't be expressed without upstream changes (see Clay #603 and related discussion). Rather than paper over that constraint with fragile workarounds, this PR ships the v1 subset that Clay genuinely supports today and tombstones the rest behind specific upstream unblockers.

Approach

The contract is established in specs/transitions-spec.md: six normative invariants (time driven by delta, no callbacks across the WASM boundary, animating signal accuracy, cancellation-as-structural), a single declarative transition: { duration, easing, properties, interactive? } shape on open(), a fixed 8-byte wire encoding, and a §13 "Deferred Until Upstream Clay" section that makes the current limits explicit and points at the fixes that unblock them.

On the rendering side, Term now tracks performance.now() across frames and passes deltaTime through to Clay_EndLayout. The C side pre-registers four handlers (one per easing kind — linear, easeIn, easeOut, easeInOut) and picks the right one at element-configuration time from an enum byte in the directive buffer. An animating_count on the Term context is incremented by in-progress handlers and exposed as RenderResult.animating, so callers know whether to schedule another frame. The Clay submodule was bumped to pick up the transitions API (pinned to 938967a to work around an upstream copy-paste typo in a later commit that duplicated a WASM export name).

One subtlety worth calling out: Clay is delta-based and has no notion of when a transition began, so idle time between renders would otherwise inflate the elapsed clock and cause transitions to complete instantly after a long idle gap. Term handles this by passing deltaTime=0 whenever the previous render reported animating=false, giving each new transition a clean elapsed=0 starting point. Real deltas flow through on every frame where a transition is already in flight.

Two demos land with the feature: a minimal interactive sidebar that expands and collapses on Enter/ESC, and a port of Clay's raylib-transitions example (a grid of colored boxes that shuffle position and recolor their backgrounds, with mouse hover tints).

Alternate Designs

  • Forking Clay to add userData. Would have enabled the full spec (per-property easing, enter/exit deltas, cubic bezier) immediately, but at the cost of maintaining a fork across every Clay bump. Rejected in favor of shipping the Clay-native subset now and restoring the deferred pieces once upstream lands nicbarker/clay#603.
  • Timestamp-based animation. Storing a per-transition startTime and computing progress from wall-clock deltas would sidestep the deltaTime-vs-mutation-time ambiguity entirely. Rejected because Clay's API is irreducibly delta-based and reworking that upstream is out of scope for this change. The idle-reset behavior approximates the timestamp model where it matters most.
  • Presets for enter/exit (as in an earlier thefrontside/clayterm draft). Would have given a taste of mount/unmount animation using a fixed set of hardcoded patterns (slide-from-left, etc.). Rejected because it trades the spec's declarative, per-element expressiveness for a small enum of baked-in behaviors — a dead end that would have to be torn out when userData lands.

Possible Drawbacks or Risks

The Clay submodule pin is one commit behind upstream main so we can sidestep the duplicated-WASM-export bug. When that bug is fixed upstream, we can bump forward and pick up the small number of transition fixes we're currently missing.

The spec's §7.3 lists "overlay" as a transition property, but OpenElement has no overlay? field in v1, so supplying an overlay color from the TS side isn't possible yet. The clay-transitions demo sidesteps this by tinting bg for hover feedback. Adding overlay?: number to OpenElement with a PROP_OVERLAY_COLOR wire bit is a small follow-up if anyone wants it.

TODOs and Open Questions

  • Advance Clay submodule past the duplicate-WASM-export bug when upstream fixes it.
  • File or support Clay #603 (userData on transition arguments) to unblock the deferred v2 features: per-property longhand, per-element enter/exit deltas, cubicBezier easing.
  • Consider adding overlay?: number to OpenElement so the spec's "overlay" property is actually reachable from the TS side.

Learning

cowboyd added 28 commits April 21, 2026 21:32
Design spec for clayterm transitions: frame-snapshot-compatible interpolation
of element position, size, and color properties. Defines the deltaTime
convention, the animating signal on RenderResult, declarative enter/exit
semantics that replace Clay's function-pointer callbacks, and cancellation
as a structural consequence of re-describing state. Implementation is
gated on bumping the Clay submodule past the upstream transition commit.
Scope v1 to what Clay currently supports without userData on transition
callbacks: one duration and one easing per element, applied to all listed
properties. Drop per-property longhand, enter/exit deltas, cubicBezier,
and corner radius — each with an explicit "Deferred Until Upstream Clay"
entry in §13 referencing nicbarker/clay#603 and the forthcoming exit-flag
work. Easings are plain string literals ("linear" | "easeIn" | "easeOut"
| "easeInOut") since v1 has no parametric easings.
Ports the spirit of the raylib-transitions demo to clayterm: a 4×4 grid
of colored boxes that animate position, size, and bg color. Shuffle (s)
animates positions via Clay's transition system; recolor (c) toggles
between two palettes with animated bg interpolation; hover tints each
box by blending its bg toward white (overlay-color field is not yet in
the v1 command buffer, so lighten-on-hover substitutes). Full mouse
tracking is wired via mouseTracking() + pointer state from input events.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant