Reveal Styles — (reveal :style …) Grammar
Authoring contract: Cart authors. Engine implementers should pair this doc with ADR-0033 for decision narrative and ADR-0005 for the FFI signature row.
1. Purpose
Section titled “1. Purpose”(reveal …) is the unified KN-86 primitive for animated presentation of a text or ASCII surface — characters appear over time according to a named style, rather than landing in one frame. It is the single doctrine surface for:
- The boot animation handoff into the first interactive frame (
:diagonal). - CIPHER fragment arrival on the CIPHER-LINE OLED (
:char-flickeror:radial). - Mission contract-accept reveals on the primary display (
:char-flicker). - The
DECRYPT/INTERROGATE/ANALYZEverb response style (:char-flicker). - Carousel tab inbound animation on the Bare Deck Terminal (
:diagonal). - Any cart-driven appearance flourish on a static text or ASCII surface.
One verb, multiple styles. No cart authors a per-cartridge variant; every reveal in the system dispatches through this primitive.
Out of scope.
(reveal …)animates the appearance of an already-composed surface. It is not a video player; it does not synthesize source content. The companion:hover(brighten / scatter) and:click(ripple / spread) interaction grammars fromdocs/influences/effect/ascii-effects.md§“Interaction grammar” remain in inspiration / future-spec status — they are not part of this primitive and not in scope for v0.1.
2. Signature
Section titled “2. Signature”(reveal surface :style style-symbol [:duration seconds] [:rate chars-per-second] [:trail trail-length] [:from origin]) → reveal-handlesurface— the cell that owns the text region being revealed. Cart-Lisp’s existing cell handle (the same kind passed totext-puts/draw-ascii/ cell event dispatch). The reveal binds to the cell’s rectangle as of call time; subsequent edits to the cell while a reveal is in flight are out of spec (see §6 Open Questions).:style— required. One of the symbols in §3.:duration— total seconds for the reveal to complete. Style-specific defaults in §3; ranges in §4.:rate— characters per second of per-cell flicker. Used by:char-flickeronly; ignored by other styles.:trail— number of intermediate random characters each cell cycles through before settling. Used by:char-flickeronly.:from— origin offset for:radialreveals; ignored otherwise. Defaults to surface center. Format:(x . y)cons cell in cell coordinates relative to the surface origin.
Return value: an opaque reveal handle (Lisp record). The handle is the cart’s grip on the in-flight animation.
Companion primitives
Section titled “Companion primitives”(reveal-cancel handle) → nil(reveal-complete? handle) → bool(unreveal surface :style style-symbol [:duration seconds]) → reveal-handlereveal-cancel— interrupt the animation; snap the surface to its final state. Idempotent; calling on a completed handle is a no-op.reveal-complete?— non-blocking poll. Returns#tonce the animation has finished or been cancelled.unreveal— the inverse, for removing a surface with animation (e.g., CRT-shrink power-off usesunreveal :radial). Same style enum minus:char-flicker(which doesn’t compose as an inverse). Returns a handle with the same lifecycle.
3. Style enum
Section titled “3. Style enum”v0.1 ships three styles. The remaining candidates are deferred (see ADR-0033 §3 Style enum).
| Style | Default duration | Default lifecycle behaviour | Use case |
|---|---|---|---|
:char-flicker | 1.0 s | Asynchronous — handle returned immediately; cart polls or ignores | no-more-secrets / Sneakers decryption reveal. Each cell flickers through :trail random characters at :rate chars/sec, then settles to its target glyph. The project-default reveal for CIPHER fragments and DECRYPT-class verb responses. |
:no-more-secrets | 1.0 s | Asynchronous | Alias for :char-flicker — kept as a named symbol so cart sources can self-document intent (“this is a Sneakers-style decryption reveal” vs “this is a generic per-cell flicker”). Identical runtime behaviour. |
:radial | 0.4 s | Asynchronous | Reveal expands outward from :from (defaults to surface center). Reads as birth — cart-load splash, CIPHER-LINE fragment arrival on the OLED, mission contract-accept on the primary display. |
Deferred styles (post-v0.1)
Section titled “Deferred styles (post-v0.1)”The following styles are named here so that cart sources can refer to them by symbol once landed, and so that the implementation knows the enum’s intended growth path. Calling (reveal …) with a deferred style raises :reveal-style-not-implemented in v0.1.
| Style | Defer rationale |
|---|---|
:random | Cell-by-cell random-order reveal (each cell jumps in at a random time). Visually adjacent to :char-flicker for small surfaces and to :radial for large surfaces; the marginal aesthetic value over the two shipping styles is unclear. Reconsider after operator-eye review on hardware bring-up. |
:diagonal | Top-left → bottom-right wipe. Has a strong identity (used by the boot animation handoff and carousel tab transitions per docs/influences/effect/ascii-effects.md). Deferred only because the boot animation and carousel surfaces are themselves not yet wired through (reveal …); lifting them is its own engineering task. Re-promote once the carousel idle state lands (synthesis §3 item 9). |
:fft | Live-FFT-shaped reveal — cells settle in order of frequency-bucket arrival. Composes with AetherTune’s audio visualizer. Deferred because it requires the PSG bridge to expose a per-frame frequency-bucket stream that the reveal scheduler can subscribe to; that stream does not yet exist. |
A v0.1 cartridge that wants :random today should approximate with :char-flicker (the visual is similar — flicker-to-settle, just per-cell rather than column-by-column).
4. Parameter ranges and defaults
Section titled “4. Parameter ranges and defaults”| Param | Default | Range | Validation | Notes |
|---|---|---|---|---|
:duration | per-style (see §3) | 0.1–4.0 s | clamp; out-of-range silently clamps and logs to stderr per ADR-0005 silent-clamp convention | Total animation time. |
:rate | 30 chars/sec | 5–120 chars/sec | clamp | :char-flicker only. Per-cell flicker rate. |
:trail | 8 | 1–32 | clamp | :char-flicker only. How many random characters each cell visits before settling. |
:from | surface center | within surface bounds | out-of-bounds clamps to nearest edge | :radial only. |
:rate and :trail together set the no-more-secrets per-cell visual length: a cell visits :trail random characters at :rate chars/sec → trail / rate seconds per cell. The scheduler stages cell start times across the full :duration to land all cells on or before the deadline; cells whose trail / rate budget exceeds the remaining time get a shortened trail (silent — no error raised).
5. Lifecycle — asynchronous, cart-interruptible
Section titled “5. Lifecycle — asynchronous, cart-interruptible”(reveal …) is asynchronous. The call returns a handle as soon as the animation is enqueued; the cart continues running. The runtime ticks the animation off the existing redraw loop (per ADR-0005 §“Dispatch Contract” — 20 fps animation cap, event-driven idle) and writes glyphs into the surface’s cell rectangle as the animation progresses.
Cart authors get three knobs:
- Fire and forget. Discard the handle. Most reveals (CIPHER fragment arrival, decoration flourishes) want this.
- Wait for completion. Poll
(reveal-complete? handle)from the cart’s tick handler. Common for “advance phase only after the contract-accept reveal lands.” - Cancel mid-flight. Call
(reveal-cancel handle)to snap the surface to its final state. Useful when operator input pre-empts the reveal (operator hits[NIL]to abort; the cart cancels in-flight reveals before unwinding).
Why asynchronous?
Section titled “Why asynchronous?”A synchronous (reveal …) that blocks the cart until the animation completes would conflict with the 5 ms / 10 ms cell-handler latency contract from ADR-0005 §“Dispatch Contract.” A :char-flicker reveal at :duration 1.0 would block the handler for a full second — orders of magnitude over the contract.
Asynchronous + handle-returned + cart-polls is the only model that respects the handler-latency contract without forcing the cart to author an animation loop in Lisp by hand. It also lets the cart interleave a reveal with continued input handling (operator can press SYS or NIL while the reveal is mid-flight).
Operator-pre-empt semantics
Section titled “Operator-pre-empt semantics”If the operator presses any key while a (reveal …) is in flight, the runtime automatically calls (reveal-cancel …) on every active handle for the current cart and snaps all in-flight surfaces to their final state before dispatching the key event. This matches the no-more-secrets nms CLI behaviour (any keypress immediately resolves the decryption) and is the right operator UX — nobody wants to wait for a cosmetic animation when they’re trying to act.
Carts that want to ignore operator pre-empt (a rare case — perhaps a forced cutscene reveal) can re-issue the (reveal …) from their key handler. There is no opt-out flag in v0.1.
6. Performance budget — Pi Zero 2 W
Section titled “6. Performance budget — Pi Zero 2 W”The on-device implementation reference is docs/influences/effect/libcaca.md — CPU-only, no GPU dependency, well-suited to the Pi Zero 2 W. The no-more-secrets reference implementation (docs/influences/research/no-more-secrets.md) is ~50 lines of C for the :char-flicker case; KN-86 reimplements from first principles (GPL incompatibility per that doc’s License note).
Frame ceilings (estimated; bring-up benchmark required)
Section titled “Frame ceilings (estimated; bring-up benchmark required)”| Style | Surface size | Estimated steady-state fps | Notes |
|---|---|---|---|
:char-flicker | up to 80×23 (full content area) | ≥ 30 fps | Per-cell state is a small struct (target glyph, current-flicker glyph, remaining-trail counter, settle-deadline tick). Tick cost is O(cells). At 80×23 = 1840 cells × ~10 cycles/sec × trivial work per cycle, comfortably inside the 20 fps redraw cap. |
:char-flicker | OLED 4×32 (CIPHER-LINE) | ≥ 60 fps | 128 cells; trivially below the budget. |
:radial | up to 80×23 | ≥ 30 fps | Per-cell state is one tick (already-revealed bool). Per-tick work is computing the radius threshold and walking the cell rectangle once. O(cells). |
:radial | OLED 4×32 | ≥ 60 fps |
Frame-rate ceilings are estimates derived from the libcaca and no-more-secrets reference algorithms scaled to the KN-86 cell count. They are not measured. A bring-up benchmark on the Pi Zero 2 W (planned: bench test against a stub cart that fires (reveal …) on every key event, measure wall-clock duration of the full reveal at each style and surface size) is the right validation. If real device performance falls below the 20 fps redraw cap, the implementation moves to a coarser tick rate per style (e.g., halve the per-cell flicker rate; widen the radial step).
Transition-only vs in-play allowed
Section titled “Transition-only vs in-play allowed”Both shipping styles (:char-flicker, :radial) are transition-only in v0.1 — they animate the appearance of a static surface and then complete. None of them are continuous animations that hold the runtime in animation mode indefinitely.
This means:
- Concurrent reveals are allowed, up to ~4 active handles per cart. The runtime budgets the scheduler against the 20 fps cap; if too many reveals are in flight simultaneously, the ticker degrades gracefully (some cells settle a tick late).
- The redraw loop returns to event-driven idle once all reveals complete. Battery-friendly; no animation-mode tax once the surface is fully revealed.
- In-play continuous styles (e.g., the deferred
:fftstyle which would tick continuously with audio) are out of scope until the deferred styles land; their performance characterization is separate work.
Implementation reference
Section titled “Implementation reference”- Per-cell flicker (
:char-flicker):docs/influences/research/no-more-secrets.md— algorithm shape (cell-by-cell iterate, swap each cell through N random characters, settle to target). KN-86 reimplements; GPL forbids vendoring. - CPU-only rendering pipeline:
docs/influences/effect/libcaca.md— engine reference. KN-86 ships its own implementation against the same algorithmic shape. - Interaction-grammar prop matrix:
docs/influences/effect/react-video-ascii.md— the WebGL2 prior art whose props the KN-86 style enum distills. KN-86 cannot follow the WebGL2 implementation path on the Pi Zero 2 W; the algorithmic shape transfers, the rendering target does not.
7. Caller tiers
Section titled “7. Caller tiers”(reveal …), (reveal-cancel …), (reveal-complete? …), and (unreveal …) are Tier 1 (All-Carts) primitives per ADR-0005. Available always; no mission-context restriction. Available in REPL via Tier 3 — but Tier 3 is read-only, so the REPL exposure is gated to a no-op stub that returns nil and logs to stderr (consistent with the broader Tier 3 forbidden-mutation rule). The full reveal surface is for cart code only.
8. Error symbols
Section titled “8. Error symbols”| Symbol | Raised when |
|---|---|
:reveal-style-not-implemented | A deferred style (:random, :diagonal, :fft) is passed in v0.1. |
:reveal-unknown-style | The :style symbol is not in the enum at all. |
:invalid-surface | The surface handle is nil or invalid. |
:reveal-too-many-active | More than 4 reveal handles are active for the current cart. The cart should (reveal-cancel …) an outstanding handle before issuing another. |
Out-of-range :duration, :rate, :trail, :from values are silently clamped per ADR-0005 silent-clamp convention; they do not raise. Stderr logging at the bridge layer per the Tier 1 graphics + sound precedent (ADR-0005 Amendment Log 2026-04-25).
9. Composition with existing surfaces
Section titled “9. Composition with existing surfaces”| Surface | Reveal style | Source |
|---|---|---|
| CIPHER fragment arrival on CIPHER-LINE OLED | :char-flicker (default) or :radial (alternative) | per-fragment cart choice; CIPHER voice style guide may settle a project-wide default in a follow-on pass |
| Mission contract-accept on primary display | :char-flicker | ADR-0028 / ADR-0029 mission-acceptance flow |
DECRYPT / INTERROGATE / ANALYZE verb response | :char-flicker | cart-author convention |
| Cart-load splash | :radial | cart-author convention; CIPHERgarden / Black Ledger title screens are the canonical examples |
| CRT power-off shrink | (unreveal :style :radial …) | AetherTune lineage per docs/influences/inspiration/aethertune.md; runtime-internal, not cart-callable for v0.1 |
| Boot animation handoff to first interactive frame | :diagonal (deferred — wires once :diagonal lands) | BOOTSTRA.386 / AetherTune lineage |
| Carousel tab inbound on Bare Deck Terminal | :diagonal (deferred — wires once :diagonal lands) | blessed-contrib lineage; synthesis §3 item 9 |
10. Open questions
Section titled “10. Open questions”- Mid-reveal surface edits. If a cart calls
text-putson cells inside a surface that is being revealed, the spec is currently undefined. Likely resolution: the reveal scheduler reads the surface’s cell contents at each tick, so a mid-reveal edit will show up on the next cycle (the new glyph becomes the new target for the flicker). Confirm during implementation. - Per-style aesthetic tuning on hardware.
:duration,:rate,:traildefaults are derived from the no-more-secrets and react-video-ascii reference projects, not measured on amber-on-black at Press Start 2P 12×24. An operator-eye pass at hardware bring-up may move the defaults. Update this doc when that happens. - Operator-pre-empt scope. Currently the runtime cancels all active reveals on any keypress (§5). A cart that wants per-key granularity (cancel-on-NIL-only, ignore other keys) has no opt-in. Park for a v2 ADR if the use case shows up.
:fftstyle stream wiring. The deferred:fftstyle needs a per-frame frequency-bucket stream from the PSG. Coprocessor-bridge work pending; not on the v0.1 critical path.
11. References
Section titled “11. References”- ADR-0033 — the decision narrative for promoting this spec to canonical and adding
(reveal …)to NoshAPI. - ADR-0005 — NoshAPI FFI surface enumeration; the Tier 1 catalog hosts the row for this primitive.
docs/influences/effect/ascii-effects.md— the inspiration-tier predecessor of this spec. Retained for the broader:hover/:click/(draw-ascii …)grammar that is out of scope for(reveal …)v0.1.docs/influences/effect/libcaca.md— engine reference for the on-device implementation.docs/influences/effect/react-video-ascii.md— prop-matrix predecessor.docs/influences/research/no-more-secrets.md—:char-flickeralgorithm reference (GPL — reimplement from first principles).docs/influences/synthesis.md§3 item 3 + §6 item 7 + §2 L6 — the cross-domain convergence that drove this promotion.