Skip to Content
ftui-extrasVFX rasterizer

Visual FX rasterizer

A terminal cell is roughly 10×20 pixels on a typical monospace font, but from the renderer’s perspective it is one addressable unit. That is a brutal lower bound on resolution — a 100-column window gives you 100 pixels of horizontal resolution if you use one cell per dot.

The way out is sub-character rasterization: pack multiple logical pixels into one cell using Unicode glyphs that subdivide the cell grid. FrankenTUI’s visual FX rasterizer supports three such mappings:

  • Braille patterns () give a 2×4 dot grid per cell → 8× effective resolution at the cost of single-colour drawing.
  • Half-blocks (, ) give 1×2 vertical resolution with independent fore and back colours → 2× vertical, same horizontal, two colours per cell.
  • Quadrants (▘▝▖▗ and friends) give 2×2 resolution with independent quadrant colours → 2× each axis, four colours per cell.

This page explains what each mode gets you and how the effects pick between them.

Source: visual_fx.rs.

The cell budget

Every cell carries:

  • A glyph (up to one grapheme cluster)
  • A foreground colour
  • A background colour
  • A small set of style attributes (bold, underline, …)

Sub-character rasterization reinterprets the glyph as a pixel payload rather than a character. The fore / back colours become the ink used for filled / empty pixels in that pack.

Braille: 2×4 dots per cell

Braille patterns encode 8 dots in a single code point from U+2800 (, all dots off) through U+28FF (, all dots on). The bit layout:

bit 0 bit 3 col 0 col 1 bit 1 bit 4 row 0: • • bit 2 bit 5 row 1: • • bit 6 bit 7 row 2: • • row 3: • •

Given a boolean pixel grid, compute an 8-bit mask per cell and index into the Braille range. Result: a 2-pixel-wide, 4-pixel-tall subgrid per cell.

A 100×40 terminal becomes a 200×160 pixel canvas for Braille rendering.

Braille cells carry a single colour (the foreground). You cannot tint individual dots within a Braille cell. Effects that need smooth colour gradients typically overlay the Braille output onto a coloured background, or switch to half-blocks.

When to use Braille

  • Field-like effects (Metaballs, Plasma iso-surfaces, flow lines).
  • Anything that is a single-colour silhouette on a coloured background.
  • Line-drawing where thickness matters more than per-pixel hue.

Half-blocks: 1×2 pixels with two colours

The half-block glyphs (upper half) and (lower half) let one cell carry two independent colours: foreground for the filled half, background for the other. A sequence of glyphs with varying fore / back colours gives 2× vertical resolution.

fg=red bg=blue ▀ → red pixel above a blue pixel fg=blue bg=red ▀ → blue pixel above a red pixel

A 100×40 cell window becomes a 100×80 pixel canvas with two colours per column.

When to use half-blocks

  • Coloured image rendering (pixel art, thumbnails).
  • Doom Fire (uses the half-block cellular-automaton directly).
  • Any effect where vertical resolution matters and you want full colour.

Quadrants: 2×2 per cell

The 16 glyphs ▘▝▖▗▚▞▛▜▙▟▐▌▀▄█ (and cousins from U+2596) partition the cell into four quadrants. Combined correctly you can paint any of the 24=162^4 = 16 subsets independently, but you are limited to two colours per cell (foreground for the quadrants you fill, background for the rest).

A 100×40 cell window becomes a 200×80 pixel canvas with two colours per cell.

When to use quadrants

  • Low-resolution image blits where half-blocks look too boxy.
  • Effects that want a denser fill than half-blocks but fewer dots than Braille.

Mode comparison

ModeH-res × V-res per cellColours per cellBest for
1:11 × 12 (fore / back)Text, borders, typography
Quadrants2 × 22Blocky coloured fills
Half-blocks1 × 22 (top / bottom)Coloured pixel art
Braille2 × 41Single-colour fields, iso-surfaces

The rasterizer pipeline

Visual effects do not write Unicode glyphs directly. They produce a pixel buffer (typically a flat Vec<f32> of field values), then hand it to a canvas adapter that chooses a mode and emits cells.

The adapters live at visual_fx/effects/canvas_adapters.rs:

  • MetaballsCanvasAdapter — picks between Braille and half-blocks based on quality tier and area.
  • PlasmaCanvasAdapter — always uses half-blocks (colour is essential to Plasma).

You can write your own adapter against the same Canvas trait exported from ftui_extras::canvas.

Coordinate math

A 1:1 rasterizer places pixel (x, y) at cell (x, y). A Braille rasterizer places pixel (px, py) at cell (px / 2, py / 4) and sets the dot bit at (px % 2, py % 4).

For Metaballs, the sampler generates a float field at sub-cell resolution. cell_to_normalized (sampling.rs) normalises cell coordinates to [1,1][-1, 1] so effects can use scale-free math:

let (nx, ny) = cell_to_normalized(cx, cy, area.width, area.height); let field_value = sampler.sample(nx, ny);

Then fill_normalized_coords iterates the cell grid (or sub-cell grid for Braille) and calls the effect’s field function.

Worked example: Metaballs rendered to Braille

use ftui_extras::visual_fx::effects::{MetaballsFx, MetaballsCanvasAdapter}; use ftui_extras::visual_fx::{ThemeInputs, FxQuality}; pub struct Model { metaballs: MetaballsFx, theme_inputs: ThemeInputs, } fn view(&self, frame: &mut ftui_render::frame::Frame) { let area = frame.buffer.bounds(); let quality = FxQuality::from_degradation(frame.degradation()); // Step the simulation. self.metaballs.tick(); // Render: adapter picks Braille or half-blocks internally. let adapter = MetaballsCanvasAdapter::new(&self.theme_inputs, quality); adapter.render(&self.metaballs, area, frame); }

The adapter internals:

  1. Compute the field at sub-cell resolution (2×4 for Braille).
  2. Threshold the field at the iso-value.
  3. Pack each 2×4 block into a Braille code point.
  4. Emit one Cell per terminal cell with that glyph + the ink colour.

GPU acceleration (optional)

The fx-gpu feature wraps wgpu around a subset of effects. When enabled and a GPU is available, the field computation happens on-GPU and the CPU just reads back the final mask. This is overkill for most scenarios but useful when running a dense effect on a 4K terminal.

Source: visual_fx/gpu.rs and the companion WGSL shader gpu_metaballs.wgsl.

Pitfalls

  • Mixing modes within one region. A Braille glyph next to a half-block glyph alignment does not line up neatly — the perceived resolution jumps. Pick one mode per effect.
  • Font coverage. Some terminal fonts are missing half of the quadrant glyphs. Test on the fonts your users are likely to run.
  • Braille with multiple colours. Each Braille cell is one colour. Approximating a gradient with Braille looks like banded single-tone stripes.
  • Forgetting to honour FxQuality. At Off, your effect must render nothing. Do not “just render a little bit” — the whole point of the tier is to free the budget.

Where next