Terminal-native 2D graphics engine — 18 GPU-quality effects at 60fps using braille characters + true color
What happens when you try to build a real graphics engine inside a terminal?
No GPU. No framebuffer. No pixel-level addressing. Just Unicode characters and ANSI escape codes. The constraint sounds impossible — but Unicode braille characters (U+2800–U+28FF) encode 8 dots per cell in a 2x4 grid, giving you 4x the resolution of half-block rendering. Pair that with 24-bit true color and suddenly a 200-column terminal becomes a 400x200 pixel canvas running at 60fps.
This started as a weekend experiment: could I get SPH fluid dynamics running in a terminal? One effect became two, two became six, and now it's an 18-effect demoscene engine with Perlin noise fields, fractal zooms, Reynolds flocking, and a spinning 3D torus — all rendered through braille dot patterns.
Built with Bubbletea v2 and pure Go. No CGO. No shaders. Just fmt.Fprintf and math.
go install github.com/stussysenik/vfx@latestOr build from source:
git clone https://github.com/stussysenik/vfx.git
cd vfx
go build -o vfx .
./vfxNote: You need
./vfx(not justvfx) because the current directory isn't in your PATH. Usinggo install .instead will place the binary in$GOPATH/bin(usually~/go/bin), which is typically in your PATH.
| Language | Go 1.25 — pure Go, zero CGO |
| TUI framework | Bubbletea v2 — Elm-architecture terminal UI |
| CLI | Cobra — command routing (play, list, demo, screenshot) |
| Styling | Lip Gloss v2 — terminal layout and color |
| Rendering | Unicode braille (U+2800–U+28FF) — 8 sub-pixels per character cell |
| Color | 24-bit ANSI true color (\e[38;2;R;G;Bm) — 16.7M colors |
| Pixel buffer | []float32 RGBA — same layout as a GPU framebuffer |
| Noise | Custom Perlin noise (2D/3D) — no external dependency |
| Screenshots | image/png stdlib — headless rendering to PNG |
No GPU. No shaders. No external C libraries. Every pixel is computed in Go and drawn through Unicode characters.
vfx # Interactive browser — pick effects from a list
vfx play fire # Play a specific effect fullscreen
vfx list # List all 18 effects
vfx demo # Screensaver mode — auto-cycles every 10 seconds
vfx --seed 42 play plasma # Reproducible with a fixed seed![]() plasma Sinusoidal color plasma |
![]() lissajous Parametric oscilloscope curves |
![]() starfield 3D warp speed projection |
![]() waves Sine wave interference |
![]() matrix Digital rain cascade |
![]() aurora Northern lights curtains |
![]() fireworks Particle burst explosions |
![]() mandelbrot Animated Mandelbrot zoom |
![]() julia Animated Julia set fractal |
![]() donut Spinning 3D torus |
![]() boids Reynolds flocking simulation |
![]() fluid SPH fluid dynamics |
| Key | Action |
|---|---|
h |
Toggle HUD overlay (effect name, FPS, controls) |
space |
Pause / resume |
n |
Next effect |
N |
Previous effect |
r |
Randomize seed |
p |
Cycle color palette (where supported) |
q / esc |
Quit to browser (or exit) |
| Key | Action |
|---|---|
↑/↓ / j/k |
Navigate effect list |
enter |
Launch selected effect fullscreen |
/ |
Filter effects |
q |
Quit |
13 built-in color palettes, each defined as gradient stops with linear interpolation:
fire · neon · ocean · mono · dracula · tokyo-night · catppuccin · gruvbox · nord · solarized · retrowave · sunset · matrix
Press p during any effect to cycle through them.
Each terminal character cell maps to a 2x4 braille dot grid. The Unicode braille block (U+2800–U+28FF) encodes which of the 8 dots are "lit" as a bitmask:
Cell layout: Bit positions:
[·][·] [0][3]
[·][·] [1][4]
[·][·] [2][5]
[·][·] [6][7]
An 80x24 terminal becomes a 160x96 pixel canvas. A fullscreen 200x50 terminal becomes 400x200 — enough for smooth particle physics and fluid dynamics.
Each braille character gets a single 24-bit foreground color via ANSI escape codes (\e[38;2;R;G;Bm), computed by averaging the lit pixels in that cell.
Effect.Update(dt) → advance simulation state
Effect.Draw(canvas) → write float32 RGBA pixels to buffer
Canvas.Render() → convert pixels to braille chars + ANSI color
Bubbletea View() → display frame in alternate screen
The canvas uses a flat []float32 buffer (RGBA per pixel) — the same layout as a GPU framebuffer. Effects write directly to it using SetPixel, AddPixel (additive blending), or drawing primitives (Line, Circle, FilledCircle, Rect).
cmd/ CLI commands (cobra)
├── root.go Browser mode (default)
├── play.go vfx play <effect>
├── list.go vfx list
├── demo.go vfx demo
└── screenshot.go vfx screenshot — headless PNG export
internal/
├── canvas/ Pixel buffer + terminal rendering
│ ├── canvas.go float32 RGBA buffer → braille/halfblock + ANSI + PNG export
│ ├── color.go HSL↔RGB, LerpColor, RGBA type
│ └── blend.go Normal, additive, multiply, screen blending
├── engine/ Core abstractions
│ ├── engine.go Effect interface, Control, Range
│ ├── particle.go Reusable ParticleSystem + Emitter
│ ├── noise.go Permutation-based Perlin noise (2D/3D)
│ └── physics.go Gravity, force calculations
├── effects/ 18 visual effects (one file each)
│ ├── registry.go Effect registration
│ ├── helpers.go Shared utilities (clampf, cyclePalette)
│ ├── fire.go DOOM fire propagation
│ ├── plasma.go Sinusoidal plasma
│ └── ... (15 more)
├── palette/ Color gradient system
│ ├── palette.go Gradient stops + linear interpolation
│ └── builtin.go 13 curated palettes
└── tui/ Bubbletea v2 terminal UI
├── app.go Root model (browser↔player transitions)
├── browser.go Effect list with live preview
├── player.go Fullscreen 60fps renderer
├── overlay.go HUD (FPS, controls, effect name)
└── theme.go Terminal color theme
Every effect implements a single interface:
type Effect interface {
Name() string
Description() string
Category() string
Init(width, height int, seed int64)
Update(dt float64)
Draw(c *canvas.Canvas)
Controls() []Control
HandleInput(key string)
}To add a new effect: create a file in internal/effects/, implement the interface, register it in registry.go. That's it — the browser, player, demo mode, and palette cycling all work automatically.
The screenshot command renders effects headlessly to PNG — useful for documentation, sharing, or testing:
vfx screenshot plasma -o plasma.png # Single effect
vfx screenshot --all -o docs/ # All 18 effects into a directory
vfx screenshot julia --width 640 --height 360 # Custom resolution
vfx screenshot --all --seed 42 -o docs/ # Reproducible with fixed seedFlags: --width (default 320), --height (default 180), --frames (default 120 = 2 seconds at 60fps).
This project builds on ideas from many sources:
- donut.c by Andy Sloane — the rotating torus math
- DOOM PSX fire by Fabien Sanglard — upward heat propagation
- Boids by Craig Reynolds (1987) — separation, alignment, cohesion flocking rules
- Improved Perlin Noise by Ken Perlin (2002) — the smoothstep and gradient functions
- SPH fluid simulation by Muller et al. (2003) — pressure and viscosity kernels
- Classic demoscene plasma — sinusoidal interference patterns as a tradition
- drawille — braille characters as pixel grids
VHS tape files are included in docs/ for reproducible recordings:
brew install vhs
vhs docs/fire.tape # → docs/fire.gif
vhs docs/plasma.tape # → docs/plasma.gif- Go 1.25+
- A terminal with true color support (iTerm2, WezTerm, Ghostty, Kitty, Alacritty)
- Terminal.app works but with limited color accuracy
See ARCHITECTURE.md for a deep dive into the technical decisions behind vfx.


















