Graphics Pipeline

From pixels to terminal characters, with control over every step.

The <img> and <canvas> components render pixel data to terminal cells using Unicode block characters. You choose the graphics mode (resolution vs. compatibility), dithering algorithm (smooth gradients on limited palettes), and optional per-pixel shaders or filters.

1. Graphics modes

Each mode maps pixels to terminal characters differently, trading resolution for compatibility.

melker --stdout examples/graphics/gfx-modes.melker
 Graphics Modes

      sextant          quadrant          halfblock          pattern


   🬞🬵🬚🬋🬋🬋🬩🬋🬋🬩🬱🬭🬏         ▀▀▀     .j+---+--+L_,
 🬞🬻🬹🬭🬭🬭🬭🬱🬹🬭🬭🬭🬭🬱🬹🬏       .M*____L*____L*,
 🬻🬻🬦🬦🬹🬹🬏🬏🬭🬞🬵🬹🬓🬓🬴      MM||**,,_.j*|||B
 🬬🬨🬊🬕🬬🬱🬵🬍🬆🬂🬕🬕      B*||+*BLjT+"||**
 🬹🬹🬹🬹🬍🬬🬝🬄🬹🬓🬹🬹      ***|*|TBB||*||**
 🬨🬊🬻🬆🬂🬂🬊🬵🬄🬎      *+||M|+""+|j|||=
 🬊🬬🬂🬁🬂🬀🬂🬂🬂🬂🬁🬂🬀🬂🬆🬆      +B"'"`""""'"`"++
  🬁🬊🬎🬎🬊🬂🬎🬎🬎🬊🬆🬎🬎🬂    ▀▀▀▀▀▀       '+==+"===++=="

View source

Set the mode per element with the gfxMode prop on <img> or <canvas>. Left to right: sextant (2x3 block characters, highest resolution), quadrant (2x2), halfblock (1x2), and pattern (ASCII characters chosen by brightness).

gfx-modes.melker
<policy>{ "permissions": { "read": ["*"] } }</policy>

<container style="flex-direction: row; gap: 2">
    <container style="flex-direction: column; align-items: center">
        <text>sextant</text>
        <img src="logo.png" width="16" height="12"
            gfxMode="sextant" style="object-fit: contain"/>
    </container>
    <container style="flex-direction: column; align-items: center">
        <text>quadrant</text>
        <img src="logo.png" width="16" height="12"
            gfxMode="quadrant" style="object-fit: contain"/>
    </container>
    <!-- same for halfblock, pattern -->
</container>

The default mode is sextant (2x3 pixels per cell, highest resolution). Sextant characters (Unicode 13.0, 2020) require font support — most modern monospace fonts include them, including Fira Code. Run melker --test-sextant to check your terminal.

Reference: Graphics modes

2. Dithering

Simulate colors outside the available palette by distributing quantization error across neighboring pixels.

