# Cellophane
A terminal animation framework for Rust. Implement one trait, get a complete
rendering pipeline with frame diffing, resize handling, and input forwarding.

## Features
- **One trait** — implement `Animation` and you're done
- **Frame diffing** — only changed cells are redrawn each frame
- **ANSI parsing** — `FrameBuilder` parses ansi-styled text (including 24-bit color) into a cell grid via VTE
- **Resize handling** — terminal resize events are detected and forwarded automatically
- **Input forwarding** — key and mouse events are passed to your animation for interactive use
- **Terminal lifecycle** — alternate screen, raw mode, cursor visibility, and cleanup on drop
- **Unicode support** — `Grapheme` type handles multi-codepoint characters with stack allocation via `SmallVec`
## Quick start
Add cellophane to your project:
```sh
cargo add cellophane
```
Implement the `Animation` trait and let `Animator` handle the rest:
```rust
use std::time::Duration;
use cellophane::{Animation, Animator, Frame, Cell};
use cellophane::crossterm::style::Color;
struct Rainbow {
tick: usize,
rows: usize,
cols: usize,
}
impl Animation for Rainbow {
fn init(&mut self, initial: Frame) {
let (rows, cols) = initial.dims().unwrap_or((0, 0));
self.rows = rows;
self.cols = cols;
}
fn update(&mut self, _dt: Duration) -> Frame {
let mut frame = Frame::with_capacity(self.cols, self.rows);
for row in 0..self.rows {
for col in 0..self.cols {
let hue = ((col + row + self.tick) % 256) as u8;
if let Some(cell) = frame.get_cell_mut(row, col) {
*cell = Cell::default()
.with_bg(Color::Rgb { r: hue, g: 100, b: 255 - hue });
}
}
}
self.tick += 1;
frame
}
fn is_done(&self) -> bool { false }
fn resize(&mut self, w: usize, h: usize) {
self.cols = w;
self.rows = h;
}
}
fn main() -> std::io::Result<()> {
let anim = Box::new(Rainbow { tick: 0, rows: 0, cols: 0 });
let mut animator = Animator::enter_with(anim)?;
loop {
match animator.tick() {
Ok(true) => continue,
Ok(false) => break,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => break,
Err(e) => return Err(e),
}
}
Ok(())
}
```
## Core types
| `Animation` | The trait you implement to define an animation |
| `Animator` | Drives your animation with a frame-rate-limited render loop |
| `Frame` | A grid of styled `Cell`s representing one frame of output |
| `FrameBuilder` | Parses ANSI-escaped text into a `Frame` |
| `Cell` | A single terminal cell with character, colors, and attributes |
| `CellFlags` | Bitflags for text attributes (bold, italic, underline, etc.) |
| `Grapheme` | A unicode grapheme cluster backed by `SmallVec<[char; 4]>` |
## How it works
The `Animator` manages the full terminal lifecycle:
1. Enters the alternate screen, enables raw mode, hides the cursor
2. Each `tick()` polls for events, calls your `update()`, and renders the frame
3. Only cells that differ from the previous frame are written to the terminal
4. Resize events call your `resize()`, input events call your `on_event()`
5. Ctrl+C returns `Err(ErrorKind::Interrupted)` for clean shutdown
6. Terminal state is restored automatically on drop
## Built with cellophane
- [whoa](https://github.com/km-clay/whoa) — a terminal screensaver featuring EarthBound battle backgrounds, procedural simulations, cellular automata, and more