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.
Each mode maps pixels to terminal characters differently, trading resolution for compatibility.
Graphics Modes sextant quadrant halfblock pattern 🬞🬵🬚🬋🬋🬋🬩🬋🬋🬩🬱🬭🬏 ▗▄▟▀▀▛▀▛▜▀▀▙▄▖ ▄▄▀▀▀▀▀▀▀▀▀▄▄ .j+---+--+L_, 🬞🬻🬹🬭🬭🬭🬭🬱🬹🬭🬭🬭🬭🬱🬹🬏 ▗▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ .M*____L*____L*, 🬻🬻🬦🬦🬹🬹🬏🬏🬭🬞🬵🬹🬓🬓▌🬴 ▀▀▀▐▀▀▄▖▜▗▟▀▌▌▀▀ ▀██▀▀▀▀▀▀▀▀▀▀▀▀▀ MM||**,,_.j*|||B 🬬🬨▐▐🬊🬕🬬🬱🬵🬍🬆🬂▌▌🬕🬕 ▄▄▄▐▄▌▜▙▟▌▘▄▌▄▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ B*||+*BLjT+"||** 🬹🬹🬹▐🬹▌🬍🬬🬝🬄▐🬹▌🬓🬹🬹 ▀▀▀▐▀▌▀▀▀▘▐▀▌▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ***|*|TBB||*||** 🬨🬊▐▐🬻▌🬆🬂🬂🬊▐🬵▌▌🬄🬎 ▄▄▄▐▄▌▄▄▄▄▐▄▌▙▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ *+||M|+""+|j|||= 🬊🬬🬂🬁🬂🬀🬂🬂🬂🬂🬁🬂🬀🬂🬆🬆 ▝▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +B"'"`""""'"`"++ 🬁🬊🬎🬎🬊🬂🬎🬎🬎🬊🬆🬎🬎🬂 ▝▀▀▀▀▀▀▀▀▀▀▀▀▘ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ '+==+"===++=="
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).
<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
Simulate colors outside the available palette by distributing quantization error across neighboring pixels.
Dither Algorithms none sierra atkinson ordered 🬞🬵🬚🬋🬋🬋🬩🬋🬋🬩🬱🬭🬏 🬭🬵🬹🬹🬹🬹🬹🬹🬹🬏🬹🬭🬏 🬭🬵🬹🬹🬹🬹🬹🬹🬹🬹🬹🬭🬏 🬭🬵🬹🬹🬹🬹🬹🬹🬹🬹🬹🬭🬏 🬞🬻🬹🬭🬭🬭🬭🬱🬹🬭🬭🬭🬭🬱🬹🬏 🬦█🬏🬢🬦🬟🬢🬠🬃🬯🬢🬖🬏🬢█🬱 🬦█🬏🬢🬖🬢🬋🬖🬢🬓🬶🬢🬖🬞🬃🬱 🬦█🬃🬞🬢🬢🬃🬢🬃🬢🬢🬢🬃█🬃🬱 🬻🬻🬦🬦🬹🬹🬏🬏🬭🬞🬵🬹🬓🬓▌🬴 █🬦🬟🬇🬹🬹🬏🬺🬸🬞🬵🬹🬓🬲🬉█ █🬟🬘🬦🬹🬹🬏🬺🬴🬦🬵🬹🬓🬲🬅🬏 🬏🬐🬗🬸🬹🬹🬏🬸🬶🬭🬵🬹🬓🬗🬐🬐 🬬🬨▐▐🬊🬕🬬🬱🬵🬍🬆🬂▌▌🬕🬕 ██▌🬉🬎🬕🬬🬓🬵🬻🬆🬊▌▌🬠█ █🬃🬖🬉🬎🬕🬬🬱🬵🬆🬆🬆▌🬒🬇█ ██🬤▐🬥🬕🬬🬱🬦🬻🬆🬆▌🬅🬃🬃 🬹🬹🬹▐🬹▌🬍🬬🬝🬄▐🬹▌🬓🬹🬹 █🬓🬫🬦🬻▌🬏🬬🬝🬄🬦🬻▌🬛🬓🬦 █🬩🬘▐🬹▌🬑🬬🬝🬄🬦🬻▌🬛🬣🬃 █🬖🬗🬦🬶▌🬀🬬🬝🬀🬦🬺▌🬺🬐🬏 🬨🬊▐▐🬻▌🬆🬂🬂🬊▐🬵▌▌🬄🬎 █▌🬨▐🬛▌🬇🬬🬕🬀▐🬛▌█🬁🬓 🬁🬓🬨▐🬹▌█🬎🬝🬇▐🬹▌🬇🬑🬅 🬃🬅🬫▐🬫▌🬬🬬🬬🬬▐🬻▌🬥🬅🬃 🬊🬬🬂🬁🬂🬀🬂🬂🬂🬂🬁🬂🬀🬂🬆🬆 🬨██🬁🬂🬆█🬇█🬑🬉🬂🬀🬃█🬝 🬨██🬁🬂🬂█🬃█🬏🬂🬂🬀██🬝 🬨██🬂🬂🬀🬐🬀🬏🬀🬒🬂🬀🬀█🬝 🬁🬊🬎🬎🬊🬂🬎🬎🬎🬊🬆🬎🬎🬂 🬂🬎🬎🬁🬎🬎🬎🬎🬎🬎🬎🬎🬎🬆 🬂🬎🬎🬎🬇🬎🬎🬎🬎🬎🬎🬎🬎🬆 🬂🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬎🬆
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.
<img src="logo.png" width="16" height="12" dither="sierra-stable" ditherBits="2" gfxMode="sextant" style="object-fit: contain"/>
Available dither algorithms:
none - no dithering, direct color quantizationsierra-stable - Sierra error diffusion (deterministic)floyd-steinberg - Floyd-Steinberg error diffusionatkinson - Atkinson error diffusion (less color bleed)ordered - Bayer matrix ordered ditheringblue-noise - Blue noise threshold dithering
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.
Run a function for every pixel on every frame. Animate plasma, noise, gradients, or any procedural effect.
Per-pixel Shader 🬂🬂🬂🬂🬊🬎🬬🬦🬵🬹🬹🬹🬹🬱🬭🬯🬂🬊🬬🬨🬝🬎🬌🬹🬱🬭🬭🬂🬎🬎🬬🬰🬰🬰🬰🬺🬹🬹🬹🬭🬰🬒🬂🬡🬰🬰🬰🬰🬰🬰 🬹🬋🬋🬋🬹🬹🬭🬯🬂🬎🬎🬪🬱🬭🬸🬛▌🬔▐🬬▌🬒🬂🬡🬰🬂🬂🬊🬌🬹🬱🬭🬰🬒🬂🬂🬂🬂🬂🬒🬒🬂🬊🬎🬎🬎🬎🬎🬎🬎 🬲▌🬐🬩🬹🬯🬂🬂🬊🬌🬹🬱🬭🬰🬒🬂🬂🬀🬁🬂🬎🬎🬋🬋🬋🬋🬋🬋🬋🬋🬍🬬🬰🬝🬎🬎🬎🬎🬎🬋🬹🬹🬱🬓🬔🬠🬯🬭🬭🬭 🬎🬄🬄🬳🬰🬰🬭🬭🬭🬵🬵🬷🬸🬺🬹🬹🬹🬱🬭🬭🬏🬍🬋🬩🬹🬹🬹🬹🬹🬹🬱🬑🬊🬬🬺🬱🬭🬰🬸▐▐▌▌▌▐▐▐🬩🬋🬚 🬋🬢🬭🬭🬵🬹🬎🬎🬆🬂🬊🬎🬎🬣🬕🬂🬂🬂🬂🬂🬊🬌🬩🬱🬯🬒🬂🬎🬎🬎🬎🬄🬟▐🬊🬎🬹🬹🬹🬹🬎🬎🬄▌🬱🬯🬂🬂🬰🬰 🬋🬎🬂🬡🬭🬵🬹🬹🬩🬹🬹🬱🬭🬂🬎🬪🬱🬭🬭🬰🬰🬭🬯🬠🬂🬊🬌🬹🬹🬹🬹🬹🬹🬹🬹🬱🬕🬆🬆🬂🬂🬂🬂🬒🬊🬎🬦🬹🬜🬆 🬦🬩🬍🬎🬎🬪🬹🬹🬹🬹🬻🬝▌🬲🬱🬁🬂🬊🬎🬹🬹🬹🬹🬋🬎🬎🬎🬆🬆🬂🬂🬂🬂🬂🬊🬨▌▌▌🬦🬵🬹🬹🬹🬹🬱🬑🬊🬎🬺 🬠🬠🬯🬯🬯🬯🬯🬯🬯🬯🬭🬹🬹🬹🬱🬓🬁▐▐🬻🬕🬆🬂🬂🬂🬰🬭🬭🬭🬭🬭🬭🬭🬵🬹🬜🬆▌▌🬁🬊🬊🬊🬎🬆🬄🬲🬱🬁🬂 ▐▐🬩🬹🬋🬎🬎🬎🬎🬎🬎🬎🬎🬆🬆🬀🬦▐🬉🬎🬺🬹🬹🬚🬋🬎🬎🬂🬰🬰🬰🬰🬰🬒🬂🬂🬂🬂🬀🬲🬷🬨▐▐🬵🬭🬱🬱🬱🬏 🬁🬂🬂🬂🬂🬰🬭🬭🬭🬭🬭🬭🬭🬱🬚🬎🬂🬂🬂🬂🬰🬰🬭🬵🬹🬹🬻🬰🬰🬰🬝🬎🬎🬂🬭🬭🬵🬹🬹🬹🬱🬭🬏🬊🬎🬎🬎🬎🬎🬄 🬏🬊🬦🬹🬝🬆🬂🬂🬊🬊🬬🬲▌▌🬞🬵🬵🬹🬹🬹🬹🬚🬎🬎🬎🬆🬂🬂🬰🬭🬵🬹🬍🬆🬂🬂🬂🬒🬂🬂🬡🬨🬺🬹🬱🬭🬰🬰🬮🬭 ▌🬓🬁🬊🬪🬹🬱🬭🬵🬵🬷▌▌▌🬉🬊🬎🬎🬎🬎🬎🬆🬆🬄🬮🬯🬁🬊🬊🬎🬎🬌🬩🬹🬹🬹🬹🬋🬋🬎🬆🬂🬂🬂🬂🬊🬦🬵🬹🬹
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.
<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:
fbm(x, y, z) - fractal Brownian motion noisepalette(t, a, b, c, d) - cosine-based color palettesmoothstep(edge0, edge1, x) - Hermite interpolationmix(a, b, t) - linear interpolationfract(x) - fractional partnoise2d, simplex2d, perlin2d, perlin3d - noise generators
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.
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].
<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.
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.
Set the graphics mode globally or per element. Per-element props take priority.
Three ways to set the graphics mode, in order of priority:
<img src="photo.png" gfxMode="quadrant"/> <canvas gfxMode="halfblock" onPaint="$app.draw"/>
melker --gfx-mode=quadrant my-app.melker
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:
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