Terminal animation

The definitive guide to creating motion graphics using ASCII characters in command-line interfaces — from fundamental concepts to advanced optimization techniques.

~/demo.sh

                    

Introduction

Terminal animation represents a unique intersection of programming, design, and creative constraint. Since the 1960s, developers have been pushing the boundaries of what's possible using nothing but text characters, creating everything from simple loading spinners to full-featured games and complex data visualizations.

What is terminal animation?

Terminal animation is the art and technique of creating motion graphics within text-based interfaces. Unlike traditional animation that relies on pixels and raster graphics, terminal animation works exclusively with characters from various encoding schemes (ASCII, Unicode, etc.) and control sequences that manipulate cursor position and display attributes.

Why terminal animation?

Despite the ubiquity of graphical interfaces, terminal animation remains relevant for several compelling reasons:

  • Universal compatibility: Works on any system with a terminal emulator, from embedded systems to supercomputers
  • Minimal resource usage: Text rendering is orders of magnitude lighter than graphics rendering
  • SSH-friendly: Animations work seamlessly over remote connections with limited bandwidth
  • Accessibility: Screen readers can interpret text-based interfaces more effectively
  • Aesthetic appeal: The retro-computing aesthetic has enduring cultural cachet
  • Creative constraints: Limitations often breed innovation and unique artistic expression

Brief history

Terminal animation evolved alongside computing itself. Early teleprinters in the 1960s could only print characters sequentially, but programmers quickly discovered they could create the illusion of motion by printing, then overwriting characters. The introduction of the DEC VT100 in 1978, with its ANSI escape sequence support, revolutionized the field by enabling precise cursor control.

The BBS era of the 1980s-90s saw an explosion of ASCII art and animation, with dedicated tools and thriving communities. Today, we're experiencing a CLI renaissance, with modern terminal emulators supporting millions of colors, ligatures, and sophisticated rendering — while maintaining backward compatibility with decades-old techniques.

Fundamentals

Core concepts every terminal animator should understand.

How terminals work

Modern terminal emulators are software programs that simulate physical hardware terminals. They maintain an internal grid of character cells (typically 80×24 or larger) and render each cell using a monospace font. Understanding this grid-based model is fundamental to terminal animation.

// A terminal is essentially a 2D grid // Each cell contains: // - A character (ASCII or Unicode) // - Foreground color // - Background color // - Text attributes (bold, italic, underline, etc.) // Example: 80 columns × 24 rows = 1,920 cells const COLS = 80 const ROWS = 24 const grid = Array(ROWS).fill(null).map(() => Array(COLS).fill({ char: ' ', fg: 7, bg: 0 }) )

ANSI escape sequences

ANSI escape sequences are special character combinations that control terminal behavior. They start with ESC (ASCII 27 or \x1b) followed by specific commands. These sequences are the foundation of terminal control.

// Cursor positioning '\x1b[{row};{col}H' // Move cursor to row, column '\x1b[{n}A' // Move cursor up n lines '\x1b[{n}B' // Move cursor down n lines '\x1b[{n}C' // Move cursor right n columns '\x1b[{n}D' // Move cursor left n columns // Screen manipulation '\x1b[2J' // Clear entire screen '\x1b[K' // Clear from cursor to end of line '\x1b[H' // Move cursor to home (0,0) // Text attributes '\x1b[0m' // Reset all attributes '\x1b[1m' // Bold '\x1b[4m' // Underline '\x1b[7m' // Reverse video // Colors (basic 16-color) '\x1b[30-37m' // Foreground colors '\x1b[40-47m' // Background colors '\x1b[90-97m' // Bright foreground colors

Frame rate and timing

Terminal animation isn't constrained by display refresh rates like traditional animation. Frame rate is entirely under your control, typically ranging from 10-60 FPS depending on complexity and desired smoothness.

