Skip to content

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.

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:

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.


  • 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

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"]