termgrid-core 1.5.5

Deterministic terminal grid state engine with invariant enforcement and backend decoupling.
Documentation
# Animation Cookbook

This cookbook focuses on building low-flicker, high-frequency terminal animations using `termgrid-core` plus an ANSI backend (e.g., `ansi-host`). The target workload is “BBS door game” style ASCII/ANSI animation where frame cadence matters and full-screen redraws cause flicker.

termgrid-core provides two key primitives for animation backends:

- A deterministic `Renderer` that mutates an in-memory grid.
- Conservative damage reporting via `Renderer::apply_with_damage()`.

The backend’s job is to efficiently translate (grid, damage) into escape sequences with minimal terminal I/O.

---

## 1. Baseline Animation Loop

A typical game loop produces a `Frame` each tick.

```rust
use termgrid_core::{Renderer, GlyphRegistry};

fn tick(renderer: &mut Renderer, reg: &GlyphRegistry, backend: &mut impl Backend, frame: &Frame) {
    // Apply frame and compute damage
    let dmg = renderer.apply_with_damage(frame);

    // Repaint only what changed
    if dmg.full_redraw {
        backend.repaint_full(renderer.grid(), reg);
    } else {
        for r in &dmg.rects {
            backend.repaint_rect(renderer.grid(), reg, *r);
        }
    }

    backend.flush();
}
```

**Contract expectation:** repainting the returned damage regions restores visual correctness (damage is conservative).

---

## 2. Double-Buffering Strategies

“Double buffering” in a terminal context usually means one of these patterns:

### 2.1 In-memory double buffer (recommended)
Maintain two grids: `front` (what you believe is on the terminal) and `back` (what you want next). Diff between them to emit minimal updates.

With termgrid-core, you can avoid a full diff by using damage rectangles as a guided diff window:

- Keep a `Renderer` for `back` (authoritative desired state).
- Keep a second lightweight snapshot of `front` (previous presented state).
- On each tick, apply frame to `back`, get damage, then compare only within damage rects to update `front` and emit minimal output.

This reduces I/O while preventing correctness drift when the backend performs optimizations.

### 2.2 Terminal alternate screen buffer
If your backend supports it (ansi-host should), you can enter the alternate screen (e.g., `CSI ?1049h`) to avoid disturbing the user’s normal scrollback.

This is not a replacement for in-memory buffering; it’s an environment isolation technique.

### 2.3 “Full redraw with hidden cursor” fallback
When `Damage.full_redraw` is true, you will repaint everything. To reduce perceived flicker:

- Hide cursor before redraw.
- Prefer writing row-runs rather than cell-by-cell.
- Show cursor after redraw if needed.

---

## 3. Frame Pacing

### 3.1 Fixed timestep
Run simulation at a fixed tick rate (e.g., 30 or 60 Hz) and render each tick.

- Pros: deterministic replay, stable physics.
- Cons: can overload slow terminals/backends.

### 3.2 Variable timestep with cap
Advance simulation by measured delta, but clamp large deltas (e.g., after a pause) to avoid “teleporting.”

### 3.3 Adaptive rendering
If a terminal link is slow, you can reduce render frequency while keeping simulation stable:

- Simulate at 60 Hz.
- Render at 30 Hz when output cost is high.
- Still process input at full rate.

Backends can measure output cost by tracking bytes written per frame.

---

## 4. ANSI-host Optimization Patterns

These patterns assume an ANSI escape emitting backend.

### 4.1 Batch by rows within a rect
For each dirty rect, iterate row-by-row and emit contiguous spans of “interesting” cells as a single cursor move + write, rather than per-cell writes.

Heuristic:
- Skip runs of empty/plain spaces when it’s cheaper than moving cursor.
- Coalesce adjacent styled spans when the style is identical.

### 4.2 Minimize SGR churn
Maintain a “current style” while emitting a row-run:

- Only emit SGR sequences when style changes.
- At the end of a run, do not emit resets unless required by your backend policy.

### 4.3 Treat continuation cells as non-writable
Continuation cells are not independent glyphs. When emitting a row, either:
- skip them entirely, or
- emit only when needed to overwrite (rare; renderer should already handle clearing a wide glyph when overwriting a continuation region).

### 4.4 Avoid full clears unless necessary
A full-screen clear (e.g., `CSI 2J`) often causes flicker and scrollback artifacts.

Prefer:
- targeted clears (rect-based) by emitting spaces, or
- targeted line clears for regions that are known to be blanked.

### 4.5 Rect merge policy alignment
termgrid-core merges damage rects and caps at 64. Align backend behavior:
- If many small updates occur, you will receive merged rects; do not re-split unless you have a measured reason.
- If `full_redraw`, commit to repainting all rows with efficient row-runs.

---

## 5. Edge Cases that Affect Animation

### 5.1 Wide glyph transitions
When animating sprites that contain wide glyphs:
- Movement by 1 cell can leave trailing continuation artifacts if a backend naively overwrites only the lead cell.
- termgrid-core prevents this in the grid model; the backend must respect the grid representation when emitting output.

### 5.2 Combining marks and ZWJ sequences
Do not attempt to “optimize” by splitting Unicode sequences:
- combining sequences and ZWJ clusters must be treated as atomic graphemes.
- termgrid-core’s width handling assumes grapheme-aware processing.

### 5.3 Bidi text
Does not reorder bidi. If your UI includes RTL languages:
- preprocess text (layout/shaping) before writing spans.
- treat the result as visual-order text for the grid.

---

## 6. Testing Animation Output

### 6.1 Deterministic playback tests
Record (frame inputs, RNG seed, expected final grid) and assert:
- final grid matches
- damage never under-reports: repainting only damage yields correct terminal state

### 6.2 Performance budget tests (optional)
In a backend test harness, measure:
- bytes emitted per frame
- max cursor moves per frame

Use fixture-based regressions to prevent accidental output explosions.

---

## 7. Recommended Defaults

- Use `Renderer::apply_with_damage()` for every frame.
- Backend should repaint rects row-by-row with run coalescing.
- Escalate to full redraw when `Damage.full_redraw` is true.
- Use alternate screen for interactive TUIs where appropriate.
- Hide cursor during redraw; show cursor on exit.

---

## Mermaid Overview

```mermaid
flowchart TD
    A["Simulation tick"] --> B["Build Frame"]
    B --> C["Renderer.apply_with_damage"]
    C --> D{"Damage.full_redraw?"}
    D -- "Yes" --> E["Backend repaint_full"]
    D -- "No" --> F["Backend repaint_rect (row runs)"]
    E --> G["flush"]
    F --> G["flush"]
```