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 pixelA 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
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
| Mode | H-res × V-res per cell | Colours per cell | Best for |
|---|---|---|---|
| 1:1 | 1 × 1 | 2 (fore / back) | Text, borders, typography |
| Quadrants | 2 × 2 | 2 | Blocky coloured fills |
| Half-blocks | 1 × 2 | 2 (top / bottom) | Coloured pixel art |
| Braille | 2 × 4 | 1 | Single-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 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:
- Compute the field at sub-cell resolution (2×4 for Braille).
- Threshold the field at the iso-value.
- Pack each 2×4 block into a Braille code point.
- Emit one
Cellper 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. AtOff, your effect must render nothing. Do not “just render a little bit” — the whole point of the tier is to free the budget.