// Calculate frame delay for target FPS const FPS = 30 const FRAME_TIME = 1000 / FPS // ~33ms per frame // Animation loop setInterval(() => { updateAnimation() render() }, FRAME_TIME) // For more precise timing, use performance.now() let lastFrame = performance.now() function gameLoop(currentTime) { const delta = currentTime - lastFrame if (delta >= FRAME_TIME) { update(delta) render() lastFrame = currentTime } requestAnimationFrame(gameLoop) }

Rendering strategies

There are three primary approaches to rendering terminal animation, each with distinct tradeoffs:

Full redraw

Clear the screen and redraw everything each frame. Simple but can cause flicker.

Pros: Simple, no state tracking needed
Cons: Flickering, high terminal overhead

Differential updates

Track changes and only update modified cells. Smooth but requires state management.

Pros: No flicker, efficient
Cons: Complex state tracking

Double buffering

Maintain two buffers, swap between them. Professional-grade smoothness.

Pros: Perfectly smooth, atomic updates
Cons: 2× memory usage

Core techniques

Four fundamental approaches to creating terminal animation.

Frame-based animation

Pre-render multiple frames as strings and cycle through them. This is the simplest approach and works well for logos, loading indicators, and character animations. Memory-intensive but extremely predictable and smooth.

When to use

  • Loading spinners and progress indicators
  • Logo animations and splash screens
  • Character sprites in games
  • Animations with hand-crafted artistic content

Implementation

const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] let frameIndex = 0 setInterval(() => { process.stdout.write('\r' + frames[frameIndex]) frameIndex = (frameIndex + 1) % frames.length }, 80)

                            

Procedural generation

Calculate each frame mathematically using functions like sine waves, Perlin noise, or physics simulations. Enables complex effects like plasma, fire, water ripples, and particle systems with minimal memory overhead.

When to use

  • Algorithmic visual effects (plasma, fire, waves)
  • Particle systems and simulations
  • Data visualizations and graphs
  • Generative art and infinite loops

Implementation

