pythonicDraw.hpp is the foundation for all terminal graphics in Pythonic. This tutorial will teach you how it works, from basic concepts to advanced rendering.
- What are Braille Characters?
- The Braille Dot Layout
- BrailleCanvas Class
- ColorCanvas for True Color
- ColoredBrailleCanvas
- Signal Handling
- Video Playback
- GPU Acceleration
Unicode includes 256 Braille patterns (U+2800 to U+28FF). Each pattern represents a combination of 8 dots that can be on or off. We abuse this for graphics!
Standard Braille (for reading): Our use (for graphics):
⠿ = letters/numbers ⠿ = 8 tiny pixels in one cell!
Why Braille?
| Method | Resolution per character | Color |
|---|---|---|
ASCII art (#, .) |
1×1 | No |
Half-block (▀) |
1×2 | Yes |
Braille (⠿) |
2×4 | Limited |
Braille gives us 8× the resolution of regular text!
Each Braille character has 8 dots arranged in a 2×4 grid:
/**
* Braille dot positions (Unicode offset from 0x2800):
* Col 0 Col 1
* [1] [4] Row 0 (bits 0, 3)
* [2] [5] Row 1 (bits 1, 4)
* [3] [6] Row 2 (bits 2, 5)
* [7] [8] Row 3 (bits 6, 7)
*/
constexpr uint8_t BRAILLE_DOTS[4][2] = {
{0x01, 0x08}, // Row 0: bit 0 (0x01), bit 3 (0x08)
{0x02, 0x10}, // Row 1: bit 1 (0x02), bit 4 (0x10)
{0x04, 0x20}, // Row 2: bit 2 (0x04), bit 5 (0x20)
{0x40, 0x80} // Row 3: bit 6 (0x40), bit 7 (0x80)
};Visual mapping:
Braille character: ⡇ (Unicode U+2847)
Binary: 0100 0111 = 0x47
Bit layout:
bit0=1 bit3=0 → ●
bit1=1 bit4=0 → ●
bit2=1 bit5=0 → ●
bit6=0 bit7=1 → ●
Displayed as: ⡇
Example: Creating a pattern
// Want to draw:
// ● (top-left)
// ● (middle-right)
// ● (bottom-left)
uint8_t pattern = 0;
pattern |= 0x01; // Row 0, Col 0 (top-left)
pattern |= 0x10; // Row 1, Col 1 (middle-right)
pattern |= 0x40; // Row 3, Col 0 (bottom-left)
// pattern = 0x51 = ⡑
char32_t codepoint = 0x2800 + pattern; // = 0x2851The BrailleCanvas stores a grid of Braille patterns:
class BrailleCanvas
{
private:
size_t _char_width; // Width in terminal characters
size_t _char_height; // Height in terminal characters
size_t _pixel_width; // char_width × 2 (2 dots wide per char)
size_t _pixel_height; // char_height × 4 (4 dots tall per char)
// One byte per character cell, bits = which dots are "on"
std::vector<std::vector<uint8_t>> _canvas;Dimensions example:
Terminal: 80 × 24 characters
Canvas: 80 × 24 bytes (one per cell)
Pixels: 160 × 96 addressable points
void set_pixel(int x, int y, bool on = true)
{
if (x < 0 || x >= (int)_pixel_width ||
y < 0 || y >= (int)_pixel_height)
return; // Bounds check
int char_x = x / 2; // Which character column
int char_y = y / 4; // Which character row
int local_x = x % 2; // Position within cell (0 or 1)
int local_y = y % 4; // Position within cell (0-3)
uint8_t bit = BRAILLE_DOTS[local_y][local_x];
if (on)
_canvas[char_y][char_x] |= bit; // Turn on
else
_canvas[char_y][char_x] &= ~bit; // Turn off
}Visual walkthrough:
set_pixel(5, 9, true)
Step 1: Find character cell
char_x = 5 / 2 = 2 (third column)
char_y = 9 / 4 = 2 (third row)
Step 2: Find position within cell
local_x = 5 % 2 = 1 (right column of dots)
local_y = 9 % 4 = 1 (second row of dots)
Step 3: Look up bit
BRAILLE_DOTS[1][1] = 0x10 (bit 4)
Step 4: Set bit
_canvas[2][2] |= 0x10
For video, we process entire 2×4 blocks at once:
void set_block_gray(int char_x, int char_y,
const uint8_t gray[8],
uint8_t threshold)
{
// gray[0..7] = grayscale values for 8 pixels
// Order: row0_col0, row0_col1, row1_col0, row1_col1, ...
uint8_t pattern = 0;
pattern |= (gray[0] >= threshold) ? 0x01 : 0; // row 0, col 0
pattern |= (gray[1] >= threshold) ? 0x08 : 0; // row 0, col 1
pattern |= (gray[2] >= threshold) ? 0x02 : 0; // row 1, col 0
pattern |= (gray[3] >= threshold) ? 0x10 : 0; // row 1, col 1
pattern |= (gray[4] >= threshold) ? 0x04 : 0; // row 2, col 0
pattern |= (gray[5] >= threshold) ? 0x20 : 0; // row 2, col 1
pattern |= (gray[6] >= threshold) ? 0x40 : 0; // row 3, col 0
pattern |= (gray[7] >= threshold) ? 0x80 : 0; // row 3, col 1
_canvas[char_y][char_x] = pattern;
}This is 8× faster than calling set_pixel 8 times!
When you need full color, use half-block characters:
class ColorCanvas
{
private:
size_t _pixel_width; // Same as char_width
size_t _pixel_height; // char_height × 2
std::vector<std::vector<RGB>> _pixels; // Full RGB per pixelHalf-block rendering:
Character: ▀ (upper half block, U+2580)
Foreground color → top pixel
Background color → bottom pixel
One character = 2 vertical pixels
Rendering code:
std::string render() const
{
const char *UPPER_HALF = "\xe2\x96\x80"; // ▀ in UTF-8
for (size_t cy = 0; cy < _char_height; ++cy) {
size_t py_top = cy * 2;
size_t py_bot = py_top + 1;
for (size_t cx = 0; cx < _char_width; ++cx) {
RGB top = _pixels[py_top][cx];
RGB bot = _pixels[py_bot][cx];
// Set foreground color (top pixel)
out += "\033[38;2;" + std::to_string(top.r) + ";"
+ std::to_string(top.g) + ";"
+ std::to_string(top.b) + "m";
// Set background color (bottom pixel)
out += "\033[48;2;" + std::to_string(bot.r) + ";"
+ std::to_string(bot.g) + ";"
+ std::to_string(bot.b) + "m";
out += UPPER_HALF;
}
out += "\033[0m\n"; // Reset colors, newline
}
}ANSI escape code breakdown:
\033[38;2;R;G;Bm = Set foreground to RGB(R,G,B)
\033[48;2;R;G;Bm = Set background to RGB(R,G,B)
\033[0m = Reset all attributes
Combines high-resolution Braille with color:
class ColoredBrailleCanvas
{
private:
std::vector<std::vector<uint8_t>> _patterns; // Braille dot patterns
std::vector<std::vector<RGB>> _colors; // Color per cellThe limitation: Each Braille cell can only have ONE foreground color (all dots share it).
Color averaging:
void load_frame_rgb(const uint8_t *data, int width, int height,
uint8_t threshold = 128)
{
for (size_t cy = 0; cy < _char_height; ++cy) {
for (size_t cx = 0; cx < _char_width; ++cx) {
uint8_t pattern = 0;
int r_sum = 0, g_sum = 0, b_sum = 0;
int on_count = 0;
// Process 2×4 pixel block
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 2; ++col) {
// Get RGB from source image
uint8_t r = ..., g = ..., b = ...;
// Convert to grayscale for thresholding
uint8_t gray = (299*r + 587*g + 114*b) / 1000;
if (gray >= threshold) {
// Turn on this dot
pattern |= BRAILLE_DOTS[row][col];
// Accumulate color
r_sum += r;
g_sum += g;
b_sum += b;
on_count++;
}
}
}
_patterns[cy][cx] = pattern;
// Average color of "on" pixels
if (on_count > 0) {
_colors[cy][cx] = RGB(
r_sum / on_count,
g_sum / on_count,
b_sum / on_count
);
}
}
}
}Proper cleanup when user presses Ctrl+C:
namespace signal_handler
{
inline void restore_terminal()
{
// Show cursor, reset colors, clear screen
const char *restore = "\033[?25h\033[0m\033[H\033[J";
write(STDOUT_FILENO, restore, strlen(restore));
}
inline void signal_handler_func(int signum)
{
restore_terminal();
// Re-raise signal for normal exit
std::signal(signum, SIG_DFL);
std::raise(signum);
}
inline void install()
{
std::signal(SIGINT, signal_handler_func); // Ctrl+C
std::signal(SIGTERM, signal_handler_func); // kill
#ifndef _WIN32
std::signal(SIGHUP, signal_handler_func); // Terminal closed
#endif
}
}Why this matters:
Without cleanup:
$ ./video_player
^C
← Cursor is invisible!
← Terminal shows garbage colors
With cleanup:
$ ./video_player
^C
$ ← Normal prompt, cursor visible
The VideoPlayer class decodes video frames using FFmpeg:
class VideoPlayer
{
private:
FILE *_ffmpeg_pipe; // Pipe from FFmpeg
std::vector<uint8_t> _frame_buffer;
void open_video(const std::string &path, int width)
{
// FFmpeg command to decode video to raw RGB
std::string cmd = "ffmpeg -i \"" + path + "\" "
"-f rawvideo -pix_fmt rgb24 "
"-s " + std::to_string(width) + "x" +
std::to_string(height) + " "
"-r 30 " // 30 fps
"-loglevel quiet "
"-"; // Output to stdout
_ffmpeg_pipe = popen(cmd.c_str(), "r");
}
bool read_frame()
{
size_t frame_size = _width * _height * 3; // RGB = 3 bytes/pixel
size_t bytes_read = fread(
_frame_buffer.data(), 1, frame_size, _ffmpeg_pipe
);
return bytes_read == frame_size;
}Frame rendering loop:
void play()
{
signal_handler::start_playback();
std::cout << ansi::HIDE_CURSOR;
std::cout << ansi::CLEAR_SCREEN;
auto frame_time = std::chrono::microseconds(1000000 / 30); // 30 fps
while (read_frame()) {
auto start = std::chrono::steady_clock::now();
// Convert frame to Braille/ColorCanvas
_canvas.load_frame_rgb(_frame_buffer.data(), _width, _height);
// Output to terminal
std::cout << ansi::CURSOR_HOME;
std::cout << _canvas.render();
// Wait for next frame
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed < frame_time) {
std::this_thread::sleep_for(frame_time - elapsed);
}
}
std::cout << ansi::SHOW_CURSOR;
signal_handler::end_playback();
}For faster rendering with OpenCL:
#ifdef PYTHONIC_ENABLE_OPENCL
class GPURenderer
{
private:
cl::Context _context;
cl::CommandQueue _queue;
cl::Kernel _rgb_to_ansi_kernel;
static constexpr const char *KERNEL_SOURCE = R"(
__kernel void rgb_to_ansi(
__global const uchar* input, // RGB pixels
__global uchar* output, // Color values for ANSI
int width, int height)
{
int gid = get_global_id(0);
int char_y = gid / width;
int char_x = gid % width;
// Top and bottom pixel for half-block
int top_idx = (char_y * 2 * width + char_x) * 3;
int bot_idx = ((char_y * 2 + 1) * width + char_x) * 3;
// Output 6 bytes: top RGB + bottom RGB
int out_idx = gid * 6;
output[out_idx] = input[top_idx]; // Top R
output[out_idx+1] = input[top_idx+1]; // Top G
output[out_idx+2] = input[top_idx+2]; // Top B
output[out_idx+3] = input[bot_idx]; // Bot R
output[out_idx+4] = input[bot_idx+1]; // Bot G
output[out_idx+5] = input[bot_idx+2]; // Bot B
}
)";GPU vs CPU:
| Resolution | CPU Time | GPU Time | Speedup |
|---|---|---|---|
| 80×48 | 2ms | 0.5ms | 4× |
| 160×96 | 8ms | 1ms | 8× |
| 320×192 | 30ms | 3ms | 10× |
Enable with:
g++ -std=c++20 -DPYTHONIC_ENABLE_OPENCL ... -lOpenCLExample: Drawing a circle
#include "pythonic/pythonicDraw.hpp"
using namespace pythonic::draw;
int main() {
BrailleCanvas canvas(40, 20); // 80×80 pixel resolution
// Draw circle at center with radius 30
int cx = 40, cy = 40, r = 30;
for (int angle = 0; angle < 360; ++angle) {
double rad = angle * 3.14159 / 180.0;
int x = cx + r * cos(rad);
int y = cy + r * sin(rad);
canvas.set_pixel(x, y);
}
std::cout << canvas.render();
return 0;
}Output:
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠶⠛⠉⠀⠀⠀⠀⠉⠛⠶⢤⣀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⡴⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢦⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣆⠀⠀⠀⠀
⠀⠀⠀⢰⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡆⠀⠀⠀
⠀⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⠀⠀⠀
⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀
⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀
⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀
⠀⠀⢹⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠀
⠀⠀⠈⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠁⠀⠀
⠀⠀⠀⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡟⠀⠀⠀
⠀⠀⠀⠀⠹⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠟⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠈⠳⢤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠞⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠶⠤⣤⣤⣤⣤⣤⣤⠤⠶⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀
- LiveDraw Tutorial - Interactive drawing
- Plot Tutorial - Data visualization
- Media Tutorial - Image and video handling