melker --stdout examples/graphics/dithering.melker
 Dither Algorithms

       none             sierra           atkinson           ordered


   🬞🬵🬚🬋🬋🬋🬩🬋🬋🬩🬱🬭🬏     🬭🬵🬹🬹🬹🬹🬹🬹🬹🬏🬹🬭🬏     🬭🬵🬹🬹🬹🬹🬹🬹🬹🬹🬹🬭🬏     🬭🬵🬹🬹🬹🬹🬹🬹🬹🬹🬹🬭🬏
 🬞🬻🬹🬭🬭🬭🬭🬱🬹🬭🬭🬭🬭🬱🬹🬏  🬦█🬏🬢🬦🬟🬢🬠🬃🬯🬢🬖🬏🬢█🬱  🬦█🬏🬢🬖🬢🬋🬖🬢🬓🬶🬢🬖🬞🬃🬱  🬦█🬃🬞🬢🬢🬃🬢🬃🬢🬢🬢🬃🬃🬱
 🬻🬻🬦🬦🬹🬹🬏🬏🬭🬞🬵🬹🬓🬓🬴  🬦🬟🬇🬹🬹🬏🬺🬸🬞🬵🬹🬓🬲🬉  🬟🬘🬦🬹🬹🬏🬺🬴🬦🬵🬹🬓🬲🬅🬏  🬏🬐🬗🬸🬹🬹🬏🬸🬶🬭🬵🬹🬓🬗🬐🬐
 🬬🬨🬊🬕🬬🬱🬵🬍🬆🬂🬕🬕  ██🬉🬎🬕🬬🬓🬵🬻🬆🬊▌🬠  🬃🬖🬉🬎🬕🬬🬱🬵🬆🬆🬆🬒🬇  ██🬤🬥🬕🬬🬱🬦🬻🬆🬆🬅🬃🬃
 🬹🬹🬹🬹🬍🬬🬝🬄🬹🬓🬹🬹  🬓🬫🬦🬻🬏🬬🬝🬄🬦🬻🬛🬓🬦  🬩🬘🬹🬑🬬🬝🬄🬦🬻🬛🬣🬃  🬖🬗🬦🬶🬀🬬🬝🬀🬦🬺🬺🬐🬏
 🬨🬊🬻🬆🬂🬂🬊🬵🬄🬎  ▌🬨🬛🬇🬬🬕🬀🬛🬁🬓  🬁🬓🬨🬹🬎🬝🬇🬹🬇🬑🬅  🬃🬅🬫🬫🬬🬬🬬🬬🬻🬥🬅🬃
 🬊🬬🬂🬁🬂🬀🬂🬂🬂🬂🬁🬂🬀🬂🬆🬆  🬨██🬁🬂🬆🬇🬑🬉🬂🬀🬃█🬝  🬨██🬁🬂🬂🬃🬏🬂🬂🬀██🬝  🬨██🬂🬂🬀🬐🬀🬏🬀🬒🬂🬀🬀█🬝
  🬁🬊🬎🬎🬊🬂🬎🬎🬎🬊🬆🬎🬎🬂    🬂🬎🬎🬁🬎🬎🬎🬎🬎🬎🬎🬎🬎🬆    🬂🬎🬎🬎🬇🬎🬎🬎🬎🬎🬎🬎🬎🬆    🬂🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬆

View source

The first image (none) uses full 24-bit color. The other three reduce to 2-bit color depth (ditherBits="2") and apply different algorithms to approximate the missing colors. Error diffusion algorithms (sierra, atkinson) spread quantization error to neighboring pixels. Ordered dithering uses a fixed threshold matrix with no error propagation.

dithering.melker
<img src="logo.png" width="16" height="12"
    dither="sierra-stable" ditherBits="2"
    gfxMode="sextant" style="object-fit: contain"/>

Available dither algorithms:

The ditherBits prop (1-8) controls color depth per channel. Lower values produce more visible dithering patterns. At 8 bits (the default), dithering has no visible effect.

3. Per-pixel shaders

Run a function for every pixel on every frame. Animate plasma, noise, gradients, or any procedural effect.