let time = 0 const chars = ' .:-=+*#%@' function render() { let output = '' for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { // Sine wave function const value = Math.sin(x * 0.2 + time) * Math.sin(y * 0.2 + time * 0.8) const index = Math.floor((value + 1) * 0.5 * (chars.length - 1)) output += chars[index] } output += '\n' } console.clear() console.log(output) time += 0.1 } setInterval(render, 50)

                            

Buffer manipulation

Maintain a 2D array representing the screen, update specific cells, then render the entire buffer. This approach allows for complex multi-element scenes like games, dashboards, and interactive UIs where many independent objects move simultaneously.

When to use

  • Terminal games (Snake, Tetris, roguelikes)
  • Complex dashboards with multiple updating regions
  • Scenes with many independent moving objects
  • Applications requiring collision detection

Implementation

const buffer = Array(rows).fill(null) .map(() => Array(cols).fill(' ')) // Update game objects function update() { // Clear previous positions buffer[oldY][oldX] = ' ' // Update position x += vx y += vy // Draw at new position buffer[y][x] = '█' } // Render entire buffer function render() { const frame = buffer.map(row => row.join('')).join('\n') process.stdout.write('\x1b[H' + frame) }

                            

Character replacement

Use ANSI escape codes to reposition the cursor and overwrite specific characters without redrawing the entire screen. This is the most efficient approach for updating small portions of large displays, essential for dashboards and status monitors.

When to use

  • Status monitors with mostly static content
  • Progress bars and live counters
  • Large displays where only small regions change
  • Performance-critical applications

Implementation

// Move cursor to specific position function moveTo(row, col) { process.stdout.write(`\x1b[${row};${col}H`) } // Update single character function drawAt(row, col, char) { moveTo(row, col) process.stdout.write(char) } // Update a region efficiently function updateRegion(row, col, text) { moveTo(row, col) process.stdout.write(text) } // Example: Live counter let count = 0 setInterval(() => { drawAt(5, 10, count.toString()) count++ }, 100)

                            

Advanced techniques

Sophisticated methods for professional-grade terminal animation.

Double buffering

Double buffering eliminates flicker by preparing the next frame in a hidden buffer, then swapping it atomically with the visible buffer. This is essential for smooth animation in complex scenes.

class DoubleBuffer { constructor(rows, cols) { this.rows = rows this.cols = cols // Create two identical buffers this.buffers = [ this.createBuffer(), this.createBuffer() ] this.currentBuffer = 0 } createBuffer() { return Array(this.rows).fill(null) .map(() => Array(this.cols).fill(' ')) } get active() { return this.buffers[this.currentBuffer] } get hidden() { return this.buffers[1 - this.currentBuffer] } swap() { this.currentBuffer = 1 - this.currentBuffer } render() { const frame = this.active.map(row => row.join('')).join('\n') process.stdout.write('\x1b[H' + frame) } } // Usage const buffer = new DoubleBuffer(24, 80) function animate() { // Draw to hidden buffer updateScene(buffer.hidden) // Atomic swap and render buffer.swap() buffer.render() }

Color and styling

Modern terminals support 256 colors or even true color (16.7 million colors). Strategic use of color enhances readability and aesthetics.

// 256-color mode function color256(fg, bg = null) { let code = `\x1b[38;5;${fg}m` if (bg !== null) code += `\x1b[48;5;${bg}m` return code } // True color (RGB) function colorRGB(r, g, b, bg = false) { const type = bg ? 48 : 38 return `\x1b[${type};2;${r};${g};${b}m` } // Gradient example for (let i = 0; i < 100; i++) { const intensity = Math.floor(i * 2.55) process.stdout.write( colorRGB(intensity, 0, 255 - intensity) + '█' ) } process.stdout.write('\x1b[0m\n')

Sub-character resolution

Unicode block elements allow rendering at 2× vertical resolution. Braille patterns enable up to 4× horizontal and 2× vertical resolution, effectively creating pixel-perfect graphics.

// Half-block technique (2x vertical resolution) const halfBlocks = ['▀', '▄', '█', ' '] function drawPixel(x, y, value) { const charX = Math.floor(x) const charY = Math.floor(y / 2) const subY = y % 2 // Map two vertical pixels to one character const topPixel = getPixel(charX, charY * 2) const bottomPixel = getPixel(charX, charY * 2 + 1) if (topPixel && bottomPixel) return '█' if (topPixel) return '▀' if (bottomPixel) return '▄' return ' ' } // Braille patterns for even higher resolution const brailleBase = 0x2800 function brailleChar(dots) { // dots is 8-bit number representing which dots are filled return String.fromCharCode(brailleBase + dots) } // Example: plot a circle with sub-character precision for (let angle = 0; angle < Math.PI * 2; angle += 0.1) { const x = Math.cos(angle) * radius const y = Math.sin(angle) * radius setPixel(x, y) // Uses braille for precision }

Easing and interpolation

Smooth motion requires easing functions that control acceleration and deceleration. These mathematical functions transform linear time into natural-feeling movement.

// Common easing functions const easing = { linear: t => t, easeInQuad: t => t * t, easeOutQuad: t => t * (2 - t), easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, easeInCubic: t => t * t * t, easeOutCubic: t => (--t) * t * t + 1, easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 } // Interpolate between two values function lerp(start, end, t, easeFn = easing.linear) { return start + (end - start) * easeFn(t) } // Animate object position const duration = 1000 // milliseconds const startPos = 0 const endPos = 80 let startTime = Date.now() function animate() { const elapsed = Date.now() - startTime const t = Math.min(elapsed / duration, 1) const pos = lerp(startPos, endPos, t, easing.easeInOutQuad) drawAt(pos, '█') if (t < 1) requestAnimationFrame(animate) }

Sprite systems

For complex animations with many moving objects, a sprite system organizes rendering and updates efficiently.

class Sprite { constructor(x, y, frames) { this.x = x this.y = y this.frames = frames this.frameIndex = 0 this.vx = 0 this.vy = 0 } update(deltaTime) { this.x += this.vx * deltaTime this.y += this.vy * deltaTime this.frameIndex = (this.frameIndex + 1) % this.frames.length } render(buffer) { const frame = this.frames[this.frameIndex] const lines = frame.split('\n') for (let dy = 0; dy < lines.length; dy++) { for (let dx = 0; dx < lines[dy].length; dx++) { const bx = Math.floor(this.x + dx) const by = Math.floor(this.y + dy) if (bx >= 0 && bx < buffer[0].length && by >= 0 && by < buffer.length) { buffer[by][bx] = lines[dy][dx] } } } } } // Sprite manager class SpriteManager { constructor() { this.sprites = [] } add(sprite) { this.sprites.push(sprite) } update(deltaTime) { this.sprites.forEach(s => s.update(deltaTime)) } render(buffer) { this.sprites.forEach(s => s.render(buffer)) } }

Character sets

Understanding which characters to use for different visual effects.

Block elements

█ ▓ ▒ ░ ▄ ▀ ▌ ▐ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟

Used for solid shapes, gradients, progress bars, and pixel-style graphics. Unicode range U+2580–U+259F. Half-blocks (▀ ▄) enable 2× vertical resolution. Quarter-blocks enable 2×2 sub-character precision.

// Common block characters '█' // Full block (U+2588) '▓' // Dark shade (U+2593) '▒' // Medium shade (U+2592) '░' // Light shade (U+2591) '▀' // Upper half block (U+2580) '▄' // Lower half block (U+2584)

Box drawing

─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼ ═ ║ ╔ ╗ ╚ ╝

Essential for frames, borders, tables, diagrams, and UI elements. Unicode U+2500–U+257F. Comes in light, heavy, and double-line variants.

// Box drawing components '─' '━' // Horizontal lines (light/heavy) '│' '┃' // Vertical lines (light/heavy) '┌' '┐' // Top corners '└' '┘' // Bottom corners '├' '┤' // T-junctions '┼' // Cross junction // Double lines '═' '║' '╔' '╗' '╚' '╝'

Braille patterns

⠀ ⠁ ⠂ ⠃ ⠄ ⠅ ⠆ ⠇ ⠈ ⠉ ⠊ ⠋ ⠌ ⠍ ⠎ ⠏ ⣿

Each braille character represents a 2×4 grid of dots, enabling smooth curves and high-resolution graphics. Unicode U+2800–U+28FF. All 256 combinations available.

// Braille patterns for graphics const brailleBase = 0x2800 // Dot positions: // 1 4 // 2 5 // 3 6 // 7 8 // Create braille character from dot pattern function braille(dots) { let value = 0 for (let i = 0; i < 8; i++) { if (dots & (1 << i)) value |= (1 << i) } return String.fromCharCode(brailleBase + value) } // Set pixel in 4×2 grid function setPixel(x, y) { const dotIndex = (x % 2) * 4 + (y % 4) return 1 << dotIndex }

Geometric shapes

■ □ ▪ ▫ ● ○ ◆ ◇ ◈ ◉ ◊ ○ ◐ ◑ ◒ ◓

Icons, bullets, decorative elements, and status indicators. Various Unicode ranges including U+25A0–U+25FF (geometric shapes) and U+2600–U+26FF (misc symbols).

Line drawing

╱ ╲ ╳ ⁄ ∕ ∖ ∕ ⧵ ⧸ ⧹ ╱ ╲

Diagonal lines for graphs, charts, and geometric patterns. Limited options compared to horizontal/vertical lines.

Special symbols

░ ▒ ▓ █ ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇

Vertical bars for bar charts, audio visualizers, and progress indicators. Unicode includes 8 different heights for smooth vertical gradients.

// Vertical bar heights (eighths) const bars = ' ▁▂▃▄▅▆▇█' // Create bar chart function barChart(values) { return values.map(v => { const index = Math.floor(v * 8) return bars[index] }).join('') }

Common patterns

Practical examples you'll encounter in real terminal applications.

Progress indicator


                            

Loading spinner


                            

Live counter


                            

Text reveal


                            

Marquee scroll


                            

Blink effect

Best practices

Guidelines for creating professional, maintainable terminal animations.

Terminal detection and fallbacks

Not all terminals support the same features. Always detect capabilities and provide graceful degradation.

// Detect terminal capabilities const hasColors = process.stdout.isTTY && process.stdout.hasColors() const supports256 = process.env.TERM && process.env.TERM.includes('256') const supportsTrueColor = process.env.COLORTERM === 'truecolor' // Check terminal dimensions const { columns, rows } = process.stdout if (columns < 80) { // Use compact layout } // Graceful degradation function drawSpinner(frame) { if (hasColors) { return `\x1b[32m${spinnerFrames[frame]}\x1b[0m` } return spinnerFrames[frame] }

Signal handling and cleanup

Always restore terminal state when your program exits, even on interruption.

// Save initial terminal state const readline = require('readline') function setupTerminal() { // Hide cursor process.stdout.write('\x1b[?25l') // Enter alternate screen buffer process.stdout.write('\x1b[?1049h') // Disable line buffering readline.emitKeypressEvents(process.stdin) process.stdin.setRawMode(true) } function restoreTerminal() { // Show cursor process.stdout.write('\x1b[?25h') // Exit alternate screen buffer process.stdout.write('\x1b[?1049l') // Reset attributes process.stdout.write('\x1b[0m') } // Handle all exit scenarios process.on('exit', restoreTerminal) process.on('SIGINT', () => { restoreTerminal() process.exit(0) }) process.on('SIGTERM', () => { restoreTerminal() process.exit(0) }) setupTerminal()

Performance considerations

Terminal rendering can be surprisingly expensive. Follow these guidelines for optimal performance.

  • Minimize writes: Batch multiple updates into a single stdout write
  • Avoid unnecessary clears: Only redraw changed regions
  • Cache strings: Pre-compute static elements outside the animation loop
  • Profile carefully: Terminal I/O is often the bottleneck, not computation
  • Use alternate screen: Prevents scrollback pollution and enables full-screen apps
  • Limit frame rate: 30-60 FPS is usually sufficient; higher rates waste resources

Accessibility

Terminal animation should be accessible to all users, including those using screen readers or with motion sensitivity.

// Respect NO_COLOR environment variable const shouldUseColor = !process.env.NO_COLOR && process.stdout.isTTY // Provide --no-animation flag const program = require('commander') program.option('--no-animation', 'Disable animations') program.parse() if (program.opts().animation === false) { // Show static output instead console.log('Processing... Done!') } else { // Show animated progress animateProgress() } // Add descriptive labels process.stdout.write('Loading dependencies: ') showSpinner() // Announce state changes for screen readers process.stdout.write('\nInstallation complete\n')

Performance optimization

Techniques for maximizing smoothness and minimizing resource usage.

Measuring performance

Before optimizing, measure. Terminal animation performance has unique characteristics.

class PerformanceMonitor { constructor() { this.frameTimes = [] this.maxSamples = 60 } recordFrame(deltaTime) { this.frameTimes.push(deltaTime) if (this.frameTimes.length > this.maxSamples) { this.frameTimes.shift() } } getStats() { const avg = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length const fps = 1000 / avg const min = Math.min(...this.frameTimes) const max = Math.max(...this.frameTimes) return { avg, fps, min, max } } } // Usage const monitor = new PerformanceMonitor() let lastTime = performance.now() function gameLoop() { const now = performance.now() const delta = now - lastTime update(delta) render() monitor.recordFrame(delta) lastTime = now requestAnimationFrame(gameLoop) }

Dirty rectangle optimization

Only redraw regions of the screen that have changed. Essential for large displays.

class DirtyRectTracker { constructor(rows, cols) { this.rows = rows this.cols = cols this.reset() } reset() { this.minRow = Infinity this.maxRow = -Infinity this.minCol = Infinity this.maxCol = -Infinity } markDirty(row, col) { this.minRow = Math.min(this.minRow, row) this.maxRow = Math.max(this.maxRow, row) this.minCol = Math.min(this.minCol, col) this.maxCol = Math.max(this.maxCol, col) } getDirtyRect() { if (this.minRow === Infinity) return null return { row: this.minRow, col: this.minCol, width: this.maxCol - this.minCol + 1, height: this.maxRow - this.minRow + 1 } } } // Render only dirty region function renderOptimized(buffer, dirtyTracker) { const rect = dirtyTracker.getDirtyRect() if (!rect) return for (let r = rect.row; r < rect.row + rect.height; r++) { moveTo(r, rect.col) const line = buffer[r].slice(rect.col, rect.col + rect.width).join('') process.stdout.write(line) } dirtyTracker.reset() }

String building optimization

String concatenation in tight loops can be expensive. Use arrays or buffers for better performance.

// Slow: repeated string concatenation function renderSlow(buffer) { let output = '' for (let row of buffer) { for (let char of row) { output += char // Creates new string each time } output += '\n' } return output } // Fast: array join function renderFast(buffer) { return buffer.map(row => row.join('')).join('\n') } // Even faster: pre-allocate and reuse class StringBuffer { constructor(size) { this.buffer = new Array(size) this.index = 0 } append(str) { this.buffer[this.index++] = str } toString() { const result = this.buffer.slice(0, this.index).join('') this.index = 0 return result } }

Frame rate adaptation

Automatically adjust frame rate based on performance to maintain smoothness.

class AdaptiveFrameRate { constructor(targetFPS = 30, minFPS = 15) { this.targetFrameTime = 1000 / targetFPS this.minFrameTime = 1000 / minFPS this.currentFrameTime = this.targetFrameTime this.frameTimes = [] } update(actualFrameTime) { this.frameTimes.push(actualFrameTime) if (this.frameTimes.length > 10) { this.frameTimes.shift() } const avgTime = this.frameTimes.reduce((a, b) => a + b) / this.frameTimes.length // Adjust target based on actual performance if (avgTime > this.currentFrameTime * 1.2) { // Running slow, reduce frame rate this.currentFrameTime = Math.min( this.currentFrameTime * 1.1, this.minFrameTime ) } else if (avgTime < this.currentFrameTime * 0.8) { // Running fast, increase frame rate this.currentFrameTime = Math.max( this.currentFrameTime * 0.9, this.targetFrameTime ) } } shouldRender(elapsed) { return elapsed >= this.currentFrameTime } }

Tools & libraries

Ecosystem of libraries and tools for terminal animation.

JavaScript/Node.js

blessed

High-level terminal UI library with widgets, layouts, and event handling. Ideal for complex dashboards and TUIs.

const blessed = require('blessed') const screen = blessed.screen() const box = blessed.box({ top: 'center', left: 'center', width: '50%', height: '50%', content: 'Hello!', border: { type: 'line' } }) screen.append(box) screen.render()

chalk

Terminal string styling with color support. Simple API for adding color and style to text.

const chalk = require('chalk') console.log(chalk.blue('Hello') + ' World!') console.log(chalk.rgb(123, 45, 67).bold('Custom color')) console.log(chalk.bgGreen.black(' Success '))

ink

React-based framework for terminal UIs. Build complex interfaces using React components.

const { render, Text } = require('ink') const App = () => ( Hello from Ink! ) render()

cli-spinners

Collection of 80+ animated spinners for CLIs. Ready-to-use loading indicators.

ora

Elegant terminal spinner with promise support. Great for showing async operation progress.

Python

Rich

Modern Python library for rich text and beautiful formatting in the terminal. Includes tables, syntax highlighting, progress bars, and more.

from rich.console import Console from rich.table import Table console = Console() table = Table(title="Star Wars Movies") table.add_column("Released", style="cyan") table.add_column("Title", style="magenta") console.print(table)

asciimatics

Package to create full-screen text UIs and ASCII art animations. Comprehensive framework for complex terminal applications.

blessed

Terminal formatting and cursor positioning. Python equivalent of the Node.js library.

Rust

crossterm

Pure Rust library for terminal manipulation. Cross-platform, efficient, and type-safe.

use crossterm::{cursor, execute}; execute!( stdout(), cursor::MoveTo(5, 5), style::Print("Hello!") )?;

tui-rs

Terminal UI library with immediate-mode rendering. Build complex layouts with widgets.

Go

termbox-go

Minimalist library for creating text-based UIs. Low-level control with simple API.

bubbletea

Elm-inspired framework for terminal applications. Based on The Elm Architecture pattern.

Notable examples

Influential works that pushed the boundaries of terminal animation.

Star Wars ASCIImation

Simon Jansen • 1997 • telnet

The entire Star Wars Episode IV recreated in ASCII art, playable via telnet. Originally 16,000+ hand-crafted frames of animation synchronized with audio cues. A legendary achievement in ASCII animation that's still accessible today.

telnet towel.blinkenlights.nl

ASCII Quake

AAlib project • 1999 • C

Real-time 3D game rendering in ASCII using sophisticated dithering algorithms. Demonstrated that even complex graphics could be represented in text, converting Quake's 3D engine output to ASCII in real-time at playable frame rates.

sl (Steam Locomotive)

Toyoda Masashi • 1993 • C

A playful ASCII animation that runs when you mistype 'ls'. Became a beloved Unix easter egg demonstrating smooth multi-sprite animation with a steam locomotive crossing the terminal. A masterclass in frame-based animation.

brew install sl && sl

asciinema

Marcin Kulik • 2011 • Python

Record and share terminal sessions as lightweight, text-based "videos." Revolutionized technical documentation and demonstrations by creating perfectly reproducible terminal recordings.

asciinema rec demo.cast

cmatrix

Chris Allegretta • 1999 • C

The iconic "Matrix" digital rain effect for terminals. Showcases procedural generation and efficient character updates. A perfect example of atmospheric terminal animation.

brew install cmatrix && cmatrix

htop

Hisham Muhammad • 2004 • C

Interactive process viewer with real-time updates, color coding, and responsive UI. Demonstrates production-quality terminal UX with complex data visualization updating at high frequency.

brew install htop && htop

blessed-contrib

Yaron Naveh • 2014 • JavaScript

Dashboard framework with live charts, graphs, and widgets. Showcases advanced buffer manipulation and complex layouts for data visualization in terminals.

Dwarf Fortress

Tarn Adams • 2006 • C++

Complex simulation game rendered entirely in ASCII. Demonstrates that deep, engaging experiences don't require graphics — procedural generation creates entire worlds using text characters.

Resources

Curated learning materials and references for going deeper.

Documentation

  • ANSI Escape Sequences

    Complete reference for terminal control codes, cursor movement, and styling.

  • VT100 User Guide

    Original DEC VT100 documentation. Historical reference for understanding terminal capabilities.

  • xterm Control Sequences

    Comprehensive list of escape sequences supported by xterm, the de facto standard.

  • Unicode Character Database

    Official Unicode documentation for box drawing, blocks, and special characters.

Articles & tutorials

  • "Build Your Own Text Editor"

    Step-by-step tutorial covering raw mode, ANSI codes, and screen manipulation.

  • "The TTY Demystified"

    Deep dive into terminal and TTY architecture from kernel to user space.

  • "Terminal Colors"

    Comprehensive guide to 16-color, 256-color, and true color support.

Communities

  • r/ascii_art

    Reddit community for ASCII and ANSI art. Share work and get feedback.

  • ASCII Art Academy

    Tutorials, tools, and galleries dedicated to text art.

  • 16colo.rs

    Archive of ANSI art from BBS era to present. Historical reference and inspiration.

Tools

  • asciinema

    Record, share, and embed terminal sessions. Essential for documentation.

  • jp2a

    Convert images to ASCII art. Great for creating static assets.

  • figlet

    Generate ASCII text banners in various fonts. Perfect for splash screens.

  • boxes

    Draw boxes around text. Useful for creating framed content.

Fonts

  • Fira Code, JetBrains Mono, Cascadia Code

    Modern monospace fonts optimized for programming and terminal use. Include ligatures and excellent Unicode support.

  • Terminus, Tamsyn

    Bitmap fonts designed for maximum readability at small sizes.