From 3c5fee2c91dd45bc29a5ab03ee27853143b5e066 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 21:32:22 -0500 Subject: [PATCH 01/35] =?UTF-8?q?=F0=9F=93=9D=20add=20transitions=20design?= =?UTF-8?q?=20specification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- specs/transitions-spec.md | 658 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 specs/transitions-spec.md diff --git a/specs/transitions-spec.md b/specs/transitions-spec.md new file mode 100644 index 0000000..494dd79 --- /dev/null +++ b/specs/transitions-spec.md @@ -0,0 +1,658 @@ +# Clayterm Transitions Specification + +**Version:** 0.1 (draft) **Status:** Design specification for a not-yet-implemented +feature. Normative where it establishes invariants and contract. Descriptive +where surfaces may settle during implementation. + +--- + +## 1. Purpose + +A transition smoothly interpolates an element's visual properties over time. +This specification defines how transitions integrate with Clayterm's frame-snapshot +rendering model: how they are declared, how time is supplied, how enter and +exit behaviors are expressed, and how callers observe in-flight animation so +they can drive the render loop. + +Transitions are a first-class extension of the rendering contract defined in +the [Clayterm Renderer Specification](renderer-spec.md). They do not change +the architectural model, do not introduce a component tree, and do not require +callers to hold cross-frame identity beyond the stable element identifiers they +already use. + +--- + +## 2. Scope + +### In scope (normative) + +- The transition model and its relationship to the frame-snapshot rendering contract +- Time handling and the `deltaTime` convention +- The animating signal returned from `render()` +- The declarative enter and exit model (no callbacks across the WASM boundary) +- Element identity requirements for transitions +- Cancellation semantics (as a consequence of the frame-snapshot model) + +### In scope (non-normative, descriptive) + +- The shape of the `transition` field on the `open()` directive (shorthand and longhand) +- The set of easing functions exposed in the initial surface +- The wire encoding of transition data in the directive buffer +- Interaction with line mode +- Testing strategy + +### Out of scope + +- Custom (JavaScript-authored) easing functions. Reserved for a future extension; + the enum space is designed not to preclude them. +- Proportional reversal (CSS-style dynamic shortening of duration when a + transition is cancelled mid-flight). +- Physics-based animation, spring interpolation, or keyframe sequences. +- Any framework-level concept of "animation groups," "timelines," or choreography + across multiple elements. Orchestration is a caller concern. +- Input parsing (see [Input Specification](input-spec.md)). + +--- + +## 3. Terminology + +**Transition.** A time-based interpolation of one or more of an element's +visual properties between an initial value and a target value. + +**Transition property.** A specific visual attribute of an element that can be +interpolated: position (x, y), size (width, height), background color, overlay +color, border color, border width, or corner radius. + +**Easing.** A function mapping normalized progress in [0, 1] to an eased value +in [0, 1]. Clayterm exposes a fixed set of built-in easings. + +**Transition state.** One of four modes an element can be in with respect to a +given transition: idle, entering (element newly mounted), transitioning +(property target changed on an existing element), or exiting (element removed +from the tree but still being animated out). + +**Enter transition.** The animation played when an element first appears in the +directive tree. Its initial state is derived from the element's target state +by applying caller-supplied deltas (e.g., offset position, transparent color). + +**Exit transition.** The animation played when an element disappears from the +directive tree. Its final state is derived from the element's last-seen state +by applying caller-supplied deltas. The element is still rendered during its +exit even though it is no longer in the directive tree. + +**Delta time (`deltaTime`).** The number of seconds elapsed since the previous +render transaction. Used by the renderer to advance interpolation. + +**Animating signal.** A boolean flag in the render result indicating whether +any transition is currently in progress. Callers use it to decide whether to +schedule another frame. + +--- + +## 4. Architectural Model + +_This section is normative._ + +### 4.1 Relationship to the frame-snapshot model + +Transitions do not alter the frame-snapshot contract defined in INV-3 of the +renderer specification. The directive array still fully describes the desired +state for its frame. Transitions interpolate between the previous frame's +state and the current frame's target state; they do not reintroduce a +persistent component tree on the caller side. + +What transitions add is the requirement that element identifiers remain stable +across frames for any element on which animation is desired. This is not a new +invariant — the existing pointer-event subsystem already relies on stable +identifiers — but it becomes load-bearing for transitions. + +### 4.2 Time ownership + +The `Term` instance is the sole source of frame-to-frame time. On each +`render()` call, the Term reads a monotonic clock and computes the elapsed +seconds since the previous render. That value is passed to the layout engine +to advance any in-flight transitions. + +The caller MAY override the computed delta via an explicit `deltaTime` option +on `render()`. Use cases include deterministic testing, snapshot rendering, +and compute-only renders where the caller is querying bounds without +displaying output. + +The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). Wall-clock +time can move backward under NTP adjustments or DST, which would produce +negative deltas and corrupt interpolation. + +### 4.3 Delta clamping + +Clayterm does not clamp `deltaTime`. Long gaps between frames (process +suspension, backgrounded terminal, debugger pause) produce large deltas. The +underlying interpolation is duration-based and naturally clamps at 1.0 of +progress, so a large delta causes in-flight transitions to complete rather +than to overshoot or become unstable. + +This differs from physics-based engines, which clamp deltas to prevent +tunneling. Transitions as specified here are not physics-based, so clamping +is unnecessary. + +### 4.4 Animation-loop signaling + +The render result MUST surface whether any transition is currently active. +Callers use this signal to schedule the next frame. When no transition is +active, callers may stop rendering until the next external event (input, +resize, application state change). + +This requirement exists because terminal applications typically render +on-demand rather than at a fixed refresh rate. Without an explicit animating +signal, a caller has no way to know that a transition it triggered is still +in progress. + +### 4.5 Boundary preservation + +Transitions MUST NOT require function pointers, callbacks, or other +non-serializable values to cross the TS→WASM boundary. Easing and +enter/exit initial-state computation are implemented on the C side using +declarative configuration carried in the directive buffer. + +This preserves INV-2 (single transaction per frame): one binary buffer in, +one result struct out. + +--- + +## 5. Core Invariants + +_This section is normative._ + +**INV-T1. Time is driven by delta, not wall clock.** All transition +interpolation advances by `deltaTime`, a per-frame seconds value. The +renderer does not subscribe to an internal timer or schedule work of its own. + +**INV-T2. Render remains pure under time override.** When the caller supplies +an explicit `deltaTime`, the render result depends only on the directive +array, the previous frame's cell buffer, and the supplied `deltaTime`. This +makes deterministic rendering possible for tests and snapshots. + +**INV-T3. No callbacks across the boundary.** Transition configuration MUST +be fully serializable. No function pointers, closures, or callback registries +cross the TS→WASM boundary during a render transaction. + +**INV-T4. Identity is drawn from element IDs.** Transition state is associated +with elements by their declared `id`. Callers using transitions on an element +MUST assign it a stable, unique `id` across frames. Reusing an `id` for a +different logical element in a later frame is a caller error; behavior is +unspecified. + +**INV-T5. Animating signal is accurate per transaction.** The `animating` +flag returned by `render()` reflects the state of transitions as of the end +of that transaction. If it is `true`, at least one transition has non-zero +remaining progress and calling `render()` again with positive `deltaTime` +will advance it. + +**INV-T6. Cancellation is structural.** There is no imperative `cancel()` +API. Transitions are cancelled by re-describing the previous target in a +later frame; the transition infrastructure re-anchors the interpolation from +the current visible value to the new target. + +--- + +## 6. Rendering Contract Additions + +_This section is normative._ + +### 6.1 `render()` signature + +The `render()` method accepts an optional `deltaTime` field in its options +argument: + +``` +render(ops: Op[], options?: RenderOptions): RenderResult + +interface RenderOptions { + mode?: "line"; + row?: number; + pointer?: { x, y, down }; + deltaTime?: number; // seconds; overrides Term's internal clock +} +``` + +Each `render()` call advances transitions by its `deltaTime`: + +- If `deltaTime` is omitted, Term computes it as the monotonic wall-clock + time elapsed since the previous `render()` call. +- If `deltaTime` is provided, it is used verbatim for that frame. + +On every `render()` call, Term captures the current monotonic timestamp as +the reference point for the next implicit delta. The two modes can be +freely mixed, but mixing within a single session is primarily useful for +tests that step time manually and should otherwise be avoided. + +### 6.2 `RenderResult` addition + +The render result gains one field: + +``` +interface RenderResult { + output: Uint8Array; + events: PointerEvent[]; + info: RenderInfo; + errors: ClayError[]; + animating: boolean; // NEW +} +``` + +`animating` is `true` if and only if at least one element has an in-flight +transition at the end of the transaction. + +### 6.3 The `transition` field on `open()` + +An element may declare a transition by adding a `transition` field to its +open-element directive. The field is optional. Its absence means the element +has no transitions, which is the default. + +The field accepts either shorthand or longhand form (Section 7). + +--- + +## 7. Declarative Transition Surface + +_This section is descriptive. The shapes may be revised during implementation, +but the architectural commitments above do not change._ + +### 7.1 Shorthand form + +All listed properties share one duration and one easing: + +```ts +open("sidebar", { + layout: { width: fixed(20) }, + bg: rgba(30, 30, 30, 255), + transition: { + duration: 0.2, + easing: easeOut(), + properties: ["x", "width", "bg"], + }, +}) +``` + +### 7.2 Longhand form + +Each property declares its own duration and easing independently: + +```ts +open("sidebar", { + transition: [ + { property: "x", duration: 0.3, easing: easeInOut() }, + { property: "width", duration: 0.3, easing: easeInOut() }, + { property: "bg", duration: 0.15, easing: easeOut() }, + ], +}) +``` + +The shorthand form is expanded to longhand during directive packing. The wire +encoding carries only longhand. + +### 7.3 Extended form (enter, exit, interaction handling) + +```ts +open("toast", { + transition: { + properties: [ + { property: "y", duration: 0.25, easing: easeOut() }, + { property: "bg", duration: 0.15, easing: linear() }, + ], + enter: { + independently: false, + from: { y: -2, bg: rgba(0, 0, 0, 0) }, + }, + exit: { + independently: false, + to: { y: -2, bg: rgba(0, 0, 0, 0) }, + paintOrder: "natural", + }, + interactive: false, + }, +}) +``` + +**`enter.from`** declares deltas relative to the element's target state. The +initial state used by the enter transition is `target + from`. A missing +`from` entry for a given property means the enter transition starts at the +target value for that property (no visible animation on that axis). + +**`exit.to`** declares deltas relative to the element's last-seen state. +The final state used by the exit transition is `initial + to`. + +**`enter.independently` / `exit.independently`** (default `false`) control +whether the element's enter/exit plays when its parent is also entering or +exiting in the same frame. The default couples the element to its parent: +child elements do not play their own enter/exit when the parent is itself +entering or exiting (this prevents cascaded animations when an entire +container mounts or unmounts). Setting `independently: true` opts in to +playing the animation unconditionally. + +**`exit.paintOrder`** controls how an exiting element is drawn relative to +its reflowing siblings during the exit animation. One of: + +- `"natural"` (default) — paints in the element's natural DOM order. +- `"underSiblings"` — paints beneath siblings; reflowing neighbors cover the + exiting element. +- `"overSiblings"` — paints on top of siblings; the exiting element remains + visually prominent until its animation completes. + +**`interactive`** (default `false`) — when `false`, pointer interactions +with the element are disabled while a position transition is in progress. +When `true`, pointer interactions remain enabled throughout position +transitions. + +### 7.4 Easing helpers + +Exported from the top-level module: + +```ts +linear() +easeIn() +easeOut() +easeInOut() +cubicBezier(x1: number, y1: number, x2: number, y2: number) +``` + +Each returns an `Easing` value: a tagged byte with optional parameters. The +easing enum space is deliberately larger than the current surface to allow +future additions (including a potential `custom()` form that bridges to a +JavaScript function) without breaking serialized frames. + +### 7.5 Property names + +```ts +type TransitionProperty = + | "x" | "y" | "position" + | "width" | "height" | "size" + | "bg" | "overlay" | "borderColor" + | "cornerRadius" | "borderWidth" + | "all"; +``` + +Group names (`position`, `size`, `all`) expand to the underlying property +set during packing and are equivalent to listing the constituent properties +explicitly in longhand form. + +--- + +## 8. Wire Encoding + +_This section is descriptive._ + +The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. +Its presence is indicated in the element's property bitmask (existing +mechanism for optional fields). When present, its layout is: + +``` +transition_block { + flags: u8 // bit 0: enter present + // bit 1: exit present + // bit 2: interactive (0 = disabled, 1 = enabled) + entry_count: u8 // number of property_transition entries + entries: property_transition[] // entry_count entries, in stable property order + enter?: transition_side // present iff flags bit 0 + exit?: transition_side // present iff flags bit 1 +} + +property_transition { + property: u16 // single-bit mask from Clay's property enum + duration: f32 // seconds, non-negative + easing: u8 // easing kind + params: f32[0 or 4] // 4 floats iff easing == cubicBezier +} + +transition_side { + flags: u8 // bit 0: independently + // bits 1-2: paintOrder (exit only: 0 natural, 1 under, 2 over) + mask: u16 // which properties have deltas + values: bytes // packed in stable property order; widths per property +} +``` + +Value widths are property-specific: `f32` for position and size, `u32` for +colors, `u8[4]` for border widths, `u8[4]` for corner radii (8-bit +resolution per corner is consistent with the existing cornerRadius +encoding). + +The shorthand form is never present on the wire. TS fans shorthand out to +per-property longhand entries before packing. The C side sees only longhand. + +### 8.1 Validation + +The existing `validate()` utility gains checks: + +- `duration >= 0` for every entry. +- `easing` is one of the defined enum values. +- Property names in entries are valid and appear at most once. +- Property names in `enter.from` / `exit.to` are a subset of the entries + (deltas for a property not being transitioned are ignored or flagged). + +--- + +## 9. Cancellation Semantics + +_This section is normative._ + +A caller cancels an in-flight transition by emitting a new frame whose +directive for that element describes a different target state. The +transition infrastructure re-anchors the interpolation: + +- The new `initial` value becomes the element's currently-visible value. +- `elapsedTime` resets to zero. +- The new `target` is the value declared in the current frame. + +The transition duration is unchanged. A cancelled-and-reversed transition +takes its full configured duration regardless of how far it had progressed +at the time of cancellation. + +There is no `term.cancelTransition(id)` call. The frame-snapshot model +makes cancellation a structural consequence of re-describing the desired +state rather than an imperative operation. + +--- + +## 10. Interaction with Line Mode + +_This section is descriptive; the concrete behavior will be finalized +during implementation._ + +Line mode emits cells as newline-separated rows without absolute cursor +positioning. Position transitions (`x`, `y`) have no meaningful effect in +this mode: the rendering output places each row at the current cursor, +not at absolute coordinates. + +Expected behavior in line mode: + +- Color and size transitions proceed normally. +- Position transitions are silently skipped (treated as if the property is + not being transitioned for that frame). +- Enter/exit transitions that declare `from` or `to` deltas on position + properties have those position deltas dropped; other delta properties + still apply. + +The `animating` signal reports accurately regardless of mode; line-mode +color or size transitions still report as animating. + +--- + +## 11. Testing Strategy + +_This section is descriptive._ + +The `deltaTime` override enables deterministic, snapshot-friendly tests. +A test sequence looks like: + +```ts +term.render(opsA, { deltaTime: 0 }); +term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed +term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition +term.render(opsB, { deltaTime: 0.1 }); // 100%, completed +``` + +Test coverage should include, at minimum: + +- Shorthand and longhand produce identical output for equivalent configs. +- Enter transitions with `independently: true` and `false`. +- Exit transitions with each `paintOrder` value. +- Cancellation: target change mid-flight re-anchors initial to current. +- Re-appearance during an exit transition. +- Transition config present one frame and absent the next. +- Multiple concurrent transitions on a single element (longhand). +- Multiple concurrent transitions on multiple elements. +- Line mode rendering: color and size transitions apply, position transitions + are silently skipped. + +--- + +## 12. Implementation Notes + +_This section is descriptive and may change without affecting contract._ + +### 12.1 Clay submodule version + +clayterm currently pins Clay at commit `76ec363`. The transition API was +introduced upstream in commit `ee192f4`, with follow-up bug fixes. Before +implementing transitions, the Clay submodule must be advanced to a post- +`ee192f4` commit. Non-transition Clay changes introduced between the current +pin and the target pin — notably the `Clay_OnHover` signature change and the +element ID scheme split — require an audit of existing clayterm integration. + +Upgrading Clay is a prerequisite and should be treated as its own commit +ahead of transition work. + +### 12.2 Handler architecture + +Each `Term` registers a single C-side transition handler with Clay. +Per-element transition metadata (per-property duration, easing, easing +params, enter deltas, exit deltas) is stored in a side table keyed by +Clay element ID, owned by the Term's context. + +The handler: + +1. Resolves the active Term context. +2. Looks up metadata for the element by its Clay ID. +3. For each property in the active bitmask, computes local progress as + `clamp(elapsedTime / property.duration, 0, 1)`, applies the property's + easing, writes the interpolated value into the output struct. +4. Increments the Term context's `animating_count`. +5. Returns `true` if any property's local progress is below 1.0. + +At the start of each `render()`, the Term resets its `animating_count` to +zero. At the end, the value is copied into the result struct as the +`animating` flag (true if count > 0). + +The `setInitialState` and `setFinalState` callbacks Clay expects are +implemented as fixed C functions that apply the per-element `from` / `to` +deltas from the side table to the target / initial state Clay passes in. + +### 12.3 Per-element storage lifetime + +Metadata is repopulated each frame during directive unpacking. Clay's +handler is invoked synchronously inside `Clay_EndLayout`, so per-frame +metadata remains valid when the handler fires. No metadata needs to persist +across frames on our side; Clay's internal hashmap persists the actual +transition state (elapsed time, current value, state machine phase). + +### 12.4 Multiple Term instances + +`animating_count` and the metadata side table live on the Term's C-side +context, not as module-level state. Multiple Terms created in the same +process remain isolated. + +--- + +## 13. Open Questions + +These items remain undecided and will be resolved during implementation. +They do not affect the contract. + +### 13.1 First-frame delta + +On the very first `render()` after `createTerm()`, there is no previous +frame to compute a delta against. Clay's own behavior on its first +`Clay_EndLayout(deltaTime)` call (with a non-zero delta) is the source of +truth: clayterm will pass through whatever delta it has computed and adopt +whatever Clay does. Verification and documentation occur during +integration. + +### 13.2 Mid-transition target change + +The cancellation semantics in Section 9 require that a target change +mid-flight re-anchors `initial` to the current visible value. Clay's +`TRANSITIONING` state machine is expected to handle this, but it must be +verified. If Clay does not re-anchor, our handler adds the logic by +tracking the last-seen target per element. + +### 13.3 Element re-appearance mid-exit + +If an element is exiting and reappears in the next frame's directives, +the expected behavior is to cancel the exit and interpolate from the +current visible state to the new target. Implementation-dependent on Clay. + +### 13.4 Transition removed mid-flight + +If an element has a transition one frame and the `transition` field is +absent in the next frame, Clay's behavior for in-flight transitions +determines the outcome. Two reasonable options: (a) in-flight transitions +complete using their original config; (b) they freeze at their current +value. Deferred to Clay's observed behavior. Documented once verified. + +### 13.5 Custom easing escape hatch + +The easing enum space is deliberately larger than the initial surface. A +future `custom()` easing that bridges to a JavaScript function is +anticipated but not specified here. Its design must preserve INV-T3 +(no callbacks across the boundary during a render transaction) — likely +via a pre-sampled lookup table supplied in the directive buffer. + +--- + +## 14. Demos + +Two demos accompany the feature: + +1. **`demo/transitions.ts`** — a clayterm-native demo meaningfully + exercising transitions in a terminal context (e.g., a collapsing + sidebar, a list reorder, or a toast notification). Primary purpose: + surface real-world sharp edges in the API. + +2. **A reproduction of Clay's upstream `raylib-transitions` demo** — + the example that accompanied the Clay transition-API commit + (`ee192f4`). Primary purpose: provide a reference implementation + that can be visually compared to upstream, validating that the + clayterm integration faithfully exercises the full transition API + surface. + +--- + +## Appendix A. Relationship to the Renderer Specification + +This specification extends, but does not modify, the renderer specification. +Specifically: + +- **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock + for `deltaTime` computation. A clock read is not terminal IO and does + not violate this invariant. The renderer still produces bytes only; it + does not read or write terminals. + +- **INV-2 (Single transaction per frame).** Transitions preserve this. + All transition configuration is serialized into the single directive + buffer; no additional boundary crossings occur during rendering. + +- **INV-3 (Frame-snapshot independence).** Transitions preserve this at + the API level. Each directive array still fully describes the desired + state. Element IDs carry more weight (Section 4.1) but callers do not + acquire new cross-frame bookkeeping responsibilities. + +- **INV-4 (ANSI byte output).** Unchanged. + +- **INV-5 (Layout/render/diff ownership).** The renderer additionally + owns transition interpolation. Interpolated values feed into the + existing layout and diff pipeline at the same pipeline stage that + resolved values would. + +The "Deferred/Future Areas" section of the renderer specification should +be updated to remove transitions from its list and to reference this +specification. From f7bf4131ad1b29c82d6de4fd2cbe74805442a646 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 21:50:04 -0500 Subject: [PATCH 02/35] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20bump=20Clay=20submod?= =?UTF-8?q?ule=20to=20latest=20main=20(transitions=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clay | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clay b/clay index 76ec363..0896380 160000 --- a/clay +++ b/clay @@ -1 +1 @@ -Subproject commit 76ec3632d80c145158136fd44db501448e7b17c4 +Subproject commit 08963800a4db8a6980bf0f130f2e33ba88b096c4 From 5b4216fea6cac4d1b1c0989a7b6c24782cefcec4 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 21:51:32 -0500 Subject: [PATCH 03/35] =?UTF-8?q?=F0=9F=94=A7=20adapt=20clayterm=20to=20ne?= =?UTF-8?q?w=20Clay=20signatures=20(OpenTextElement,=20EndLayout)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/clayterm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clayterm.c b/src/clayterm.c index 2af9afd..069d105 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -577,7 +577,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { /* attrs byte -> alpha channel for render_text to extract */ config.textColor.a = (float)((cfg >> 24) & 0xff); - Clay__OpenTextElement(text, Clay__StoreTextElementConfig(config)); + Clay__OpenTextElement(text, config); break; } @@ -590,7 +590,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { } } - Clay_RenderCommandArray cmds = Clay_EndLayout(); + Clay_RenderCommandArray cmds = Clay_EndLayout(0.0f); /* reset output state */ ct->out.length = 0; From 272acd0b1fb910c0c472b23f6768219aa215f0e2 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 21:53:49 -0500 Subject: [PATCH 04/35] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20pin=20Clay=20to=2093?= =?UTF-8?q?8967a=20(work=20around=20upstream=20CLAY=5FWASM=5FEXPORT=20typo?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clay | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clay b/clay index 0896380..938967a 160000 --- a/clay +++ b/clay @@ -1 +1 @@ -Subproject commit 08963800a4db8a6980bf0f130f2e33ba88b096c4 +Subproject commit 938967ac9a62d3115bc25f8e4827cd46567f4bca From 04ae09cec7784b948e62f8bdc80c0c5cc2de62ec Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:28:12 -0500 Subject: [PATCH 05/35] =?UTF-8?q?=E2=9C=A8=20add=20deltaTime=20parameter?= =?UTF-8?q?=20to=20reduce()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/clayterm.c | 4 ++-- src/clayterm.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clayterm.c b/src/clayterm.c index 069d105..59faabe 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -467,7 +467,7 @@ struct Clayterm *init(void *mem, int w, int h) { return ct; } -void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime) { int i = 0; ct->error_count = 0; @@ -590,7 +590,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { } } - Clay_RenderCommandArray cmds = Clay_EndLayout(0.0f); + Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime); /* reset output state */ ct->out.length = 0; diff --git a/src/clayterm.h b/src/clayterm.h index 5065ed5..701c890 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -12,7 +12,7 @@ struct Clayterm; /* WASM exports */ int clayterm_size(int w, int h); struct Clayterm *init(void *mem, int w, int h); -void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row); +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime); char *output(struct Clayterm *ct); int length(struct Clayterm *ct); void measure(int ret, int txt); From db1a1243311072031b01bdcbdddf49a21c0b838e Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:28:59 -0500 Subject: [PATCH 06/35] =?UTF-8?q?=F0=9F=94=A7=20add=20deltaTime=20to=20Nat?= =?UTF-8?q?ive.reduce=20signature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- term-native.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/term-native.ts b/term-native.ts index 40e646d..370cabc 100644 --- a/term-native.ts +++ b/term-native.ts @@ -20,7 +20,7 @@ export interface Native { memory: WebAssembly.Memory; statePtr: number; opsBuf: number; - reduce(ct: number, buf: number, len: number, mode: number, row: number): void; + reduce(ct: number, buf: number, len: number, mode: number, row: number, deltaTime: number): void; output(ct: number): number; length(ct: number): number; setPointer(x: number, y: number, down: boolean): void; @@ -75,6 +75,7 @@ export async function createTermNative( len: number, mode: number, row: number, + deltaTime: number, ): void; output(ct: number): number; length(ct: number): number; From 00db8aa25d3e2c8004c2793ac4733b51dc6d55d8 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:30:12 -0500 Subject: [PATCH 07/35] =?UTF-8?q?=E2=9C=A8=20track=20deltaTime=20on=20Term?= =?UTF-8?q?,=20accept=20deltaTime=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- term.ts | 14 +++++++++++++- test/transitions.test.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/transitions.test.ts diff --git a/term.ts b/term.ts index 12517d0..b727820 100644 --- a/term.ts +++ b/term.ts @@ -25,6 +25,7 @@ export interface RenderOptions { y: number; down: boolean; }; + deltaTime?: number; } export type PointerEvent = @@ -78,13 +79,24 @@ export async function createTerm(options: TermOptions): Promise { let prev = new Set(); let pressed = new Set(); let wasDown = false; + let lastRenderAt: number | undefined; return { render(ops: Op[], options?: RenderOptions): RenderResult { let len = pack(ops, memory.buffer, opsBuf, memory.buffer.byteLength); let mode = options?.mode === "line" ? 1 : 0; let row = options?.row ?? 1; - native.reduce(statePtr, opsBuf, len, mode, row); + let now = performance.now() / 1000; + let dt: number; + if (options?.deltaTime !== undefined) { + dt = options.deltaTime; + } else if (lastRenderAt === undefined) { + dt = 0; + } else { + dt = now - lastRenderAt; + } + lastRenderAt = now; + native.reduce(statePtr, opsBuf, len, mode, row, dt); if (options?.pointer) { let { x, y, down } = options.pointer; diff --git a/test/transitions.test.ts b/test/transitions.test.ts new file mode 100644 index 0000000..5bf578a --- /dev/null +++ b/test/transitions.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "./suite.ts"; +import { close, createTerm, grow, open, text } from "../mod.ts"; + +describe("deltaTime", () => { + it("accepts explicit deltaTime without throwing", async () => { + let term = await createTerm({ width: 40, height: 10 }); + let result = term.render([ + open("root", { layout: { width: grow(), height: grow() } }), + text("hi"), + close(), + ], { deltaTime: 0.016 }); + expect(result.output).toBeInstanceOf(Uint8Array); + }); +}); From e486d56bfdf48b486a19dae4e0ecf67425a6d4e9 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:35:40 -0500 Subject: [PATCH 08/35] =?UTF-8?q?=E2=9C=A8=20add=20animating=5Fcount=20to?= =?UTF-8?q?=20Clayterm=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/clayterm.c | 4 ++++ src/clayterm.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/clayterm.c b/src/clayterm.c index 59faabe..b871526 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -51,6 +51,7 @@ struct Clayterm { /* error collection */ Clay_ErrorData errors[MAX_ERRORS]; int error_count; + int animating_count; }; /* Memory layout inside the arena provided by the host: @@ -470,6 +471,7 @@ struct Clayterm *init(void *mem, int w, int h) { void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime) { int i = 0; ct->error_count = 0; + ct->animating_count = 0; Clay_BeginLayout(); @@ -644,6 +646,8 @@ char *output(struct Clayterm *ct) { return ct->out.data; } int length(struct Clayterm *ct) { return ct->out.length; } +int animating(struct Clayterm *ct) { return ct->animating_count; } + int get_element_bounds(const char *name, int name_len, float *out) { Clay_String str = {.length = name_len, .chars = name}; Clay_ElementId eid = Clay__HashString(str, 0); diff --git a/src/clayterm.h b/src/clayterm.h index 701c890..4e7845e 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -15,6 +15,7 @@ struct Clayterm *init(void *mem, int w, int h); void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime); char *output(struct Clayterm *ct); int length(struct Clayterm *ct); +int animating(struct Clayterm *ct); void measure(int ret, int txt); int get_element_bounds(const char *name, int name_len, float *out); From 7b3afcb46c8f10341c68e8766520ac620c183e6b Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:36:28 -0500 Subject: [PATCH 09/35] =?UTF-8?q?=F0=9F=94=A7=20expose=20animating()=20via?= =?UTF-8?q?=20Native=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- term-native.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/term-native.ts b/term-native.ts index 370cabc..78d850f 100644 --- a/term-native.ts +++ b/term-native.ts @@ -26,6 +26,7 @@ export interface Native { setPointer(x: number, y: number, down: boolean): void; getPointerOverIds(): string[]; getElementBounds(id: string): BoundingBox | undefined; + animating(ct: number): number; errorCount(ct: number): number; errorType(ct: number, index: number): number; errorMessage(ct: number, index: number): string; @@ -84,6 +85,7 @@ export async function createTermNative( pointer_over_id_string_length(index: number): number; pointer_over_id_string_ptr(index: number): number; get_element_bounds(name: number, len: number, out: number): number; + animating(ct: number): number; error_count(ct: number): number; error_type(ct: number, index: number): number; error_message_length(ct: number, index: number): number; @@ -111,6 +113,7 @@ export async function createTermNative( reduce: ct.reduce, output: ct.output, length: ct.length, + animating: ct.animating as Native["animating"], setPointer(x: number, y: number, down: boolean) { let view = new DataView(memory.buffer); view.setFloat32(opsBuf, x, true); From 1aa74a38cb7adcdb43f52c6bdc1e546a8603d731 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 21 Apr 2026 22:37:22 -0500 Subject: [PATCH 10/35] =?UTF-8?q?=E2=9C=A8=20surface=20animating:=20boolea?= =?UTF-8?q?n=20on=20RenderResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- term.ts | 3 ++- test/transitions.test.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/term.ts b/term.ts index b727820..74a66ea 100644 --- a/term.ts +++ b/term.ts @@ -65,6 +65,7 @@ export interface RenderResult { events: PointerEvent[]; info: RenderInfo; errors: ClayError[]; + animating: boolean; } export interface Term { @@ -164,7 +165,7 @@ export async function createTerm(options: TermOptions): Promise { }); } - return { output, events, info, errors }; + return { output, events, info, errors, animating: native.animating(statePtr) > 0 }; }, }; } diff --git a/test/transitions.test.ts b/test/transitions.test.ts index 5bf578a..184db3f 100644 --- a/test/transitions.test.ts +++ b/test/transitions.test.ts @@ -12,3 +12,15 @@ describe("deltaTime", () => { expect(result.output).toBeInstanceOf(Uint8Array); }); }); + +describe("animating", () => { + it("reports animating=false for a static frame", async () => { + let term = await createTerm({ width: 40, height: 10 }); + let result = term.render([ + open("root", { layout: { width: grow(), height: grow() } }), + text("hi"), + close(), + ]); + expect(result.animating).toBe(false); + }); +}); From 0be2409f146465e24bfdd3935a6d7844b983a67c Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 11:14:22 -0500 Subject: [PATCH 11/35] =?UTF-8?q?=F0=9F=93=9D=20rewrite=20transitions=20sp?= =?UTF-8?q?ec=20for=20v1=20(Clay-supported=20subset)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- specs/transitions-spec.md | 566 ++++++++++++++++---------------------- 1 file changed, 240 insertions(+), 326 deletions(-) diff --git a/specs/transitions-spec.md b/specs/transitions-spec.md index 494dd79..1c957d4 100644 --- a/specs/transitions-spec.md +++ b/specs/transitions-spec.md @@ -1,6 +1,6 @@ # Clayterm Transitions Specification -**Version:** 0.1 (draft) **Status:** Design specification for a not-yet-implemented +**Version:** 0.1 (draft) **Status:** Design specification for a work-in-progress feature. Normative where it establishes invariants and contract. Descriptive where surfaces may settle during implementation. @@ -8,17 +8,25 @@ where surfaces may settle during implementation. ## 1. Purpose -A transition smoothly interpolates an element's visual properties over time. -This specification defines how transitions integrate with Clayterm's frame-snapshot -rendering model: how they are declared, how time is supplied, how enter and -exit behaviors are expressed, and how callers observe in-flight animation so -they can drive the render loop. +A transition smoothly interpolates an element's visual properties over time +when they change between frames. This specification defines how transitions +integrate with Clayterm's frame-snapshot rendering model: how they are +declared, how time is supplied, and how callers observe in-flight animation +so they can drive the render loop. Transitions are a first-class extension of the rendering contract defined in the [Clayterm Renderer Specification](renderer-spec.md). They do not change -the architectural model, do not introduce a component tree, and do not require -callers to hold cross-frame identity beyond the stable element identifiers they -already use. +the architectural model, do not introduce a component tree, and do not +require callers to hold cross-frame identity beyond the stable element +identifiers they already use. + +This specification covers what clayterm ships against the current upstream +Clay layout engine. Several capabilities that the rendering model naturally +invites — per-property easing, per-element enter/exit behaviors, custom +bezier easings — are intentionally excluded from v1 because the underlying +Clay API cannot express them without upstream changes that are still in +flight. Section 13 records these deferrals and the upstream dependencies +that unblock them. --- @@ -26,31 +34,32 @@ already use. ### In scope (normative) -- The transition model and its relationship to the frame-snapshot rendering contract +- The transition model and its relationship to the frame-snapshot rendering + contract - Time handling and the `deltaTime` convention - The animating signal returned from `render()` -- The declarative enter and exit model (no callbacks across the WASM boundary) - Element identity requirements for transitions - Cancellation semantics (as a consequence of the frame-snapshot model) ### In scope (non-normative, descriptive) -- The shape of the `transition` field on the `open()` directive (shorthand and longhand) -- The set of easing functions exposed in the initial surface +- The shape of the `transition` field on the `open()` directive +- The set of easing functions exposed in v1 +- The set of transition properties exposed in v1 - The wire encoding of transition data in the directive buffer - Interaction with line mode - Testing strategy -### Out of scope +### Out of scope (v1) -- Custom (JavaScript-authored) easing functions. Reserved for a future extension; - the enum space is designed not to preclude them. -- Proportional reversal (CSS-style dynamic shortening of duration when a - transition is cancelled mid-flight). -- Physics-based animation, spring interpolation, or keyframe sequences. -- Any framework-level concept of "animation groups," "timelines," or choreography - across multiple elements. Orchestration is a caller concern. -- Input parsing (see [Input Specification](input-spec.md)). +See Section 13 for the deferred features and their upstream unblockers. + +### Out of scope (indefinitely) + +- Physics-based animation, spring interpolation, keyframe sequences +- Framework-level concepts of "animation groups" or cross-element choreography + (orchestration is a caller concern) +- Input parsing (see [Input Specification](input-spec.md)) --- @@ -59,29 +68,15 @@ already use. **Transition.** A time-based interpolation of one or more of an element's visual properties between an initial value and a target value. -**Transition property.** A specific visual attribute of an element that can be -interpolated: position (x, y), size (width, height), background color, overlay -color, border color, border width, or corner radius. - -**Easing.** A function mapping normalized progress in [0, 1] to an eased value -in [0, 1]. Clayterm exposes a fixed set of built-in easings. - -**Transition state.** One of four modes an element can be in with respect to a -given transition: idle, entering (element newly mounted), transitioning -(property target changed on an existing element), or exiting (element removed -from the tree but still being animated out). - -**Enter transition.** The animation played when an element first appears in the -directive tree. Its initial state is derived from the element's target state -by applying caller-supplied deltas (e.g., offset position, transparent color). +**Transition property.** A specific visual attribute of an element that can +be interpolated: position (x, y), size (width, height), background color, +overlay color, border color, or border width. -**Exit transition.** The animation played when an element disappears from the -directive tree. Its final state is derived from the element's last-seen state -by applying caller-supplied deltas. The element is still rendered during its -exit even though it is no longer in the directive tree. +**Easing.** A function mapping normalized progress in [0, 1] to an eased +value in [0, 1]. Clayterm exposes a fixed set of built-in easings. -**Delta time (`deltaTime`).** The number of seconds elapsed since the previous -render transaction. Used by the renderer to advance interpolation. +**Delta time (`deltaTime`).** The number of seconds elapsed since the +previous render transaction. Used by the renderer to advance interpolation. **Animating signal.** A boolean flag in the render result indicating whether any transition is currently in progress. Callers use it to decide whether to @@ -96,43 +91,39 @@ _This section is normative._ ### 4.1 Relationship to the frame-snapshot model Transitions do not alter the frame-snapshot contract defined in INV-3 of the -renderer specification. The directive array still fully describes the desired -state for its frame. Transitions interpolate between the previous frame's -state and the current frame's target state; they do not reintroduce a -persistent component tree on the caller side. +renderer specification. The directive array still fully describes the +desired state for its frame. Transitions interpolate between the previous +frame's state and the current frame's target state; they do not reintroduce +a persistent component tree on the caller side. -What transitions add is the requirement that element identifiers remain stable -across frames for any element on which animation is desired. This is not a new -invariant — the existing pointer-event subsystem already relies on stable -identifiers — but it becomes load-bearing for transitions. +What transitions add is the requirement that element identifiers remain +stable across frames for any element on which animation is desired. This is +not a new invariant — the existing pointer-event subsystem already relies +on stable identifiers — but it becomes load-bearing for transitions. ### 4.2 Time ownership The `Term` instance is the sole source of frame-to-frame time. On each `render()` call, the Term reads a monotonic clock and computes the elapsed -seconds since the previous render. That value is passed to the layout engine -to advance any in-flight transitions. +seconds since the previous render. That value is passed to the layout +engine to advance any in-flight transitions. -The caller MAY override the computed delta via an explicit `deltaTime` option -on `render()`. Use cases include deterministic testing, snapshot rendering, -and compute-only renders where the caller is querying bounds without -displaying output. +The caller MAY override the computed delta via an explicit `deltaTime` +option on `render()`. Use cases include deterministic testing, snapshot +rendering, and compute-only renders where the caller is querying bounds +without displaying output. -The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). Wall-clock -time can move backward under NTP adjustments or DST, which would produce -negative deltas and corrupt interpolation. +The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). +Wall-clock time can move backward under NTP adjustments or DST, which would +produce negative deltas and corrupt interpolation. ### 4.3 Delta clamping Clayterm does not clamp `deltaTime`. Long gaps between frames (process -suspension, backgrounded terminal, debugger pause) produce large deltas. The -underlying interpolation is duration-based and naturally clamps at 1.0 of -progress, so a large delta causes in-flight transitions to complete rather -than to overshoot or become unstable. - -This differs from physics-based engines, which clamp deltas to prevent -tunneling. Transitions as specified here are not physics-based, so clamping -is unnecessary. +suspension, backgrounded terminal, debugger pause) produce large deltas. +The underlying interpolation is duration-based and naturally clamps at 1.0 +of progress, so a large delta causes in-flight transitions to complete +rather than to overshoot or become unstable. ### 4.4 Animation-loop signaling @@ -142,19 +133,19 @@ active, callers may stop rendering until the next external event (input, resize, application state change). This requirement exists because terminal applications typically render -on-demand rather than at a fixed refresh rate. Without an explicit animating -signal, a caller has no way to know that a transition it triggered is still -in progress. +on-demand rather than at a fixed refresh rate. Without an explicit +animating signal, a caller has no way to know that a transition it +triggered is still in progress. ### 4.5 Boundary preservation -Transitions MUST NOT require function pointers, callbacks, or other -non-serializable values to cross the TS→WASM boundary. Easing and -enter/exit initial-state computation are implemented on the C side using -declarative configuration carried in the directive buffer. +Transition configuration MUST be fully serializable. No function pointers, +closures, or callback registries cross the TS→WASM boundary during a +render transaction. This preserves INV-2 (single transaction per frame): one binary buffer in, -one result struct out. +one result struct out. On the C side, a fixed set of easing handlers is +pre-registered; the directive selects one by enum value. --- @@ -164,33 +155,35 @@ _This section is normative._ **INV-T1. Time is driven by delta, not wall clock.** All transition interpolation advances by `deltaTime`, a per-frame seconds value. The -renderer does not subscribe to an internal timer or schedule work of its own. +renderer does not subscribe to an internal timer or schedule work of its +own. -**INV-T2. Render remains pure under time override.** When the caller supplies -an explicit `deltaTime`, the render result depends only on the directive -array, the previous frame's cell buffer, and the supplied `deltaTime`. This -makes deterministic rendering possible for tests and snapshots. +**INV-T2. Render remains pure under time override.** When the caller +supplies an explicit `deltaTime`, the render result depends only on the +directive array, the previous frame's cell buffer, and the supplied +`deltaTime`. This makes deterministic rendering possible for tests and +snapshots. **INV-T3. No callbacks across the boundary.** Transition configuration MUST -be fully serializable. No function pointers, closures, or callback registries -cross the TS→WASM boundary during a render transaction. +be fully serializable. No function pointers, closures, or callback +registries cross the TS→WASM boundary during a render transaction. -**INV-T4. Identity is drawn from element IDs.** Transition state is associated -with elements by their declared `id`. Callers using transitions on an element -MUST assign it a stable, unique `id` across frames. Reusing an `id` for a -different logical element in a later frame is a caller error; behavior is -unspecified. +**INV-T4. Identity is drawn from element IDs.** Transition state is +associated with elements by their declared `id`. Callers using transitions +on an element MUST assign it a stable, unique `id` across frames. Reusing +an `id` for a different logical element in a later frame is a caller +error; behavior is unspecified. **INV-T5. Animating signal is accurate per transaction.** The `animating` -flag returned by `render()` reflects the state of transitions as of the end -of that transaction. If it is `true`, at least one transition has non-zero -remaining progress and calling `render()` again with positive `deltaTime` -will advance it. +flag returned by `render()` reflects the state of transitions as of the +end of that transaction. If it is `true`, at least one transition has +non-zero remaining progress and calling `render()` again with positive +`deltaTime` will advance it. **INV-T6. Cancellation is structural.** There is no imperative `cancel()` API. Transitions are cancelled by re-describing the previous target in a -later frame; the transition infrastructure re-anchors the interpolation from -the current visible value to the new target. +later frame; the transition infrastructure re-anchors the interpolation +from the current visible value to the new target. --- @@ -210,7 +203,7 @@ interface RenderOptions { mode?: "line"; row?: number; pointer?: { x, y, down }; - deltaTime?: number; // seconds; overrides Term's internal clock + deltaTime?: number; } ``` @@ -235,7 +228,7 @@ interface RenderResult { events: PointerEvent[]; info: RenderInfo; errors: ClayError[]; - animating: boolean; // NEW + animating: boolean; } ``` @@ -245,21 +238,20 @@ transition at the end of the transaction. ### 6.3 The `transition` field on `open()` An element may declare a transition by adding a `transition` field to its -open-element directive. The field is optional. Its absence means the element -has no transitions, which is the default. +open-element directive. The field is optional. Its absence means the +element has no transitions, which is the default. -The field accepts either shorthand or longhand form (Section 7). +See Section 7 for the shape. --- ## 7. Declarative Transition Surface -_This section is descriptive. The shapes may be revised during implementation, -but the architectural commitments above do not change._ +_This section is descriptive._ -### 7.1 Shorthand form +### 7.1 The `transition` field -All listed properties share one duration and one easing: +All listed properties share a single duration and a single easing. ```ts open("sidebar", { @@ -267,113 +259,57 @@ open("sidebar", { bg: rgba(30, 30, 30, 255), transition: { duration: 0.2, - easing: easeOut(), + easing: "easeOut", properties: ["x", "width", "bg"], - }, -}) -``` - -### 7.2 Longhand form - -Each property declares its own duration and easing independently: - -```ts -open("sidebar", { - transition: [ - { property: "x", duration: 0.3, easing: easeInOut() }, - { property: "width", duration: 0.3, easing: easeInOut() }, - { property: "bg", duration: 0.15, easing: easeOut() }, - ], -}) -``` - -The shorthand form is expanded to longhand during directive packing. The wire -encoding carries only longhand. - -### 7.3 Extended form (enter, exit, interaction handling) - -```ts -open("toast", { - transition: { - properties: [ - { property: "y", duration: 0.25, easing: easeOut() }, - { property: "bg", duration: 0.15, easing: linear() }, - ], - enter: { - independently: false, - from: { y: -2, bg: rgba(0, 0, 0, 0) }, - }, - exit: { - independently: false, - to: { y: -2, bg: rgba(0, 0, 0, 0) }, - paintOrder: "natural", - }, interactive: false, }, }) ``` -**`enter.from`** declares deltas relative to the element's target state. The -initial state used by the enter transition is `target + from`. A missing -`from` entry for a given property means the enter transition starts at the -target value for that property (no visible animation on that axis). - -**`exit.to`** declares deltas relative to the element's last-seen state. -The final state used by the exit transition is `initial + to`. - -**`enter.independently` / `exit.independently`** (default `false`) control -whether the element's enter/exit plays when its parent is also entering or -exiting in the same frame. The default couples the element to its parent: -child elements do not play their own enter/exit when the parent is itself -entering or exiting (this prevents cascaded animations when an entire -container mounts or unmounts). Setting `independently: true` opts in to -playing the animation unconditionally. +**`duration`** — seconds. Must be non-negative. -**`exit.paintOrder`** controls how an exiting element is drawn relative to -its reflowing siblings during the exit animation. One of: +**`easing`** — a string naming one of the built-in easing curves +(Section 7.2). Defaults to `"linear"` when omitted. -- `"natural"` (default) — paints in the element's natural DOM order. -- `"underSiblings"` — paints beneath siblings; reflowing neighbors cover the - exiting element. -- `"overSiblings"` — paints on top of siblings; the exiting element remains - visually prominent until its animation completes. +**`properties`** — list of property names to interpolate. Group names +(`position`, `size`, `all`) expand to the union of the underlying +properties. **`interactive`** (default `false`) — when `false`, pointer interactions with the element are disabled while a position transition is in progress. -When `true`, pointer interactions remain enabled throughout position -transitions. +When `true`, pointer interactions remain enabled throughout. -### 7.4 Easing helpers +### 7.2 Easing values -Exported from the top-level module: +The `easing` field takes one of four string values: ```ts -linear() -easeIn() -easeOut() -easeInOut() -cubicBezier(x1: number, y1: number, x2: number, y2: number) +type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut"; ``` -Each returns an `Easing` value: a tagged byte with optional parameters. The -easing enum space is deliberately larger than the current surface to allow -future additions (including a potential `custom()` form that bridges to a -JavaScript function) without breaking serialized frames. +Each value maps to a wire byte (see Section 8). The byte space is +deliberately larger than this set so additional easings can be added +later without breaking serialized frames. A future parametric easing +(e.g., cubic bezier) would extend the type to a discriminated union: +`"linear" | "easeIn" | ... | { cubicBezier: [number, number, number, number] }`. +Today all values are non-parametric, so the type is a plain string union. -### 7.5 Property names +### 7.3 Property names ```ts type TransitionProperty = | "x" | "y" | "position" | "width" | "height" | "size" | "bg" | "overlay" | "borderColor" - | "cornerRadius" | "borderWidth" + | "borderWidth" | "all"; ``` -Group names (`position`, `size`, `all`) expand to the underlying property -set during packing and are equivalent to listing the constituent properties -explicitly in longhand form. +Group names expand as follows: + +- `position` → `x`, `y` +- `size` → `width`, `height` +- `all` → every individual property above --- @@ -382,52 +318,44 @@ explicitly in longhand form. _This section is descriptive._ The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. -Its presence is indicated in the element's property bitmask (existing -mechanism for optional fields). When present, its layout is: +Its presence is indicated by a bit in the open-element property mask. +When present, the block is a fixed 8-byte record: ``` transition_block { - flags: u8 // bit 0: enter present - // bit 1: exit present - // bit 2: interactive (0 = disabled, 1 = enabled) - entry_count: u8 // number of property_transition entries - entries: property_transition[] // entry_count entries, in stable property order - enter?: transition_side // present iff flags bit 0 - exit?: transition_side // present iff flags bit 1 + duration: f32 // seconds, non-negative + properties: u16 // Clay-native bitmask (see below) + easing: u8 // easing kind (0 = linear, 1 = easeIn, 2 = easeOut, 3 = easeInOut) + flags: u8 // bit 0: interactive (0 = disable, 1 = allow) } +``` -property_transition { - property: u16 // single-bit mask from Clay's property enum - duration: f32 // seconds, non-negative - easing: u8 // easing kind - params: f32[0 or 4] // 4 floats iff easing == cubicBezier -} +The `properties` value is the Clay transition property bitmask: -transition_side { - flags: u8 // bit 0: independently - // bits 1-2: paintOrder (exit only: 0 natural, 1 under, 2 over) - mask: u16 // which properties have deltas - values: bytes // packed in stable property order; widths per property -} +``` +CLAY_TRANSITION_PROPERTY_X = 1 +CLAY_TRANSITION_PROPERTY_Y = 2 +CLAY_TRANSITION_PROPERTY_WIDTH = 4 +CLAY_TRANSITION_PROPERTY_HEIGHT = 8 +CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR = 16 +CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR = 32 +CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128 +CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256 ``` -Value widths are property-specific: `f32` for position and size, `u32` for -colors, `u8[4]` for border widths, `u8[4]` for corner radii (8-bit -resolution per corner is consistent with the existing cornerRadius -encoding). +(Value 64, `CLAY_TRANSITION_PROPERTY_CORNER_RADIUS`, is defined upstream +but has no field in `Clay_TransitionData` and is not emitted by clayterm.) -The shorthand form is never present on the wire. TS fans shorthand out to -per-property longhand entries before packing. The C side sees only longhand. +The property-name helpers on the TS side expand to this bitmask during +packing. ### 8.1 Validation -The existing `validate()` utility gains checks: +`validate()` checks: -- `duration >= 0` for every entry. -- `easing` is one of the defined enum values. -- Property names in entries are valid and appear at most once. -- Property names in `enter.from` / `exit.to` are a subset of the entries - (deltas for a property not being transitioned are ignored or flagged). +- `duration >= 0`. +- `easing` is one of the defined enum values (0-3). +- Property names are from the defined set (Section 7.3). --- @@ -455,25 +383,19 @@ state rather than an imperative operation. ## 10. Interaction with Line Mode -_This section is descriptive; the concrete behavior will be finalized -during implementation._ +_This section is descriptive._ Line mode emits cells as newline-separated rows without absolute cursor positioning. Position transitions (`x`, `y`) have no meaningful effect in -this mode: the rendering output places each row at the current cursor, -not at absolute coordinates. +this mode: rows are placed at the current cursor, not at absolute +coordinates. Expected behavior in line mode: - Color and size transitions proceed normally. -- Position transitions are silently skipped (treated as if the property is - not being transitioned for that frame). -- Enter/exit transitions that declare `from` or `to` deltas on position - properties have those position deltas dropped; other delta properties - still apply. - -The `animating` signal reports accurately regardless of mode; line-mode -color or size transitions still report as animating. +- Position transitions are silently skipped (the property bits for x and y + are cleared before the configuration reaches Clay). +- The `animating` signal reports accurately regardless of mode. --- @@ -486,23 +408,22 @@ A test sequence looks like: ```ts term.render(opsA, { deltaTime: 0 }); -term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed -term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition -term.render(opsB, { deltaTime: 0.1 }); // 100%, completed +term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed +term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition +term.render(opsB, { deltaTime: 0.1 }); // 100%, completed ``` Test coverage should include, at minimum: -- Shorthand and longhand produce identical output for equivalent configs. -- Enter transitions with `independently: true` and `false`. -- Exit transitions with each `paintOrder` value. -- Cancellation: target change mid-flight re-anchors initial to current. -- Re-appearance during an exit transition. -- Transition config present one frame and absent the next. -- Multiple concurrent transitions on a single element (longhand). +- Property change mid-stream interpolates and completes. +- `animating` is false on static frames, true during interpolation, false + again when the transition completes. +- Mid-transition target change re-anchors initial to current value. - Multiple concurrent transitions on multiple elements. -- Line mode rendering: color and size transitions apply, position transitions - are silently skipped. +- Line mode: color and size transitions apply, position transitions are + silently skipped. +- Each easing enum produces distinct progression (linear, easeIn, easeOut, + easeInOut). --- @@ -510,127 +431,120 @@ Test coverage should include, at minimum: _This section is descriptive and may change without affecting contract._ -### 12.1 Clay submodule version +### 12.1 Clay submodule pin -clayterm currently pins Clay at commit `76ec363`. The transition API was -introduced upstream in commit `ee192f4`, with follow-up bug fixes. Before -implementing transitions, the Clay submodule must be advanced to a post- -`ee192f4` commit. Non-transition Clay changes introduced between the current -pin and the target pin — notably the `Clay_OnHover` signature change and the -element ID scheme split — require an audit of existing clayterm integration. - -Upgrading Clay is a prerequisite and should be treated as its own commit -ahead of transition work. +clayterm pins Clay at a specific commit that includes the transition API +introduced upstream in commit `ee192f4`. The pin is recorded in the `clay` +submodule pointer. Advancing the pin is a prerequisite when upstream adds +capabilities clayterm depends on (Section 13). ### 12.2 Handler architecture -Each `Term` registers a single C-side transition handler with Clay. -Per-element transition metadata (per-property duration, easing, easing -params, enter deltas, exit deltas) is stored in a side table keyed by -Clay element ID, owned by the Term's context. +Each `Term` registers one C-side transition handler per easing kind (four +total for v1: linear, easeIn, easeOut, easeInOut). At element-configuration +time the decoder selects the handler matching the element's easing enum +and stores it on the `Clay_TransitionElementConfig`. -The handler: +Each handler: -1. Resolves the active Term context. -2. Looks up metadata for the element by its Clay ID. -3. For each property in the active bitmask, computes local progress as - `clamp(elapsedTime / property.duration, 0, 1)`, applies the property's - easing, writes the interpolated value into the output struct. -4. Increments the Term context's `animating_count`. -5. Returns `true` if any property's local progress is below 1.0. +1. Computes progress as `clamp(elapsedTime / duration, 0, 1)`. +2. Applies its easing curve to progress. +3. Lerps each property named in the `properties` bitmask from `initial` to + `target`. +4. Increments the Term context's `animating_count` unless progress is 1.0. +5. Returns `true` if progress is 1.0 (transition complete), `false` + otherwise. -At the start of each `render()`, the Term resets its `animating_count` to +At the start of each `render()`, the Term resets `animating_count` to zero. At the end, the value is copied into the result struct as the -`animating` flag (true if count > 0). - -The `setInitialState` and `setFinalState` callbacks Clay expects are -implemented as fixed C functions that apply the per-element `from` / `to` -deltas from the side table to the target / initial state Clay passes in. +`animating` flag (`true` if count > 0). -### 12.3 Per-element storage lifetime +### 12.3 Per-Term isolation -Metadata is repopulated each frame during directive unpacking. Clay's -handler is invoked synchronously inside `Clay_EndLayout`, so per-frame -metadata remains valid when the handler fires. No metadata needs to persist -across frames on our side; Clay's internal hashmap persists the actual -transition state (elapsed time, current value, state machine phase). +The `animating_count` lives on the Term's C-side context, not as +module-level state. Multiple Terms created in the same process remain +isolated. -### 12.4 Multiple Term instances +### 12.4 Resolving the active Term inside the handler -`animating_count` and the metadata side table live on the Term's C-side -context, not as module-level state. Multiple Terms created in the same -process remain isolated. +Clay's transition-handler signature does not carry a `userData` pointer or +element ID. Each `reduce()` call records the currently-active Term pointer +in a module-level variable (`ct_active_context`) and clears it at the end. +The handler reads this variable to reach the Term's `animating_count`. A +single render pass cannot overlap with another (renders are synchronous), +so there is no concurrency concern. --- -## 13. Open Questions +## 13. Deferred Until Upstream Clay -These items remain undecided and will be resolved during implementation. -They do not affect the contract. +These capabilities are intentionally not in v1 because the required Clay +primitives are either missing or in flight upstream. The absence is +motivated; re-adding them is straightforward once Clay lands the pieces. -### 13.1 First-frame delta +### 13.1 Per-property easing and duration -On the very first `render()` after `createTerm()`, there is no previous -frame to compute a delta against. Clay's own behavior on its first -`Clay_EndLayout(deltaTime)` call (with a non-zero delta) is the source of -truth: clayterm will pass through whatever delta it has computed and adopt -whatever Clay does. Verification and documentation occur during -integration. +The directive API could allow each property to have its own duration and +easing (e.g., "fade bg in 150ms, slide x in 300ms"). Clay's +`Clay_TransitionElementConfig` carries a single `duration`, a single +`handler`, and a single `properties` bitmask per element, so the handler +has no way to distinguish per-property timing. Working around this +requires per-element metadata addressable from inside the handler. -### 13.2 Mid-transition target change +**Unblocked by:** Clay adding `void* userData` to the transition +arguments (upstream PR +[nicbarker/clay#603](https://github.com/nicbarker/clay/pull/603)). -The cancellation semantics in Section 9 require that a target change -mid-flight re-anchors `initial` to the current visible value. Clay's -`TRANSITIONING` state machine is expected to handle this, but it must be -verified. If Clay does not re-anchor, our handler adds the logic by -tracking the last-seen target per element. +### 13.2 Enter and exit transitions -### 13.3 Element re-appearance mid-exit +Elements mounted or removed between frames cannot express per-element +initial or final state deltas. Clay exposes `setInitialState` and +`setFinalState` callbacks with signatures that take no element identifier +or user pointer, so there is no way to look up per-element deltas from +inside the callbacks. Additionally, exit transitions require their +configuration to survive past the frame on which the element was last +declared, which requires a lifetime signal. -If an element is exiting and reappears in the next frame's directives, -the expected behavior is to cancel the exit and interpolate from the -current visible state to the new target. Implementation-dependent on Clay. +**Unblocked by:** -### 13.4 Transition removed mid-flight +- Clay `userData` on transition arguments (PR #603, above). +- An exit-completion callback or an `exiting` flag on the render command, + both of which have been discussed upstream with Clay's maintainer as + forthcoming. -If an element has a transition one frame and the `transition` field is -absent in the next frame, Clay's behavior for in-flight transitions -determines the outcome. Two reasonable options: (a) in-flight transitions -complete using their original config; (b) they freeze at their current -value. Deferred to Clay's observed behavior. Documented once verified. +### 13.3 `cubicBezier` easing -### 13.5 Custom easing escape hatch +Custom cubic-bezier curves need per-element control-point parameters, and +Clay's fixed handler signature has no mechanism to thread parameters to a +shared handler. -The easing enum space is deliberately larger than the initial surface. A -future `custom()` easing that bridges to a JavaScript function is -anticipated but not specified here. Its design must preserve INV-T3 -(no callbacks across the boundary during a render transaction) — likely -via a pre-sampled lookup table supplied in the directive buffer. +**Unblocked by:** the same Clay `userData` addition as 13.1. + +### 13.4 Corner-radius transitions + +`CLAY_TRANSITION_PROPERTY_CORNER_RADIUS` is defined in the Clay property +enum, but `Clay_TransitionData` has no field carrying corner radius. +Upstream `Clay_EaseOut` does not interpolate it. Clayterm cannot either. + +**Unblocked by:** Clay adding a `cornerRadius` field to +`Clay_TransitionData` and interpolating it in layout. --- ## 14. Demos -Two demos accompany the feature: - -1. **`demo/transitions.ts`** — a clayterm-native demo meaningfully - exercising transitions in a terminal context (e.g., a collapsing - sidebar, a list reorder, or a toast notification). Primary purpose: - surface real-world sharp edges in the API. +One demo accompanies v1: -2. **A reproduction of Clay's upstream `raylib-transitions` demo** — - the example that accompanied the Clay transition-API commit - (`ee192f4`). Primary purpose: provide a reference implementation - that can be visually compared to upstream, validating that the - clayterm integration faithfully exercises the full transition API - surface. +**`demo/transitions.ts`** — exercises v1 transitions meaningfully in a +terminal context (e.g., a collapsing sidebar or a colored highlight that +fades between states). Purpose: surface real-world API sharp edges. --- ## Appendix A. Relationship to the Renderer Specification -This specification extends, but does not modify, the renderer specification. -Specifically: +This specification extends, but does not modify, the renderer +specification. Specifically: - **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock for `deltaTime` computation. A clock read is not terminal IO and does @@ -654,5 +568,5 @@ Specifically: resolved values would. The "Deferred/Future Areas" section of the renderer specification should -be updated to remove transitions from its list and to reference this -specification. +be updated to reference this specification rather than list transitions +as a single bullet. From c2395439270004ddb130b28b86dac8f4f5446cf3 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 11:15:17 -0500 Subject: [PATCH 12/35] =?UTF-8?q?=E2=9C=A8=20add=20transition=20property?= =?UTF-8?q?=20names,=20bitmask=20helpers,=20and=20Easing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mod.ts | 1 + ops-transitions.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 ops-transitions.ts diff --git a/mod.ts b/mod.ts index 8862d13..4a5f09a 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1,5 @@ export * from "./ops.ts"; +export * from "./ops-transitions.ts"; export * from "./term.ts"; export * from "./input.ts"; export * from "./settings.ts"; diff --git a/ops-transitions.ts b/ops-transitions.ts new file mode 100644 index 0000000..ce5bd71 --- /dev/null +++ b/ops-transitions.ts @@ -0,0 +1,53 @@ +export type TransitionProperty = + | "x" | "y" | "position" + | "width" | "height" | "size" + | "bg" | "overlay" | "borderColor" + | "borderWidth" + | "all"; + +export const TP_X = 1; +export const TP_Y = 2; +export const TP_WIDTH = 4; +export const TP_HEIGHT = 8; +export const TP_BG = 16; +export const TP_OVERLAY = 32; +export const TP_BORDER_COLOR = 128; +export const TP_BORDER_WIDTH = 256; + +export const TP_POSITION = TP_X | TP_Y; +export const TP_SIZE = TP_WIDTH | TP_HEIGHT; +export const TP_ALL = + TP_X | TP_Y | TP_WIDTH | TP_HEIGHT | + TP_BG | TP_OVERLAY | TP_BORDER_COLOR | TP_BORDER_WIDTH; + +export function propertyMask(name: TransitionProperty): number { + switch (name) { + case "x": return TP_X; + case "y": return TP_Y; + case "position": return TP_POSITION; + case "width": return TP_WIDTH; + case "height": return TP_HEIGHT; + case "size": return TP_SIZE; + case "bg": return TP_BG; + case "overlay": return TP_OVERLAY; + case "borderColor": return TP_BORDER_COLOR; + case "borderWidth": return TP_BORDER_WIDTH; + case "all": return TP_ALL; + } +} + +export type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut"; + +export const EASING_LINEAR = 0; +export const EASING_EASE_IN = 1; +export const EASING_EASE_OUT = 2; +export const EASING_EASE_IN_OUT = 3; + +export function easingByte(easing: Easing): number { + switch (easing) { + case "linear": return EASING_LINEAR; + case "easeIn": return EASING_EASE_IN; + case "easeOut": return EASING_EASE_OUT; + case "easeInOut": return EASING_EASE_IN_OUT; + } +} From 015349cc7f4ff572a28d93987cc3368de6197fc0 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 11:16:08 -0500 Subject: [PATCH 13/35] =?UTF-8?q?=E2=9C=A8=20add=20transition=20field=20ty?= =?UTF-8?q?pe=20to=20OpenElement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops-transitions.ts | 7 +++++++ ops.ts | 3 +++ 2 files changed, 10 insertions(+) diff --git a/ops-transitions.ts b/ops-transitions.ts index ce5bd71..ad636fd 100644 --- a/ops-transitions.ts +++ b/ops-transitions.ts @@ -51,3 +51,10 @@ export function easingByte(easing: Easing): number { case "easeInOut": return EASING_EASE_IN_OUT; } } + +export interface Transition { + duration: number; + easing?: Easing; + properties: TransitionProperty[]; + interactive?: boolean; +} diff --git a/ops.ts b/ops.ts index 3344eea..db266e3 100644 --- a/ops.ts +++ b/ops.ts @@ -1,3 +1,5 @@ +import type { Transition } from "./ops-transitions.ts"; + /* Command buffer opcodes — mirrors ops.h */ const OP_OPEN_ELEMENT = 0x02; const OP_TEXT = 0x03; @@ -269,6 +271,7 @@ export interface OpenElement { attachPoints?: number; zIndex?: number; }; + transition?: Transition; } export interface Text { From 7eea06930856c74766ed8917c8263a0092d7cc9d Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 11:17:38 -0500 Subject: [PATCH 14/35] =?UTF-8?q?=E2=9C=A8=20encode=20transition=20block?= =?UTF-8?q?=20in=20pack()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops.ts | 18 +++++++++++++++ test/transitions-pack.test.ts | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/transitions-pack.test.ts diff --git a/ops.ts b/ops.ts index db266e3..bb363fb 100644 --- a/ops.ts +++ b/ops.ts @@ -1,4 +1,5 @@ import type { Transition } from "./ops-transitions.ts"; +import { easingByte, propertyMask } from "./ops-transitions.ts"; /* Command buffer opcodes — mirrors ops.h */ const OP_OPEN_ELEMENT = 0x02; @@ -12,6 +13,7 @@ const PROP_CORNER_RADIUS = 0x04; const PROP_BORDER = 0x08; const PROP_CLIP = 0x10; const PROP_FLOATING = 0x20; +const PROP_TRANSITION = 0x40; const encoder = new TextEncoder(); @@ -93,6 +95,7 @@ export function pack( if (op.border) mask |= PROP_BORDER; if (op.clip) mask |= PROP_CLIP; if (op.floating) mask |= PROP_FLOATING; + if (op.transition) mask |= PROP_TRANSITION; view.setUint32(o, mask, true); o += 4; @@ -175,6 +178,21 @@ export function pack( ); o += 4; } + + if (op.transition) { + let t = op.transition; + let pmask = 0; + for (let name of t.properties) pmask |= propertyMask(name); + + view.setFloat32(o, t.duration, true); + o += 4; + view.setUint16(o, pmask, true); + o += 2; + view.setUint8(o, easingByte(t.easing ?? "linear")); + o += 1; + view.setUint8(o, t.interactive ? 1 : 0); + o += 1; + } break; } diff --git a/test/transitions-pack.test.ts b/test/transitions-pack.test.ts new file mode 100644 index 0000000..885a89a --- /dev/null +++ b/test/transitions-pack.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "./suite.ts"; +import { close, open, pack } from "../mod.ts"; + +describe("pack transition", () => { + it("encodes a transition without throwing", () => { + let mem = new ArrayBuffer(4096); + let len = pack( + [ + open("a", { + transition: { + duration: 0.2, + easing: "easeOut", + properties: ["x", "bg"], + }, + }), + close(), + ], + mem, + 0, + 4096, + ); + expect(len).toBeGreaterThan(0); + }); + + it("writes a longer buffer when a transition is present", () => { + let mem1 = new ArrayBuffer(4096); + let withoutLen = pack([open("a", {}), close()], mem1, 0, 4096); + let mem2 = new ArrayBuffer(4096); + let withLen = pack( + [ + open("a", { transition: { duration: 0.2, properties: ["x"] } }), + close(), + ], + mem2, + 0, + 4096, + ); + expect(withLen).toBeGreaterThan(withoutLen); + // The transition block is exactly 8 bytes = 2 words. + expect(withLen - withoutLen).toBe(2); + }); +}); From c18a97b2e0921d2406d4c4240feb093a85ef433d Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 11:22:11 -0500 Subject: [PATCH 15/35] =?UTF-8?q?=E2=9C=A8=20register=20Clay=20handlers,?= =?UTF-8?q?=20interpolate=20on=20property=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: --- src/clayterm.c | 22 ++++++ src/module.c | 1 + src/transitions.c | 131 +++++++++++++++++++++++++++++++++++ src/transitions.h | 19 +++++ test/transitions-run.test.ts | 51 ++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 src/transitions.c create mode 100644 src/transitions.h create mode 100644 test/transitions-run.test.ts diff --git a/src/clayterm.c b/src/clayterm.c index b871526..6984633 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -12,6 +12,7 @@ */ #include "clayterm.h" +#include "transitions.h" #include "../clay/clay.h" #include "buffer.h" #include "cell.h" @@ -19,6 +20,8 @@ #include "utf8.h" #include "wcwidth.h" +struct Clayterm *ct_active_context = NULL; + /* ── Command buffer protocol ──────────────────────────────────────── */ #define OP_BEGIN_LAYOUT 0x01 @@ -33,6 +36,7 @@ #define PROP_BORDER 0x08 #define PROP_CLIP 0x10 #define PROP_FLOATING 0x20 +#define PROP_TRANSITION 0x40 /* ── Instance state ───────────────────────────────────────────────── */ @@ -470,6 +474,7 @@ struct Clayterm *init(void *mem, int w, int h) { void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime) { int i = 0; + ct_active_context = ct; ct->error_count = 0; ct->animating_count = 0; @@ -557,6 +562,21 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, floa decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); } + if (mask & PROP_TRANSITION) { + float duration = rdf(buf, len, &i); + uint32_t props_and_flags = rd(buf, len, &i); + uint16_t props = props_and_flags & 0xFFFF; + uint8_t easing = (props_and_flags >> 16) & 0xFF; + uint8_t interactive = (props_and_flags >> 24) & 0xFF; + + decl.transition.handler = ct_handler_for(easing); + decl.transition.duration = duration; + decl.transition.properties = (Clay_TransitionProperty)props; + decl.transition.interactionHandling = interactive + ? CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION + : CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION; + } + Clay__ConfigureOpenElement(decl); break; } @@ -640,6 +660,8 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, floa } else { present_cups(ct, row); } + + ct_active_context = NULL; } char *output(struct Clayterm *ct) { return ct->out.data; } diff --git a/src/module.c b/src/module.c index 709884d..bca0757 100644 --- a/src/module.c +++ b/src/module.c @@ -8,5 +8,6 @@ #include "utf8.c" #include "wcwidth.c" #include "clayterm.c" +#include "transitions.c" #include "trie.c" #include "input.c" diff --git a/src/transitions.c b/src/transitions.c new file mode 100644 index 0000000..6c7d15e --- /dev/null +++ b/src/transitions.c @@ -0,0 +1,131 @@ +#include "transitions.h" +#include "clayterm.h" + +extern struct Clayterm *ct_active_context; + +static float clampf(float v, float lo, float hi) { + if (v < lo) { + return lo; + } else if (v > hi) { + return hi; + } else { + return v; + } +} + +static float ease_in(float t) { + return t * t; +} + +static float ease_out(float t) { + float inv = 1.0f - t; + return 1.0f - inv * inv; +} + +static float ease_in_out(float t) { + if (t < 0.5f) { + return 2.0f * t * t; + } else { + float inv = 1.0f - t; + return 1.0f - 2.0f * inv * inv; + } +} + +static float lerpf(float a, float b, float t) { + return a + (b - a) * t; +} + +static Clay_Color lerp_color(Clay_Color a, Clay_Color b, float t) { + Clay_Color out; + out.r = lerpf(a.r, b.r, t); + out.g = lerpf(a.g, b.g, t); + out.b = lerpf(a.b, b.b, t); + out.a = lerpf(a.a, b.a, t); + return out; +} + +static bool apply(Clay_TransitionCallbackArguments args, float eased, bool done) { + if (args.properties & CLAY_TRANSITION_PROPERTY_X) { + args.current->boundingBox.x = + lerpf(args.initial.boundingBox.x, args.target.boundingBox.x, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_Y) { + args.current->boundingBox.y = + lerpf(args.initial.boundingBox.y, args.target.boundingBox.y, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_WIDTH) { + args.current->boundingBox.width = + lerpf(args.initial.boundingBox.width, args.target.boundingBox.width, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_HEIGHT) { + args.current->boundingBox.height = + lerpf(args.initial.boundingBox.height, args.target.boundingBox.height, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) { + args.current->backgroundColor = + lerp_color(args.initial.backgroundColor, args.target.backgroundColor, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) { + args.current->overlayColor = + lerp_color(args.initial.overlayColor, args.target.overlayColor, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) { + args.current->borderColor = + lerp_color(args.initial.borderColor, args.target.borderColor, eased); + } + if (args.properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) { + args.current->borderWidth.left = + (uint16_t)lerpf(args.initial.borderWidth.left, args.target.borderWidth.left, eased); + args.current->borderWidth.right = + (uint16_t)lerpf(args.initial.borderWidth.right, args.target.borderWidth.right, eased); + args.current->borderWidth.top = + (uint16_t)lerpf(args.initial.borderWidth.top, args.target.borderWidth.top, eased); + args.current->borderWidth.bottom = + (uint16_t)lerpf(args.initial.borderWidth.bottom, args.target.borderWidth.bottom, eased); + args.current->borderWidth.betweenChildren = + (uint16_t)lerpf(args.initial.borderWidth.betweenChildren, args.target.borderWidth.betweenChildren, eased); + } + if (ct_active_context && !done) { + ct_active_context->animating_count++; + } + return done; +} + +static float progress(Clay_TransitionCallbackArguments args) { + if (args.duration <= 0.0f) { + return 1.0f; + } else { + return clampf(args.elapsedTime / args.duration, 0.0f, 1.0f); + } +} + +bool ct_handler_linear(Clay_TransitionCallbackArguments args) { + float p = progress(args); + return apply(args, p, p >= 1.0f); +} + +bool ct_handler_ease_in(Clay_TransitionCallbackArguments args) { + float p = progress(args); + return apply(args, ease_in(p), p >= 1.0f); +} + +bool ct_handler_ease_out(Clay_TransitionCallbackArguments args) { + float p = progress(args); + return apply(args, ease_out(p), p >= 1.0f); +} + +bool ct_handler_ease_in_out(Clay_TransitionCallbackArguments args) { + float p = progress(args); + return apply(args, ease_in_out(p), p >= 1.0f); +} + +bool (*ct_handler_for(int kind))(Clay_TransitionCallbackArguments) { + switch (kind) { + case CT_EASING_EASE_IN: return ct_handler_ease_in; + case CT_EASING_EASE_OUT: return ct_handler_ease_out; + case CT_EASING_EASE_IN_OUT: return ct_handler_ease_in_out; + case CT_EASING_LINEAR: + default: + return ct_handler_linear; + } +} diff --git a/src/transitions.h b/src/transitions.h new file mode 100644 index 0000000..4a68e43 --- /dev/null +++ b/src/transitions.h @@ -0,0 +1,19 @@ +#ifndef CLAYTERM_TRANSITIONS_H +#define CLAYTERM_TRANSITIONS_H + +#include +#include "../clay/clay.h" + +#define CT_EASING_LINEAR 0 +#define CT_EASING_EASE_IN 1 +#define CT_EASING_EASE_OUT 2 +#define CT_EASING_EASE_IN_OUT 3 + +bool ct_handler_linear(Clay_TransitionCallbackArguments args); +bool ct_handler_ease_in(Clay_TransitionCallbackArguments args); +bool ct_handler_ease_out(Clay_TransitionCallbackArguments args); +bool ct_handler_ease_in_out(Clay_TransitionCallbackArguments args); + +bool (*ct_handler_for(int kind))(Clay_TransitionCallbackArguments); + +#endif diff --git a/test/transitions-run.test.ts b/test/transitions-run.test.ts new file mode 100644 index 0000000..184084a --- /dev/null +++ b/test/transitions-run.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "./suite.ts"; +import { + close, + createTerm, + fixed, + grow, + open, + rgba, + type Op, +} from "../mod.ts"; + +describe("transition lifecycle", () => { + it("animates bg change between frames", async () => { + let term = await createTerm({ width: 20, height: 5 }); + let frame = (bg: number): Op[] => [ + open("box", { + layout: { width: fixed(10), height: fixed(3) }, + bg, + transition: { duration: 0.2, easing: "easeInOut", properties: ["bg"] }, + }), + close(), + ]; + + let r0 = term.render(frame(rgba(255, 0, 0)), { deltaTime: 0 }); + expect(r0.animating).toBe(false); + + term.render(frame(rgba(0, 0, 255)), { deltaTime: 0 }); + let mid = term.render(frame(rgba(0, 0, 255)), { deltaTime: 0.1 }); + expect(mid.animating).toBe(true); + + term.render(frame(rgba(0, 0, 255)), { deltaTime: 0.15 }); + let done = term.render(frame(rgba(0, 0, 255)), { deltaTime: 0.05 }); + expect(done.animating).toBe(false); + }); + + it("reports animating=false when duration is 0", async () => { + let term = await createTerm({ width: 10, height: 3 }); + let frame = (bg: number): Op[] => [ + open("box", { + layout: { width: fixed(5), height: fixed(2) }, + bg, + transition: { duration: 0, properties: ["bg"] }, + }), + close(), + ]; + + term.render(frame(rgba(255, 0, 0)), { deltaTime: 0 }); + let r = term.render(frame(rgba(0, 0, 255)), { deltaTime: 0.1 }); + expect(r.animating).toBe(false); + }); +}); From cbd6109da5595e7975ad36ea64ce1baa4699c0c8 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 19:34:07 -0500 Subject: [PATCH 16/35] =?UTF-8?q?=E2=9C=A8=20reset=20deltaTime=20to=200=20?= =?UTF-8?q?after=20idle=20(preserve=20transitions=20across=20long=20gaps)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/transitions-spec.md | 18 +++++++++++++++--- term.ts | 7 +++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/specs/transitions-spec.md b/specs/transitions-spec.md index 1c957d4..6b9cf92 100644 --- a/specs/transitions-spec.md +++ b/specs/transitions-spec.md @@ -108,6 +108,16 @@ The `Term` instance is the sole source of frame-to-frame time. On each seconds since the previous render. That value is passed to the layout engine to advance any in-flight transitions. +If the previous render reported `animating=false`, the Term passes +`deltaTime=0` to the layout engine on the current render, regardless of +wall-clock time elapsed. The rationale: Clay is delta-based and has no +concept of when a transition began. Idle time between renders must not +count toward any subsequent transition's elapsed clock, otherwise a long +idle gap followed by a mutation would cause the transition to complete +instantly. Passing `deltaTime=0` on the first frame of any new transition +gives it a clean elapsed=0 starting point; real deltas resume once the +previous render signals `animating=true`. + The caller MAY override the computed delta via an explicit `deltaTime` option on `render()`. Use cases include deterministic testing, snapshot rendering, and compute-only renders where the caller is querying bounds @@ -209,9 +219,11 @@ interface RenderOptions { Each `render()` call advances transitions by its `deltaTime`: -- If `deltaTime` is omitted, Term computes it as the monotonic wall-clock - time elapsed since the previous `render()` call. -- If `deltaTime` is provided, it is used verbatim for that frame. +- If `deltaTime` is provided explicitly, it is used verbatim. +- Otherwise, if the previous render reported `animating=false`, + `deltaTime=0` (see §4.2 for rationale). +- Otherwise, `deltaTime` is the monotonic wall-clock time elapsed since + the previous `render()` call. On every `render()` call, Term captures the current monotonic timestamp as the reference point for the next implicit delta. The two modes can be diff --git a/term.ts b/term.ts index 74a66ea..db61018 100644 --- a/term.ts +++ b/term.ts @@ -81,6 +81,7 @@ export async function createTerm(options: TermOptions): Promise { let pressed = new Set(); let wasDown = false; let lastRenderAt: number | undefined; + let wasAnimating = false; return { render(ops: Op[], options?: RenderOptions): RenderResult { @@ -91,7 +92,7 @@ export async function createTerm(options: TermOptions): Promise { let dt: number; if (options?.deltaTime !== undefined) { dt = options.deltaTime; - } else if (lastRenderAt === undefined) { + } else if (!wasAnimating || lastRenderAt === undefined) { dt = 0; } else { dt = now - lastRenderAt; @@ -165,7 +166,9 @@ export async function createTerm(options: TermOptions): Promise { }); } - return { output, events, info, errors, animating: native.animating(statePtr) > 0 }; + let animating = native.animating(statePtr) > 0; + wasAnimating = animating; + return { output, events, info, errors, animating }; }, }; } From 732516450d3170ce9b0342383553b22e97e5ce33 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 19:37:03 -0500 Subject: [PATCH 17/35] =?UTF-8?q?=E2=9C=85=20verify=20color=20transitions?= =?UTF-8?q?=20work=20in=20line=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/transitions-run.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/transitions-run.test.ts b/test/transitions-run.test.ts index 184084a..283a3e1 100644 --- a/test/transitions-run.test.ts +++ b/test/transitions-run.test.ts @@ -49,3 +49,23 @@ describe("transition lifecycle", () => { expect(r.animating).toBe(false); }); }); + +describe("transitions in line mode", () => { + it("runs color transitions in line mode", async () => { + let term = await createTerm({ width: 20, height: 5 }); + let frame = (bg: number): Op[] => [ + open("box", { + layout: { width: fixed(10), height: fixed(2) }, + bg, + transition: { duration: 0.2, properties: ["bg"] }, + }), + close(), + ]; + + term.render(frame(rgba(255, 0, 0)), { deltaTime: 0, mode: "line" }); + term.render(frame(rgba(0, 255, 0)), { deltaTime: 0, mode: "line" }); + let r = term.render(frame(rgba(0, 255, 0)), { deltaTime: 0.1, mode: "line" }); + expect(r.animating).toBe(true); + expect(r.output).toBeInstanceOf(Uint8Array); + }); +}); From 9e273d4377a4b225907cf32c4dab8fd6f32e945a Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 20:14:12 -0500 Subject: [PATCH 18/35] =?UTF-8?q?=F0=9F=8E=A8=20apply=20deno=20fmt=20and?= =?UTF-8?q?=20clang-format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops-transitions.ts | 60 +++-- specs/transitions-spec.md | 445 +++++++++++++++++------------------ src/clayterm.c | 10 +- src/clayterm.h | 3 +- src/transitions.c | 65 ++--- term-native.ts | 9 +- test/transitions-run.test.ts | 15 +- 7 files changed, 312 insertions(+), 295 deletions(-) diff --git a/ops-transitions.ts b/ops-transitions.ts index ad636fd..f3e2cd5 100644 --- a/ops-transitions.ts +++ b/ops-transitions.ts @@ -1,7 +1,13 @@ export type TransitionProperty = - | "x" | "y" | "position" - | "width" | "height" | "size" - | "bg" | "overlay" | "borderColor" + | "x" + | "y" + | "position" + | "width" + | "height" + | "size" + | "bg" + | "overlay" + | "borderColor" | "borderWidth" | "all"; @@ -16,23 +22,33 @@ export const TP_BORDER_WIDTH = 256; export const TP_POSITION = TP_X | TP_Y; export const TP_SIZE = TP_WIDTH | TP_HEIGHT; -export const TP_ALL = - TP_X | TP_Y | TP_WIDTH | TP_HEIGHT | +export const TP_ALL = TP_X | TP_Y | TP_WIDTH | TP_HEIGHT | TP_BG | TP_OVERLAY | TP_BORDER_COLOR | TP_BORDER_WIDTH; export function propertyMask(name: TransitionProperty): number { switch (name) { - case "x": return TP_X; - case "y": return TP_Y; - case "position": return TP_POSITION; - case "width": return TP_WIDTH; - case "height": return TP_HEIGHT; - case "size": return TP_SIZE; - case "bg": return TP_BG; - case "overlay": return TP_OVERLAY; - case "borderColor": return TP_BORDER_COLOR; - case "borderWidth": return TP_BORDER_WIDTH; - case "all": return TP_ALL; + case "x": + return TP_X; + case "y": + return TP_Y; + case "position": + return TP_POSITION; + case "width": + return TP_WIDTH; + case "height": + return TP_HEIGHT; + case "size": + return TP_SIZE; + case "bg": + return TP_BG; + case "overlay": + return TP_OVERLAY; + case "borderColor": + return TP_BORDER_COLOR; + case "borderWidth": + return TP_BORDER_WIDTH; + case "all": + return TP_ALL; } } @@ -45,10 +61,14 @@ export const EASING_EASE_IN_OUT = 3; export function easingByte(easing: Easing): number { switch (easing) { - case "linear": return EASING_LINEAR; - case "easeIn": return EASING_EASE_IN; - case "easeOut": return EASING_EASE_OUT; - case "easeInOut": return EASING_EASE_IN_OUT; + case "linear": + return EASING_LINEAR; + case "easeIn": + return EASING_EASE_IN; + case "easeOut": + return EASING_EASE_OUT; + case "easeInOut": + return EASING_EASE_IN_OUT; } } diff --git a/specs/transitions-spec.md b/specs/transitions-spec.md index 6b9cf92..10ec2f5 100644 --- a/specs/transitions-spec.md +++ b/specs/transitions-spec.md @@ -8,25 +8,24 @@ where surfaces may settle during implementation. ## 1. Purpose -A transition smoothly interpolates an element's visual properties over time -when they change between frames. This specification defines how transitions -integrate with Clayterm's frame-snapshot rendering model: how they are -declared, how time is supplied, and how callers observe in-flight animation -so they can drive the render loop. - -Transitions are a first-class extension of the rendering contract defined in -the [Clayterm Renderer Specification](renderer-spec.md). They do not change -the architectural model, do not introduce a component tree, and do not -require callers to hold cross-frame identity beyond the stable element -identifiers they already use. - -This specification covers what clayterm ships against the current upstream -Clay layout engine. Several capabilities that the rendering model naturally -invites — per-property easing, per-element enter/exit behaviors, custom -bezier easings — are intentionally excluded from v1 because the underlying -Clay API cannot express them without upstream changes that are still in -flight. Section 13 records these deferrals and the upstream dependencies -that unblock them. +A transition smoothly interpolates an element's visual properties over time when +they change between frames. This specification defines how transitions integrate +with Clayterm's frame-snapshot rendering model: how they are declared, how time +is supplied, and how callers observe in-flight animation so they can drive the +render loop. + +Transitions are a first-class extension of the rendering contract defined in the +[Clayterm Renderer Specification](renderer-spec.md). They do not change the +architectural model, do not introduce a component tree, and do not require +callers to hold cross-frame identity beyond the stable element identifiers they +already use. + +This specification covers what clayterm ships against the current upstream Clay +layout engine. Several capabilities that the rendering model naturally invites — +per-property easing, per-element enter/exit behaviors, custom bezier easings — +are intentionally excluded from v1 because the underlying Clay API cannot +express them without upstream changes that are still in flight. Section 13 +records these deferrals and the upstream dependencies that unblock them. --- @@ -65,21 +64,21 @@ See Section 13 for the deferred features and their upstream unblockers. ## 3. Terminology -**Transition.** A time-based interpolation of one or more of an element's -visual properties between an initial value and a target value. +**Transition.** A time-based interpolation of one or more of an element's visual +properties between an initial value and a target value. -**Transition property.** A specific visual attribute of an element that can -be interpolated: position (x, y), size (width, height), background color, -overlay color, border color, or border width. +**Transition property.** A specific visual attribute of an element that can be +interpolated: position (x, y), size (width, height), background color, overlay +color, border color, or border width. -**Easing.** A function mapping normalized progress in [0, 1] to an eased -value in [0, 1]. Clayterm exposes a fixed set of built-in easings. +**Easing.** A function mapping normalized progress in [0, 1] to an eased value +in [0, 1]. Clayterm exposes a fixed set of built-in easings. -**Delta time (`deltaTime`).** The number of seconds elapsed since the -previous render transaction. Used by the renderer to advance interpolation. +**Delta time (`deltaTime`).** The number of seconds elapsed since the previous +render transaction. Used by the renderer to advance interpolation. -**Animating signal.** A boolean flag in the render result indicating whether -any transition is currently in progress. Callers use it to decide whether to +**Animating signal.** A boolean flag in the render result indicating whether any +transition is currently in progress. Callers use it to decide whether to schedule another frame. --- @@ -91,70 +90,68 @@ _This section is normative._ ### 4.1 Relationship to the frame-snapshot model Transitions do not alter the frame-snapshot contract defined in INV-3 of the -renderer specification. The directive array still fully describes the -desired state for its frame. Transitions interpolate between the previous -frame's state and the current frame's target state; they do not reintroduce -a persistent component tree on the caller side. +renderer specification. The directive array still fully describes the desired +state for its frame. Transitions interpolate between the previous frame's state +and the current frame's target state; they do not reintroduce a persistent +component tree on the caller side. -What transitions add is the requirement that element identifiers remain -stable across frames for any element on which animation is desired. This is -not a new invariant — the existing pointer-event subsystem already relies -on stable identifiers — but it becomes load-bearing for transitions. +What transitions add is the requirement that element identifiers remain stable +across frames for any element on which animation is desired. This is not a new +invariant — the existing pointer-event subsystem already relies on stable +identifiers — but it becomes load-bearing for transitions. ### 4.2 Time ownership The `Term` instance is the sole source of frame-to-frame time. On each `render()` call, the Term reads a monotonic clock and computes the elapsed -seconds since the previous render. That value is passed to the layout -engine to advance any in-flight transitions. - -If the previous render reported `animating=false`, the Term passes -`deltaTime=0` to the layout engine on the current render, regardless of -wall-clock time elapsed. The rationale: Clay is delta-based and has no -concept of when a transition began. Idle time between renders must not -count toward any subsequent transition's elapsed clock, otherwise a long -idle gap followed by a mutation would cause the transition to complete -instantly. Passing `deltaTime=0` on the first frame of any new transition -gives it a clean elapsed=0 starting point; real deltas resume once the -previous render signals `animating=true`. - -The caller MAY override the computed delta via an explicit `deltaTime` -option on `render()`. Use cases include deterministic testing, snapshot -rendering, and compute-only renders where the caller is querying bounds -without displaying output. - -The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). -Wall-clock time can move backward under NTP adjustments or DST, which would -produce negative deltas and corrupt interpolation. +seconds since the previous render. That value is passed to the layout engine to +advance any in-flight transitions. + +If the previous render reported `animating=false`, the Term passes `deltaTime=0` +to the layout engine on the current render, regardless of wall-clock time +elapsed. The rationale: Clay is delta-based and has no concept of when a +transition began. Idle time between renders must not count toward any subsequent +transition's elapsed clock, otherwise a long idle gap followed by a mutation +would cause the transition to complete instantly. Passing `deltaTime=0` on the +first frame of any new transition gives it a clean elapsed=0 starting point; +real deltas resume once the previous render signals `animating=true`. + +The caller MAY override the computed delta via an explicit `deltaTime` option on +`render()`. Use cases include deterministic testing, snapshot rendering, and +compute-only renders where the caller is querying bounds without displaying +output. + +The Term MUST NOT use a non-monotonic clock (e.g., `Date.now()`). Wall-clock +time can move backward under NTP adjustments or DST, which would produce +negative deltas and corrupt interpolation. ### 4.3 Delta clamping Clayterm does not clamp `deltaTime`. Long gaps between frames (process -suspension, backgrounded terminal, debugger pause) produce large deltas. -The underlying interpolation is duration-based and naturally clamps at 1.0 -of progress, so a large delta causes in-flight transitions to complete -rather than to overshoot or become unstable. +suspension, backgrounded terminal, debugger pause) produce large deltas. The +underlying interpolation is duration-based and naturally clamps at 1.0 of +progress, so a large delta causes in-flight transitions to complete rather than +to overshoot or become unstable. ### 4.4 Animation-loop signaling The render result MUST surface whether any transition is currently active. Callers use this signal to schedule the next frame. When no transition is -active, callers may stop rendering until the next external event (input, -resize, application state change). +active, callers may stop rendering until the next external event (input, resize, +application state change). -This requirement exists because terminal applications typically render -on-demand rather than at a fixed refresh rate. Without an explicit -animating signal, a caller has no way to know that a transition it -triggered is still in progress. +This requirement exists because terminal applications typically render on-demand +rather than at a fixed refresh rate. Without an explicit animating signal, a +caller has no way to know that a transition it triggered is still in progress. ### 4.5 Boundary preservation Transition configuration MUST be fully serializable. No function pointers, -closures, or callback registries cross the TS→WASM boundary during a -render transaction. +closures, or callback registries cross the TS→WASM boundary during a render +transaction. -This preserves INV-2 (single transaction per frame): one binary buffer in, -one result struct out. On the C side, a fixed set of easing handlers is +This preserves INV-2 (single transaction per frame): one binary buffer in, one +result struct out. On the C side, a fixed set of easing handlers is pre-registered; the directive selects one by enum value. --- @@ -164,36 +161,33 @@ pre-registered; the directive selects one by enum value. _This section is normative._ **INV-T1. Time is driven by delta, not wall clock.** All transition -interpolation advances by `deltaTime`, a per-frame seconds value. The -renderer does not subscribe to an internal timer or schedule work of its -own. - -**INV-T2. Render remains pure under time override.** When the caller -supplies an explicit `deltaTime`, the render result depends only on the -directive array, the previous frame's cell buffer, and the supplied -`deltaTime`. This makes deterministic rendering possible for tests and -snapshots. - -**INV-T3. No callbacks across the boundary.** Transition configuration MUST -be fully serializable. No function pointers, closures, or callback -registries cross the TS→WASM boundary during a render transaction. - -**INV-T4. Identity is drawn from element IDs.** Transition state is -associated with elements by their declared `id`. Callers using transitions -on an element MUST assign it a stable, unique `id` across frames. Reusing -an `id` for a different logical element in a later frame is a caller -error; behavior is unspecified. - -**INV-T5. Animating signal is accurate per transaction.** The `animating` -flag returned by `render()` reflects the state of transitions as of the -end of that transaction. If it is `true`, at least one transition has -non-zero remaining progress and calling `render()` again with positive -`deltaTime` will advance it. - -**INV-T6. Cancellation is structural.** There is no imperative `cancel()` -API. Transitions are cancelled by re-describing the previous target in a -later frame; the transition infrastructure re-anchors the interpolation -from the current visible value to the new target. +interpolation advances by `deltaTime`, a per-frame seconds value. The renderer +does not subscribe to an internal timer or schedule work of its own. + +**INV-T2. Render remains pure under time override.** When the caller supplies an +explicit `deltaTime`, the render result depends only on the directive array, the +previous frame's cell buffer, and the supplied `deltaTime`. This makes +deterministic rendering possible for tests and snapshots. + +**INV-T3. No callbacks across the boundary.** Transition configuration MUST be +fully serializable. No function pointers, closures, or callback registries cross +the TS→WASM boundary during a render transaction. + +**INV-T4. Identity is drawn from element IDs.** Transition state is associated +with elements by their declared `id`. Callers using transitions on an element +MUST assign it a stable, unique `id` across frames. Reusing an `id` for a +different logical element in a later frame is a caller error; behavior is +unspecified. + +**INV-T5. Animating signal is accurate per transaction.** The `animating` flag +returned by `render()` reflects the state of transitions as of the end of that +transaction. If it is `true`, at least one transition has non-zero remaining +progress and calling `render()` again with positive `deltaTime` will advance it. + +**INV-T6. Cancellation is structural.** There is no imperative `cancel()` API. +Transitions are cancelled by re-describing the previous target in a later frame; +the transition infrastructure re-anchors the interpolation from the current +visible value to the new target. --- @@ -220,15 +214,15 @@ interface RenderOptions { Each `render()` call advances transitions by its `deltaTime`: - If `deltaTime` is provided explicitly, it is used verbatim. -- Otherwise, if the previous render reported `animating=false`, - `deltaTime=0` (see §4.2 for rationale). -- Otherwise, `deltaTime` is the monotonic wall-clock time elapsed since - the previous `render()` call. +- Otherwise, if the previous render reported `animating=false`, `deltaTime=0` + (see §4.2 for rationale). +- Otherwise, `deltaTime` is the monotonic wall-clock time elapsed since the + previous `render()` call. -On every `render()` call, Term captures the current monotonic timestamp as -the reference point for the next implicit delta. The two modes can be -freely mixed, but mixing within a single session is primarily useful for -tests that step time manually and should otherwise be avoided. +On every `render()` call, Term captures the current monotonic timestamp as the +reference point for the next implicit delta. The two modes can be freely mixed, +but mixing within a single session is primarily useful for tests that step time +manually and should otherwise be avoided. ### 6.2 `RenderResult` addition @@ -250,8 +244,8 @@ transition at the end of the transaction. ### 6.3 The `transition` field on `open()` An element may declare a transition by adding a `transition` field to its -open-element directive. The field is optional. Its absence means the -element has no transitions, which is the default. +open-element directive. The field is optional. Its absence means the element has +no transitions, which is the default. See Section 7 for the shape. @@ -275,21 +269,20 @@ open("sidebar", { properties: ["x", "width", "bg"], interactive: false, }, -}) +}); ``` **`duration`** — seconds. Must be non-negative. -**`easing`** — a string naming one of the built-in easing curves -(Section 7.2). Defaults to `"linear"` when omitted. +**`easing`** — a string naming one of the built-in easing curves (Section 7.2). +Defaults to `"linear"` when omitted. **`properties`** — list of property names to interpolate. Group names -(`position`, `size`, `all`) expand to the union of the underlying -properties. +(`position`, `size`, `all`) expand to the union of the underlying properties. -**`interactive`** (default `false`) — when `false`, pointer interactions -with the element are disabled while a position transition is in progress. -When `true`, pointer interactions remain enabled throughout. +**`interactive`** (default `false`) — when `false`, pointer interactions with +the element are disabled while a position transition is in progress. When +`true`, pointer interactions remain enabled throughout. ### 7.2 Easing values @@ -299,10 +292,10 @@ The `easing` field takes one of four string values: type Easing = "linear" | "easeIn" | "easeOut" | "easeInOut"; ``` -Each value maps to a wire byte (see Section 8). The byte space is -deliberately larger than this set so additional easings can be added -later without breaking serialized frames. A future parametric easing -(e.g., cubic bezier) would extend the type to a discriminated union: +Each value maps to a wire byte (see Section 8). The byte space is deliberately +larger than this set so additional easings can be added later without breaking +serialized frames. A future parametric easing (e.g., cubic bezier) would extend +the type to a discriminated union: `"linear" | "easeIn" | ... | { cubicBezier: [number, number, number, number] }`. Today all values are non-parametric, so the type is a plain string union. @@ -310,9 +303,15 @@ Today all values are non-parametric, so the type is a plain string union. ```ts type TransitionProperty = - | "x" | "y" | "position" - | "width" | "height" | "size" - | "bg" | "overlay" | "borderColor" + | "x" + | "y" + | "position" + | "width" + | "height" + | "size" + | "bg" + | "overlay" + | "borderColor" | "borderWidth" | "all"; ``` @@ -329,9 +328,9 @@ Group names expand as follows: _This section is descriptive._ -The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. -Its presence is indicated by a bit in the open-element property mask. -When present, the block is a fixed 8-byte record: +The transition block is a new optional tagged section on `OP_OPEN_ELEMENT`. Its +presence is indicated by a bit in the open-element property mask. When present, +the block is a fixed 8-byte record: ``` transition_block { @@ -355,11 +354,10 @@ CLAY_TRANSITION_PROPERTY_BORDER_COLOR = 128 CLAY_TRANSITION_PROPERTY_BORDER_WIDTH = 256 ``` -(Value 64, `CLAY_TRANSITION_PROPERTY_CORNER_RADIUS`, is defined upstream -but has no field in `Clay_TransitionData` and is not emitted by clayterm.) +(Value 64, `CLAY_TRANSITION_PROPERTY_CORNER_RADIUS`, is defined upstream but has +no field in `Clay_TransitionData` and is not emitted by clayterm.) -The property-name helpers on the TS side expand to this bitmask during -packing. +The property-name helpers on the TS side expand to this bitmask during packing. ### 8.1 Validation @@ -375,21 +373,21 @@ packing. _This section is normative._ -A caller cancels an in-flight transition by emitting a new frame whose -directive for that element describes a different target state. The -transition infrastructure re-anchors the interpolation: +A caller cancels an in-flight transition by emitting a new frame whose directive +for that element describes a different target state. The transition +infrastructure re-anchors the interpolation: - The new `initial` value becomes the element's currently-visible value. - `elapsedTime` resets to zero. - The new `target` is the value declared in the current frame. -The transition duration is unchanged. A cancelled-and-reversed transition -takes its full configured duration regardless of how far it had progressed -at the time of cancellation. +The transition duration is unchanged. A cancelled-and-reversed transition takes +its full configured duration regardless of how far it had progressed at the time +of cancellation. -There is no `term.cancelTransition(id)` call. The frame-snapshot model -makes cancellation a structural consequence of re-describing the desired -state rather than an imperative operation. +There is no `term.cancelTransition(id)` call. The frame-snapshot model makes +cancellation a structural consequence of re-describing the desired state rather +than an imperative operation. --- @@ -398,15 +396,14 @@ state rather than an imperative operation. _This section is descriptive._ Line mode emits cells as newline-separated rows without absolute cursor -positioning. Position transitions (`x`, `y`) have no meaningful effect in -this mode: rows are placed at the current cursor, not at absolute -coordinates. +positioning. Position transitions (`x`, `y`) have no meaningful effect in this +mode: rows are placed at the current cursor, not at absolute coordinates. Expected behavior in line mode: - Color and size transitions proceed normally. -- Position transitions are silently skipped (the property bits for x and y - are cleared before the configuration reaches Clay). +- Position transitions are silently skipped (the property bits for x and y are + cleared before the configuration reaches Clay). - The `animating` signal reports accurately regardless of mode. --- @@ -415,25 +412,25 @@ Expected behavior in line mode: _This section is descriptive._ -The `deltaTime` override enables deterministic, snapshot-friendly tests. -A test sequence looks like: +The `deltaTime` override enables deterministic, snapshot-friendly tests. A test +sequence looks like: ```ts term.render(opsA, { deltaTime: 0 }); -term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed -term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition -term.render(opsB, { deltaTime: 0.1 }); // 100%, completed +term.render(opsB, { deltaTime: 0 }); // target change, no time elapsed +term.render(opsB, { deltaTime: 0.1 }); // 50% through a 0.2s transition +term.render(opsB, { deltaTime: 0.1 }); // 100%, completed ``` Test coverage should include, at minimum: - Property change mid-stream interpolates and completes. -- `animating` is false on static frames, true during interpolation, false - again when the transition completes. +- `animating` is false on static frames, true during interpolation, false again + when the transition completes. - Mid-transition target change re-anchors initial to current value. - Multiple concurrent transitions on multiple elements. -- Line mode: color and size transitions apply, position transitions are - silently skipped. +- Line mode: color and size transitions apply, position transitions are silently + skipped. - Each easing enum produces distinct progression (linear, easeIn, easeOut, easeInOut). @@ -452,10 +449,10 @@ capabilities clayterm depends on (Section 13). ### 12.2 Handler architecture -Each `Term` registers one C-side transition handler per easing kind (four -total for v1: linear, easeIn, easeOut, easeInOut). At element-configuration -time the decoder selects the handler matching the element's easing enum -and stores it on the `Clay_TransitionElementConfig`. +Each `Term` registers one C-side transition handler per easing kind (four total +for v1: linear, easeIn, easeOut, easeInOut). At element-configuration time the +decoder selects the handler matching the element's easing enum and stores it on +the `Clay_TransitionElementConfig`. Each handler: @@ -464,82 +461,77 @@ Each handler: 3. Lerps each property named in the `properties` bitmask from `initial` to `target`. 4. Increments the Term context's `animating_count` unless progress is 1.0. -5. Returns `true` if progress is 1.0 (transition complete), `false` - otherwise. +5. Returns `true` if progress is 1.0 (transition complete), `false` otherwise. -At the start of each `render()`, the Term resets `animating_count` to -zero. At the end, the value is copied into the result struct as the -`animating` flag (`true` if count > 0). +At the start of each `render()`, the Term resets `animating_count` to zero. At +the end, the value is copied into the result struct as the `animating` flag +(`true` if count > 0). ### 12.3 Per-Term isolation -The `animating_count` lives on the Term's C-side context, not as -module-level state. Multiple Terms created in the same process remain -isolated. +The `animating_count` lives on the Term's C-side context, not as module-level +state. Multiple Terms created in the same process remain isolated. ### 12.4 Resolving the active Term inside the handler Clay's transition-handler signature does not carry a `userData` pointer or -element ID. Each `reduce()` call records the currently-active Term pointer -in a module-level variable (`ct_active_context`) and clears it at the end. -The handler reads this variable to reach the Term's `animating_count`. A -single render pass cannot overlap with another (renders are synchronous), -so there is no concurrency concern. +element ID. Each `reduce()` call records the currently-active Term pointer in a +module-level variable (`ct_active_context`) and clears it at the end. The +handler reads this variable to reach the Term's `animating_count`. A single +render pass cannot overlap with another (renders are synchronous), so there is +no concurrency concern. --- ## 13. Deferred Until Upstream Clay These capabilities are intentionally not in v1 because the required Clay -primitives are either missing or in flight upstream. The absence is -motivated; re-adding them is straightforward once Clay lands the pieces. +primitives are either missing or in flight upstream. The absence is motivated; +re-adding them is straightforward once Clay lands the pieces. ### 13.1 Per-property easing and duration -The directive API could allow each property to have its own duration and -easing (e.g., "fade bg in 150ms, slide x in 300ms"). Clay's -`Clay_TransitionElementConfig` carries a single `duration`, a single -`handler`, and a single `properties` bitmask per element, so the handler -has no way to distinguish per-property timing. Working around this -requires per-element metadata addressable from inside the handler. +The directive API could allow each property to have its own duration and easing +(e.g., "fade bg in 150ms, slide x in 300ms"). Clay's +`Clay_TransitionElementConfig` carries a single `duration`, a single `handler`, +and a single `properties` bitmask per element, so the handler has no way to +distinguish per-property timing. Working around this requires per-element +metadata addressable from inside the handler. -**Unblocked by:** Clay adding `void* userData` to the transition -arguments (upstream PR -[nicbarker/clay#603](https://github.com/nicbarker/clay/pull/603)). +**Unblocked by:** Clay adding `void* userData` to the transition arguments +(upstream PR [nicbarker/clay#603](https://github.com/nicbarker/clay/pull/603)). ### 13.2 Enter and exit transitions -Elements mounted or removed between frames cannot express per-element -initial or final state deltas. Clay exposes `setInitialState` and -`setFinalState` callbacks with signatures that take no element identifier -or user pointer, so there is no way to look up per-element deltas from -inside the callbacks. Additionally, exit transitions require their -configuration to survive past the frame on which the element was last -declared, which requires a lifetime signal. +Elements mounted or removed between frames cannot express per-element initial or +final state deltas. Clay exposes `setInitialState` and `setFinalState` callbacks +with signatures that take no element identifier or user pointer, so there is no +way to look up per-element deltas from inside the callbacks. Additionally, exit +transitions require their configuration to survive past the frame on which the +element was last declared, which requires a lifetime signal. **Unblocked by:** - Clay `userData` on transition arguments (PR #603, above). -- An exit-completion callback or an `exiting` flag on the render command, - both of which have been discussed upstream with Clay's maintainer as - forthcoming. +- An exit-completion callback or an `exiting` flag on the render command, both + of which have been discussed upstream with Clay's maintainer as forthcoming. ### 13.3 `cubicBezier` easing -Custom cubic-bezier curves need per-element control-point parameters, and -Clay's fixed handler signature has no mechanism to thread parameters to a -shared handler. +Custom cubic-bezier curves need per-element control-point parameters, and Clay's +fixed handler signature has no mechanism to thread parameters to a shared +handler. **Unblocked by:** the same Clay `userData` addition as 13.1. ### 13.4 Corner-radius transitions -`CLAY_TRANSITION_PROPERTY_CORNER_RADIUS` is defined in the Clay property -enum, but `Clay_TransitionData` has no field carrying corner radius. -Upstream `Clay_EaseOut` does not interpolate it. Clayterm cannot either. +`CLAY_TRANSITION_PROPERTY_CORNER_RADIUS` is defined in the Clay property enum, +but `Clay_TransitionData` has no field carrying corner radius. Upstream +`Clay_EaseOut` does not interpolate it. Clayterm cannot either. -**Unblocked by:** Clay adding a `cornerRadius` field to -`Clay_TransitionData` and interpolating it in layout. +**Unblocked by:** Clay adding a `cornerRadius` field to `Clay_TransitionData` +and interpolating it in layout. --- @@ -547,38 +539,37 @@ Upstream `Clay_EaseOut` does not interpolate it. Clayterm cannot either. One demo accompanies v1: -**`demo/transitions.ts`** — exercises v1 transitions meaningfully in a -terminal context (e.g., a collapsing sidebar or a colored highlight that -fades between states). Purpose: surface real-world API sharp edges. +**`demo/transitions.ts`** — exercises v1 transitions meaningfully in a terminal +context (e.g., a collapsing sidebar or a colored highlight that fades between +states). Purpose: surface real-world API sharp edges. --- ## Appendix A. Relationship to the Renderer Specification -This specification extends, but does not modify, the renderer -specification. Specifically: +This specification extends, but does not modify, the renderer specification. +Specifically: -- **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock - for `deltaTime` computation. A clock read is not terminal IO and does - not violate this invariant. The renderer still produces bytes only; it - does not read or write terminals. +- **INV-1 (Zero IO).** Transitions introduce reading of a monotonic clock for + `deltaTime` computation. A clock read is not terminal IO and does not violate + this invariant. The renderer still produces bytes only; it does not read or + write terminals. -- **INV-2 (Single transaction per frame).** Transitions preserve this. - All transition configuration is serialized into the single directive - buffer; no additional boundary crossings occur during rendering. +- **INV-2 (Single transaction per frame).** Transitions preserve this. All + transition configuration is serialized into the single directive buffer; no + additional boundary crossings occur during rendering. -- **INV-3 (Frame-snapshot independence).** Transitions preserve this at - the API level. Each directive array still fully describes the desired - state. Element IDs carry more weight (Section 4.1) but callers do not - acquire new cross-frame bookkeeping responsibilities. +- **INV-3 (Frame-snapshot independence).** Transitions preserve this at the API + level. Each directive array still fully describes the desired state. Element + IDs carry more weight (Section 4.1) but callers do not acquire new cross-frame + bookkeeping responsibilities. - **INV-4 (ANSI byte output).** Unchanged. -- **INV-5 (Layout/render/diff ownership).** The renderer additionally - owns transition interpolation. Interpolated values feed into the - existing layout and diff pipeline at the same pipeline stage that - resolved values would. +- **INV-5 (Layout/render/diff ownership).** The renderer additionally owns + transition interpolation. Interpolated values feed into the existing layout + and diff pipeline at the same pipeline stage that resolved values would. -The "Deferred/Future Areas" section of the renderer specification should -be updated to reference this specification rather than list transitions -as a single bullet. +The "Deferred/Future Areas" section of the renderer specification should be +updated to reference this specification rather than list transitions as a single +bullet. diff --git a/src/clayterm.c b/src/clayterm.c index 6984633..64f705d 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -472,7 +472,8 @@ struct Clayterm *init(void *mem, int w, int h) { return ct; } -void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime) { +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, + float deltaTime) { int i = 0; ct_active_context = ct; ct->error_count = 0; @@ -572,9 +573,10 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, floa decl.transition.handler = ct_handler_for(easing); decl.transition.duration = duration; decl.transition.properties = (Clay_TransitionProperty)props; - decl.transition.interactionHandling = interactive - ? CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION - : CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION; + decl.transition.interactionHandling = + interactive + ? CLAY_TRANSITION_ALLOW_INTERACTIONS_WHILE_TRANSITIONING_POSITION + : CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION; } Clay__ConfigureOpenElement(decl); diff --git a/src/clayterm.h b/src/clayterm.h index 4e7845e..8a24db4 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -12,7 +12,8 @@ struct Clayterm; /* WASM exports */ int clayterm_size(int w, int h); struct Clayterm *init(void *mem, int w, int h); -void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime); +void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, + float deltaTime); char *output(struct Clayterm *ct); int length(struct Clayterm *ct); int animating(struct Clayterm *ct); diff --git a/src/transitions.c b/src/transitions.c index 6c7d15e..7c6837b 100644 --- a/src/transitions.c +++ b/src/transitions.c @@ -13,9 +13,7 @@ static float clampf(float v, float lo, float hi) { } } -static float ease_in(float t) { - return t * t; -} +static float ease_in(float t) { return t * t; } static float ease_out(float t) { float inv = 1.0f - t; @@ -31,9 +29,7 @@ static float ease_in_out(float t) { } } -static float lerpf(float a, float b, float t) { - return a + (b - a) * t; -} +static float lerpf(float a, float b, float t) { return a + (b - a) * t; } static Clay_Color lerp_color(Clay_Color a, Clay_Color b, float t) { Clay_Color out; @@ -44,46 +40,48 @@ static Clay_Color lerp_color(Clay_Color a, Clay_Color b, float t) { return out; } -static bool apply(Clay_TransitionCallbackArguments args, float eased, bool done) { +static bool apply(Clay_TransitionCallbackArguments args, float eased, + bool done) { if (args.properties & CLAY_TRANSITION_PROPERTY_X) { args.current->boundingBox.x = - lerpf(args.initial.boundingBox.x, args.target.boundingBox.x, eased); + lerpf(args.initial.boundingBox.x, args.target.boundingBox.x, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_Y) { args.current->boundingBox.y = - lerpf(args.initial.boundingBox.y, args.target.boundingBox.y, eased); + lerpf(args.initial.boundingBox.y, args.target.boundingBox.y, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_WIDTH) { - args.current->boundingBox.width = - lerpf(args.initial.boundingBox.width, args.target.boundingBox.width, eased); + args.current->boundingBox.width = lerpf( + args.initial.boundingBox.width, args.target.boundingBox.width, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_HEIGHT) { - args.current->boundingBox.height = - lerpf(args.initial.boundingBox.height, args.target.boundingBox.height, eased); + args.current->boundingBox.height = lerpf( + args.initial.boundingBox.height, args.target.boundingBox.height, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_BACKGROUND_COLOR) { - args.current->backgroundColor = - lerp_color(args.initial.backgroundColor, args.target.backgroundColor, eased); + args.current->backgroundColor = lerp_color( + args.initial.backgroundColor, args.target.backgroundColor, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_OVERLAY_COLOR) { args.current->overlayColor = - lerp_color(args.initial.overlayColor, args.target.overlayColor, eased); + lerp_color(args.initial.overlayColor, args.target.overlayColor, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_BORDER_COLOR) { args.current->borderColor = - lerp_color(args.initial.borderColor, args.target.borderColor, eased); + lerp_color(args.initial.borderColor, args.target.borderColor, eased); } if (args.properties & CLAY_TRANSITION_PROPERTY_BORDER_WIDTH) { - args.current->borderWidth.left = - (uint16_t)lerpf(args.initial.borderWidth.left, args.target.borderWidth.left, eased); - args.current->borderWidth.right = - (uint16_t)lerpf(args.initial.borderWidth.right, args.target.borderWidth.right, eased); - args.current->borderWidth.top = - (uint16_t)lerpf(args.initial.borderWidth.top, args.target.borderWidth.top, eased); - args.current->borderWidth.bottom = - (uint16_t)lerpf(args.initial.borderWidth.bottom, args.target.borderWidth.bottom, eased); + args.current->borderWidth.left = (uint16_t)lerpf( + args.initial.borderWidth.left, args.target.borderWidth.left, eased); + args.current->borderWidth.right = (uint16_t)lerpf( + args.initial.borderWidth.right, args.target.borderWidth.right, eased); + args.current->borderWidth.top = (uint16_t)lerpf( + args.initial.borderWidth.top, args.target.borderWidth.top, eased); + args.current->borderWidth.bottom = (uint16_t)lerpf( + args.initial.borderWidth.bottom, args.target.borderWidth.bottom, eased); args.current->borderWidth.betweenChildren = - (uint16_t)lerpf(args.initial.borderWidth.betweenChildren, args.target.borderWidth.betweenChildren, eased); + (uint16_t)lerpf(args.initial.borderWidth.betweenChildren, + args.target.borderWidth.betweenChildren, eased); } if (ct_active_context && !done) { ct_active_context->animating_count++; @@ -121,11 +119,14 @@ bool ct_handler_ease_in_out(Clay_TransitionCallbackArguments args) { bool (*ct_handler_for(int kind))(Clay_TransitionCallbackArguments) { switch (kind) { - case CT_EASING_EASE_IN: return ct_handler_ease_in; - case CT_EASING_EASE_OUT: return ct_handler_ease_out; - case CT_EASING_EASE_IN_OUT: return ct_handler_ease_in_out; - case CT_EASING_LINEAR: - default: - return ct_handler_linear; + case CT_EASING_EASE_IN: + return ct_handler_ease_in; + case CT_EASING_EASE_OUT: + return ct_handler_ease_out; + case CT_EASING_EASE_IN_OUT: + return ct_handler_ease_in_out; + case CT_EASING_LINEAR: + default: + return ct_handler_linear; } } diff --git a/term-native.ts b/term-native.ts index 78d850f..cdd0637 100644 --- a/term-native.ts +++ b/term-native.ts @@ -20,7 +20,14 @@ export interface Native { memory: WebAssembly.Memory; statePtr: number; opsBuf: number; - reduce(ct: number, buf: number, len: number, mode: number, row: number, deltaTime: number): void; + reduce( + ct: number, + buf: number, + len: number, + mode: number, + row: number, + deltaTime: number, + ): void; output(ct: number): number; length(ct: number): number; setPointer(x: number, y: number, down: boolean): void; diff --git a/test/transitions-run.test.ts b/test/transitions-run.test.ts index 283a3e1..f3eda68 100644 --- a/test/transitions-run.test.ts +++ b/test/transitions-run.test.ts @@ -1,13 +1,5 @@ import { describe, expect, it } from "./suite.ts"; -import { - close, - createTerm, - fixed, - grow, - open, - rgba, - type Op, -} from "../mod.ts"; +import { close, createTerm, fixed, grow, type Op, open, rgba } from "../mod.ts"; describe("transition lifecycle", () => { it("animates bg change between frames", async () => { @@ -64,7 +56,10 @@ describe("transitions in line mode", () => { term.render(frame(rgba(255, 0, 0)), { deltaTime: 0, mode: "line" }); term.render(frame(rgba(0, 255, 0)), { deltaTime: 0, mode: "line" }); - let r = term.render(frame(rgba(0, 255, 0)), { deltaTime: 0.1, mode: "line" }); + let r = term.render(frame(rgba(0, 255, 0)), { + deltaTime: 0.1, + mode: "line", + }); expect(r.animating).toBe(true); expect(r.output).toBeInstanceOf(Uint8Array); }); From 31ce2cb93f8a512eae4a6c30fa71e2a9e32f6624 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 20:15:45 -0500 Subject: [PATCH 19/35] =?UTF-8?q?=E2=9C=A8=20add=20transitions=20demo=20(c?= =?UTF-8?q?ollapsing=20sidebar)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/transitions.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 demo/transitions.ts diff --git a/demo/transitions.ts b/demo/transitions.ts new file mode 100644 index 0000000..a521c0f --- /dev/null +++ b/demo/transitions.ts @@ -0,0 +1,89 @@ +/** + * Transitions demo — a sidebar that smoothly toggles between collapsed and expanded states. + * + * Exercises v1 transitions: one duration, one easing, multiple properties + * (width + bg) on a single element. + */ + +import { main, sleep, until } from "effection"; +import { + close, + createTerm, + cursor, + fixed, + grow, + open, + rgba, + settings, + text, +} from "../mod.ts"; +import { alternateBuffer } from "../settings.ts"; + +const BG_COLLAPSED = rgba(30, 30, 60); +const BG_EXPANDED = rgba(80, 80, 140); +const CONTENT_BG = rgba(20, 20, 20); +const TEXT_COLOR = rgba(220, 220, 220); + +await main(function* () { + let term = yield* until(createTerm({ width: 60, height: 18 })); + let tty = settings(alternateBuffer(), cursor(false)); + Deno.stdout.writeSync(tty.apply); + + try { + let expanded = false; + let lastToggle = 0; + + for (let i = 0; i < 400; i++) { + let wallMs = i * 25; + if (wallMs - lastToggle > 2000) { + expanded = !expanded; + lastToggle = wallMs; + } + + let ops = [ + open("root", { + layout: { width: grow(), height: grow(), direction: "ltr" }, + }), + open("sidebar", { + layout: { + width: fixed(expanded ? 24 : 4), + height: grow(), + padding: { left: 1, right: 1, top: 1, bottom: 1 }, + direction: "ttb", + }, + bg: expanded ? BG_EXPANDED : BG_COLLAPSED, + transition: { + duration: 0.4, + easing: "easeInOut", + properties: ["width", "bg"], + }, + }), + open("label", { + layout: { width: grow(), height: fixed(1) }, + }), + text(expanded ? "Menu" : "", { color: TEXT_COLOR }), + close(), + close(), + open("content", { + layout: { + width: grow(), + height: grow(), + padding: { left: 2, right: 2, top: 1, bottom: 1 }, + }, + bg: CONTENT_BG, + }), + open("body", { layout: { width: grow(), height: grow() } }), + text("clayterm transitions demo", { color: TEXT_COLOR }), + close(), + close(), + close(), + ]; + + let r = term.render(ops); + Deno.stdout.writeSync(r.output); + yield* sleep(25); + } + } finally { + Deno.stdout.writeSync(tty.revert); + } +}); From 83decb43e22dc229b42f3f71012a6760d2992a0e Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 20:16:21 -0500 Subject: [PATCH 20/35] =?UTF-8?q?=F0=9F=93=9D=20reference=20transitions-sp?= =?UTF-8?q?ec=20from=20renderer-spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/renderer-spec.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..fabda38 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -25,6 +25,9 @@ pointer event model and certain wrapper types — those are described in Section Input parsing is specified separately in the [Clayterm Input Specification](input-spec.md). +Transitions are specified separately in the +[Clayterm Transitions Specification](transitions-spec.md). + --- ## 2. Scope From 53bc7233f788af05b79f0a01ef35fc862bfd8b40 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 20:59:38 -0500 Subject: [PATCH 21/35] =?UTF-8?q?=E2=9C=A8=20rewrite=20transitions=20demo?= =?UTF-8?q?=20as=20interactive=20full-screen=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/transitions.ts | 326 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 262 insertions(+), 64 deletions(-) diff --git a/demo/transitions.ts b/demo/transitions.ts index a521c0f..5a884dd 100644 --- a/demo/transitions.ts +++ b/demo/transitions.ts @@ -1,89 +1,287 @@ /** - * Transitions demo — a sidebar that smoothly toggles between collapsed and expanded states. + * Interactive transitions demo — a sidebar that smoothly expands and collapses. * - * Exercises v1 transitions: one duration, one easing, multiple properties - * (width + bg) on a single element. + * Press Enter to open the menu sidebar, Esc to close it, q or Ctrl+C to quit. + * Exercises v1 transitions: width + bg animated simultaneously. */ -import { main, sleep, until } from "effection"; +import { + createChannel, + each, + ensure, + main, + race, + resource, + sleep, + spawn, + type Stream, + until, +} from "effection"; import { close, createTerm, - cursor, fixed, grow, + type InputEvent, + type Op, open, + percent, rgba, - settings, text, } from "../mod.ts"; -import { alternateBuffer } from "../settings.ts"; +import { alternateBuffer, cursor, settings } from "../settings.ts"; +import { useInput } from "./use-input.ts"; +import { useStdin } from "./use-stdin.ts"; + +const SIDEBAR_BG_OPEN = rgba(80, 80, 140); +const SIDEBAR_BG_CLOSED = rgba(30, 30, 50); +const CONTENT_BG = rgba(18, 18, 22); +const MODELINE_BG = rgba(40, 40, 55); +const TEXT = rgba(220, 220, 220); +const DIM = rgba(130, 130, 150); +const HEADING = rgba(255, 220, 120); +const MENU_ITEM = rgba(180, 200, 240); +const KEY_LABEL = rgba(255, 220, 120); + +const MENU_ITEMS = [ + "New file", + "Open file…", + "Save", + "Save as…", + "—", + "Preferences", + "Quit (q)", +]; + +const BODY = [ + { kind: "h1", text: "Lorem Ipsum" }, + { + kind: "p", + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + }, + { + kind: "p", + text: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + }, + { kind: "h2", text: "Section" }, + { + kind: "p", + text: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", + }, + { + kind: "p", + text: "Duis aute irure dolor in reprehenderit in voluptate velit esse.", + }, + { + kind: "p", + text: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui.", + }, +]; + +interface State { + menuOpen: boolean; +} + +function view(state: State): Op[] { + let ops: Op[] = []; + + ops.push( + open("root", { + layout: { width: grow(), height: grow(), direction: "ttb" }, + }), + ); + + ops.push( + open("main-row", { + layout: { width: grow(), height: grow(), direction: "ltr" }, + }), + ); + + ops.push( + open("sidebar", { + layout: { + width: state.menuOpen ? percent(0.2) : fixed(2), + height: grow(), + direction: "ttb", + padding: { left: 1, right: 1, top: 1, bottom: 1 }, + gap: 1, + }, + bg: state.menuOpen ? SIDEBAR_BG_OPEN : SIDEBAR_BG_CLOSED, + transition: { + duration: 0.25, + easing: "easeInOut", + properties: ["width", "bg"], + }, + }), + ); + + if (state.menuOpen) { + ops.push( + open("menu-title", { layout: { height: fixed(1) } }), + text("Menu", { color: HEADING }), + close(), + ); + for (let item of MENU_ITEMS) { + ops.push( + open(`menu:${item}`, { layout: { height: fixed(1) } }), + text(item, { color: item === "—" ? DIM : MENU_ITEM }), + close(), + ); + } + } + + ops.push(close()); // sidebar + + ops.push( + open("content", { + layout: { + width: grow(), + height: grow(), + direction: "ttb", + padding: { left: 3, right: 3, top: 1, bottom: 1 }, + gap: 1, + }, + bg: CONTENT_BG, + }), + ); + + for (let { kind, text: t } of BODY) { + ops.push(open(`body:${t.slice(0, 8)}`, { layout: { height: fixed(1) } })); + let color = kind === "h1" ? HEADING : kind === "h2" ? KEY_LABEL : TEXT; + ops.push(text(t, { color })); + ops.push(close()); + } + + ops.push(close()); // content + + ops.push(close()); // main-row + + ops.push( + open("modeline", { + layout: { + width: grow(), + height: fixed(1), + direction: "ltr", + padding: { left: 1, right: 1 }, + gap: 2, + }, + bg: MODELINE_BG, + }), + open("mod:quit", { layout: { direction: "ltr", gap: 0 } }), + text("q", { color: KEY_LABEL }), + text(" quit", { color: TEXT }), + close(), + open("mod:menu", { layout: { direction: "ltr", gap: 0 } }), + text("enter", { color: KEY_LABEL }), + text(" show menu", { color: TEXT }), + close(), + open("mod:hide", { layout: { direction: "ltr", gap: 0 } }), + text("esc", { color: KEY_LABEL }), + text(" hide menu", { color: TEXT }), + close(), + close(), // modeline + ); + + ops.push(close()); // root -const BG_COLLAPSED = rgba(30, 30, 60); -const BG_EXPANDED = rgba(80, 80, 140); -const CONTENT_BG = rgba(20, 20, 20); -const TEXT_COLOR = rgba(220, 220, 220); + return ops; +} + +// A stream that emits at ~60fps intervals, but only while the shared flag is true. +function ticker(flag: { animating: boolean }): Stream { + return resource(function* (provide) { + let ch = createChannel(); + yield* spawn(function* () { + while (true) { + if (flag.animating) { + yield* sleep(16); + yield* ch.send(); + } else { + // Park until animating becomes true; check every 50ms. + yield* sleep(50); + } + } + }); + let sub = yield* ch; + yield* race([provide(sub), drain(ch)]); + }); +} + +function merge( + a: Stream, + b: Stream, +): Stream { + return resource(function* (provide) { + let sub = { + a: yield* a, + b: yield* b, + }; + return yield* provide({ + *next() { + return yield* race([sub.a.next(), sub.b.next()]); + }, + }); + }); +} + +function* drain(stream: Stream) { + for (let _ of yield* each(stream)) { + yield* each.next(); + } +} await main(function* () { - let term = yield* until(createTerm({ width: 60, height: 18 })); + let { columns, rows } = Deno.stdout.isTerminal() + ? Deno.consoleSize() + : { columns: 80, rows: 24 }; + + Deno.stdin.setRaw(true); + yield* ensure(() => Deno.stdin.setRaw(false)); + + let stdin = yield* useStdin(); + let input = useInput(stdin); + + let term = yield* until(createTerm({ width: columns, height: rows })); + let tty = settings(alternateBuffer(), cursor(false)); Deno.stdout.writeSync(tty.apply); + yield* ensure(() => { + Deno.stdout.writeSync(tty.revert); + }); - try { - let expanded = false; - let lastToggle = 0; + let state: State = { menuOpen: false }; + let flag = { animating: false }; - for (let i = 0; i < 400; i++) { - let wallMs = i * 25; - if (wallMs - lastToggle > 2000) { - expanded = !expanded; - lastToggle = wallMs; - } + function draw(): void { + let { output, animating } = term.render(view(state)); + flag.animating = animating; + Deno.stdout.writeSync(output); + } - let ops = [ - open("root", { - layout: { width: grow(), height: grow(), direction: "ltr" }, - }), - open("sidebar", { - layout: { - width: fixed(expanded ? 24 : 4), - height: grow(), - padding: { left: 1, right: 1, top: 1, bottom: 1 }, - direction: "ttb", - }, - bg: expanded ? BG_EXPANDED : BG_COLLAPSED, - transition: { - duration: 0.4, - easing: "easeInOut", - properties: ["width", "bg"], - }, - }), - open("label", { - layout: { width: grow(), height: fixed(1) }, - }), - text(expanded ? "Menu" : "", { color: TEXT_COLOR }), - close(), - close(), - open("content", { - layout: { - width: grow(), - height: grow(), - padding: { left: 2, right: 2, top: 1, bottom: 1 }, - }, - bg: CONTENT_BG, - }), - open("body", { layout: { width: grow(), height: grow() } }), - text("clayterm transitions demo", { color: TEXT_COLOR }), - close(), - close(), - close(), - ]; + draw(); + + let ticks = ticker(flag); + let events = merge(input, ticks); - let r = term.render(ops); - Deno.stdout.writeSync(r.output); - yield* sleep(25); + for (let _ of yield* each(events)) { + if (_ !== undefined && typeof _ === "object" && "type" in _) { + let event = _ as InputEvent; + if (event.type === "keydown") { + if (event.ctrl && event.key === "c") { + break; + } + if (event.key === "q") { + break; + } + if (event.key === "Enter") { + state = { ...state, menuOpen: true }; + } + if (event.key === "Escape") { + state = { ...state, menuOpen: false }; + } + } } - } finally { - Deno.stdout.writeSync(tty.revert); + draw(); + yield* each.next(); } }); From 896435c5d91550101ef4df6701cfc5e08bc5c673 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 21:08:11 -0500 Subject: [PATCH 22/35] =?UTF-8?q?=E2=9C=A8=20add=20clay-transitions=20demo?= =?UTF-8?q?=20port=20(v1-compatible=20subset)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- demo/clay-transitions.ts | 451 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 demo/clay-transitions.ts diff --git a/demo/clay-transitions.ts b/demo/clay-transitions.ts new file mode 100644 index 0000000..bf05ffd --- /dev/null +++ b/demo/clay-transitions.ts @@ -0,0 +1,451 @@ +/** + * Clay-transitions demo — a port of the raylib-transitions example to clayterm. + * + * A grid of colored boxes that animate position, size, and background color. + * Press 's' to shuffle (animates position), 'c' to recolor (animates bg). + * Hover any box to see a bg-tint transition on mouse over. + * Press 'q' or Ctrl+C to quit. + * + * Omits enter/exit transitions and "Add Box" (v1 constraints). + * Overlay-color field is not yet in the v1 command buffer; hover tint is + * achieved by blending the bg color toward a highlight shade instead. + */ + +import { + createChannel, + each, + ensure, + main, + race, + resource, + sleep, + spawn, + type Stream, + until, +} from "effection"; +import { + close, + createTerm, + fixed, + grow, + type InputEvent, + type Op, + open, + type PointerEvent, + rgba, + text, +} from "../mod.ts"; +import { + alternateBuffer, + cursor, + mouseTracking, + settings, +} from "../settings.ts"; +import { useInput } from "./use-input.ts"; +import { useStdin } from "./use-stdin.ts"; + +const DEFAULT_PALETTE = [ + rgba(225, 138, 50), + rgba(111, 173, 162), + rgba(184, 87, 134), + rgba(87, 134, 184), + rgba(134, 184, 87), + rgba(184, 134, 87), + rgba(87, 184, 134), + rgba(134, 87, 184), + rgba(200, 100, 100), + rgba(100, 200, 100), + rgba(100, 100, 200), + rgba(200, 200, 100), + rgba(200, 100, 200), + rgba(100, 200, 200), + rgba(180, 160, 80), + rgba(80, 160, 180), +]; + +const PINK_PALETTE = DEFAULT_PALETTE.map((c) => { + let r = (c >> 24) & 0xff; + let g = (c >> 16) & 0xff; + let b = (c >> 8) & 0xff; + let a = c & 0xff; + let pr = Math.min(255, r + 80); + let pg = Math.max(0, g - 60); + let pb = Math.max(0, Math.min(255, b + 40)); + return rgba(pr, pg, pb, a); +}); + +// Blend a packed rgba color toward white by ratio [0,1]. +function lighten(color: number, ratio: number): number { + let r = (color >> 24) & 0xff; + let g = (color >> 16) & 0xff; + let b = (color >> 8) & 0xff; + let a = color & 0xff; + return rgba( + Math.round(r + (255 - r) * ratio), + Math.round(g + (255 - g) * ratio), + Math.round(b + (255 - b) * ratio), + a, + ); +} + +// Lighten ratio applied to bg when box is hovered (blends toward white). +const HOVER_LIGHTEN = 0.35; + +const ROOT_BG = rgba(18, 18, 22); +const TOPBAR_BG = rgba(40, 40, 55); +const MODELINE_BG = rgba(30, 30, 45); +const BTN_DEFAULT = rgba(60, 60, 80); +const BTN_HOVER = rgba(90, 90, 120); +const KEY_COLOR = rgba(255, 220, 120); +const LABEL_COLOR = rgba(200, 200, 220); + +const COLS = 4; + +interface Box { + id: number; + color: number; +} + +interface State { + boxes: Box[]; + palette: "default" | "pink"; + entered: Set; + pointer: { x: number; y: number; down: boolean } | undefined; +} + +function fisherYates(arr: T[]): T[] { + let out = arr.slice(); + for (let i = out.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + let tmp = out[i]; + out[i] = out[j]; + out[j] = tmp; + } + return out; +} + +function recolor(boxes: Box[], palette: "default" | "pink"): Box[] { + let pal = palette === "pink" ? PINK_PALETTE : DEFAULT_PALETTE; + return boxes.map((b, i) => ({ ...b, color: pal[i % pal.length] })); +} + +function button( + id: string, + label: string, + hovered: boolean, + key: string, +): Op[] { + return [ + open(id, { + layout: { + direction: "ltr", + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + alignX: 2, + alignY: 2, + height: grow(), + }, + bg: hovered ? BTN_HOVER : BTN_DEFAULT, + border: hovered + ? { color: KEY_COLOR, left: 1, right: 1, top: 1, bottom: 1 } + : undefined, + }), + text(key, { color: KEY_COLOR }), + text(` ${label}`, { color: LABEL_COLOR }), + close(), + ]; +} + +function view(state: State): Op[] { + let ops: Op[] = []; + + ops.push( + open("root", { + layout: { width: grow(), height: grow(), direction: "ttb" }, + bg: ROOT_BG, + }), + ); + + ops.push( + open("topbar", { + layout: { + width: grow(), + height: fixed(3), + direction: "ltr", + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + gap: 2, + alignY: 2, + }, + bg: TOPBAR_BG, + }), + ); + + ops.push( + ...button( + "btn:shuffle", + "shuffle", + state.entered.has("btn:shuffle"), + "s", + ), + ...button( + "btn:recolor", + "recolor", + state.entered.has("btn:recolor"), + "c", + ), + ...button("btn:quit", "quit", state.entered.has("btn:quit"), "q"), + ); + + ops.push(close()); + + ops.push( + open("grid", { + layout: { + width: grow(), + height: grow(), + direction: "ttb", + padding: { left: 1, right: 1, top: 1, bottom: 1 }, + gap: 1, + }, + }), + ); + + let boxes = state.boxes; + let rows = Math.ceil(boxes.length / COLS); + + for (let r = 0; r < rows; r++) { + ops.push( + open(`row:${r}`, { + layout: { + width: grow(), + height: fixed(3), + direction: "ltr", + gap: 1, + }, + }), + ); + + for (let c = 0; c < COLS; c++) { + let i = r * COLS + c; + if (i >= boxes.length) { + break; + } + let b = boxes[i]; + let bid = `box:${b.id}`; + let hov = state.entered.has(bid); + let bg = hov ? lighten(b.color, HOVER_LIGHTEN) : b.color; + ops.push( + open(bid, { + layout: { + width: grow(), + height: grow(), + alignX: 2, + alignY: 2, + }, + bg, + transition: { + duration: 0.5, + easing: "easeOut", + properties: ["width", "position", "bg"], + interactive: true, + }, + }), + text(`${b.id < 10 ? "0" : ""}${b.id}`, { color: LABEL_COLOR }), + close(), + ); + } + + ops.push(close()); + } + + ops.push(close()); + + ops.push( + open("modeline", { + layout: { + width: grow(), + height: fixed(1), + direction: "ltr", + padding: { left: 1, right: 1 }, + gap: 2, + }, + bg: MODELINE_BG, + }), + open("ml:s", { layout: { direction: "ltr", gap: 0 } }), + text("s", { color: KEY_COLOR }), + text(" shuffle", { color: LABEL_COLOR }), + close(), + open("ml:c", { layout: { direction: "ltr", gap: 0 } }), + text("c", { color: KEY_COLOR }), + text(" recolor", { color: LABEL_COLOR }), + close(), + open("ml:q", { layout: { direction: "ltr", gap: 0 } }), + text("q", { color: KEY_COLOR }), + text(" quit", { color: LABEL_COLOR }), + close(), + close(), + ); + + ops.push(close()); + + return ops; +} + +function ticker(flag: { animating: boolean }): Stream { + return resource(function* (provide) { + let ch = createChannel(); + yield* spawn(function* () { + while (true) { + if (flag.animating) { + yield* sleep(16); + yield* ch.send(); + } else { + yield* sleep(50); + } + } + }); + let sub = yield* ch; + yield* race([provide(sub), drain(ch)]); + }); +} + +function merge( + a: Stream, + b: Stream, +): Stream { + return resource(function* (provide) { + let sub = { + a: yield* a, + b: yield* b, + }; + return yield* provide({ + *next() { + return yield* race([sub.a.next(), sub.b.next()]); + }, + }); + }); +} + +function* drain(stream: Stream) { + for (let _ of yield* each(stream)) { + yield* each.next(); + } +} + +await main(function* () { + let { columns, rows } = Deno.stdout.isTerminal() + ? Deno.consoleSize() + : { columns: 80, rows: 24 }; + + Deno.stdin.setRaw(true); + yield* ensure(() => Deno.stdin.setRaw(false)); + + let stdin = yield* useStdin(); + let input = useInput(stdin); + + let term = yield* until(createTerm({ width: columns, height: rows })); + + let tty = settings(alternateBuffer(), cursor(false), mouseTracking()); + Deno.stdout.writeSync(tty.apply); + yield* ensure(() => { + Deno.stdout.writeSync(tty.revert); + }); + + let count = 16; + let pal = DEFAULT_PALETTE; + let initialBoxes: Box[] = Array.from({ length: count }, (_, i) => ({ + id: i, + color: pal[i % pal.length], + })); + + let state: State = { + boxes: initialBoxes, + palette: "default", + entered: new Set(), + pointer: undefined, + }; + + let flag = { animating: false }; + + function draw(): void { + let { output, animating, events } = term.render(view(state), { + pointer: state.pointer, + }); + flag.animating = animating; + for (let ev of events) { + if (ev.type === "pointerenter") { + state = { ...state, entered: new Set([...state.entered, ev.id]) }; + } else if (ev.type === "pointerleave") { + let next = new Set(state.entered); + next.delete(ev.id); + state = { ...state, entered: next }; + } + } + Deno.stdout.writeSync(output); + } + + draw(); + + let pointer = createChannel(); + let ticks = ticker(flag); + let events = merge(merge(input, pointer), ticks); + + for (let ev of yield* each(events)) { + if (ev !== undefined && typeof ev === "object" && "type" in ev) { + let e = ev as InputEvent | PointerEvent; + + if (e.type === "keydown") { + if (e.ctrl && e.key === "c") { + break; + } + if (e.key === "q") { + break; + } + if (e.key === "s") { + state = { ...state, boxes: fisherYates(state.boxes) }; + } + if (e.key === "c") { + let next: "default" | "pink" = state.palette === "default" + ? "pink" + : "default"; + state = { + ...state, + palette: next, + boxes: recolor(state.boxes, next), + }; + } + } + + if ("x" in e && "y" in e) { + let me = e as { x: number; y: number; type: string }; + state = { + ...state, + pointer: { + x: me.x, + y: me.y, + down: me.type === "mousedown", + }, + }; + } + } + + let { output, animating, events: pevents } = term.render(view(state), { + pointer: state.pointer, + }); + flag.animating = animating; + + for (let pev of pevents) { + if (pev.type === "pointerenter") { + state = { ...state, entered: new Set([...state.entered, pev.id]) }; + } else if (pev.type === "pointerleave") { + let next = new Set(state.entered); + next.delete(pev.id); + state = { ...state, entered: next }; + } + yield* pointer.send(pev); + } + + Deno.stdout.writeSync(output); + + yield* each.next(); + } +}); From 10d38abe34a6402a7c0e7b7024cb474112482724 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 21:10:32 -0500 Subject: [PATCH 23/35] =?UTF-8?q?=F0=9F=8E=A8=20let=20clay-transitions=20d?= =?UTF-8?q?emo=20rows=20fill=20available=20height?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/clay-transitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/clay-transitions.ts b/demo/clay-transitions.ts index bf05ffd..1550323 100644 --- a/demo/clay-transitions.ts +++ b/demo/clay-transitions.ts @@ -217,7 +217,7 @@ function view(state: State): Op[] { open(`row:${r}`, { layout: { width: grow(), - height: fixed(3), + height: grow(), direction: "ltr", gap: 1, }, From 8730e55d9d25e72dc631eea25c703b54af1d4f72 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 22 Apr 2026 22:21:14 -0500 Subject: [PATCH 24/35] =?UTF-8?q?=F0=9F=8E=A8=20remove=20modeline=20from?= =?UTF-8?q?=20clay-transitions=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/clay-transitions.ts | 33 +++------------------------------ demo/transitions.ts | 2 +- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/demo/clay-transitions.ts b/demo/clay-transitions.ts index 1550323..7017bae 100644 --- a/demo/clay-transitions.ts +++ b/demo/clay-transitions.ts @@ -93,7 +93,6 @@ const HOVER_LIGHTEN = 0.35; const ROOT_BG = rgba(18, 18, 22); const TOPBAR_BG = rgba(40, 40, 55); -const MODELINE_BG = rgba(30, 30, 45); const BTN_DEFAULT = rgba(60, 60, 80); const BTN_HOVER = rgba(90, 90, 120); const KEY_COLOR = rgba(255, 220, 120); @@ -243,8 +242,8 @@ function view(state: State): Op[] { }, bg, transition: { - duration: 0.5, - easing: "easeOut", + duration: 0.4, + easing: "easeInOut", properties: ["width", "position", "bg"], interactive: true, }, @@ -259,32 +258,6 @@ function view(state: State): Op[] { ops.push(close()); - ops.push( - open("modeline", { - layout: { - width: grow(), - height: fixed(1), - direction: "ltr", - padding: { left: 1, right: 1 }, - gap: 2, - }, - bg: MODELINE_BG, - }), - open("ml:s", { layout: { direction: "ltr", gap: 0 } }), - text("s", { color: KEY_COLOR }), - text(" shuffle", { color: LABEL_COLOR }), - close(), - open("ml:c", { layout: { direction: "ltr", gap: 0 } }), - text("c", { color: KEY_COLOR }), - text(" recolor", { color: LABEL_COLOR }), - close(), - open("ml:q", { layout: { direction: "ltr", gap: 0 } }), - text("q", { color: KEY_COLOR }), - text(" quit", { color: LABEL_COLOR }), - close(), - close(), - ); - ops.push(close()); return ops; @@ -296,7 +269,7 @@ function ticker(flag: { animating: boolean }): Stream { yield* spawn(function* () { while (true) { if (flag.animating) { - yield* sleep(16); + yield* sleep(2); yield* ch.send(); } else { yield* sleep(50); diff --git a/demo/transitions.ts b/demo/transitions.ts index 5a884dd..c1f178b 100644 --- a/demo/transitions.ts +++ b/demo/transitions.ts @@ -108,7 +108,7 @@ function view(state: State): Op[] { }, bg: state.menuOpen ? SIDEBAR_BG_OPEN : SIDEBAR_BG_CLOSED, transition: { - duration: 0.25, + duration: 0.2, easing: "easeInOut", properties: ["width", "bg"], }, From c6c7f87f5b0eeabb979ddd0aeff5af90de4a857a Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 23 Apr 2026 11:20:09 -0500 Subject: [PATCH 25/35] =?UTF-8?q?=F0=9F=93=9D=20note=20ct=5Factive=5Fconte?= =?UTF-8?q?xt=20is=20a=20workaround=20for=20Clay=20userData=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/clayterm.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/clayterm.c b/src/clayterm.c index 64f705d..38348df 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -20,6 +20,12 @@ #include "utf8.h" #include "wcwidth.h" +/* Module-level pointer to the Term currently executing reduce(). + * Set/cleared around each render pass so transition handlers (which Clay + * invokes with no userData — see Clay_TransitionCallbackArguments) can + * report back to the right Term's animating_count. Revisit once + * nicbarker/clay#603 lands userData on transition callbacks; then the + * handler can resolve its Term from args directly and this can go away. */ struct Clayterm *ct_active_context = NULL; /* ── Command buffer protocol ──────────────────────────────────────── */ From b7eb6bbf45982fbcb330df9f6ce33401f6180435 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 23 Apr 2026 11:22:06 -0500 Subject: [PATCH 26/35] =?UTF-8?q?=F0=9F=8E=A8=20use=20border-only=20boxes?= =?UTF-8?q?=20in=20clay-transitions=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/clay-transitions.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/demo/clay-transitions.ts b/demo/clay-transitions.ts index 7017bae..77f7ce8 100644 --- a/demo/clay-transitions.ts +++ b/demo/clay-transitions.ts @@ -231,7 +231,7 @@ function view(state: State): Op[] { let b = boxes[i]; let bid = `box:${b.id}`; let hov = state.entered.has(bid); - let bg = hov ? lighten(b.color, HOVER_LIGHTEN) : b.color; + let borderColor = hov ? lighten(b.color, HOVER_LIGHTEN) : b.color; ops.push( open(bid, { layout: { @@ -240,15 +240,21 @@ function view(state: State): Op[] { alignX: 2, alignY: 2, }, - bg, + border: { + color: borderColor, + left: 1, + right: 1, + top: 1, + bottom: 1, + }, transition: { duration: 0.4, easing: "easeInOut", - properties: ["width", "position", "bg"], + properties: ["width", "position", "borderColor"], interactive: true, }, }), - text(`${b.id < 10 ? "0" : ""}${b.id}`, { color: LABEL_COLOR }), + text(`${b.id < 10 ? "0" : ""}${b.id}`, { color: b.color }), close(), ); } From 03058a3bffa428a532b650d14e526d6626fe1f4b Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 23 Apr 2026 11:26:12 -0500 Subject: [PATCH 27/35] =?UTF-8?q?=F0=9F=8E=A8=20prevent=20menu=20text=20fr?= =?UTF-8?q?om=20wrapping=20during=20sidebar=20transition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/transitions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/transitions.ts b/demo/transitions.ts index c1f178b..96a8620 100644 --- a/demo/transitions.ts +++ b/demo/transitions.ts @@ -107,6 +107,7 @@ function view(state: State): Op[] { gap: 1, }, bg: state.menuOpen ? SIDEBAR_BG_OPEN : SIDEBAR_BG_CLOSED, + clip: { horizontal: true }, transition: { duration: 0.2, easing: "easeInOut", @@ -118,13 +119,13 @@ function view(state: State): Op[] { if (state.menuOpen) { ops.push( open("menu-title", { layout: { height: fixed(1) } }), - text("Menu", { color: HEADING }), + text("Menu", { color: HEADING, wrap: 2 }), close(), ); for (let item of MENU_ITEMS) { ops.push( open(`menu:${item}`, { layout: { height: fixed(1) } }), - text(item, { color: item === "—" ? DIM : MENU_ITEM }), + text(item, { color: item === "—" ? DIM : MENU_ITEM, wrap: 2 }), close(), ); } From 949dea5ec0033102ed2b3e1421e31beda399f2e2 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 23 Apr 2026 11:58:25 -0500 Subject: [PATCH 28/35] =?UTF-8?q?=F0=9F=94=A5=20drop=20unused=20grow=20imp?= =?UTF-8?q?ort=20in=20transitions-run=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/transitions-run.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/transitions-run.test.ts b/test/transitions-run.test.ts index f3eda68..16d77eb 100644 --- a/test/transitions-run.test.ts +++ b/test/transitions-run.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "./suite.ts"; -import { close, createTerm, fixed, grow, type Op, open, rgba } from "../mod.ts"; +import { close, createTerm, fixed, type Op, open, rgba } from "../mod.ts"; describe("transition lifecycle", () => { it("animates bg change between frames", async () => { From bd2dcc9d10f52b46644c50d44532a2f8f7a0c65e Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sat, 9 May 2026 08:04:28 -0400 Subject: [PATCH 29/35] feat: expand floating parameters --- ops.ts | 50 +++++++++++++++++++++++++++++++++++--- specs/renderer-spec.md | 30 +++++++++++++++++++++-- src/clayterm.c | 8 +++++- test/term.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++- test/validate.test.ts | 33 +++++++++++++++++++++++++ validate.ts | 11 ++++++++- 6 files changed, 179 insertions(+), 8 deletions(-) diff --git a/ops.ts b/ops.ts index 3344eea..9797c5b 100644 --- a/ops.ts +++ b/ops.ts @@ -163,12 +163,24 @@ export function pack( o += 4; view.setFloat32(o, f.y ?? 0, true); o += 4; + view.setFloat32(o, f.expand?.width ?? 0, true); + o += 4; + view.setFloat32(o, f.expand?.height ?? 0, true); + o += 4; view.setUint32(o, f.parent ?? 0, true); o += 4; view.setUint32( o, - (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | - ((f.zIndex ?? 0) << 16), + (f.attachTo ?? 0) | + ((f.attachPoints?.element ?? 0) << 8) | + ((f.attachPoints?.parent ?? 0) << 16) | + ((f.pointerCaptureMode ?? 0) << 24), + true, + ); + o += 4; + view.setUint32( + o, + (f.clipTo ?? 0) | (((f.zIndex ?? 0) & 0xffff) << 8), true, ); o += 4; @@ -264,13 +276,45 @@ export interface OpenElement { floating?: { x?: number; y?: number; + expand?: { width?: number; height?: number }; parent?: number; attachTo?: number; - attachPoints?: number; + attachPoints?: { element?: number; parent?: number }; + pointerCaptureMode?: number; + clipTo?: number; zIndex?: number; }; } +export const ATTACH_POINT = { + LEFT_TOP: 0, + LEFT_CENTER: 1, + LEFT_BOTTOM: 2, + CENTER_TOP: 3, + CENTER_CENTER: 4, + CENTER_BOTTOM: 5, + RIGHT_TOP: 6, + RIGHT_CENTER: 7, + RIGHT_BOTTOM: 8, +} as const; + +export const ATTACH_TO = { + NONE: 0, + PARENT: 1, + ELEMENT_WITH_ID: 2, + ROOT: 3, +} as const; + +export const POINTER_CAPTURE_MODE = { + CAPTURE: 0, + PASSTHROUGH: 1, +} as const; + +export const CLIP_TO = { + NONE: 0, + ATTACHED_PARENT: 1, +} as const; + export interface Text { directive: typeof OP_TEXT; content: string; diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..079363c 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -609,10 +609,36 @@ The `open()` constructor currently accepts the following property groups in its - **`cornerRadius`** — per-corner radius values, producing rounded box-drawing characters - **`clip`** — clip region configuration for scroll containers -- **`floating`** — floating-element configuration (offset, parent reference, - attach points, z-index) +- **`floating`** — floating-element configuration (offset, expansion, parent + reference, attach target, structured attach points, pointer capture mode, clip + target, z-index) - **`scroll`** — scroll container configuration +The current floating surface is: + +```ts +floating?: { + x?: number; + y?: number; + expand?: { width?: number; height?: number }; + parent?: number; + attachTo?: number; + attachPoints?: { + element?: number; + parent?: number; + }; + pointerCaptureMode?: number; + clipTo?: number; + zIndex?: number; +} +``` + +This shape extends the earlier floating surface in two ways. First, +`attachPoints` is structured as separate element and parent anchor values +instead of a single packed enum. Second, the surface exposes additional Clay +floating controls that were previously unavailable at the TypeScript layer: +`expand`, `pointerCaptureMode`, and `clipTo`. + The `text()` constructor currently accepts: `color`, `fontSize`, `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, `underline`, `strikethrough`). diff --git a/src/clayterm.c b/src/clayterm.c index 2af9afd..36c2670 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -546,13 +546,19 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { if (mask & PROP_FLOATING) { decl.floating.offset.x = rdf(buf, len, &i); decl.floating.offset.y = rdf(buf, len, &i); + decl.floating.expand.width = rdf(buf, len, &i); + decl.floating.expand.height = rdf(buf, len, &i); decl.floating.parentId = rd(buf, len, &i); uint32_t fc = rd(buf, len, &i); decl.floating.attachTo = fc & 0xff; decl.floating.attachPoints.element = (fc >> 8) & 0xff; decl.floating.attachPoints.parent = (fc >> 16) & 0xff; - decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); + decl.floating.pointerCaptureMode = (fc >> 24) & 0xff; + + uint32_t fd = rd(buf, len, &i); + decl.floating.clipTo = fd & 0xff; + decl.floating.zIndex = (int16_t)(fd >> 8); } Clay__ConfigureOpenElement(decl); diff --git a/test/term.test.ts b/test/term.test.ts index 47d8c94..16ace04 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -1,6 +1,15 @@ import { beforeEach, describe, expect, it } from "./suite.ts"; import { createTerm, type Term } from "../term.ts"; -import { close, fixed, grow, open, rgba, text } from "../ops.ts"; +import { + ATTACH_POINT, + ATTACH_TO, + close, + fixed, + grow, + open, + rgba, + text, +} from "../ops.ts"; import { print } from "./print.ts"; const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); @@ -191,6 +200,50 @@ describe("term", () => { }); }); + it("renders a floating frame with structured attach points", () => { + let out = print( + decode( + term.render([ + open("root", { + layout: { width: fixed(40), height: fixed(10), direction: "ttb" }, + }), + open("frame", { + layout: { + width: fixed(12), + height: fixed(5), + direction: "ttb", + padding: { left: 1, top: 1 }, + }, + border: { + color: rgba(255, 255, 255), + left: 1, + right: 1, + top: 1, + bottom: 1, + }, + floating: { + x: 3, + y: 1, + attachTo: ATTACH_TO.ROOT, + attachPoints: { + element: ATTACH_POINT.CENTER_CENTER, + parent: ATTACH_POINT.CENTER_CENTER, + }, + }, + }), + text("box"), + close(), + close(), + ]).output, + ), + 40, + 10, + ); + + expect(out).toContain("│box │"); + expect(out).toContain("┌──────────┐"); + }); + describe("row offset", () => { it("renders two frames at the offset position", async () => { let term = await createTerm({ width: 20, height: 5 }); diff --git a/test/validate.test.ts b/test/validate.test.ts index 8db4af9..3991288 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -78,6 +78,39 @@ describe("validate", () => { it("rejects fractional color", () => { expect(validate([text("hi", { color: 1.5 })])).toBe(false); }); + + it("accepts structured floating attach points", () => { + expect(validate([ + open("x", { + floating: { + attachPoints: { element: 4, parent: 4 }, + }, + }), + close(), + ])).toBe(true); + }); + + it("accepts floating expand and clipping fields", () => { + expect(validate([ + open("x", { + floating: { + expand: { width: 2, height: 3 }, + pointerCaptureMode: 1, + clipTo: 1, + zIndex: 1024, + }, + }), + close(), + ])).toBe(true); + }); + + it("rejects numeric floating attachPoints legacy shape", () => { + expect(validate([ + // deno-lint-ignore no-explicit-any + open("x", { floating: { attachPoints: 4 as any } }), + close(), + ])).toBe(false); + }); }); describe("validated", () => { diff --git a/validate.ts b/validate.ts index 248ea48..1e697a2 100644 --- a/validate.ts +++ b/validate.ts @@ -83,9 +83,18 @@ const Clip = Type.Object({ const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), + expand: Type.Optional(Type.Object({ + width: Type.Optional(Type.Number()), + height: Type.Optional(Type.Number()), + })), parent: Type.Optional(Type.Integer({ minimum: 0 })), attachTo: Type.Optional(u8), - attachPoints: Type.Optional(u8), + attachPoints: Type.Optional(Type.Object({ + element: Type.Optional(u8), + parent: Type.Optional(u8), + })), + pointerCaptureMode: Type.Optional(u8), + clipTo: Type.Optional(u8), zIndex: Type.Optional(u16), }); From 6e3fcd6283f9b8b1fc385485d2fc4a465d53f079 Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sat, 9 May 2026 09:16:40 -0400 Subject: [PATCH 30/35] =?UTF-8?q?=E2=9C=A8=20add=20text=20measurement=20he?= =?UTF-8?q?lpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- measure.ts | 177 +++++++++++++++++++++++++++++++++++++++++ mod.ts | 1 + ops.ts | 24 +++++- specs/renderer-spec.md | 65 +++++++++++++++ test/measure.test.ts | 159 ++++++++++++++++++++++++++++++++++++ 5 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 measure.ts create mode 100644 test/measure.test.ts diff --git a/measure.ts b/measure.ts new file mode 100644 index 0000000..dcc5e2d --- /dev/null +++ b/measure.ts @@ -0,0 +1,177 @@ +export interface WrapTextOptions { + mode?: "words" | "newlines" | "none"; +} + +export interface WrappedLine { + text: string; + width: number; +} + +export function measureCellWidth(text: string): number { + let width = 0; + for (let char of text) width += cellWidth(char); + return width; +} + +export function wrapText( + text: string, + width: number, + options: WrapTextOptions = {}, +): WrappedLine[] { + assertValidWidth(width); + if (text.length === 0) return []; + + let mode = options.mode ?? "words"; + switch (mode) { + case "none": { + let collapsed = text.replaceAll("\n", ""); + return collapsed === "" ? [] : line(collapsed); + } + case "newlines": + return text.split("\n").flatMap((part) => part === "" ? [] : line(part)); + case "words": + return wrapWords(text, width); + } +} + +export function measureWrappedHeight( + text: string, + width: number, + options: WrapTextOptions = {}, +): number { + assertValidWidth(width); + if (text.length === 0) return 0; + + let mode = options.mode ?? "words"; + switch (mode) { + case "none": + return text.replaceAll("\n", "") === "" ? 0 : 1; + case "newlines": + return countNonEmptyNewlineParts(text); + case "words": + return countWrappedWords(text, width); + } +} + +function line(text: string): WrappedLine[] { + return [{ text, width: measureCellWidth(text) }]; +} + +function assertValidWidth(width: number): void { + if (!Number.isFinite(width) || width < 0) { + throw new RangeError( + `width must be a finite non-negative number: ${width}`, + ); + } +} + +function wrapWords(text: string, maxWidth: number): WrappedLine[] { + let out: WrappedLine[] = []; + for (let paragraph of text.split("\n")) { + if (paragraph === "") continue; + let current = ""; + let currentWidth = 0; + + for (let token of tokens(paragraph)) { + let tokenWidth = measureCellWidth(token); + if (current !== "" && currentWidth + tokenWidth > maxWidth) { + out.push({ + text: current.trimEnd(), + width: measureCellWidth(current.trimEnd()), + }); + current = token.trimStart(); + currentWidth = measureCellWidth(current); + } else { + current += token; + currentWidth += tokenWidth; + } + + if (current !== "" && currentWidth > maxWidth && token.trim() !== "") { + out.push({ + text: current.trimEnd(), + width: measureCellWidth(current.trimEnd()), + }); + current = ""; + currentWidth = 0; + } + } + + if (current !== "") { + let text = current.trimEnd(); + if (text !== "") out.push({ text, width: measureCellWidth(text) }); + } + } + return out; +} + +function countWrappedWords(text: string, maxWidth: number): number { + let count = 0; + for (let paragraph of text.split("\n")) { + if (paragraph === "") continue; + let current = ""; + let currentWidth = 0; + + for (let token of tokens(paragraph)) { + let tokenWidth = measureCellWidth(token); + if (current !== "" && currentWidth + tokenWidth > maxWidth) { + let trimmed = current.trimEnd(); + if (trimmed !== "") count++; + current = token.trimStart(); + currentWidth = measureCellWidth(current); + } else { + current += token; + currentWidth += tokenWidth; + } + + if (current !== "" && currentWidth > maxWidth && token.trim() !== "") { + count++; + current = ""; + currentWidth = 0; + } + } + + if (current.trimEnd() !== "") count++; + } + return count; +} + +function countNonEmptyNewlineParts(text: string): number { + let count = 0; + for (let part of text.split("\n")) if (part !== "") count++; + return count; +} + +function* tokens(text: string): IterableIterator { + let re = /\S+\s*/g; + for (let match of text.matchAll(re)) yield match[0]; +} + +function cellWidth(char: string): number { + let code = char.codePointAt(0)!; + if (code === 0) return 0; + if (code < 32 || (code >= 0x7F && code < 0xA0)) return 0; + if (isCombining(code)) return 0; + return isWide(code) ? 2 : 1; +} + +function isCombining(code: number): boolean { + return (code >= 0x0300 && code <= 0x036F) || + (code >= 0x1AB0 && code <= 0x1AFF) || + (code >= 0x1DC0 && code <= 0x1DFF) || + (code >= 0x20D0 && code <= 0x20FF) || + (code >= 0xFE20 && code <= 0xFE2F); +} + +function isWide(code: number): boolean { + return (code >= 0x1100 && code <= 0x115F) || + code === 0x2329 || code === 0x232A || + (code >= 0x2E80 && code <= 0xA4CF && code !== 0x303F) || + (code >= 0xAC00 && code <= 0xD7A3) || + (code >= 0xF900 && code <= 0xFAFF) || + (code >= 0xFE10 && code <= 0xFE19) || + (code >= 0xFE30 && code <= 0xFE6F) || + (code >= 0xFF00 && code <= 0xFF60) || + (code >= 0xFFE0 && code <= 0xFFE6) || + (code >= 0x1F300 && code <= 0x1FAFF) || + (code >= 0x20000 && code <= 0x3FFFD); +} diff --git a/mod.ts b/mod.ts index 8862d13..a8d1dbd 100644 --- a/mod.ts +++ b/mod.ts @@ -3,3 +3,4 @@ export * from "./term.ts"; export * from "./input.ts"; export * from "./settings.ts"; export * from "./termcodes.ts"; +export * from "./measure.ts"; diff --git a/ops.ts b/ops.ts index 3344eea..c76d584 100644 --- a/ops.ts +++ b/ops.ts @@ -52,11 +52,27 @@ function packAxis(view: DataView, offset: number, axis: SizingAxis): number { return o; } -function packString(view: DataView, bytes: Uint8Array, o: number): number { +function packString( + view: DataView, + bytes: Uint8Array, + o: number, + end: number, + context: string, +): number { + let paddedLength = Math.ceil(bytes.length / 4) * 4; + let next = o + 4 + paddedLength; + if (next > end) { + throw new RangeError( + `clayterm transfer buffer capacity exceeded while packing ${context} ` + + `(${next} byte offset, ${end} byte limit). ` + + `Render a smaller visible slice or reduce frame content.`, + ); + } + view.setUint32(o, bytes.length, true); o += 4; new Uint8Array(view.buffer).set(bytes, o); - o += Math.ceil(bytes.length / 4) * 4; + o += paddedLength; return o; } @@ -82,7 +98,7 @@ export function pack( o += 4; let bytes = encoder.encode(op.id); - o = packString(view, bytes, o); + o = packString(view, bytes, o, end, "element id"); let mask = 0; if (op.layout) mask |= PROP_LAYOUT; @@ -192,7 +208,7 @@ export function pack( o += 4; let str = encoder.encode(op.content); - o = packString(view, str, o); + o = packString(view, str, o, end, "text content"); break; } } diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..22bb928 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -721,6 +721,71 @@ and used in tests. array into the transfer encoding described in Section 12.1. Currently exported but not public API; its exposure is incidental to the module structure. +### 12.6 Text measurement helpers + +The module may also expose pure TypeScript text-measurement helpers for callers +that need pre-layout estimates without instantiating a `Term`: + +```ts +interface WrapTextOptions { + mode?: "words" | "newlines" | "none"; +} + +interface WrappedLine { + text: string; + width: number; +} + +measureCellWidth(text: string): number; +wrapText( + text: string, + width: number, + options?: WrapTextOptions, +): WrappedLine[]; +measureWrappedHeight( + text: string, + width: number, + options?: WrapTextOptions, +): number; +``` + +The current intended behavior is: + +- `measureCellWidth()` returns the terminal cell width of the full string using + the same Unicode-width model described in Section 13. +- `wrapText()` returns line records with both the emitted text and its measured + width. +- `measureWrappedHeight()` returns the number of wrapped lines that `wrapText()` + would produce for the same inputs. +- `mode: "words"` wraps on token boundaries while preserving explicit newline + breaks. +- `mode: "newlines"` splits only on explicit `\n` characters and does not + perform width-based wrapping. +- `mode: "none"` collapses explicit newlines and returns at most one line. +- The helpers operate on JavaScript strings directly. They do not require the + caller's text to be copied into WASM linear memory or encoded into a full + UTF-8 byte buffer as a precondition for measurement. +- Large-input behavior is bounded by host JavaScript memory, not by Clayterm's + WASM linear-memory capacity. Inputs materially larger than the renderer's + initial WASM memory footprint are intended to remain valid helper inputs. +- `measureCellWidth()` and `measureWrappedHeight()` are intended to process + large inputs in a single pass over the string without allocating auxiliary + storage proportional to the UTF-8 byte length of the entire input. + `wrapText()` necessarily allocates output proportional to the number of + wrapped lines it returns, but it likewise should not require a second + full-input UTF-8 buffer. +- Rendering oversized whole-document input remains constrained by the renderer's + transfer buffer. If a frame exceeds transfer-buffer capacity while packing + text, Clayterm MUST throw a descriptive `RangeError` identifying the capacity + failure and SHOULD direct callers to render a smaller visible slice or reduce + frame content. Clayterm MUST NOT expose only the raw host-level TypedArray + message `"offset is out of bounds"` for this condition. + +These helpers are independent of the renderer's frame lifecycle and perform no +IO or WASM interaction. They exist as convenience APIs for higher-level +frameworks and virtualized views that need width and height estimation before +building directive arrays. + --- ## 13. Implementation Notes diff --git a/test/measure.test.ts b/test/measure.test.ts new file mode 100644 index 0000000..584235a --- /dev/null +++ b/test/measure.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from "./suite.ts"; +import { + close, + createTerm, + fixed, + grow, + measureCellWidth, + measureWrappedHeight, + open, + text, + wrapText, +} from "../mod.ts"; +import { createTermNative } from "../term-native.ts"; +import { print } from "./print.ts"; + +const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); + +function makeOversizedDocument(memoryBytes: number) { + let targetBytes = memoryBytes * 2 + 65_536; + let encoder = new TextEncoder(); + let lines: string[] = []; + let bytes = 0; + for (let i = 0; bytes < targetBytes; i++) { + let line = `line-${i.toString().padStart(6, "0")} 🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂`; + lines.push(line); + bytes += encoder.encode(line + "\n").length; + } + return { content: lines.join("\n"), lines, bytes }; +} + +describe("text measurement helpers", () => { + it("exports helpers that can be called without creating a Term", () => { + expect(measureCellWidth("hello")).toBe(5); + expect(wrapText("hello world", 5).map((l) => l.text)).toEqual([ + "hello", + "world", + ]); + expect(measureWrappedHeight("hello world", 5)).toBe(2); + }); + + it("measures ASCII, combining marks, and wide characters", () => { + expect(measureCellWidth("hello")).toBe(5); + expect(measureCellWidth("e\u0301")).toBe(1); + expect(measureCellWidth("文字")).toBe(4); + expect(measureCellWidth("🙂")).toBe(2); + }); + + it("supports words, newlines, and none wrap modes", () => { + expect(wrapText("hello world", 5)).toEqual([ + { text: "hello", width: 5 }, + { text: "world", width: 5 }, + ]); + expect(wrapText("hello world", 5, { mode: "words" })).toEqual( + wrapText("hello world", 5), + ); + + expect(wrapText("hello world\nwide", 5, { mode: "newlines" })).toEqual([ + { text: "hello world", width: 11 }, + { text: "wide", width: 4 }, + ]); + + expect(wrapText("hello\nworld", 5, { mode: "none" })).toEqual([ + { text: "helloworld", width: 10 }, + ]); + }); + + it("keeps measureWrappedHeight equal to wrapText length", () => { + for (let mode of ["words", "newlines", "none"] as const) { + let input = "one two three\nfour five"; + expect(measureWrappedHeight(input, 7, { mode })).toBe( + wrapText(input, 7, { mode }).length, + ); + } + }); + + it("handles empty input deterministically", () => { + expect(measureCellWidth("")).toBe(0); + expect(wrapText("", 10)).toEqual([]); + expect(measureWrappedHeight("", 10)).toBe(0); + }); + + it("rejects invalid widths", () => { + for (let width of [-1, Number.NaN, Number.POSITIVE_INFINITY]) { + expect(() => wrapText("x", width)).toThrow(RangeError); + expect(() => measureWrappedHeight("x", width)).toThrow(RangeError); + } + }); + + it("does not instantiate WebAssembly or UTF-8 encode as a precondition for measurement", () => { + let instantiate = WebAssembly.instantiate; + let encode = TextEncoder.prototype.encode; + try { + WebAssembly.instantiate = (() => { + throw new Error("unexpected wasm instantiate"); + }) as typeof WebAssembly.instantiate; + TextEncoder.prototype.encode = function () { + throw new Error("unexpected text encode"); + }; + + expect(measureCellWidth("hello 🙂")).toBe(8); + expect(measureWrappedHeight("hello world", 5)).toBe(2); + expect(wrapText("hello world", 5).length).toBe(2); + } finally { + WebAssembly.instantiate = instantiate; + TextEncoder.prototype.encode = encode; + } + }); + + it("renders a visible window from a UTF-8 document larger than 2x renderer memory", async () => { + let native = await createTermNative(80, 24); + let document = makeOversizedDocument(native.memory.buffer.byteLength); + expect(document.bytes).toBeGreaterThan(native.memory.buffer.byteLength * 2); + + let term = await createTerm({ width: 80, height: 24 }); + let error: unknown; + try { + term.render([ + open("root", { layout: { width: grow(), height: grow() } }), + text(document.content), + close(), + ]); + } catch (caught) { + error = caught; + } + expect(error).toBeInstanceOf(RangeError); + expect((error as Error).message).toMatch( + /transfer buffer|capacity|packing/, + ); + expect((error as Error).message).not.toBe("offset is out of bounds"); + expect((error as Error).message).toMatch( + /smaller visible slice|reduce frame content/, + ); + + expect(measureWrappedHeight(document.content, 80, { mode: "newlines" })) + .toBe(document.lines.length); + + let start = Math.floor(document.lines.length / 2); + let visible = document.lines.slice(start, start + 3).join("\n"); + let out = print( + decode( + term.render([ + open("root", { + layout: { width: fixed(80), height: fixed(24), direction: "ttb" }, + }), + text(visible), + close(), + ]).output, + ), + 80, + 24, + ); + + let marker = (index: number) => document.lines[index].slice(0, 11); + expect(out).toContain(marker(start)); + expect(out).toContain(marker(start + 2)); + expect(out).not.toContain(marker(start - 1)); + expect(out).not.toContain(marker(start + 3)); + }); +}); From ea34f5bdf292bebe0ebd4883a862998967ccf080 Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 10 May 2026 07:07:51 -0400 Subject: [PATCH 31/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20string=20liter?= =?UTF-8?q?als=20for=20floating=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops.ts | 109 +++++++++++++++++++++++++++-------------- specs/renderer-spec.md | 42 ++++++++++++---- test/term.test.ts | 17 ++----- test/validate.test.ts | 13 +++-- validate.ts | 39 +++++++++++++-- 5 files changed, 151 insertions(+), 69 deletions(-) diff --git a/ops.ts b/ops.ts index 9797c5b..7f327f8 100644 --- a/ops.ts +++ b/ops.ts @@ -171,16 +171,16 @@ export function pack( o += 4; view.setUint32( o, - (f.attachTo ?? 0) | - ((f.attachPoints?.element ?? 0) << 8) | - ((f.attachPoints?.parent ?? 0) << 16) | - ((f.pointerCaptureMode ?? 0) << 24), + encodeAttachTo(f.attachTo) | + (encodeAttachPoint(f.attachPoints?.element) << 8) | + (encodeAttachPoint(f.attachPoints?.parent) << 16) | + (encodePointerCaptureMode(f.pointerCaptureMode) << 24), true, ); o += 4; view.setUint32( o, - (f.clipTo ?? 0) | (((f.zIndex ?? 0) & 0xffff) << 8), + encodeClipTo(f.clipTo) | (((f.zIndex ?? 0) & 0xffff) << 8), true, ); o += 4; @@ -278,42 +278,77 @@ export interface OpenElement { y?: number; expand?: { width?: number; height?: number }; parent?: number; - attachTo?: number; - attachPoints?: { element?: number; parent?: number }; - pointerCaptureMode?: number; - clipTo?: number; + attachTo?: AttachTo; + attachPoints?: { element?: AttachPoint; parent?: AttachPoint }; + pointerCaptureMode?: PointerCaptureMode; + clipTo?: ClipTo; zIndex?: number; }; } -export const ATTACH_POINT = { - LEFT_TOP: 0, - LEFT_CENTER: 1, - LEFT_BOTTOM: 2, - CENTER_TOP: 3, - CENTER_CENTER: 4, - CENTER_BOTTOM: 5, - RIGHT_TOP: 6, - RIGHT_CENTER: 7, - RIGHT_BOTTOM: 8, -} as const; - -export const ATTACH_TO = { - NONE: 0, - PARENT: 1, - ELEMENT_WITH_ID: 2, - ROOT: 3, -} as const; - -export const POINTER_CAPTURE_MODE = { - CAPTURE: 0, - PASSTHROUGH: 1, -} as const; - -export const CLIP_TO = { - NONE: 0, - ATTACHED_PARENT: 1, -} as const; +export type AttachPoint = + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; + +export type AttachTo = "none" | "parent" | "element" | "root"; + +export type PointerCaptureMode = "capture" | "passthrough"; + +export type ClipTo = "none" | "attached-parent"; + +const ATTACH_POINT: Record = { + "left-top": 0, + "left-center": 1, + "left-bottom": 2, + "center-top": 3, + "center-center": 4, + "center-bottom": 5, + "right-top": 6, + "right-center": 7, + "right-bottom": 8, +}; + +const ATTACH_TO: Record = { + none: 0, + parent: 1, + element: 2, + root: 3, +}; + +const POINTER_CAPTURE_MODE: Record = { + capture: 0, + passthrough: 1, +}; + +const CLIP_TO: Record = { + none: 0, + "attached-parent": 1, +}; + +function encodeAttachPoint(value: AttachPoint | undefined): number { + return value === undefined ? 0 : ATTACH_POINT[value]; +} + +function encodeAttachTo(value: AttachTo | undefined): number { + return value === undefined ? 0 : ATTACH_TO[value]; +} + +function encodePointerCaptureMode( + value: PointerCaptureMode | undefined, +): number { + return value === undefined ? 0 : POINTER_CAPTURE_MODE[value]; +} + +function encodeClipTo(value: ClipTo | undefined): number { + return value === undefined ? 0 : CLIP_TO[value]; +} export interface Text { directive: typeof OP_TEXT; diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index 079363c..0fe559e 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -622,22 +622,44 @@ floating?: { y?: number; expand?: { width?: number; height?: number }; parent?: number; - attachTo?: number; + attachTo?: "none" | "parent" | "element" | "root"; attachPoints?: { - element?: number; - parent?: number; + element?: + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; + parent?: + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; }; - pointerCaptureMode?: number; - clipTo?: number; + pointerCaptureMode?: "capture" | "passthrough"; + clipTo?: "none" | "attached-parent"; zIndex?: number; } ``` -This shape extends the earlier floating surface in two ways. First, -`attachPoints` is structured as separate element and parent anchor values -instead of a single packed enum. Second, the surface exposes additional Clay -floating controls that were previously unavailable at the TypeScript layer: -`expand`, `pointerCaptureMode`, and `clipTo`. +The `floating` object configures Clay floating layout behavior. `x` and `y` +provide the floating offset. `expand` expands the floating bounds. `parent` +identifies the target element when `attachTo` is `"element"`. `attachTo` selects +whether the element is attached to no target, its parent, an element, or the +layout root. `attachPoints.element` describes the anchor on the floating +element, and `attachPoints.parent` describes the anchor on the attached target. +`pointerCaptureMode` controls whether the floating element captures pointer +input or lets it pass through, `clipTo` controls inherited clipping, and +`zIndex` controls floating order. The `text()` constructor currently accepts: `color`, `fontSize`, `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, diff --git a/test/term.test.ts b/test/term.test.ts index 16ace04..06b970d 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -1,15 +1,6 @@ import { beforeEach, describe, expect, it } from "./suite.ts"; import { createTerm, type Term } from "../term.ts"; -import { - ATTACH_POINT, - ATTACH_TO, - close, - fixed, - grow, - open, - rgba, - text, -} from "../ops.ts"; +import { close, fixed, grow, open, rgba, text } from "../ops.ts"; import { print } from "./print.ts"; const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); @@ -224,10 +215,10 @@ describe("term", () => { floating: { x: 3, y: 1, - attachTo: ATTACH_TO.ROOT, + attachTo: "root", attachPoints: { - element: ATTACH_POINT.CENTER_CENTER, - parent: ATTACH_POINT.CENTER_CENTER, + element: "center-center", + parent: "center-center", }, }, }), diff --git a/test/validate.test.ts b/test/validate.test.ts index 3991288..35bcf08 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -83,7 +83,7 @@ describe("validate", () => { expect(validate([ open("x", { floating: { - attachPoints: { element: 4, parent: 4 }, + attachPoints: { element: "center-center", parent: "center-center" }, }, }), close(), @@ -95,8 +95,8 @@ describe("validate", () => { open("x", { floating: { expand: { width: 2, height: 3 }, - pointerCaptureMode: 1, - clipTo: 1, + pointerCaptureMode: "passthrough", + clipTo: "attached-parent", zIndex: 1024, }, }), @@ -104,7 +104,12 @@ describe("validate", () => { ])).toBe(true); }); - it("rejects numeric floating attachPoints legacy shape", () => { + it("rejects numeric floating enum values", () => { + expect(validate([ + // deno-lint-ignore no-explicit-any + open("x", { floating: { attachTo: 3 as any } }), + close(), + ])).toBe(false); expect(validate([ // deno-lint-ignore no-explicit-any open("x", { floating: { attachPoints: 4 as any } }), diff --git a/validate.ts b/validate.ts index 1e697a2..8d2ad63 100644 --- a/validate.ts +++ b/validate.ts @@ -80,6 +80,35 @@ const Clip = Type.Object({ vertical: Type.Optional(Type.Boolean()), }); +const AttachPoint = Type.Union([ + Type.Literal("left-top"), + Type.Literal("left-center"), + Type.Literal("left-bottom"), + Type.Literal("center-top"), + Type.Literal("center-center"), + Type.Literal("center-bottom"), + Type.Literal("right-top"), + Type.Literal("right-center"), + Type.Literal("right-bottom"), +]); + +const AttachTo = Type.Union([ + Type.Literal("none"), + Type.Literal("parent"), + Type.Literal("element"), + Type.Literal("root"), +]); + +const PointerCaptureMode = Type.Union([ + Type.Literal("capture"), + Type.Literal("passthrough"), +]); + +const ClipTo = Type.Union([ + Type.Literal("none"), + Type.Literal("attached-parent"), +]); + const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), @@ -88,13 +117,13 @@ const Floating = Type.Object({ height: Type.Optional(Type.Number()), })), parent: Type.Optional(Type.Integer({ minimum: 0 })), - attachTo: Type.Optional(u8), + attachTo: Type.Optional(AttachTo), attachPoints: Type.Optional(Type.Object({ - element: Type.Optional(u8), - parent: Type.Optional(u8), + element: Type.Optional(AttachPoint), + parent: Type.Optional(AttachPoint), })), - pointerCaptureMode: Type.Optional(u8), - clipTo: Type.Optional(u8), + pointerCaptureMode: Type.Optional(PointerCaptureMode), + clipTo: Type.Optional(ClipTo), zIndex: Type.Optional(u16), }); From c099165dd75ec2ef6ea492a4a81b6dbfc09ffec0 Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 31 May 2026 08:55:14 -0400 Subject: [PATCH 32/35] =?UTF-8?q?=F0=9F=90=9B=20align=20text=20measurement?= =?UTF-8?q?=20with=20renderer=20wcwidth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- measure.ts | 31 +- test/measure.test.ts | 6 + wcwidth.ts | 2176 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2187 insertions(+), 26 deletions(-) create mode 100644 wcwidth.ts diff --git a/measure.ts b/measure.ts index dcc5e2d..83f4c79 100644 --- a/measure.ts +++ b/measure.ts @@ -1,3 +1,5 @@ +import { wcwidth } from "./wcwidth.ts"; + export interface WrapTextOptions { mode?: "words" | "newlines" | "none"; } @@ -148,30 +150,7 @@ function* tokens(text: string): IterableIterator { function cellWidth(char: string): number { let code = char.codePointAt(0)!; - if (code === 0) return 0; - if (code < 32 || (code >= 0x7F && code < 0xA0)) return 0; - if (isCombining(code)) return 0; - return isWide(code) ? 2 : 1; -} - -function isCombining(code: number): boolean { - return (code >= 0x0300 && code <= 0x036F) || - (code >= 0x1AB0 && code <= 0x1AFF) || - (code >= 0x1DC0 && code <= 0x1DFF) || - (code >= 0x20D0 && code <= 0x20FF) || - (code >= 0xFE20 && code <= 0xFE2F); -} - -function isWide(code: number): boolean { - return (code >= 0x1100 && code <= 0x115F) || - code === 0x2329 || code === 0x232A || - (code >= 0x2E80 && code <= 0xA4CF && code !== 0x303F) || - (code >= 0xAC00 && code <= 0xD7A3) || - (code >= 0xF900 && code <= 0xFAFF) || - (code >= 0xFE10 && code <= 0xFE19) || - (code >= 0xFE30 && code <= 0xFE6F) || - (code >= 0xFF00 && code <= 0xFF60) || - (code >= 0xFFE0 && code <= 0xFFE6) || - (code >= 0x1F300 && code <= 0x1FAFF) || - (code >= 0x20000 && code <= 0x3FFFD); + if (code >= 0xD800 && code <= 0xDFFF) code = 0xFFFD; + let width = wcwidth(code); + return width > 0 ? width : 0; } diff --git a/test/measure.test.ts b/test/measure.test.ts index 584235a..b8760dc 100644 --- a/test/measure.test.ts +++ b/test/measure.test.ts @@ -45,6 +45,12 @@ describe("text measurement helpers", () => { expect(measureCellWidth("🙂")).toBe(2); }); + it("matches renderer zero-width handling for default-ignorable codepoints", () => { + expect(measureCellWidth("a\u200db")).toBe(2); + expect(measureCellWidth("x\uFE0Fy")).toBe(2); + expect(measureCellWidth("👩‍💻")).toBe(4); + }); + it("supports words, newlines, and none wrap modes", () => { expect(wrapText("hello world", 5)).toEqual([ { text: "hello", width: 5 }, diff --git a/wcwidth.ts b/wcwidth.ts new file mode 100644 index 0000000..00558c9 --- /dev/null +++ b/wcwidth.ts @@ -0,0 +1,2176 @@ +/* Unicode character width lookup. + * + * This is a TypeScript port of src/wcwidth.c, which is extracted from + * termbox2 and covers Unicode 15.0. Keep this table in sync with the renderer + * table so pure TypeScript measurement matches Clayterm layout measurement. + */ + +const WCWIDTH_TABLE: ReadonlyArray = [ + [0x000001, 0x00001F, -1], + [0x000020, 0x00007E, 1], + [0x00007F, 0x00009F, -1], + [0x0000A0, 0x0002FF, 1], + [0x000300, 0x00036F, 0], + [0x000370, 0x000377, 1], + [0x000378, 0x000379, -1], + [0x00037A, 0x00037F, 1], + [0x000380, 0x000383, -1], + [0x000384, 0x00038A, 1], + [0x00038B, 0x00038B, -1], + [0x00038C, 0x00038C, 1], + [0x00038D, 0x00038D, -1], + [0x00038E, 0x0003A1, 1], + [0x0003A2, 0x0003A2, -1], + [0x0003A3, 0x000482, 1], + [0x000483, 0x000489, 0], + [0x00048A, 0x00052F, 1], + [0x000530, 0x000530, -1], + [0x000531, 0x000556, 1], + [0x000557, 0x000558, -1], + [0x000559, 0x00058A, 1], + [0x00058B, 0x00058C, -1], + [0x00058D, 0x00058F, 1], + [0x000590, 0x000590, -1], + [0x000591, 0x0005BD, 0], + [0x0005BE, 0x0005BE, 1], + [0x0005BF, 0x0005BF, 0], + [0x0005C0, 0x0005C0, 1], + [0x0005C1, 0x0005C2, 0], + [0x0005C3, 0x0005C3, 1], + [0x0005C4, 0x0005C5, 0], + [0x0005C6, 0x0005C6, 1], + [0x0005C7, 0x0005C7, 0], + [0x0005C8, 0x0005CF, -1], + [0x0005D0, 0x0005EA, 1], + [0x0005EB, 0x0005EE, -1], + [0x0005EF, 0x0005F4, 1], + [0x0005F5, 0x0005FF, -1], + [0x000600, 0x00060F, 1], + [0x000610, 0x00061A, 0], + [0x00061B, 0x00061B, 1], + [0x00061C, 0x00061C, 0], + [0x00061D, 0x00064A, 1], + [0x00064B, 0x00065F, 0], + [0x000660, 0x00066F, 1], + [0x000670, 0x000670, 0], + [0x000671, 0x0006D5, 1], + [0x0006D6, 0x0006DC, 0], + [0x0006DD, 0x0006DE, 1], + [0x0006DF, 0x0006E4, 0], + [0x0006E5, 0x0006E6, 1], + [0x0006E7, 0x0006E8, 0], + [0x0006E9, 0x0006E9, 1], + [0x0006EA, 0x0006ED, 0], + [0x0006EE, 0x00070D, 1], + [0x00070E, 0x00070E, -1], + [0x00070F, 0x000710, 1], + [0x000711, 0x000711, 0], + [0x000712, 0x00072F, 1], + [0x000730, 0x00074A, 0], + [0x00074B, 0x00074C, -1], + [0x00074D, 0x0007A5, 1], + [0x0007A6, 0x0007B0, 0], + [0x0007B1, 0x0007B1, 1], + [0x0007B2, 0x0007BF, -1], + [0x0007C0, 0x0007EA, 1], + [0x0007EB, 0x0007F3, 0], + [0x0007F4, 0x0007FA, 1], + [0x0007FB, 0x0007FC, -1], + [0x0007FD, 0x0007FD, 0], + [0x0007FE, 0x000815, 1], + [0x000816, 0x000819, 0], + [0x00081A, 0x00081A, 1], + [0x00081B, 0x000823, 0], + [0x000824, 0x000824, 1], + [0x000825, 0x000827, 0], + [0x000828, 0x000828, 1], + [0x000829, 0x00082D, 0], + [0x00082E, 0x00082F, -1], + [0x000830, 0x00083E, 1], + [0x00083F, 0x00083F, -1], + [0x000840, 0x000858, 1], + [0x000859, 0x00085B, 0], + [0x00085C, 0x00085D, -1], + [0x00085E, 0x00085E, 1], + [0x00085F, 0x00085F, -1], + [0x000860, 0x00086A, 1], + [0x00086B, 0x00086F, -1], + [0x000870, 0x00088E, 1], + [0x00088F, 0x00088F, -1], + [0x000890, 0x000891, 1], + [0x000892, 0x000896, -1], + [0x000897, 0x00089F, 0], + [0x0008A0, 0x0008C9, 1], + [0x0008CA, 0x0008E1, 0], + [0x0008E2, 0x0008E2, 1], + [0x0008E3, 0x000902, 0], + [0x000903, 0x000939, 1], + [0x00093A, 0x00093A, 0], + [0x00093B, 0x00093B, 1], + [0x00093C, 0x00093C, 0], + [0x00093D, 0x000940, 1], + [0x000941, 0x000948, 0], + [0x000949, 0x00094C, 1], + [0x00094D, 0x00094D, 0], + [0x00094E, 0x000950, 1], + [0x000951, 0x000957, 0], + [0x000958, 0x000961, 1], + [0x000962, 0x000963, 0], + [0x000964, 0x000980, 1], + [0x000981, 0x000981, 0], + [0x000982, 0x000983, 1], + [0x000984, 0x000984, -1], + [0x000985, 0x00098C, 1], + [0x00098D, 0x00098E, -1], + [0x00098F, 0x000990, 1], + [0x000991, 0x000992, -1], + [0x000993, 0x0009A8, 1], + [0x0009A9, 0x0009A9, -1], + [0x0009AA, 0x0009B0, 1], + [0x0009B1, 0x0009B1, -1], + [0x0009B2, 0x0009B2, 1], + [0x0009B3, 0x0009B5, -1], + [0x0009B6, 0x0009B9, 1], + [0x0009BA, 0x0009BB, -1], + [0x0009BC, 0x0009BC, 0], + [0x0009BD, 0x0009C0, 1], + [0x0009C1, 0x0009C4, 0], + [0x0009C5, 0x0009C6, -1], + [0x0009C7, 0x0009C8, 1], + [0x0009C9, 0x0009CA, -1], + [0x0009CB, 0x0009CC, 1], + [0x0009CD, 0x0009CD, 0], + [0x0009CE, 0x0009CE, 1], + [0x0009CF, 0x0009D6, -1], + [0x0009D7, 0x0009D7, 1], + [0x0009D8, 0x0009DB, -1], + [0x0009DC, 0x0009DD, 1], + [0x0009DE, 0x0009DE, -1], + [0x0009DF, 0x0009E1, 1], + [0x0009E2, 0x0009E3, 0], + [0x0009E4, 0x0009E5, -1], + [0x0009E6, 0x0009FD, 1], + [0x0009FE, 0x0009FE, 0], + [0x0009FF, 0x000A00, -1], + [0x000A01, 0x000A02, 0], + [0x000A03, 0x000A03, 1], + [0x000A04, 0x000A04, -1], + [0x000A05, 0x000A0A, 1], + [0x000A0B, 0x000A0E, -1], + [0x000A0F, 0x000A10, 1], + [0x000A11, 0x000A12, -1], + [0x000A13, 0x000A28, 1], + [0x000A29, 0x000A29, -1], + [0x000A2A, 0x000A30, 1], + [0x000A31, 0x000A31, -1], + [0x000A32, 0x000A33, 1], + [0x000A34, 0x000A34, -1], + [0x000A35, 0x000A36, 1], + [0x000A37, 0x000A37, -1], + [0x000A38, 0x000A39, 1], + [0x000A3A, 0x000A3B, -1], + [0x000A3C, 0x000A3C, 0], + [0x000A3D, 0x000A3D, -1], + [0x000A3E, 0x000A40, 1], + [0x000A41, 0x000A42, 0], + [0x000A43, 0x000A46, -1], + [0x000A47, 0x000A48, 0], + [0x000A49, 0x000A4A, -1], + [0x000A4B, 0x000A4D, 0], + [0x000A4E, 0x000A50, -1], + [0x000A51, 0x000A51, 0], + [0x000A52, 0x000A58, -1], + [0x000A59, 0x000A5C, 1], + [0x000A5D, 0x000A5D, -1], + [0x000A5E, 0x000A5E, 1], + [0x000A5F, 0x000A65, -1], + [0x000A66, 0x000A6F, 1], + [0x000A70, 0x000A71, 0], + [0x000A72, 0x000A74, 1], + [0x000A75, 0x000A75, 0], + [0x000A76, 0x000A76, 1], + [0x000A77, 0x000A80, -1], + [0x000A81, 0x000A82, 0], + [0x000A83, 0x000A83, 1], + [0x000A84, 0x000A84, -1], + [0x000A85, 0x000A8D, 1], + [0x000A8E, 0x000A8E, -1], + [0x000A8F, 0x000A91, 1], + [0x000A92, 0x000A92, -1], + [0x000A93, 0x000AA8, 1], + [0x000AA9, 0x000AA9, -1], + [0x000AAA, 0x000AB0, 1], + [0x000AB1, 0x000AB1, -1], + [0x000AB2, 0x000AB3, 1], + [0x000AB4, 0x000AB4, -1], + [0x000AB5, 0x000AB9, 1], + [0x000ABA, 0x000ABB, -1], + [0x000ABC, 0x000ABC, 0], + [0x000ABD, 0x000AC0, 1], + [0x000AC1, 0x000AC5, 0], + [0x000AC6, 0x000AC6, -1], + [0x000AC7, 0x000AC8, 0], + [0x000AC9, 0x000AC9, 1], + [0x000ACA, 0x000ACA, -1], + [0x000ACB, 0x000ACC, 1], + [0x000ACD, 0x000ACD, 0], + [0x000ACE, 0x000ACF, -1], + [0x000AD0, 0x000AD0, 1], + [0x000AD1, 0x000ADF, -1], + [0x000AE0, 0x000AE1, 1], + [0x000AE2, 0x000AE3, 0], + [0x000AE4, 0x000AE5, -1], + [0x000AE6, 0x000AF1, 1], + [0x000AF2, 0x000AF8, -1], + [0x000AF9, 0x000AF9, 1], + [0x000AFA, 0x000AFF, 0], + [0x000B00, 0x000B00, -1], + [0x000B01, 0x000B01, 0], + [0x000B02, 0x000B03, 1], + [0x000B04, 0x000B04, -1], + [0x000B05, 0x000B0C, 1], + [0x000B0D, 0x000B0E, -1], + [0x000B0F, 0x000B10, 1], + [0x000B11, 0x000B12, -1], + [0x000B13, 0x000B28, 1], + [0x000B29, 0x000B29, -1], + [0x000B2A, 0x000B30, 1], + [0x000B31, 0x000B31, -1], + [0x000B32, 0x000B33, 1], + [0x000B34, 0x000B34, -1], + [0x000B35, 0x000B39, 1], + [0x000B3A, 0x000B3B, -1], + [0x000B3C, 0x000B3C, 0], + [0x000B3D, 0x000B3E, 1], + [0x000B3F, 0x000B3F, 0], + [0x000B40, 0x000B40, 1], + [0x000B41, 0x000B44, 0], + [0x000B45, 0x000B46, -1], + [0x000B47, 0x000B48, 1], + [0x000B49, 0x000B4A, -1], + [0x000B4B, 0x000B4C, 1], + [0x000B4D, 0x000B4D, 0], + [0x000B4E, 0x000B54, -1], + [0x000B55, 0x000B56, 0], + [0x000B57, 0x000B57, 1], + [0x000B58, 0x000B5B, -1], + [0x000B5C, 0x000B5D, 1], + [0x000B5E, 0x000B5E, -1], + [0x000B5F, 0x000B61, 1], + [0x000B62, 0x000B63, 0], + [0x000B64, 0x000B65, -1], + [0x000B66, 0x000B77, 1], + [0x000B78, 0x000B81, -1], + [0x000B82, 0x000B82, 0], + [0x000B83, 0x000B83, 1], + [0x000B84, 0x000B84, -1], + [0x000B85, 0x000B8A, 1], + [0x000B8B, 0x000B8D, -1], + [0x000B8E, 0x000B90, 1], + [0x000B91, 0x000B91, -1], + [0x000B92, 0x000B95, 1], + [0x000B96, 0x000B98, -1], + [0x000B99, 0x000B9A, 1], + [0x000B9B, 0x000B9B, -1], + [0x000B9C, 0x000B9C, 1], + [0x000B9D, 0x000B9D, -1], + [0x000B9E, 0x000B9F, 1], + [0x000BA0, 0x000BA2, -1], + [0x000BA3, 0x000BA4, 1], + [0x000BA5, 0x000BA7, -1], + [0x000BA8, 0x000BAA, 1], + [0x000BAB, 0x000BAD, -1], + [0x000BAE, 0x000BB9, 1], + [0x000BBA, 0x000BBD, -1], + [0x000BBE, 0x000BBF, 1], + [0x000BC0, 0x000BC0, 0], + [0x000BC1, 0x000BC2, 1], + [0x000BC3, 0x000BC5, -1], + [0x000BC6, 0x000BC8, 1], + [0x000BC9, 0x000BC9, -1], + [0x000BCA, 0x000BCC, 1], + [0x000BCD, 0x000BCD, 0], + [0x000BCE, 0x000BCF, -1], + [0x000BD0, 0x000BD0, 1], + [0x000BD1, 0x000BD6, -1], + [0x000BD7, 0x000BD7, 1], + [0x000BD8, 0x000BE5, -1], + [0x000BE6, 0x000BFA, 1], + [0x000BFB, 0x000BFF, -1], + [0x000C00, 0x000C00, 0], + [0x000C01, 0x000C03, 1], + [0x000C04, 0x000C04, 0], + [0x000C05, 0x000C0C, 1], + [0x000C0D, 0x000C0D, -1], + [0x000C0E, 0x000C10, 1], + [0x000C11, 0x000C11, -1], + [0x000C12, 0x000C28, 1], + [0x000C29, 0x000C29, -1], + [0x000C2A, 0x000C39, 1], + [0x000C3A, 0x000C3B, -1], + [0x000C3C, 0x000C3C, 0], + [0x000C3D, 0x000C3D, 1], + [0x000C3E, 0x000C40, 0], + [0x000C41, 0x000C44, 1], + [0x000C45, 0x000C45, -1], + [0x000C46, 0x000C48, 0], + [0x000C49, 0x000C49, -1], + [0x000C4A, 0x000C4D, 0], + [0x000C4E, 0x000C54, -1], + [0x000C55, 0x000C56, 0], + [0x000C57, 0x000C57, -1], + [0x000C58, 0x000C5A, 1], + [0x000C5B, 0x000C5C, -1], + [0x000C5D, 0x000C5D, 1], + [0x000C5E, 0x000C5F, -1], + [0x000C60, 0x000C61, 1], + [0x000C62, 0x000C63, 0], + [0x000C64, 0x000C65, -1], + [0x000C66, 0x000C6F, 1], + [0x000C70, 0x000C76, -1], + [0x000C77, 0x000C80, 1], + [0x000C81, 0x000C81, 0], + [0x000C82, 0x000C8C, 1], + [0x000C8D, 0x000C8D, -1], + [0x000C8E, 0x000C90, 1], + [0x000C91, 0x000C91, -1], + [0x000C92, 0x000CA8, 1], + [0x000CA9, 0x000CA9, -1], + [0x000CAA, 0x000CB3, 1], + [0x000CB4, 0x000CB4, -1], + [0x000CB5, 0x000CB9, 1], + [0x000CBA, 0x000CBB, -1], + [0x000CBC, 0x000CBC, 0], + [0x000CBD, 0x000CBE, 1], + [0x000CBF, 0x000CBF, 0], + [0x000CC0, 0x000CC4, 1], + [0x000CC5, 0x000CC5, -1], + [0x000CC6, 0x000CC6, 0], + [0x000CC7, 0x000CC8, 1], + [0x000CC9, 0x000CC9, -1], + [0x000CCA, 0x000CCB, 1], + [0x000CCC, 0x000CCD, 0], + [0x000CCE, 0x000CD4, -1], + [0x000CD5, 0x000CD6, 1], + [0x000CD7, 0x000CDC, -1], + [0x000CDD, 0x000CDE, 1], + [0x000CDF, 0x000CDF, -1], + [0x000CE0, 0x000CE1, 1], + [0x000CE2, 0x000CE3, 0], + [0x000CE4, 0x000CE5, -1], + [0x000CE6, 0x000CEF, 1], + [0x000CF0, 0x000CF0, -1], + [0x000CF1, 0x000CF3, 1], + [0x000CF4, 0x000CFF, -1], + [0x000D00, 0x000D01, 0], + [0x000D02, 0x000D0C, 1], + [0x000D0D, 0x000D0D, -1], + [0x000D0E, 0x000D10, 1], + [0x000D11, 0x000D11, -1], + [0x000D12, 0x000D3A, 1], + [0x000D3B, 0x000D3C, 0], + [0x000D3D, 0x000D40, 1], + [0x000D41, 0x000D44, 0], + [0x000D45, 0x000D45, -1], + [0x000D46, 0x000D48, 1], + [0x000D49, 0x000D49, -1], + [0x000D4A, 0x000D4C, 1], + [0x000D4D, 0x000D4D, 0], + [0x000D4E, 0x000D4F, 1], + [0x000D50, 0x000D53, -1], + [0x000D54, 0x000D61, 1], + [0x000D62, 0x000D63, 0], + [0x000D64, 0x000D65, -1], + [0x000D66, 0x000D7F, 1], + [0x000D80, 0x000D80, -1], + [0x000D81, 0x000D81, 0], + [0x000D82, 0x000D83, 1], + [0x000D84, 0x000D84, -1], + [0x000D85, 0x000D96, 1], + [0x000D97, 0x000D99, -1], + [0x000D9A, 0x000DB1, 1], + [0x000DB2, 0x000DB2, -1], + [0x000DB3, 0x000DBB, 1], + [0x000DBC, 0x000DBC, -1], + [0x000DBD, 0x000DBD, 1], + [0x000DBE, 0x000DBF, -1], + [0x000DC0, 0x000DC6, 1], + [0x000DC7, 0x000DC9, -1], + [0x000DCA, 0x000DCA, 0], + [0x000DCB, 0x000DCE, -1], + [0x000DCF, 0x000DD1, 1], + [0x000DD2, 0x000DD4, 0], + [0x000DD5, 0x000DD5, -1], + [0x000DD6, 0x000DD6, 0], + [0x000DD7, 0x000DD7, -1], + [0x000DD8, 0x000DDF, 1], + [0x000DE0, 0x000DE5, -1], + [0x000DE6, 0x000DEF, 1], + [0x000DF0, 0x000DF1, -1], + [0x000DF2, 0x000DF4, 1], + [0x000DF5, 0x000E00, -1], + [0x000E01, 0x000E30, 1], + [0x000E31, 0x000E31, 0], + [0x000E32, 0x000E33, 1], + [0x000E34, 0x000E3A, 0], + [0x000E3B, 0x000E3E, -1], + [0x000E3F, 0x000E46, 1], + [0x000E47, 0x000E4E, 0], + [0x000E4F, 0x000E5B, 1], + [0x000E5C, 0x000E80, -1], + [0x000E81, 0x000E82, 1], + [0x000E83, 0x000E83, -1], + [0x000E84, 0x000E84, 1], + [0x000E85, 0x000E85, -1], + [0x000E86, 0x000E8A, 1], + [0x000E8B, 0x000E8B, -1], + [0x000E8C, 0x000EA3, 1], + [0x000EA4, 0x000EA4, -1], + [0x000EA5, 0x000EA5, 1], + [0x000EA6, 0x000EA6, -1], + [0x000EA7, 0x000EB0, 1], + [0x000EB1, 0x000EB1, 0], + [0x000EB2, 0x000EB3, 1], + [0x000EB4, 0x000EBC, 0], + [0x000EBD, 0x000EBD, 1], + [0x000EBE, 0x000EBF, -1], + [0x000EC0, 0x000EC4, 1], + [0x000EC5, 0x000EC5, -1], + [0x000EC6, 0x000EC6, 1], + [0x000EC7, 0x000EC7, -1], + [0x000EC8, 0x000ECE, 0], + [0x000ECF, 0x000ECF, -1], + [0x000ED0, 0x000ED9, 1], + [0x000EDA, 0x000EDB, -1], + [0x000EDC, 0x000EDF, 1], + [0x000EE0, 0x000EFF, -1], + [0x000F00, 0x000F17, 1], + [0x000F18, 0x000F19, 0], + [0x000F1A, 0x000F34, 1], + [0x000F35, 0x000F35, 0], + [0x000F36, 0x000F36, 1], + [0x000F37, 0x000F37, 0], + [0x000F38, 0x000F38, 1], + [0x000F39, 0x000F39, 0], + [0x000F3A, 0x000F47, 1], + [0x000F48, 0x000F48, -1], + [0x000F49, 0x000F6C, 1], + [0x000F6D, 0x000F70, -1], + [0x000F71, 0x000F7E, 0], + [0x000F7F, 0x000F7F, 1], + [0x000F80, 0x000F84, 0], + [0x000F85, 0x000F85, 1], + [0x000F86, 0x000F87, 0], + [0x000F88, 0x000F8C, 1], + [0x000F8D, 0x000F97, 0], + [0x000F98, 0x000F98, -1], + [0x000F99, 0x000FBC, 0], + [0x000FBD, 0x000FBD, -1], + [0x000FBE, 0x000FC5, 1], + [0x000FC6, 0x000FC6, 0], + [0x000FC7, 0x000FCC, 1], + [0x000FCD, 0x000FCD, -1], + [0x000FCE, 0x000FDA, 1], + [0x000FDB, 0x000FFF, -1], + [0x001000, 0x00102C, 1], + [0x00102D, 0x001030, 0], + [0x001031, 0x001031, 1], + [0x001032, 0x001037, 0], + [0x001038, 0x001038, 1], + [0x001039, 0x00103A, 0], + [0x00103B, 0x00103C, 1], + [0x00103D, 0x00103E, 0], + [0x00103F, 0x001057, 1], + [0x001058, 0x001059, 0], + [0x00105A, 0x00105D, 1], + [0x00105E, 0x001060, 0], + [0x001061, 0x001070, 1], + [0x001071, 0x001074, 0], + [0x001075, 0x001081, 1], + [0x001082, 0x001082, 0], + [0x001083, 0x001084, 1], + [0x001085, 0x001086, 0], + [0x001087, 0x00108C, 1], + [0x00108D, 0x00108D, 0], + [0x00108E, 0x00109C, 1], + [0x00109D, 0x00109D, 0], + [0x00109E, 0x0010C5, 1], + [0x0010C6, 0x0010C6, -1], + [0x0010C7, 0x0010C7, 1], + [0x0010C8, 0x0010CC, -1], + [0x0010CD, 0x0010CD, 1], + [0x0010CE, 0x0010CF, -1], + [0x0010D0, 0x0010FF, 1], + [0x001100, 0x00115F, 2], + [0x001160, 0x0011FF, 0], + [0x001200, 0x001248, 1], + [0x001249, 0x001249, -1], + [0x00124A, 0x00124D, 1], + [0x00124E, 0x00124F, -1], + [0x001250, 0x001256, 1], + [0x001257, 0x001257, -1], + [0x001258, 0x001258, 1], + [0x001259, 0x001259, -1], + [0x00125A, 0x00125D, 1], + [0x00125E, 0x00125F, -1], + [0x001260, 0x001288, 1], + [0x001289, 0x001289, -1], + [0x00128A, 0x00128D, 1], + [0x00128E, 0x00128F, -1], + [0x001290, 0x0012B0, 1], + [0x0012B1, 0x0012B1, -1], + [0x0012B2, 0x0012B5, 1], + [0x0012B6, 0x0012B7, -1], + [0x0012B8, 0x0012BE, 1], + [0x0012BF, 0x0012BF, -1], + [0x0012C0, 0x0012C0, 1], + [0x0012C1, 0x0012C1, -1], + [0x0012C2, 0x0012C5, 1], + [0x0012C6, 0x0012C7, -1], + [0x0012C8, 0x0012D6, 1], + [0x0012D7, 0x0012D7, -1], + [0x0012D8, 0x001310, 1], + [0x001311, 0x001311, -1], + [0x001312, 0x001315, 1], + [0x001316, 0x001317, -1], + [0x001318, 0x00135A, 1], + [0x00135B, 0x00135C, -1], + [0x00135D, 0x00135F, 0], + [0x001360, 0x00137C, 1], + [0x00137D, 0x00137F, -1], + [0x001380, 0x001399, 1], + [0x00139A, 0x00139F, -1], + [0x0013A0, 0x0013F5, 1], + [0x0013F6, 0x0013F7, -1], + [0x0013F8, 0x0013FD, 1], + [0x0013FE, 0x0013FF, -1], + [0x001400, 0x00169C, 1], + [0x00169D, 0x00169F, -1], + [0x0016A0, 0x0016F8, 1], + [0x0016F9, 0x0016FF, -1], + [0x001700, 0x001711, 1], + [0x001712, 0x001714, 0], + [0x001715, 0x001715, 1], + [0x001716, 0x00171E, -1], + [0x00171F, 0x001731, 1], + [0x001732, 0x001733, 0], + [0x001734, 0x001736, 1], + [0x001737, 0x00173F, -1], + [0x001740, 0x001751, 1], + [0x001752, 0x001753, 0], + [0x001754, 0x00175F, -1], + [0x001760, 0x00176C, 1], + [0x00176D, 0x00176D, -1], + [0x00176E, 0x001770, 1], + [0x001771, 0x001771, -1], + [0x001772, 0x001773, 0], + [0x001774, 0x00177F, -1], + [0x001780, 0x0017B3, 1], + [0x0017B4, 0x0017B5, 0], + [0x0017B6, 0x0017B6, 1], + [0x0017B7, 0x0017BD, 0], + [0x0017BE, 0x0017C5, 1], + [0x0017C6, 0x0017C6, 0], + [0x0017C7, 0x0017C8, 1], + [0x0017C9, 0x0017D3, 0], + [0x0017D4, 0x0017DC, 1], + [0x0017DD, 0x0017DD, 0], + [0x0017DE, 0x0017DF, -1], + [0x0017E0, 0x0017E9, 1], + [0x0017EA, 0x0017EF, -1], + [0x0017F0, 0x0017F9, 1], + [0x0017FA, 0x0017FF, -1], + [0x001800, 0x00180A, 1], + [0x00180B, 0x00180F, 0], + [0x001810, 0x001819, 1], + [0x00181A, 0x00181F, -1], + [0x001820, 0x001878, 1], + [0x001879, 0x00187F, -1], + [0x001880, 0x001884, 1], + [0x001885, 0x001886, 0], + [0x001887, 0x0018A8, 1], + [0x0018A9, 0x0018A9, 0], + [0x0018AA, 0x0018AA, 1], + [0x0018AB, 0x0018AF, -1], + [0x0018B0, 0x0018F5, 1], + [0x0018F6, 0x0018FF, -1], + [0x001900, 0x00191E, 1], + [0x00191F, 0x00191F, -1], + [0x001920, 0x001922, 0], + [0x001923, 0x001926, 1], + [0x001927, 0x001928, 0], + [0x001929, 0x00192B, 1], + [0x00192C, 0x00192F, -1], + [0x001930, 0x001931, 1], + [0x001932, 0x001932, 0], + [0x001933, 0x001938, 1], + [0x001939, 0x00193B, 0], + [0x00193C, 0x00193F, -1], + [0x001940, 0x001940, 1], + [0x001941, 0x001943, -1], + [0x001944, 0x00196D, 1], + [0x00196E, 0x00196F, -1], + [0x001970, 0x001974, 1], + [0x001975, 0x00197F, -1], + [0x001980, 0x0019AB, 1], + [0x0019AC, 0x0019AF, -1], + [0x0019B0, 0x0019C9, 1], + [0x0019CA, 0x0019CF, -1], + [0x0019D0, 0x0019DA, 1], + [0x0019DB, 0x0019DD, -1], + [0x0019DE, 0x001A16, 1], + [0x001A17, 0x001A18, 0], + [0x001A19, 0x001A1A, 1], + [0x001A1B, 0x001A1B, 0], + [0x001A1C, 0x001A1D, -1], + [0x001A1E, 0x001A55, 1], + [0x001A56, 0x001A56, 0], + [0x001A57, 0x001A57, 1], + [0x001A58, 0x001A5E, 0], + [0x001A5F, 0x001A5F, -1], + [0x001A60, 0x001A60, 0], + [0x001A61, 0x001A61, 1], + [0x001A62, 0x001A62, 0], + [0x001A63, 0x001A64, 1], + [0x001A65, 0x001A6C, 0], + [0x001A6D, 0x001A72, 1], + [0x001A73, 0x001A7C, 0], + [0x001A7D, 0x001A7E, -1], + [0x001A7F, 0x001A7F, 0], + [0x001A80, 0x001A89, 1], + [0x001A8A, 0x001A8F, -1], + [0x001A90, 0x001A99, 1], + [0x001A9A, 0x001A9F, -1], + [0x001AA0, 0x001AAD, 1], + [0x001AAE, 0x001AAF, -1], + [0x001AB0, 0x001ACE, 0], + [0x001ACF, 0x001AFF, -1], + [0x001B00, 0x001B03, 0], + [0x001B04, 0x001B33, 1], + [0x001B34, 0x001B34, 0], + [0x001B35, 0x001B35, 1], + [0x001B36, 0x001B3A, 0], + [0x001B3B, 0x001B3B, 1], + [0x001B3C, 0x001B3C, 0], + [0x001B3D, 0x001B41, 1], + [0x001B42, 0x001B42, 0], + [0x001B43, 0x001B4C, 1], + [0x001B4D, 0x001B4D, -1], + [0x001B4E, 0x001B6A, 1], + [0x001B6B, 0x001B73, 0], + [0x001B74, 0x001B7F, 1], + [0x001B80, 0x001B81, 0], + [0x001B82, 0x001BA1, 1], + [0x001BA2, 0x001BA5, 0], + [0x001BA6, 0x001BA7, 1], + [0x001BA8, 0x001BA9, 0], + [0x001BAA, 0x001BAA, 1], + [0x001BAB, 0x001BAD, 0], + [0x001BAE, 0x001BE5, 1], + [0x001BE6, 0x001BE6, 0], + [0x001BE7, 0x001BE7, 1], + [0x001BE8, 0x001BE9, 0], + [0x001BEA, 0x001BEC, 1], + [0x001BED, 0x001BED, 0], + [0x001BEE, 0x001BEE, 1], + [0x001BEF, 0x001BF1, 0], + [0x001BF2, 0x001BF3, 1], + [0x001BF4, 0x001BFB, -1], + [0x001BFC, 0x001C2B, 1], + [0x001C2C, 0x001C33, 0], + [0x001C34, 0x001C35, 1], + [0x001C36, 0x001C37, 0], + [0x001C38, 0x001C3A, -1], + [0x001C3B, 0x001C49, 1], + [0x001C4A, 0x001C4C, -1], + [0x001C4D, 0x001C8A, 1], + [0x001C8B, 0x001C8F, -1], + [0x001C90, 0x001CBA, 1], + [0x001CBB, 0x001CBC, -1], + [0x001CBD, 0x001CC7, 1], + [0x001CC8, 0x001CCF, -1], + [0x001CD0, 0x001CD2, 0], + [0x001CD3, 0x001CD3, 1], + [0x001CD4, 0x001CE0, 0], + [0x001CE1, 0x001CE1, 1], + [0x001CE2, 0x001CE8, 0], + [0x001CE9, 0x001CEC, 1], + [0x001CED, 0x001CED, 0], + [0x001CEE, 0x001CF3, 1], + [0x001CF4, 0x001CF4, 0], + [0x001CF5, 0x001CF7, 1], + [0x001CF8, 0x001CF9, 0], + [0x001CFA, 0x001CFA, 1], + [0x001CFB, 0x001CFF, -1], + [0x001D00, 0x001DBF, 1], + [0x001DC0, 0x001DFF, 0], + [0x001E00, 0x001F15, 1], + [0x001F16, 0x001F17, -1], + [0x001F18, 0x001F1D, 1], + [0x001F1E, 0x001F1F, -1], + [0x001F20, 0x001F45, 1], + [0x001F46, 0x001F47, -1], + [0x001F48, 0x001F4D, 1], + [0x001F4E, 0x001F4F, -1], + [0x001F50, 0x001F57, 1], + [0x001F58, 0x001F58, -1], + [0x001F59, 0x001F59, 1], + [0x001F5A, 0x001F5A, -1], + [0x001F5B, 0x001F5B, 1], + [0x001F5C, 0x001F5C, -1], + [0x001F5D, 0x001F5D, 1], + [0x001F5E, 0x001F5E, -1], + [0x001F5F, 0x001F7D, 1], + [0x001F7E, 0x001F7F, -1], + [0x001F80, 0x001FB4, 1], + [0x001FB5, 0x001FB5, -1], + [0x001FB6, 0x001FC4, 1], + [0x001FC5, 0x001FC5, -1], + [0x001FC6, 0x001FD3, 1], + [0x001FD4, 0x001FD5, -1], + [0x001FD6, 0x001FDB, 1], + [0x001FDC, 0x001FDC, -1], + [0x001FDD, 0x001FEF, 1], + [0x001FF0, 0x001FF1, -1], + [0x001FF2, 0x001FF4, 1], + [0x001FF5, 0x001FF5, -1], + [0x001FF6, 0x001FFE, 1], + [0x001FFF, 0x001FFF, -1], + [0x002000, 0x00200A, 1], + [0x00200B, 0x00200F, 0], + [0x002010, 0x002027, 1], + [0x002028, 0x002029, -1], + [0x00202A, 0x00202E, 0], + [0x00202F, 0x00205F, 1], + [0x002060, 0x002064, 0], + [0x002065, 0x002065, -1], + [0x002066, 0x00206F, 0], + [0x002070, 0x002071, 1], + [0x002072, 0x002073, -1], + [0x002074, 0x00208E, 1], + [0x00208F, 0x00208F, -1], + [0x002090, 0x00209C, 1], + [0x00209D, 0x00209F, -1], + [0x0020A0, 0x0020C0, 1], + [0x0020C1, 0x0020CF, -1], + [0x0020D0, 0x0020F0, 0], + [0x0020F1, 0x0020FF, -1], + [0x002100, 0x00218B, 1], + [0x00218C, 0x00218F, -1], + [0x002190, 0x002319, 1], + [0x00231A, 0x00231B, 2], + [0x00231C, 0x002328, 1], + [0x002329, 0x00232A, 2], + [0x00232B, 0x0023E8, 1], + [0x0023E9, 0x0023EC, 2], + [0x0023ED, 0x0023EF, 1], + [0x0023F0, 0x0023F0, 2], + [0x0023F1, 0x0023F2, 1], + [0x0023F3, 0x0023F3, 2], + [0x0023F4, 0x002429, 1], + [0x00242A, 0x00243F, -1], + [0x002440, 0x00244A, 1], + [0x00244B, 0x00245F, -1], + [0x002460, 0x0025FC, 1], + [0x0025FD, 0x0025FE, 2], + [0x0025FF, 0x002613, 1], + [0x002614, 0x002615, 2], + [0x002616, 0x00262F, 1], + [0x002630, 0x002637, 2], + [0x002638, 0x002647, 1], + [0x002648, 0x002653, 2], + [0x002654, 0x00267E, 1], + [0x00267F, 0x00267F, 2], + [0x002680, 0x002689, 1], + [0x00268A, 0x00268F, 2], + [0x002690, 0x002692, 1], + [0x002693, 0x002693, 2], + [0x002694, 0x0026A0, 1], + [0x0026A1, 0x0026A1, 2], + [0x0026A2, 0x0026A9, 1], + [0x0026AA, 0x0026AB, 2], + [0x0026AC, 0x0026BC, 1], + [0x0026BD, 0x0026BE, 2], + [0x0026BF, 0x0026C3, 1], + [0x0026C4, 0x0026C5, 2], + [0x0026C6, 0x0026CD, 1], + [0x0026CE, 0x0026CE, 2], + [0x0026CF, 0x0026D3, 1], + [0x0026D4, 0x0026D4, 2], + [0x0026D5, 0x0026E9, 1], + [0x0026EA, 0x0026EA, 2], + [0x0026EB, 0x0026F1, 1], + [0x0026F2, 0x0026F3, 2], + [0x0026F4, 0x0026F4, 1], + [0x0026F5, 0x0026F5, 2], + [0x0026F6, 0x0026F9, 1], + [0x0026FA, 0x0026FA, 2], + [0x0026FB, 0x0026FC, 1], + [0x0026FD, 0x0026FD, 2], + [0x0026FE, 0x002704, 1], + [0x002705, 0x002705, 2], + [0x002706, 0x002709, 1], + [0x00270A, 0x00270B, 2], + [0x00270C, 0x002727, 1], + [0x002728, 0x002728, 2], + [0x002729, 0x00274B, 1], + [0x00274C, 0x00274C, 2], + [0x00274D, 0x00274D, 1], + [0x00274E, 0x00274E, 2], + [0x00274F, 0x002752, 1], + [0x002753, 0x002755, 2], + [0x002756, 0x002756, 1], + [0x002757, 0x002757, 2], + [0x002758, 0x002794, 1], + [0x002795, 0x002797, 2], + [0x002798, 0x0027AF, 1], + [0x0027B0, 0x0027B0, 2], + [0x0027B1, 0x0027BE, 1], + [0x0027BF, 0x0027BF, 2], + [0x0027C0, 0x002B1A, 1], + [0x002B1B, 0x002B1C, 2], + [0x002B1D, 0x002B4F, 1], + [0x002B50, 0x002B50, 2], + [0x002B51, 0x002B54, 1], + [0x002B55, 0x002B55, 2], + [0x002B56, 0x002B73, 1], + [0x002B74, 0x002B75, -1], + [0x002B76, 0x002B95, 1], + [0x002B96, 0x002B96, -1], + [0x002B97, 0x002CEE, 1], + [0x002CEF, 0x002CF1, 0], + [0x002CF2, 0x002CF3, 1], + [0x002CF4, 0x002CF8, -1], + [0x002CF9, 0x002D25, 1], + [0x002D26, 0x002D26, -1], + [0x002D27, 0x002D27, 1], + [0x002D28, 0x002D2C, -1], + [0x002D2D, 0x002D2D, 1], + [0x002D2E, 0x002D2F, -1], + [0x002D30, 0x002D67, 1], + [0x002D68, 0x002D6E, -1], + [0x002D6F, 0x002D70, 1], + [0x002D71, 0x002D7E, -1], + [0x002D7F, 0x002D7F, 0], + [0x002D80, 0x002D96, 1], + [0x002D97, 0x002D9F, -1], + [0x002DA0, 0x002DA6, 1], + [0x002DA7, 0x002DA7, -1], + [0x002DA8, 0x002DAE, 1], + [0x002DAF, 0x002DAF, -1], + [0x002DB0, 0x002DB6, 1], + [0x002DB7, 0x002DB7, -1], + [0x002DB8, 0x002DBE, 1], + [0x002DBF, 0x002DBF, -1], + [0x002DC0, 0x002DC6, 1], + [0x002DC7, 0x002DC7, -1], + [0x002DC8, 0x002DCE, 1], + [0x002DCF, 0x002DCF, -1], + [0x002DD0, 0x002DD6, 1], + [0x002DD7, 0x002DD7, -1], + [0x002DD8, 0x002DDE, 1], + [0x002DDF, 0x002DDF, -1], + [0x002DE0, 0x002DFF, 0], + [0x002E00, 0x002E5D, 1], + [0x002E5E, 0x002E7F, -1], + [0x002E80, 0x002E99, 2], + [0x002E9A, 0x002E9A, -1], + [0x002E9B, 0x002EF3, 2], + [0x002EF4, 0x002EFF, -1], + [0x002F00, 0x002FD5, 2], + [0x002FD6, 0x002FEF, -1], + [0x002FF0, 0x003029, 2], + [0x00302A, 0x00302D, 0], + [0x00302E, 0x00303E, 2], + [0x00303F, 0x00303F, 1], + [0x003040, 0x003040, -1], + [0x003041, 0x003096, 2], + [0x003097, 0x003098, -1], + [0x003099, 0x00309A, 0], + [0x00309B, 0x0030FF, 2], + [0x003100, 0x003104, -1], + [0x003105, 0x00312F, 2], + [0x003130, 0x003130, -1], + [0x003131, 0x003163, 2], + [0x003164, 0x003164, 0], + [0x003165, 0x00318E, 2], + [0x00318F, 0x00318F, -1], + [0x003190, 0x0031E5, 2], + [0x0031E6, 0x0031EE, -1], + [0x0031EF, 0x00321E, 2], + [0x00321F, 0x00321F, -1], + [0x003220, 0x00A48C, 2], + [0x00A48D, 0x00A48F, -1], + [0x00A490, 0x00A4C6, 2], + [0x00A4C7, 0x00A4CF, -1], + [0x00A4D0, 0x00A62B, 1], + [0x00A62C, 0x00A63F, -1], + [0x00A640, 0x00A66E, 1], + [0x00A66F, 0x00A672, 0], + [0x00A673, 0x00A673, 1], + [0x00A674, 0x00A67D, 0], + [0x00A67E, 0x00A69D, 1], + [0x00A69E, 0x00A69F, 0], + [0x00A6A0, 0x00A6EF, 1], + [0x00A6F0, 0x00A6F1, 0], + [0x00A6F2, 0x00A6F7, 1], + [0x00A6F8, 0x00A6FF, -1], + [0x00A700, 0x00A7CD, 1], + [0x00A7CE, 0x00A7CF, -1], + [0x00A7D0, 0x00A7D1, 1], + [0x00A7D2, 0x00A7D2, -1], + [0x00A7D3, 0x00A7D3, 1], + [0x00A7D4, 0x00A7D4, -1], + [0x00A7D5, 0x00A7DC, 1], + [0x00A7DD, 0x00A7F1, -1], + [0x00A7F2, 0x00A801, 1], + [0x00A802, 0x00A802, 0], + [0x00A803, 0x00A805, 1], + [0x00A806, 0x00A806, 0], + [0x00A807, 0x00A80A, 1], + [0x00A80B, 0x00A80B, 0], + [0x00A80C, 0x00A824, 1], + [0x00A825, 0x00A826, 0], + [0x00A827, 0x00A82B, 1], + [0x00A82C, 0x00A82C, 0], + [0x00A82D, 0x00A82F, -1], + [0x00A830, 0x00A839, 1], + [0x00A83A, 0x00A83F, -1], + [0x00A840, 0x00A877, 1], + [0x00A878, 0x00A87F, -1], + [0x00A880, 0x00A8C3, 1], + [0x00A8C4, 0x00A8C5, 0], + [0x00A8C6, 0x00A8CD, -1], + [0x00A8CE, 0x00A8D9, 1], + [0x00A8DA, 0x00A8DF, -1], + [0x00A8E0, 0x00A8F1, 0], + [0x00A8F2, 0x00A8FE, 1], + [0x00A8FF, 0x00A8FF, 0], + [0x00A900, 0x00A925, 1], + [0x00A926, 0x00A92D, 0], + [0x00A92E, 0x00A946, 1], + [0x00A947, 0x00A951, 0], + [0x00A952, 0x00A953, 1], + [0x00A954, 0x00A95E, -1], + [0x00A95F, 0x00A95F, 1], + [0x00A960, 0x00A97C, 2], + [0x00A97D, 0x00A97F, -1], + [0x00A980, 0x00A982, 0], + [0x00A983, 0x00A9B2, 1], + [0x00A9B3, 0x00A9B3, 0], + [0x00A9B4, 0x00A9B5, 1], + [0x00A9B6, 0x00A9B9, 0], + [0x00A9BA, 0x00A9BB, 1], + [0x00A9BC, 0x00A9BD, 0], + [0x00A9BE, 0x00A9CD, 1], + [0x00A9CE, 0x00A9CE, -1], + [0x00A9CF, 0x00A9D9, 1], + [0x00A9DA, 0x00A9DD, -1], + [0x00A9DE, 0x00A9E4, 1], + [0x00A9E5, 0x00A9E5, 0], + [0x00A9E6, 0x00A9FE, 1], + [0x00A9FF, 0x00A9FF, -1], + [0x00AA00, 0x00AA28, 1], + [0x00AA29, 0x00AA2E, 0], + [0x00AA2F, 0x00AA30, 1], + [0x00AA31, 0x00AA32, 0], + [0x00AA33, 0x00AA34, 1], + [0x00AA35, 0x00AA36, 0], + [0x00AA37, 0x00AA3F, -1], + [0x00AA40, 0x00AA42, 1], + [0x00AA43, 0x00AA43, 0], + [0x00AA44, 0x00AA4B, 1], + [0x00AA4C, 0x00AA4C, 0], + [0x00AA4D, 0x00AA4D, 1], + [0x00AA4E, 0x00AA4F, -1], + [0x00AA50, 0x00AA59, 1], + [0x00AA5A, 0x00AA5B, -1], + [0x00AA5C, 0x00AA7B, 1], + [0x00AA7C, 0x00AA7C, 0], + [0x00AA7D, 0x00AAAF, 1], + [0x00AAB0, 0x00AAB0, 0], + [0x00AAB1, 0x00AAB1, 1], + [0x00AAB2, 0x00AAB4, 0], + [0x00AAB5, 0x00AAB6, 1], + [0x00AAB7, 0x00AAB8, 0], + [0x00AAB9, 0x00AABD, 1], + [0x00AABE, 0x00AABF, 0], + [0x00AAC0, 0x00AAC0, 1], + [0x00AAC1, 0x00AAC1, 0], + [0x00AAC2, 0x00AAC2, 1], + [0x00AAC3, 0x00AADA, -1], + [0x00AADB, 0x00AAEB, 1], + [0x00AAEC, 0x00AAED, 0], + [0x00AAEE, 0x00AAF5, 1], + [0x00AAF6, 0x00AAF6, 0], + [0x00AAF7, 0x00AB00, -1], + [0x00AB01, 0x00AB06, 1], + [0x00AB07, 0x00AB08, -1], + [0x00AB09, 0x00AB0E, 1], + [0x00AB0F, 0x00AB10, -1], + [0x00AB11, 0x00AB16, 1], + [0x00AB17, 0x00AB1F, -1], + [0x00AB20, 0x00AB26, 1], + [0x00AB27, 0x00AB27, -1], + [0x00AB28, 0x00AB2E, 1], + [0x00AB2F, 0x00AB2F, -1], + [0x00AB30, 0x00AB6B, 1], + [0x00AB6C, 0x00AB6F, -1], + [0x00AB70, 0x00ABE4, 1], + [0x00ABE5, 0x00ABE5, 0], + [0x00ABE6, 0x00ABE7, 1], + [0x00ABE8, 0x00ABE8, 0], + [0x00ABE9, 0x00ABEC, 1], + [0x00ABED, 0x00ABED, 0], + [0x00ABEE, 0x00ABEF, -1], + [0x00ABF0, 0x00ABF9, 1], + [0x00ABFA, 0x00ABFF, -1], + [0x00AC00, 0x00D7A3, 2], + [0x00D7A4, 0x00D7AF, -1], + [0x00D7B0, 0x00D7C6, 0], + [0x00D7C7, 0x00D7CA, -1], + [0x00D7CB, 0x00D7FB, 0], + [0x00D7FC, 0x00DFFF, -1], + [0x00E000, 0x00F8FF, 1], + [0x00F900, 0x00FA6D, 2], + [0x00FA6E, 0x00FA6F, -1], + [0x00FA70, 0x00FAD9, 2], + [0x00FADA, 0x00FAFF, -1], + [0x00FB00, 0x00FB06, 1], + [0x00FB07, 0x00FB12, -1], + [0x00FB13, 0x00FB17, 1], + [0x00FB18, 0x00FB1C, -1], + [0x00FB1D, 0x00FB1D, 1], + [0x00FB1E, 0x00FB1E, 0], + [0x00FB1F, 0x00FB36, 1], + [0x00FB37, 0x00FB37, -1], + [0x00FB38, 0x00FB3C, 1], + [0x00FB3D, 0x00FB3D, -1], + [0x00FB3E, 0x00FB3E, 1], + [0x00FB3F, 0x00FB3F, -1], + [0x00FB40, 0x00FB41, 1], + [0x00FB42, 0x00FB42, -1], + [0x00FB43, 0x00FB44, 1], + [0x00FB45, 0x00FB45, -1], + [0x00FB46, 0x00FBC2, 1], + [0x00FBC3, 0x00FBD2, -1], + [0x00FBD3, 0x00FD8F, 1], + [0x00FD90, 0x00FD91, -1], + [0x00FD92, 0x00FDC7, 1], + [0x00FDC8, 0x00FDCE, -1], + [0x00FDCF, 0x00FDCF, 1], + [0x00FDD0, 0x00FDEF, -1], + [0x00FDF0, 0x00FDFF, 1], + [0x00FE00, 0x00FE0F, 0], + [0x00FE10, 0x00FE19, 2], + [0x00FE1A, 0x00FE1F, -1], + [0x00FE20, 0x00FE2F, 0], + [0x00FE30, 0x00FE52, 2], + [0x00FE53, 0x00FE53, -1], + [0x00FE54, 0x00FE66, 2], + [0x00FE67, 0x00FE67, -1], + [0x00FE68, 0x00FE6B, 2], + [0x00FE6C, 0x00FE6F, -1], + [0x00FE70, 0x00FE74, 1], + [0x00FE75, 0x00FE75, -1], + [0x00FE76, 0x00FEFC, 1], + [0x00FEFD, 0x00FEFE, -1], + [0x00FEFF, 0x00FEFF, 0], + [0x00FF00, 0x00FF00, -1], + [0x00FF01, 0x00FF60, 2], + [0x00FF61, 0x00FF9F, 1], + [0x00FFA0, 0x00FFA0, 0], + [0x00FFA1, 0x00FFBE, 1], + [0x00FFBF, 0x00FFC1, -1], + [0x00FFC2, 0x00FFC7, 1], + [0x00FFC8, 0x00FFC9, -1], + [0x00FFCA, 0x00FFCF, 1], + [0x00FFD0, 0x00FFD1, -1], + [0x00FFD2, 0x00FFD7, 1], + [0x00FFD8, 0x00FFD9, -1], + [0x00FFDA, 0x00FFDC, 1], + [0x00FFDD, 0x00FFDF, -1], + [0x00FFE0, 0x00FFE6, 2], + [0x00FFE7, 0x00FFE7, -1], + [0x00FFE8, 0x00FFEE, 1], + [0x00FFEF, 0x00FFF8, -1], + [0x00FFF9, 0x00FFFD, 1], + [0x00FFFE, 0x00FFFF, -1], + [0x010000, 0x01000B, 1], + [0x01000C, 0x01000C, -1], + [0x01000D, 0x010026, 1], + [0x010027, 0x010027, -1], + [0x010028, 0x01003A, 1], + [0x01003B, 0x01003B, -1], + [0x01003C, 0x01003D, 1], + [0x01003E, 0x01003E, -1], + [0x01003F, 0x01004D, 1], + [0x01004E, 0x01004F, -1], + [0x010050, 0x01005D, 1], + [0x01005E, 0x01007F, -1], + [0x010080, 0x0100FA, 1], + [0x0100FB, 0x0100FF, -1], + [0x010100, 0x010102, 1], + [0x010103, 0x010106, -1], + [0x010107, 0x010133, 1], + [0x010134, 0x010136, -1], + [0x010137, 0x01018E, 1], + [0x01018F, 0x01018F, -1], + [0x010190, 0x01019C, 1], + [0x01019D, 0x01019F, -1], + [0x0101A0, 0x0101A0, 1], + [0x0101A1, 0x0101CF, -1], + [0x0101D0, 0x0101FC, 1], + [0x0101FD, 0x0101FD, 0], + [0x0101FE, 0x01027F, -1], + [0x010280, 0x01029C, 1], + [0x01029D, 0x01029F, -1], + [0x0102A0, 0x0102D0, 1], + [0x0102D1, 0x0102DF, -1], + [0x0102E0, 0x0102E0, 0], + [0x0102E1, 0x0102FB, 1], + [0x0102FC, 0x0102FF, -1], + [0x010300, 0x010323, 1], + [0x010324, 0x01032C, -1], + [0x01032D, 0x01034A, 1], + [0x01034B, 0x01034F, -1], + [0x010350, 0x010375, 1], + [0x010376, 0x01037A, 0], + [0x01037B, 0x01037F, -1], + [0x010380, 0x01039D, 1], + [0x01039E, 0x01039E, -1], + [0x01039F, 0x0103C3, 1], + [0x0103C4, 0x0103C7, -1], + [0x0103C8, 0x0103D5, 1], + [0x0103D6, 0x0103FF, -1], + [0x010400, 0x01049D, 1], + [0x01049E, 0x01049F, -1], + [0x0104A0, 0x0104A9, 1], + [0x0104AA, 0x0104AF, -1], + [0x0104B0, 0x0104D3, 1], + [0x0104D4, 0x0104D7, -1], + [0x0104D8, 0x0104FB, 1], + [0x0104FC, 0x0104FF, -1], + [0x010500, 0x010527, 1], + [0x010528, 0x01052F, -1], + [0x010530, 0x010563, 1], + [0x010564, 0x01056E, -1], + [0x01056F, 0x01057A, 1], + [0x01057B, 0x01057B, -1], + [0x01057C, 0x01058A, 1], + [0x01058B, 0x01058B, -1], + [0x01058C, 0x010592, 1], + [0x010593, 0x010593, -1], + [0x010594, 0x010595, 1], + [0x010596, 0x010596, -1], + [0x010597, 0x0105A1, 1], + [0x0105A2, 0x0105A2, -1], + [0x0105A3, 0x0105B1, 1], + [0x0105B2, 0x0105B2, -1], + [0x0105B3, 0x0105B9, 1], + [0x0105BA, 0x0105BA, -1], + [0x0105BB, 0x0105BC, 1], + [0x0105BD, 0x0105BF, -1], + [0x0105C0, 0x0105F3, 1], + [0x0105F4, 0x0105FF, -1], + [0x010600, 0x010736, 1], + [0x010737, 0x01073F, -1], + [0x010740, 0x010755, 1], + [0x010756, 0x01075F, -1], + [0x010760, 0x010767, 1], + [0x010768, 0x01077F, -1], + [0x010780, 0x010785, 1], + [0x010786, 0x010786, -1], + [0x010787, 0x0107B0, 1], + [0x0107B1, 0x0107B1, -1], + [0x0107B2, 0x0107BA, 1], + [0x0107BB, 0x0107FF, -1], + [0x010800, 0x010805, 1], + [0x010806, 0x010807, -1], + [0x010808, 0x010808, 1], + [0x010809, 0x010809, -1], + [0x01080A, 0x010835, 1], + [0x010836, 0x010836, -1], + [0x010837, 0x010838, 1], + [0x010839, 0x01083B, -1], + [0x01083C, 0x01083C, 1], + [0x01083D, 0x01083E, -1], + [0x01083F, 0x010855, 1], + [0x010856, 0x010856, -1], + [0x010857, 0x01089E, 1], + [0x01089F, 0x0108A6, -1], + [0x0108A7, 0x0108AF, 1], + [0x0108B0, 0x0108DF, -1], + [0x0108E0, 0x0108F2, 1], + [0x0108F3, 0x0108F3, -1], + [0x0108F4, 0x0108F5, 1], + [0x0108F6, 0x0108FA, -1], + [0x0108FB, 0x01091B, 1], + [0x01091C, 0x01091E, -1], + [0x01091F, 0x010939, 1], + [0x01093A, 0x01093E, -1], + [0x01093F, 0x01093F, 1], + [0x010940, 0x01097F, -1], + [0x010980, 0x0109B7, 1], + [0x0109B8, 0x0109BB, -1], + [0x0109BC, 0x0109CF, 1], + [0x0109D0, 0x0109D1, -1], + [0x0109D2, 0x010A00, 1], + [0x010A01, 0x010A03, 0], + [0x010A04, 0x010A04, -1], + [0x010A05, 0x010A06, 0], + [0x010A07, 0x010A0B, -1], + [0x010A0C, 0x010A0F, 0], + [0x010A10, 0x010A13, 1], + [0x010A14, 0x010A14, -1], + [0x010A15, 0x010A17, 1], + [0x010A18, 0x010A18, -1], + [0x010A19, 0x010A35, 1], + [0x010A36, 0x010A37, -1], + [0x010A38, 0x010A3A, 0], + [0x010A3B, 0x010A3E, -1], + [0x010A3F, 0x010A3F, 0], + [0x010A40, 0x010A48, 1], + [0x010A49, 0x010A4F, -1], + [0x010A50, 0x010A58, 1], + [0x010A59, 0x010A5F, -1], + [0x010A60, 0x010A9F, 1], + [0x010AA0, 0x010ABF, -1], + [0x010AC0, 0x010AE4, 1], + [0x010AE5, 0x010AE6, 0], + [0x010AE7, 0x010AEA, -1], + [0x010AEB, 0x010AF6, 1], + [0x010AF7, 0x010AFF, -1], + [0x010B00, 0x010B35, 1], + [0x010B36, 0x010B38, -1], + [0x010B39, 0x010B55, 1], + [0x010B56, 0x010B57, -1], + [0x010B58, 0x010B72, 1], + [0x010B73, 0x010B77, -1], + [0x010B78, 0x010B91, 1], + [0x010B92, 0x010B98, -1], + [0x010B99, 0x010B9C, 1], + [0x010B9D, 0x010BA8, -1], + [0x010BA9, 0x010BAF, 1], + [0x010BB0, 0x010BFF, -1], + [0x010C00, 0x010C48, 1], + [0x010C49, 0x010C7F, -1], + [0x010C80, 0x010CB2, 1], + [0x010CB3, 0x010CBF, -1], + [0x010CC0, 0x010CF2, 1], + [0x010CF3, 0x010CF9, -1], + [0x010CFA, 0x010D23, 1], + [0x010D24, 0x010D27, 0], + [0x010D28, 0x010D2F, -1], + [0x010D30, 0x010D39, 1], + [0x010D3A, 0x010D3F, -1], + [0x010D40, 0x010D65, 1], + [0x010D66, 0x010D68, -1], + [0x010D69, 0x010D6D, 0], + [0x010D6E, 0x010D85, 1], + [0x010D86, 0x010D8D, -1], + [0x010D8E, 0x010D8F, 1], + [0x010D90, 0x010E5F, -1], + [0x010E60, 0x010E7E, 1], + [0x010E7F, 0x010E7F, -1], + [0x010E80, 0x010EA9, 1], + [0x010EAA, 0x010EAA, -1], + [0x010EAB, 0x010EAC, 0], + [0x010EAD, 0x010EAD, 1], + [0x010EAE, 0x010EAF, -1], + [0x010EB0, 0x010EB1, 1], + [0x010EB2, 0x010EC1, -1], + [0x010EC2, 0x010EC4, 1], + [0x010EC5, 0x010EFB, -1], + [0x010EFC, 0x010EFF, 0], + [0x010F00, 0x010F27, 1], + [0x010F28, 0x010F2F, -1], + [0x010F30, 0x010F45, 1], + [0x010F46, 0x010F50, 0], + [0x010F51, 0x010F59, 1], + [0x010F5A, 0x010F6F, -1], + [0x010F70, 0x010F81, 1], + [0x010F82, 0x010F85, 0], + [0x010F86, 0x010F89, 1], + [0x010F8A, 0x010FAF, -1], + [0x010FB0, 0x010FCB, 1], + [0x010FCC, 0x010FDF, -1], + [0x010FE0, 0x010FF6, 1], + [0x010FF7, 0x010FFF, -1], + [0x011000, 0x011000, 1], + [0x011001, 0x011001, 0], + [0x011002, 0x011037, 1], + [0x011038, 0x011046, 0], + [0x011047, 0x01104D, 1], + [0x01104E, 0x011051, -1], + [0x011052, 0x01106F, 1], + [0x011070, 0x011070, 0], + [0x011071, 0x011072, 1], + [0x011073, 0x011074, 0], + [0x011075, 0x011075, 1], + [0x011076, 0x01107E, -1], + [0x01107F, 0x011081, 0], + [0x011082, 0x0110B2, 1], + [0x0110B3, 0x0110B6, 0], + [0x0110B7, 0x0110B8, 1], + [0x0110B9, 0x0110BA, 0], + [0x0110BB, 0x0110C1, 1], + [0x0110C2, 0x0110C2, 0], + [0x0110C3, 0x0110CC, -1], + [0x0110CD, 0x0110CD, 1], + [0x0110CE, 0x0110CF, -1], + [0x0110D0, 0x0110E8, 1], + [0x0110E9, 0x0110EF, -1], + [0x0110F0, 0x0110F9, 1], + [0x0110FA, 0x0110FF, -1], + [0x011100, 0x011102, 0], + [0x011103, 0x011126, 1], + [0x011127, 0x01112B, 0], + [0x01112C, 0x01112C, 1], + [0x01112D, 0x011134, 0], + [0x011135, 0x011135, -1], + [0x011136, 0x011147, 1], + [0x011148, 0x01114F, -1], + [0x011150, 0x011172, 1], + [0x011173, 0x011173, 0], + [0x011174, 0x011176, 1], + [0x011177, 0x01117F, -1], + [0x011180, 0x011181, 0], + [0x011182, 0x0111B5, 1], + [0x0111B6, 0x0111BE, 0], + [0x0111BF, 0x0111C8, 1], + [0x0111C9, 0x0111CC, 0], + [0x0111CD, 0x0111CE, 1], + [0x0111CF, 0x0111CF, 0], + [0x0111D0, 0x0111DF, 1], + [0x0111E0, 0x0111E0, -1], + [0x0111E1, 0x0111F4, 1], + [0x0111F5, 0x0111FF, -1], + [0x011200, 0x011211, 1], + [0x011212, 0x011212, -1], + [0x011213, 0x01122E, 1], + [0x01122F, 0x011231, 0], + [0x011232, 0x011233, 1], + [0x011234, 0x011234, 0], + [0x011235, 0x011235, 1], + [0x011236, 0x011237, 0], + [0x011238, 0x01123D, 1], + [0x01123E, 0x01123E, 0], + [0x01123F, 0x011240, 1], + [0x011241, 0x011241, 0], + [0x011242, 0x01127F, -1], + [0x011280, 0x011286, 1], + [0x011287, 0x011287, -1], + [0x011288, 0x011288, 1], + [0x011289, 0x011289, -1], + [0x01128A, 0x01128D, 1], + [0x01128E, 0x01128E, -1], + [0x01128F, 0x01129D, 1], + [0x01129E, 0x01129E, -1], + [0x01129F, 0x0112A9, 1], + [0x0112AA, 0x0112AF, -1], + [0x0112B0, 0x0112DE, 1], + [0x0112DF, 0x0112DF, 0], + [0x0112E0, 0x0112E2, 1], + [0x0112E3, 0x0112EA, 0], + [0x0112EB, 0x0112EF, -1], + [0x0112F0, 0x0112F9, 1], + [0x0112FA, 0x0112FF, -1], + [0x011300, 0x011301, 0], + [0x011302, 0x011303, 1], + [0x011304, 0x011304, -1], + [0x011305, 0x01130C, 1], + [0x01130D, 0x01130E, -1], + [0x01130F, 0x011310, 1], + [0x011311, 0x011312, -1], + [0x011313, 0x011328, 1], + [0x011329, 0x011329, -1], + [0x01132A, 0x011330, 1], + [0x011331, 0x011331, -1], + [0x011332, 0x011333, 1], + [0x011334, 0x011334, -1], + [0x011335, 0x011339, 1], + [0x01133A, 0x01133A, -1], + [0x01133B, 0x01133C, 0], + [0x01133D, 0x01133F, 1], + [0x011340, 0x011340, 0], + [0x011341, 0x011344, 1], + [0x011345, 0x011346, -1], + [0x011347, 0x011348, 1], + [0x011349, 0x01134A, -1], + [0x01134B, 0x01134D, 1], + [0x01134E, 0x01134F, -1], + [0x011350, 0x011350, 1], + [0x011351, 0x011356, -1], + [0x011357, 0x011357, 1], + [0x011358, 0x01135C, -1], + [0x01135D, 0x011363, 1], + [0x011364, 0x011365, -1], + [0x011366, 0x01136C, 0], + [0x01136D, 0x01136F, -1], + [0x011370, 0x011374, 0], + [0x011375, 0x01137F, -1], + [0x011380, 0x011389, 1], + [0x01138A, 0x01138A, -1], + [0x01138B, 0x01138B, 1], + [0x01138C, 0x01138D, -1], + [0x01138E, 0x01138E, 1], + [0x01138F, 0x01138F, -1], + [0x011390, 0x0113B5, 1], + [0x0113B6, 0x0113B6, -1], + [0x0113B7, 0x0113BA, 1], + [0x0113BB, 0x0113C0, 0], + [0x0113C1, 0x0113C1, -1], + [0x0113C2, 0x0113C2, 1], + [0x0113C3, 0x0113C4, -1], + [0x0113C5, 0x0113C5, 1], + [0x0113C6, 0x0113C6, -1], + [0x0113C7, 0x0113CA, 1], + [0x0113CB, 0x0113CB, -1], + [0x0113CC, 0x0113CD, 1], + [0x0113CE, 0x0113CE, 0], + [0x0113CF, 0x0113CF, 1], + [0x0113D0, 0x0113D0, 0], + [0x0113D1, 0x0113D1, 1], + [0x0113D2, 0x0113D2, 0], + [0x0113D3, 0x0113D5, 1], + [0x0113D6, 0x0113D6, -1], + [0x0113D7, 0x0113D8, 1], + [0x0113D9, 0x0113E0, -1], + [0x0113E1, 0x0113E2, 0], + [0x0113E3, 0x0113FF, -1], + [0x011400, 0x011437, 1], + [0x011438, 0x01143F, 0], + [0x011440, 0x011441, 1], + [0x011442, 0x011444, 0], + [0x011445, 0x011445, 1], + [0x011446, 0x011446, 0], + [0x011447, 0x01145B, 1], + [0x01145C, 0x01145C, -1], + [0x01145D, 0x01145D, 1], + [0x01145E, 0x01145E, 0], + [0x01145F, 0x011461, 1], + [0x011462, 0x01147F, -1], + [0x011480, 0x0114B2, 1], + [0x0114B3, 0x0114B8, 0], + [0x0114B9, 0x0114B9, 1], + [0x0114BA, 0x0114BA, 0], + [0x0114BB, 0x0114BE, 1], + [0x0114BF, 0x0114C0, 0], + [0x0114C1, 0x0114C1, 1], + [0x0114C2, 0x0114C3, 0], + [0x0114C4, 0x0114C7, 1], + [0x0114C8, 0x0114CF, -1], + [0x0114D0, 0x0114D9, 1], + [0x0114DA, 0x01157F, -1], + [0x011580, 0x0115B1, 1], + [0x0115B2, 0x0115B5, 0], + [0x0115B6, 0x0115B7, -1], + [0x0115B8, 0x0115BB, 1], + [0x0115BC, 0x0115BD, 0], + [0x0115BE, 0x0115BE, 1], + [0x0115BF, 0x0115C0, 0], + [0x0115C1, 0x0115DB, 1], + [0x0115DC, 0x0115DD, 0], + [0x0115DE, 0x0115FF, -1], + [0x011600, 0x011632, 1], + [0x011633, 0x01163A, 0], + [0x01163B, 0x01163C, 1], + [0x01163D, 0x01163D, 0], + [0x01163E, 0x01163E, 1], + [0x01163F, 0x011640, 0], + [0x011641, 0x011644, 1], + [0x011645, 0x01164F, -1], + [0x011650, 0x011659, 1], + [0x01165A, 0x01165F, -1], + [0x011660, 0x01166C, 1], + [0x01166D, 0x01167F, -1], + [0x011680, 0x0116AA, 1], + [0x0116AB, 0x0116AB, 0], + [0x0116AC, 0x0116AC, 1], + [0x0116AD, 0x0116AD, 0], + [0x0116AE, 0x0116AF, 1], + [0x0116B0, 0x0116B5, 0], + [0x0116B6, 0x0116B6, 1], + [0x0116B7, 0x0116B7, 0], + [0x0116B8, 0x0116B9, 1], + [0x0116BA, 0x0116BF, -1], + [0x0116C0, 0x0116C9, 1], + [0x0116CA, 0x0116CF, -1], + [0x0116D0, 0x0116E3, 1], + [0x0116E4, 0x0116FF, -1], + [0x011700, 0x01171A, 1], + [0x01171B, 0x01171C, -1], + [0x01171D, 0x01171D, 0], + [0x01171E, 0x01171E, 1], + [0x01171F, 0x01171F, 0], + [0x011720, 0x011721, 1], + [0x011722, 0x011725, 0], + [0x011726, 0x011726, 1], + [0x011727, 0x01172B, 0], + [0x01172C, 0x01172F, -1], + [0x011730, 0x011746, 1], + [0x011747, 0x0117FF, -1], + [0x011800, 0x01182E, 1], + [0x01182F, 0x011837, 0], + [0x011838, 0x011838, 1], + [0x011839, 0x01183A, 0], + [0x01183B, 0x01183B, 1], + [0x01183C, 0x01189F, -1], + [0x0118A0, 0x0118F2, 1], + [0x0118F3, 0x0118FE, -1], + [0x0118FF, 0x011906, 1], + [0x011907, 0x011908, -1], + [0x011909, 0x011909, 1], + [0x01190A, 0x01190B, -1], + [0x01190C, 0x011913, 1], + [0x011914, 0x011914, -1], + [0x011915, 0x011916, 1], + [0x011917, 0x011917, -1], + [0x011918, 0x011935, 1], + [0x011936, 0x011936, -1], + [0x011937, 0x011938, 1], + [0x011939, 0x01193A, -1], + [0x01193B, 0x01193C, 0], + [0x01193D, 0x01193D, 1], + [0x01193E, 0x01193E, 0], + [0x01193F, 0x011942, 1], + [0x011943, 0x011943, 0], + [0x011944, 0x011946, 1], + [0x011947, 0x01194F, -1], + [0x011950, 0x011959, 1], + [0x01195A, 0x01199F, -1], + [0x0119A0, 0x0119A7, 1], + [0x0119A8, 0x0119A9, -1], + [0x0119AA, 0x0119D3, 1], + [0x0119D4, 0x0119D7, 0], + [0x0119D8, 0x0119D9, -1], + [0x0119DA, 0x0119DB, 0], + [0x0119DC, 0x0119DF, 1], + [0x0119E0, 0x0119E0, 0], + [0x0119E1, 0x0119E4, 1], + [0x0119E5, 0x0119FF, -1], + [0x011A00, 0x011A00, 1], + [0x011A01, 0x011A0A, 0], + [0x011A0B, 0x011A32, 1], + [0x011A33, 0x011A38, 0], + [0x011A39, 0x011A3A, 1], + [0x011A3B, 0x011A3E, 0], + [0x011A3F, 0x011A46, 1], + [0x011A47, 0x011A47, 0], + [0x011A48, 0x011A4F, -1], + [0x011A50, 0x011A50, 1], + [0x011A51, 0x011A56, 0], + [0x011A57, 0x011A58, 1], + [0x011A59, 0x011A5B, 0], + [0x011A5C, 0x011A89, 1], + [0x011A8A, 0x011A96, 0], + [0x011A97, 0x011A97, 1], + [0x011A98, 0x011A99, 0], + [0x011A9A, 0x011AA2, 1], + [0x011AA3, 0x011AAF, -1], + [0x011AB0, 0x011AF8, 1], + [0x011AF9, 0x011AFF, -1], + [0x011B00, 0x011B09, 1], + [0x011B0A, 0x011BBF, -1], + [0x011BC0, 0x011BE1, 1], + [0x011BE2, 0x011BEF, -1], + [0x011BF0, 0x011BF9, 1], + [0x011BFA, 0x011BFF, -1], + [0x011C00, 0x011C08, 1], + [0x011C09, 0x011C09, -1], + [0x011C0A, 0x011C2F, 1], + [0x011C30, 0x011C36, 0], + [0x011C37, 0x011C37, -1], + [0x011C38, 0x011C3D, 0], + [0x011C3E, 0x011C3E, 1], + [0x011C3F, 0x011C3F, 0], + [0x011C40, 0x011C45, 1], + [0x011C46, 0x011C4F, -1], + [0x011C50, 0x011C6C, 1], + [0x011C6D, 0x011C6F, -1], + [0x011C70, 0x011C8F, 1], + [0x011C90, 0x011C91, -1], + [0x011C92, 0x011CA7, 0], + [0x011CA8, 0x011CA8, -1], + [0x011CA9, 0x011CA9, 1], + [0x011CAA, 0x011CB0, 0], + [0x011CB1, 0x011CB1, 1], + [0x011CB2, 0x011CB3, 0], + [0x011CB4, 0x011CB4, 1], + [0x011CB5, 0x011CB6, 0], + [0x011CB7, 0x011CFF, -1], + [0x011D00, 0x011D06, 1], + [0x011D07, 0x011D07, -1], + [0x011D08, 0x011D09, 1], + [0x011D0A, 0x011D0A, -1], + [0x011D0B, 0x011D30, 1], + [0x011D31, 0x011D36, 0], + [0x011D37, 0x011D39, -1], + [0x011D3A, 0x011D3A, 0], + [0x011D3B, 0x011D3B, -1], + [0x011D3C, 0x011D3D, 0], + [0x011D3E, 0x011D3E, -1], + [0x011D3F, 0x011D45, 0], + [0x011D46, 0x011D46, 1], + [0x011D47, 0x011D47, 0], + [0x011D48, 0x011D4F, -1], + [0x011D50, 0x011D59, 1], + [0x011D5A, 0x011D5F, -1], + [0x011D60, 0x011D65, 1], + [0x011D66, 0x011D66, -1], + [0x011D67, 0x011D68, 1], + [0x011D69, 0x011D69, -1], + [0x011D6A, 0x011D8E, 1], + [0x011D8F, 0x011D8F, -1], + [0x011D90, 0x011D91, 0], + [0x011D92, 0x011D92, -1], + [0x011D93, 0x011D94, 1], + [0x011D95, 0x011D95, 0], + [0x011D96, 0x011D96, 1], + [0x011D97, 0x011D97, 0], + [0x011D98, 0x011D98, 1], + [0x011D99, 0x011D9F, -1], + [0x011DA0, 0x011DA9, 1], + [0x011DAA, 0x011EDF, -1], + [0x011EE0, 0x011EF2, 1], + [0x011EF3, 0x011EF4, 0], + [0x011EF5, 0x011EF8, 1], + [0x011EF9, 0x011EFF, -1], + [0x011F00, 0x011F01, 0], + [0x011F02, 0x011F10, 1], + [0x011F11, 0x011F11, -1], + [0x011F12, 0x011F35, 1], + [0x011F36, 0x011F3A, 0], + [0x011F3B, 0x011F3D, -1], + [0x011F3E, 0x011F3F, 1], + [0x011F40, 0x011F40, 0], + [0x011F41, 0x011F41, 1], + [0x011F42, 0x011F42, 0], + [0x011F43, 0x011F59, 1], + [0x011F5A, 0x011F5A, 0], + [0x011F5B, 0x011FAF, -1], + [0x011FB0, 0x011FB0, 1], + [0x011FB1, 0x011FBF, -1], + [0x011FC0, 0x011FF1, 1], + [0x011FF2, 0x011FFE, -1], + [0x011FFF, 0x012399, 1], + [0x01239A, 0x0123FF, -1], + [0x012400, 0x01246E, 1], + [0x01246F, 0x01246F, -1], + [0x012470, 0x012474, 1], + [0x012475, 0x01247F, -1], + [0x012480, 0x012543, 1], + [0x012544, 0x012F8F, -1], + [0x012F90, 0x012FF2, 1], + [0x012FF3, 0x012FFF, -1], + [0x013000, 0x01343F, 1], + [0x013440, 0x013440, 0], + [0x013441, 0x013446, 1], + [0x013447, 0x013455, 0], + [0x013456, 0x01345F, -1], + [0x013460, 0x0143FA, 1], + [0x0143FB, 0x0143FF, -1], + [0x014400, 0x014646, 1], + [0x014647, 0x0160FF, -1], + [0x016100, 0x01611D, 1], + [0x01611E, 0x016129, 0], + [0x01612A, 0x01612C, 1], + [0x01612D, 0x01612F, 0], + [0x016130, 0x016139, 1], + [0x01613A, 0x0167FF, -1], + [0x016800, 0x016A38, 1], + [0x016A39, 0x016A3F, -1], + [0x016A40, 0x016A5E, 1], + [0x016A5F, 0x016A5F, -1], + [0x016A60, 0x016A69, 1], + [0x016A6A, 0x016A6D, -1], + [0x016A6E, 0x016ABE, 1], + [0x016ABF, 0x016ABF, -1], + [0x016AC0, 0x016AC9, 1], + [0x016ACA, 0x016ACF, -1], + [0x016AD0, 0x016AED, 1], + [0x016AEE, 0x016AEF, -1], + [0x016AF0, 0x016AF4, 0], + [0x016AF5, 0x016AF5, 1], + [0x016AF6, 0x016AFF, -1], + [0x016B00, 0x016B2F, 1], + [0x016B30, 0x016B36, 0], + [0x016B37, 0x016B45, 1], + [0x016B46, 0x016B4F, -1], + [0x016B50, 0x016B59, 1], + [0x016B5A, 0x016B5A, -1], + [0x016B5B, 0x016B61, 1], + [0x016B62, 0x016B62, -1], + [0x016B63, 0x016B77, 1], + [0x016B78, 0x016B7C, -1], + [0x016B7D, 0x016B8F, 1], + [0x016B90, 0x016D3F, -1], + [0x016D40, 0x016D79, 1], + [0x016D7A, 0x016E3F, -1], + [0x016E40, 0x016E9A, 1], + [0x016E9B, 0x016EFF, -1], + [0x016F00, 0x016F4A, 1], + [0x016F4B, 0x016F4E, -1], + [0x016F4F, 0x016F4F, 0], + [0x016F50, 0x016F87, 1], + [0x016F88, 0x016F8E, -1], + [0x016F8F, 0x016F92, 0], + [0x016F93, 0x016F9F, 1], + [0x016FA0, 0x016FDF, -1], + [0x016FE0, 0x016FE3, 2], + [0x016FE4, 0x016FE4, 0], + [0x016FE5, 0x016FEF, -1], + [0x016FF0, 0x016FF1, 2], + [0x016FF2, 0x016FFF, -1], + [0x017000, 0x0187F7, 2], + [0x0187F8, 0x0187FF, -1], + [0x018800, 0x018CD5, 2], + [0x018CD6, 0x018CFE, -1], + [0x018CFF, 0x018D08, 2], + [0x018D09, 0x01AFEF, -1], + [0x01AFF0, 0x01AFF3, 2], + [0x01AFF4, 0x01AFF4, -1], + [0x01AFF5, 0x01AFFB, 2], + [0x01AFFC, 0x01AFFC, -1], + [0x01AFFD, 0x01AFFE, 2], + [0x01AFFF, 0x01AFFF, -1], + [0x01B000, 0x01B122, 2], + [0x01B123, 0x01B131, -1], + [0x01B132, 0x01B132, 2], + [0x01B133, 0x01B14F, -1], + [0x01B150, 0x01B152, 2], + [0x01B153, 0x01B154, -1], + [0x01B155, 0x01B155, 2], + [0x01B156, 0x01B163, -1], + [0x01B164, 0x01B167, 2], + [0x01B168, 0x01B16F, -1], + [0x01B170, 0x01B2FB, 2], + [0x01B2FC, 0x01BBFF, -1], + [0x01BC00, 0x01BC6A, 1], + [0x01BC6B, 0x01BC6F, -1], + [0x01BC70, 0x01BC7C, 1], + [0x01BC7D, 0x01BC7F, -1], + [0x01BC80, 0x01BC88, 1], + [0x01BC89, 0x01BC8F, -1], + [0x01BC90, 0x01BC99, 1], + [0x01BC9A, 0x01BC9B, -1], + [0x01BC9C, 0x01BC9C, 1], + [0x01BC9D, 0x01BC9E, 0], + [0x01BC9F, 0x01BC9F, 1], + [0x01BCA0, 0x01BCA3, 0], + [0x01BCA4, 0x01CBFF, -1], + [0x01CC00, 0x01CCF9, 1], + [0x01CCFA, 0x01CCFF, -1], + [0x01CD00, 0x01CEB3, 1], + [0x01CEB4, 0x01CEFF, -1], + [0x01CF00, 0x01CF2D, 0], + [0x01CF2E, 0x01CF2F, -1], + [0x01CF30, 0x01CF46, 0], + [0x01CF47, 0x01CF4F, -1], + [0x01CF50, 0x01CFC3, 1], + [0x01CFC4, 0x01CFFF, -1], + [0x01D000, 0x01D0F5, 1], + [0x01D0F6, 0x01D0FF, -1], + [0x01D100, 0x01D126, 1], + [0x01D127, 0x01D128, -1], + [0x01D129, 0x01D166, 1], + [0x01D167, 0x01D169, 0], + [0x01D16A, 0x01D172, 1], + [0x01D173, 0x01D182, 0], + [0x01D183, 0x01D184, 1], + [0x01D185, 0x01D18B, 0], + [0x01D18C, 0x01D1A9, 1], + [0x01D1AA, 0x01D1AD, 0], + [0x01D1AE, 0x01D1EA, 1], + [0x01D1EB, 0x01D1FF, -1], + [0x01D200, 0x01D241, 1], + [0x01D242, 0x01D244, 0], + [0x01D245, 0x01D245, 1], + [0x01D246, 0x01D2BF, -1], + [0x01D2C0, 0x01D2D3, 1], + [0x01D2D4, 0x01D2DF, -1], + [0x01D2E0, 0x01D2F3, 1], + [0x01D2F4, 0x01D2FF, -1], + [0x01D300, 0x01D356, 2], + [0x01D357, 0x01D35F, -1], + [0x01D360, 0x01D376, 2], + [0x01D377, 0x01D378, 1], + [0x01D379, 0x01D3FF, -1], + [0x01D400, 0x01D454, 1], + [0x01D455, 0x01D455, -1], + [0x01D456, 0x01D49C, 1], + [0x01D49D, 0x01D49D, -1], + [0x01D49E, 0x01D49F, 1], + [0x01D4A0, 0x01D4A1, -1], + [0x01D4A2, 0x01D4A2, 1], + [0x01D4A3, 0x01D4A4, -1], + [0x01D4A5, 0x01D4A6, 1], + [0x01D4A7, 0x01D4A8, -1], + [0x01D4A9, 0x01D4AC, 1], + [0x01D4AD, 0x01D4AD, -1], + [0x01D4AE, 0x01D4B9, 1], + [0x01D4BA, 0x01D4BA, -1], + [0x01D4BB, 0x01D4BB, 1], + [0x01D4BC, 0x01D4BC, -1], + [0x01D4BD, 0x01D4C3, 1], + [0x01D4C4, 0x01D4C4, -1], + [0x01D4C5, 0x01D505, 1], + [0x01D506, 0x01D506, -1], + [0x01D507, 0x01D50A, 1], + [0x01D50B, 0x01D50C, -1], + [0x01D50D, 0x01D514, 1], + [0x01D515, 0x01D515, -1], + [0x01D516, 0x01D51C, 1], + [0x01D51D, 0x01D51D, -1], + [0x01D51E, 0x01D539, 1], + [0x01D53A, 0x01D53A, -1], + [0x01D53B, 0x01D53E, 1], + [0x01D53F, 0x01D53F, -1], + [0x01D540, 0x01D544, 1], + [0x01D545, 0x01D545, -1], + [0x01D546, 0x01D546, 1], + [0x01D547, 0x01D549, -1], + [0x01D54A, 0x01D550, 1], + [0x01D551, 0x01D551, -1], + [0x01D552, 0x01D6A5, 1], + [0x01D6A6, 0x01D6A7, -1], + [0x01D6A8, 0x01D7CB, 1], + [0x01D7CC, 0x01D7CD, -1], + [0x01D7CE, 0x01D9FF, 1], + [0x01DA00, 0x01DA36, 0], + [0x01DA37, 0x01DA3A, 1], + [0x01DA3B, 0x01DA6C, 0], + [0x01DA6D, 0x01DA74, 1], + [0x01DA75, 0x01DA75, 0], + [0x01DA76, 0x01DA83, 1], + [0x01DA84, 0x01DA84, 0], + [0x01DA85, 0x01DA8B, 1], + [0x01DA8C, 0x01DA9A, -1], + [0x01DA9B, 0x01DA9F, 0], + [0x01DAA0, 0x01DAA0, -1], + [0x01DAA1, 0x01DAAF, 0], + [0x01DAB0, 0x01DEFF, -1], + [0x01DF00, 0x01DF1E, 1], + [0x01DF1F, 0x01DF24, -1], + [0x01DF25, 0x01DF2A, 1], + [0x01DF2B, 0x01DFFF, -1], + [0x01E000, 0x01E006, 0], + [0x01E007, 0x01E007, -1], + [0x01E008, 0x01E018, 0], + [0x01E019, 0x01E01A, -1], + [0x01E01B, 0x01E021, 0], + [0x01E022, 0x01E022, -1], + [0x01E023, 0x01E024, 0], + [0x01E025, 0x01E025, -1], + [0x01E026, 0x01E02A, 0], + [0x01E02B, 0x01E02F, -1], + [0x01E030, 0x01E06D, 1], + [0x01E06E, 0x01E08E, -1], + [0x01E08F, 0x01E08F, 0], + [0x01E090, 0x01E0FF, -1], + [0x01E100, 0x01E12C, 1], + [0x01E12D, 0x01E12F, -1], + [0x01E130, 0x01E136, 0], + [0x01E137, 0x01E13D, 1], + [0x01E13E, 0x01E13F, -1], + [0x01E140, 0x01E149, 1], + [0x01E14A, 0x01E14D, -1], + [0x01E14E, 0x01E14F, 1], + [0x01E150, 0x01E28F, -1], + [0x01E290, 0x01E2AD, 1], + [0x01E2AE, 0x01E2AE, 0], + [0x01E2AF, 0x01E2BF, -1], + [0x01E2C0, 0x01E2EB, 1], + [0x01E2EC, 0x01E2EF, 0], + [0x01E2F0, 0x01E2F9, 1], + [0x01E2FA, 0x01E2FE, -1], + [0x01E2FF, 0x01E2FF, 1], + [0x01E300, 0x01E4CF, -1], + [0x01E4D0, 0x01E4EB, 1], + [0x01E4EC, 0x01E4EF, 0], + [0x01E4F0, 0x01E4F9, 1], + [0x01E4FA, 0x01E5CF, -1], + [0x01E5D0, 0x01E5ED, 1], + [0x01E5EE, 0x01E5EF, 0], + [0x01E5F0, 0x01E5FA, 1], + [0x01E5FB, 0x01E5FE, -1], + [0x01E5FF, 0x01E5FF, 1], + [0x01E600, 0x01E7DF, -1], + [0x01E7E0, 0x01E7E6, 1], + [0x01E7E7, 0x01E7E7, -1], + [0x01E7E8, 0x01E7EB, 1], + [0x01E7EC, 0x01E7EC, -1], + [0x01E7ED, 0x01E7EE, 1], + [0x01E7EF, 0x01E7EF, -1], + [0x01E7F0, 0x01E7FE, 1], + [0x01E7FF, 0x01E7FF, -1], + [0x01E800, 0x01E8C4, 1], + [0x01E8C5, 0x01E8C6, -1], + [0x01E8C7, 0x01E8CF, 1], + [0x01E8D0, 0x01E8D6, 0], + [0x01E8D7, 0x01E8FF, -1], + [0x01E900, 0x01E943, 1], + [0x01E944, 0x01E94A, 0], + [0x01E94B, 0x01E94B, 1], + [0x01E94C, 0x01E94F, -1], + [0x01E950, 0x01E959, 1], + [0x01E95A, 0x01E95D, -1], + [0x01E95E, 0x01E95F, 1], + [0x01E960, 0x01EC70, -1], + [0x01EC71, 0x01ECB4, 1], + [0x01ECB5, 0x01ED00, -1], + [0x01ED01, 0x01ED3D, 1], + [0x01ED3E, 0x01EDFF, -1], + [0x01EE00, 0x01EE03, 1], + [0x01EE04, 0x01EE04, -1], + [0x01EE05, 0x01EE1F, 1], + [0x01EE20, 0x01EE20, -1], + [0x01EE21, 0x01EE22, 1], + [0x01EE23, 0x01EE23, -1], + [0x01EE24, 0x01EE24, 1], + [0x01EE25, 0x01EE26, -1], + [0x01EE27, 0x01EE27, 1], + [0x01EE28, 0x01EE28, -1], + [0x01EE29, 0x01EE32, 1], + [0x01EE33, 0x01EE33, -1], + [0x01EE34, 0x01EE37, 1], + [0x01EE38, 0x01EE38, -1], + [0x01EE39, 0x01EE39, 1], + [0x01EE3A, 0x01EE3A, -1], + [0x01EE3B, 0x01EE3B, 1], + [0x01EE3C, 0x01EE41, -1], + [0x01EE42, 0x01EE42, 1], + [0x01EE43, 0x01EE46, -1], + [0x01EE47, 0x01EE47, 1], + [0x01EE48, 0x01EE48, -1], + [0x01EE49, 0x01EE49, 1], + [0x01EE4A, 0x01EE4A, -1], + [0x01EE4B, 0x01EE4B, 1], + [0x01EE4C, 0x01EE4C, -1], + [0x01EE4D, 0x01EE4F, 1], + [0x01EE50, 0x01EE50, -1], + [0x01EE51, 0x01EE52, 1], + [0x01EE53, 0x01EE53, -1], + [0x01EE54, 0x01EE54, 1], + [0x01EE55, 0x01EE56, -1], + [0x01EE57, 0x01EE57, 1], + [0x01EE58, 0x01EE58, -1], + [0x01EE59, 0x01EE59, 1], + [0x01EE5A, 0x01EE5A, -1], + [0x01EE5B, 0x01EE5B, 1], + [0x01EE5C, 0x01EE5C, -1], + [0x01EE5D, 0x01EE5D, 1], + [0x01EE5E, 0x01EE5E, -1], + [0x01EE5F, 0x01EE5F, 1], + [0x01EE60, 0x01EE60, -1], + [0x01EE61, 0x01EE62, 1], + [0x01EE63, 0x01EE63, -1], + [0x01EE64, 0x01EE64, 1], + [0x01EE65, 0x01EE66, -1], + [0x01EE67, 0x01EE6A, 1], + [0x01EE6B, 0x01EE6B, -1], + [0x01EE6C, 0x01EE72, 1], + [0x01EE73, 0x01EE73, -1], + [0x01EE74, 0x01EE77, 1], + [0x01EE78, 0x01EE78, -1], + [0x01EE79, 0x01EE7C, 1], + [0x01EE7D, 0x01EE7D, -1], + [0x01EE7E, 0x01EE7E, 1], + [0x01EE7F, 0x01EE7F, -1], + [0x01EE80, 0x01EE89, 1], + [0x01EE8A, 0x01EE8A, -1], + [0x01EE8B, 0x01EE9B, 1], + [0x01EE9C, 0x01EEA0, -1], + [0x01EEA1, 0x01EEA3, 1], + [0x01EEA4, 0x01EEA4, -1], + [0x01EEA5, 0x01EEA9, 1], + [0x01EEAA, 0x01EEAA, -1], + [0x01EEAB, 0x01EEBB, 1], + [0x01EEBC, 0x01EEEF, -1], + [0x01EEF0, 0x01EEF1, 1], + [0x01EEF2, 0x01EFFF, -1], + [0x01F000, 0x01F003, 1], + [0x01F004, 0x01F004, 2], + [0x01F005, 0x01F02B, 1], + [0x01F02C, 0x01F02F, -1], + [0x01F030, 0x01F093, 1], + [0x01F094, 0x01F09F, -1], + [0x01F0A0, 0x01F0AE, 1], + [0x01F0AF, 0x01F0B0, -1], + [0x01F0B1, 0x01F0BF, 1], + [0x01F0C0, 0x01F0C0, -1], + [0x01F0C1, 0x01F0CE, 1], + [0x01F0CF, 0x01F0CF, 2], + [0x01F0D0, 0x01F0D0, -1], + [0x01F0D1, 0x01F0F5, 1], + [0x01F0F6, 0x01F0FF, -1], + [0x01F100, 0x01F18D, 1], + [0x01F18E, 0x01F18E, 2], + [0x01F18F, 0x01F190, 1], + [0x01F191, 0x01F19A, 2], + [0x01F19B, 0x01F1AD, 1], + [0x01F1AE, 0x01F1E5, -1], + [0x01F1E6, 0x01F1FF, 1], + [0x01F200, 0x01F202, 2], + [0x01F203, 0x01F20F, -1], + [0x01F210, 0x01F23B, 2], + [0x01F23C, 0x01F23F, -1], + [0x01F240, 0x01F248, 2], + [0x01F249, 0x01F24F, -1], + [0x01F250, 0x01F251, 2], + [0x01F252, 0x01F25F, -1], + [0x01F260, 0x01F265, 2], + [0x01F266, 0x01F2FF, -1], + [0x01F300, 0x01F320, 2], + [0x01F321, 0x01F32C, 1], + [0x01F32D, 0x01F335, 2], + [0x01F336, 0x01F336, 1], + [0x01F337, 0x01F37C, 2], + [0x01F37D, 0x01F37D, 1], + [0x01F37E, 0x01F393, 2], + [0x01F394, 0x01F39F, 1], + [0x01F3A0, 0x01F3CA, 2], + [0x01F3CB, 0x01F3CE, 1], + [0x01F3CF, 0x01F3D3, 2], + [0x01F3D4, 0x01F3DF, 1], + [0x01F3E0, 0x01F3F0, 2], + [0x01F3F1, 0x01F3F3, 1], + [0x01F3F4, 0x01F3F4, 2], + [0x01F3F5, 0x01F3F7, 1], + [0x01F3F8, 0x01F43E, 2], + [0x01F43F, 0x01F43F, 1], + [0x01F440, 0x01F440, 2], + [0x01F441, 0x01F441, 1], + [0x01F442, 0x01F4FC, 2], + [0x01F4FD, 0x01F4FE, 1], + [0x01F4FF, 0x01F53D, 2], + [0x01F53E, 0x01F54A, 1], + [0x01F54B, 0x01F54E, 2], + [0x01F54F, 0x01F54F, 1], + [0x01F550, 0x01F567, 2], + [0x01F568, 0x01F579, 1], + [0x01F57A, 0x01F57A, 2], + [0x01F57B, 0x01F594, 1], + [0x01F595, 0x01F596, 2], + [0x01F597, 0x01F5A3, 1], + [0x01F5A4, 0x01F5A4, 2], + [0x01F5A5, 0x01F5FA, 1], + [0x01F5FB, 0x01F64F, 2], + [0x01F650, 0x01F67F, 1], + [0x01F680, 0x01F6C5, 2], + [0x01F6C6, 0x01F6CB, 1], + [0x01F6CC, 0x01F6CC, 2], + [0x01F6CD, 0x01F6CF, 1], + [0x01F6D0, 0x01F6D2, 2], + [0x01F6D3, 0x01F6D4, 1], + [0x01F6D5, 0x01F6D7, 2], + [0x01F6D8, 0x01F6DB, -1], + [0x01F6DC, 0x01F6DF, 2], + [0x01F6E0, 0x01F6EA, 1], + [0x01F6EB, 0x01F6EC, 2], + [0x01F6ED, 0x01F6EF, -1], + [0x01F6F0, 0x01F6F3, 1], + [0x01F6F4, 0x01F6FC, 2], + [0x01F6FD, 0x01F6FF, -1], + [0x01F700, 0x01F776, 1], + [0x01F777, 0x01F77A, -1], + [0x01F77B, 0x01F7D9, 1], + [0x01F7DA, 0x01F7DF, -1], + [0x01F7E0, 0x01F7EB, 2], + [0x01F7EC, 0x01F7EF, -1], + [0x01F7F0, 0x01F7F0, 2], + [0x01F7F1, 0x01F7FF, -1], + [0x01F800, 0x01F80B, 1], + [0x01F80C, 0x01F80F, -1], + [0x01F810, 0x01F847, 1], + [0x01F848, 0x01F84F, -1], + [0x01F850, 0x01F859, 1], + [0x01F85A, 0x01F85F, -1], + [0x01F860, 0x01F887, 1], + [0x01F888, 0x01F88F, -1], + [0x01F890, 0x01F8AD, 1], + [0x01F8AE, 0x01F8AF, -1], + [0x01F8B0, 0x01F8BB, 1], + [0x01F8BC, 0x01F8BF, -1], + [0x01F8C0, 0x01F8C1, 1], + [0x01F8C2, 0x01F8FF, -1], + [0x01F900, 0x01F90B, 1], + [0x01F90C, 0x01F93A, 2], + [0x01F93B, 0x01F93B, 1], + [0x01F93C, 0x01F945, 2], + [0x01F946, 0x01F946, 1], + [0x01F947, 0x01F9FF, 2], + [0x01FA00, 0x01FA53, 1], + [0x01FA54, 0x01FA5F, -1], + [0x01FA60, 0x01FA6D, 1], + [0x01FA6E, 0x01FA6F, -1], + [0x01FA70, 0x01FA7C, 2], + [0x01FA7D, 0x01FA7F, -1], + [0x01FA80, 0x01FA89, 2], + [0x01FA8A, 0x01FA8E, -1], + [0x01FA8F, 0x01FAC6, 2], + [0x01FAC7, 0x01FACD, -1], + [0x01FACE, 0x01FADC, 2], + [0x01FADD, 0x01FADE, -1], + [0x01FADF, 0x01FAE9, 2], + [0x01FAEA, 0x01FAEF, -1], + [0x01FAF0, 0x01FAF8, 2], + [0x01FAF9, 0x01FAFF, -1], + [0x01FB00, 0x01FB92, 1], + [0x01FB93, 0x01FB93, -1], + [0x01FB94, 0x01FBF9, 1], + [0x01FBFA, 0x01FFFF, -1], + [0x020000, 0x02A6DF, 2], + [0x02A6E0, 0x02A6FF, -1], + [0x02A700, 0x02B739, 2], + [0x02B73A, 0x02B73F, -1], + [0x02B740, 0x02B81D, 2], + [0x02B81E, 0x02B81F, -1], + [0x02B820, 0x02CEA1, 2], + [0x02CEA2, 0x02CEAF, -1], + [0x02CEB0, 0x02EBE0, 2], + [0x02EBE1, 0x02EBEF, -1], + [0x02EBF0, 0x02EE5D, 2], + [0x02EE5E, 0x02F7FF, -1], + [0x02F800, 0x02FA1D, 2], + [0x02FA1E, 0x02FFFF, -1], + [0x030000, 0x03134A, 2], + [0x03134B, 0x03134F, -1], + [0x031350, 0x0323AF, 2], + [0x0323B0, 0x0E0000, -1], + [0x0E0001, 0x0E0001, 0], + [0x0E0002, 0x0E001F, -1], + [0x0E0020, 0x0E007F, 0], + [0x0E0080, 0x0E00FF, -1], + [0x0E0100, 0x0E01EF, 0], + [0x0E01F0, 0x0EFFFF, -1], + [0x0F0000, 0x0FFFFD, 1], + [0x0FFFFE, 0x0FFFFF, -1], + [0x100000, 0x10FFFD, 1], + [0x10FFFE, 0x10FFFF, -1], +]; + +export function wcwidth(code: number): number { + if (code >= 0x20 && code <= 0x7E) return 1; + if (code >= 0xA0 && code <= 0xFF) return 1; + if (code < 0x20 || (code > 0x7E && code < 0xA0)) { + return code === 0 ? 0 : -1; + } + + let lo = 0; + let hi = WCWIDTH_TABLE.length - 1; + while (lo <= hi) { + let mid = Math.floor((lo + hi) / 2); + let [start, end, width] = WCWIDTH_TABLE[mid]; + if (code < start) { + hi = mid - 1; + } else if (code > end) { + lo = mid + 1; + } else { + return width; + } + } + + return -1; +} From a533953ffedbd95e93af699eee8d26c4ff6b40ca Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 31 May 2026 09:27:58 -0400 Subject: [PATCH 33/35] =?UTF-8?q?=F0=9F=94=A7=20export=20animating=20from?= =?UTF-8?q?=20wasm=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 1103b8e..05a2dd1 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ EXPORTS = \ -Wl,--export=pointer_over_id_string_length \ -Wl,--export=pointer_over_id_string_ptr \ -Wl,--export=get_element_bounds \ + -Wl,--export=animating \ -Wl,--export=error_count \ -Wl,--export=error_type \ -Wl,--export=error_message_length \ From 4effe2e805e0dd8512f5208b717af6bd2ea7e24b Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 31 May 2026 09:29:58 -0400 Subject: [PATCH 34/35] =?UTF-8?q?=F0=9F=A7=AA=20cover=20transitions=20in?= =?UTF-8?q?=20snapshots=20and=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops.ts | 1 + test/transitions-pack.test.ts | 11 ++++++++++- test/validate.test.ts | 24 ++++++++++++++++++++++++ validate.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/ops.ts b/ops.ts index dd90163..637a0c4 100644 --- a/ops.ts +++ b/ops.ts @@ -370,6 +370,7 @@ function packSize(ops: Op[]): number { if (op.border) n += 8; if (op.clip) n += 4; if (op.floating) n += 16; + if (op.transition) n += 8; break; } case OP_TEXT: { diff --git a/test/transitions-pack.test.ts b/test/transitions-pack.test.ts index 885a89a..b505fd4 100644 --- a/test/transitions-pack.test.ts +++ b/test/transitions-pack.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "./suite.ts"; -import { close, open, pack } from "../mod.ts"; +import { close, open, pack, snapshot } from "../mod.ts"; describe("pack transition", () => { it("encodes a transition without throwing", () => { @@ -39,4 +39,13 @@ describe("pack transition", () => { // The transition block is exactly 8 bytes = 2 words. expect(withLen - withoutLen).toBe(2); }); + + it("includes transition bytes when sizing snapshots", () => { + expect(() => + snapshot([ + open("a", { transition: { duration: 0.2, properties: ["x"] } }), + close(), + ]) + ).not.toThrow(); + }); }); diff --git a/test/validate.test.ts b/test/validate.test.ts index 8db4af9..5eba9bb 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -19,6 +19,20 @@ describe("validate", () => { expect(validate([])).toBe(true); }); + it("accepts transition ops", () => { + expect(validate([ + open("x", { + transition: { + duration: 0.2, + easing: "easeOut", + properties: ["x", "bg"], + interactive: true, + }, + }), + close(), + ])).toBe(true); + }); + it("rejects ops with wrong directive", () => { expect(validate([{ directive: 0xff }])).toBe(false); }); @@ -31,6 +45,16 @@ describe("validate", () => { expect(validate([{ directive: 0x03 }])).toBe(false); }); + it("rejects invalid transition properties", () => { + expect(validate([ + open("x", { + // deno-lint-ignore no-explicit-any + transition: { duration: 0.2, properties: ["opacity" as any] }, + }), + close(), + ])).toBe(false); + }); + it("rejects non-array", () => { expect(validate("garbage")).toBe(false); }); diff --git a/validate.ts b/validate.ts index 248ea48..6a248b8 100644 --- a/validate.ts +++ b/validate.ts @@ -89,6 +89,34 @@ const Floating = Type.Object({ zIndex: Type.Optional(u16), }); +const TransitionProperty = Type.Union([ + Type.Literal("x"), + Type.Literal("y"), + Type.Literal("position"), + Type.Literal("width"), + Type.Literal("height"), + Type.Literal("size"), + Type.Literal("bg"), + Type.Literal("overlay"), + Type.Literal("borderColor"), + Type.Literal("borderWidth"), + Type.Literal("all"), +]); + +const Easing = Type.Union([ + Type.Literal("linear"), + Type.Literal("easeIn"), + Type.Literal("easeOut"), + Type.Literal("easeInOut"), +]); + +const Transition = Type.Object({ + duration: Type.Number(), + easing: Type.Optional(Easing), + properties: Type.Array(TransitionProperty), + interactive: Type.Optional(Type.Boolean()), +}); + /* ── Op types (discriminated on `directive`) ──────────────────────── */ const CloseElement = Type.Object({ directive: Type.Literal(0x04) }); @@ -102,6 +130,7 @@ const OpenElement = Type.Object({ border: Type.Optional(Border), clip: Type.Optional(Clip), floating: Type.Optional(Floating), + transition: Type.Optional(Transition), }); const TextOp = Type.Object({ From 7eab1e46b87fce704411ed0a0534f5c3d2c4a3b3 Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 31 May 2026 09:34:08 -0400 Subject: [PATCH 35/35] =?UTF-8?q?=E2=9C=85=20enforce=20nonnegative=20trans?= =?UTF-8?q?ition=20duration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/validate.test.ts | 7 +++++++ validate.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/validate.test.ts b/test/validate.test.ts index 5eba9bb..25a8e0e 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -55,6 +55,13 @@ describe("validate", () => { ])).toBe(false); }); + it("rejects negative transition duration", () => { + expect(validate([ + open("x", { transition: { duration: -1, properties: ["x"] } }), + close(), + ])).toBe(false); + }); + it("rejects non-array", () => { expect(validate("garbage")).toBe(false); }); diff --git a/validate.ts b/validate.ts index 6a248b8..a18e656 100644 --- a/validate.ts +++ b/validate.ts @@ -111,7 +111,7 @@ const Easing = Type.Union([ ]); const Transition = Type.Object({ - duration: Type.Number(), + duration: Type.Number({ minimum: 0 }), easing: Type.Optional(Easing), properties: Type.Array(TransitionProperty), interactive: Type.Optional(Type.Boolean()),