melker --stdout examples/graphics/shader.melker
 Per-pixel Shader

 🬂🬂🬂🬂🬊🬎🬬🬦🬵🬹🬹🬹🬹🬱🬭🬯🬂🬊🬬🬨🬝🬎🬌🬹🬱🬭🬭🬂🬎🬎🬬🬰🬰🬰🬰🬺🬹🬹🬹🬭🬰🬒🬂🬡🬰🬰🬰🬰🬰🬰
 🬹🬋🬋🬋🬹🬹🬭🬯🬂🬎🬎🬪🬱🬭🬸🬛🬔🬬🬒🬂🬡🬰🬂🬂🬊🬌🬹🬱🬭🬰🬒🬂🬂🬂🬂🬂🬒🬒🬂🬊🬎🬎🬎🬎🬎🬎🬎
 🬲🬐🬩🬹🬯🬂🬂🬊🬌🬹🬱🬭🬰🬒🬂🬂🬀🬁🬂🬎🬎🬋🬋🬋🬋🬋🬋🬋🬋🬍🬬🬰🬝🬎🬎🬎🬎🬎🬋🬹🬹🬱🬓🬔🬠🬯🬭🬭🬭
 🬎🬄🬄🬳🬰🬰🬭🬭🬭🬵🬵🬷🬸🬺🬹🬹🬹🬱🬭🬭🬏🬍🬋🬩🬹🬹🬹🬹🬹🬹🬱🬑🬊🬬🬺🬱🬭🬰🬸🬩🬋🬚
 🬋🬢🬭🬭🬵🬹🬎🬎🬆🬂🬊🬎🬎🬣🬕🬂🬂🬂🬂🬂🬊🬌🬩🬱🬯🬒🬂🬎🬎🬎🬎🬄🬟🬊🬎🬹🬹🬹🬹🬎🬎🬄🬱🬯🬂🬂🬰🬰
 🬋🬎🬂🬡🬭🬵🬹🬹🬩🬹🬹🬱🬭🬂🬎🬪🬱🬭🬭🬰🬰🬭🬯🬠🬂🬊🬌🬹🬹🬹🬹🬹🬹🬹🬹🬱🬕🬆🬆🬂🬂🬂🬂🬒🬊🬎🬦🬹🬜🬆
 🬦🬩🬍🬎🬎🬪🬹🬹🬹🬹🬻🬝🬲🬱🬁🬂🬊🬎🬹🬹🬹🬹🬋🬎🬎🬎🬆🬆🬂🬂🬂🬂🬂🬊🬨🬦🬵🬹🬹🬹🬹🬱🬑🬊🬎🬺
 🬠🬠🬯🬯🬯🬯🬯🬯🬯🬯🬭🬹🬹🬹🬱🬓🬁🬻🬕🬆🬂🬂🬂🬰🬭🬭🬭🬭🬭🬭🬭🬵🬹🬜🬆🬁🬊🬊🬊🬎🬆🬄🬲🬱🬁🬂
 🬩🬹🬋🬎🬎🬎🬎🬎🬎🬎🬎🬆🬆🬀🬦🬉🬎🬺🬹🬹🬚🬋🬎🬎🬂🬰🬰🬰🬰🬰🬒🬂🬂🬂🬂🬀🬲🬷🬨🬵🬭🬱🬱🬱🬏
 🬁🬂🬂🬂🬂🬰🬭🬭🬭🬭🬭🬭🬭🬱🬚🬎🬂🬂🬂🬂🬰🬰🬭🬵🬹🬹🬻🬰🬰🬰🬝🬎🬎🬂🬭🬭🬵🬹🬹🬹🬱🬭🬏🬊🬎🬎🬎🬎🬎🬄
 🬏🬊🬦🬹🬝🬆🬂🬂🬊🬊🬬🬲🬞🬵🬵🬹🬹🬹🬹🬚🬎🬎🬎🬆🬂🬂🬰🬭🬵🬹🬍🬆🬂🬂🬂🬒🬂🬂🬡🬨🬺🬹🬱🬭🬰🬰🬮🬭
 🬓🬁🬊🬪🬹🬱🬭🬵🬵🬷🬉🬊🬎🬎🬎🬎🬎🬆🬆🬄🬮🬯🬁🬊🬊🬎🬎🬌🬩🬹🬹🬹🬹🬋🬋🬎🬆🬂🬂🬂🬂🬊🬦🬵🬹🬹

View source

The onShader prop points to a function that runs for every pixel on every frame. The function receives pixel coordinates, elapsed time, canvas resolution, the source pixel buffer, and a utils object with math helpers.

shader.melker
<policy>{ "permissions": { "shader": true } }</policy>

<canvas id="c" width="50" height="12" gfxMode="sextant"
    onShader="$app.plasma" shaderFps="30" shaderRunTime="500"/>

<script type="typescript">
    export function plasma(
        x: number, y: number, time: number,
        resolution: any, _source: any, utils: any
    ) {
        const u = x / resolution.width;
        const v = y / resolution.height / resolution.pixelAspect;
        const n = utils.fbm(u * 2 + time * 0.5, v * 2 + time * 0.3, time * 0.2);
        return utils.palette(n,
            [0.5,0.5,0.5], [0.5,0.5,0.5],
            [1,1,1], [0,0.33,0.67]);
    }
</script>

Shader signature: (x, y, time, resolution, source, utils) => [r, g, b, a]

Available shader utils:

Shaders require "shader": true in the policy because they are CPU-intensive. Use shaderFps to control frame rate and shaderRunTime (ms) to stop after a fixed duration.

4. Image filters

Transform pixels once at load time. Same signature as shaders, but runs only on the initial frame.

The onFilter prop works like onShader but runs once when the image loads. Use source.getPixel(x, y) to read the original pixel and return the modified [r, g, b, a].

filter.melker
<img src="logo.png" width="12" height="8"
    gfxMode="quadrant" onFilter="$app.grayscale"/>

<script type="typescript">
    export function grayscale(
        x: number, y: number,
        _t: number, _r: any, src: any
    ) {
        const p = src.getPixel(x, y);
        if (!p) return [0, 0, 0, 0];
        const l = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
        return [l, l, l, p[3]];
    }

    export function sepia(
        x: number, y: number,
        _t: number, _r: any, src: any
    ) {
        const p = src.getPixel(x, y);
        if (!p) return [0, 0, 0, 0];
        return [
            Math.min(255, p[0]*0.393 + p[1]*0.769 + p[2]*0.189),
            Math.min(255, p[0]*0.349 + p[1]*0.686 + p[2]*0.168),
            Math.min(255, p[0]*0.272 + p[1]*0.534 + p[2]*0.131),
            p[3]
        ];
    }
</script>

Filters do not require "shader": true in the policy. They only need "read" permission for the image file.

View source

5. Mode comparison

Choose a mode based on your target terminal and desired resolution.

Mode Pixels/cell Resolution (80x24) Unicode Font support
sextant 2x3 160x72 13.0 (2020) Spotty
quadrant 2x2 160x48 1.0 (1991) Near-universal
halfblock 1x2 80x48 1.0 (1991) Near-universal
block 1x1 80x24 N/A Universal
pattern 2x3 160x72 N/A Universal (ASCII)
sixel native native N/A xterm, foot, WezTerm
kitty native native N/A Kitty
iterm2 native native N/A iTerm2, WezTerm, Rio

The character-based modes (sextant through pattern) work everywhere by encoding pixels into Unicode block characters with foreground and background colors. The protocol modes (sixel, kitty, iterm2) send raw pixel data to terminals that support native image display.

Pixel aspect ratio varies by mode. Terminal cells are taller than wide, so quadrant pixels are tall and thin (~0.5 aspect). Halfblock pixels are nearly square (~1.0). Use resolution.pixelAspect in shaders or canvas.getPixelAspectRatio() in paint handlers for aspect-correct drawing.

6. Configuration

Set the graphics mode globally or per element. Per-element props take priority.

Three ways to set the graphics mode, in order of priority:

Per-element prop (highest priority)
<img src="photo.png" gfxMode="quadrant"/>
<canvas gfxMode="halfblock" onPaint="$app.draw"/>
CLI flag
melker --gfx-mode=quadrant my-app.melker
Environment variable (lowest priority)
export MELKER_GFX_MODE=quadrant
melker my-app.melker

The CLI flag and environment variable set the default for all elements. A per-element gfxMode prop overrides them for that specific component.

To check if your terminal supports sextant characters:

Test sextant support
melker --test-sextant

If the test pattern looks broken, set MELKER_GFX_MODE=quadrant in your shell profile for the best fallback quality.

Reference: Graphics modes