Overview
Welcome to the Palette Management module! This is where we turn video frames into beautiful ASCII art. The palette system is all about choosing the right characters to represent different brightness levels—from dark spaces to light block characters, we've got options for every terminal and every aesthetic.
What does the palette system do?
The Palette Management module provides comprehensive ASCII character palette management for converting video frames to ASCII art in ascii-chat. Here's what it gives you:
- Five built-in palette types (standard, blocks, digital, minimal, cool) so you can choose the style that fits your terminal
- Custom user-defined palettes via command-line options (if you want something unique, you can create your own)
- UTF-8 character support with proper encoding handling (for terminals that support Unicode)
- Automatic UTF-8 capability detection and compatibility checking (we detect what your terminal supports and pick the right palette automatically)
- Palette validation to ensure proper character ordering (so your custom palettes work correctly)
- Luminance mapping for optimal brightness-to-character conversion (so dark pixels become dark characters and bright pixels become bright characters)
- Client compatibility checking and automatic fallback (if your terminal doesn't support UTF-8, we fall back to ASCII automatically)
Implementation: lib/palette.h
Built-in Palettes
ascii-chat provides five built-in palette types, each optimized for different visual styles and terminal capabilities. Think of them as different "art styles" for your ASCII art—some are classic and simple, others are modern and smooth. Let's explore what each one offers:
Standard Palette
The Standard palette is the classic ASCII art style—if you've ever seen ASCII art in movies or old-school terminal demos, this is probably what they were using. It's simple, reliable, and works everywhere.
Characters: ‘" ...’,;:clodxkO0KXNWM"`
This palette uses traditional ASCII characters ordered from darkest (spaces) to lightest (capital letters) for optimal brightness mapping. It's the "tried
and true" option—nothing fancy, but it works great.
What makes it special?
- Pure ASCII: No UTF-8 required, so it works in any terminal, even ancient ones
- 23 characters total: A good balance between detail and simplicity
- Wide compatibility: Works in all terminals, even terminals that don't support UTF-8
- Classic ASCII art aesthetic: The traditional look that everyone recognizes
This is probably what you want if you're not sure which palette to use—it's the safe default that works everywhere.
Blocks Palette
The Blocks palette is our high-quality option for terminals that support UTF-8. It uses Unicode block characters to create smooth gradients that look almost like real video—it's really impressive what you can do with block characters!
Characters: " ░░▒▒▓▓██"
This palette uses Unicode half-block and full-block characters to create a smooth gradient effect. The blocks range from light to dark, creating a continuous gradient that looks much smoother than ASCII characters.
What makes it special?
- Requires UTF-8 support: Your terminal needs to support UTF-8 for this to work, but most modern terminals do
- 10 characters total: Just spaces and 4 block pairs (light half-block, dark half-block, light full-block, dark full-block)
- Smooth gradient effect: The blocks create a much smoother gradient than ASCII characters
- Best visual quality for UTF-8 terminals: If your terminal supports UTF-8, this is probably the best-looking option
This is what you want if you have a modern terminal and want the best visual quality!
Digital Palette
The Digital palette has a unique glitch/digital aesthetic—think "Matrix" meets "cyberpunk terminal". It uses mathematical and geometric Unicode characters to create a technical, modern look.
Characters: " -=≡≣▰▱◼"
This palette combines mathematical symbols (equals, equivalent) with geometric shapes (squares, blocks) to create a unique visual style. It's not for everyone, but if you like the "cyberpunk hacker" aesthetic, this is for you!
What makes it special?
- Requires UTF-8 support: Needs UTF-8 for the special characters
- 10 characters total: A mix of mathematical and geometric symbols
- Unique glitch/digital aesthetic: Very distinctive look
- Technical appearance: Looks like something from a sci-fi movie
This is for when you want something unique and technical-looking.
Minimal Palette
The Minimal palette is for when you want the absolute simplest possible rendering. It uses just seven characters (spaces, dots, dashes, plus signs, asterisks, and hashes), so it's fast, simple, and works everywhere.
Characters: " .-+*#"
This is the "less is more" palette—maximum simplicity, maximum compatibility. If you're running on an ancient terminal or want the absolute fastest rendering, this is what you want.
What makes it special?
- Pure ASCII: No UTF-8 required, works everywhere
- 7 characters total: Just the basics—spaces, dots, dashes, plus, asterisk, hash
- Maximum simplicity: As simple as it gets while still being usable
- Fast rendering: Fewer characters means faster conversion
This is for minimalism lovers and performance purists.
Cool Palette
The Cool palette is another UTF-8 option that uses ascending block characters to create a smooth gradient. It's similar to the Blocks palette, but uses a different set of characters that create a slightly different look.
Characters: " ▁▂▃▄▅▆▇█"
This palette uses Unicode ascending block characters (think "stair steps going
up") to create a visually appealing gradient from dark to light. It's smooth, modern, and looks great in UTF-8 terminals.
What makes it special?
- Requires UTF-8 support: Needs UTF-8 for the ascending block characters
- 10 characters total: Spaces plus 7 ascending block characters
- Smooth gradient effect: Creates a nice smooth transition from dark to light
- Modern visual style: Clean, modern look
This is another great option if you have UTF-8 support and want smooth gradients!
UTF-8 Support
UTF-8 Capability Detection
The system automatically detects client UTF-8 support using multiple methods:
Detection Methods:
$TERM environment variable analysis
- Locale encoding detection (checks
LC_ALL, LC_CTYPE, LANG)
- Terminal type compatibility checking
- User override via
--utf8 flag
Detection Logic:
utf8_capabilities_t caps;
if (caps.utf8_support) {
} else {
}
}
bool detect_client_utf8_support(utf8_capabilities_t *caps)
palette_type_t select_compatible_palette(palette_type_t requested, bool client_utf8)
UTF-8 Character Encoding
UTF-8 palettes properly handle multi-byte characters:
Character Information:
- Byte Sequence: Up to 4 bytes per character
- Byte Length: 1-4 bytes depending on character
- Display Width: 1-2 character cells (for full-width characters)
UTF-8 Palette Structure:
void utf8_palette_destroy(utf8_palette_t *palette)
size_t utf8_palette_get_char_count(const utf8_palette_t *palette)
const utf8_char_info_t * utf8_palette_get_char(const utf8_palette_t *palette, size_t index)
utf8_palette_t * utf8_palette_create(const char *palette_string)
Important Notes:
- Character count (not byte count) is used for palette indexing
- Display width ensures proper terminal rendering
- Multi-byte characters are handled transparently
Compatibility Checking
The system automatically selects compatible palettes based on client capabilities:
palette_type_t requested = PALETTE_BLOCKS;
utf8_capabilities_t caps;
requested,
caps.utf8_support
);
Automatic Fallback:
- UTF-8 palettes (BLOCKS, DIGITAL, COOL) require UTF-8 support
- If client doesn't support UTF-8, automatically falls back to STANDARD
- ASCII palettes (STANDARD, MINIMAL) always work
- Custom palettes are validated for compatibility
Custom Palettes
Defining Custom Palettes
Users can define custom palettes via command-line options:
Command-Line Usage:
# Use custom palette characters
asciichat-client --palette-chars " .:;+=xX$&"
# Or specify custom palette in config file
palette_type = "custom"
palette_chars = " ░▒▓█"
Programmatic Usage:
const char *custom_chars = " .:;+=xX$&";
PALETTE_CUSTOM,
custom_chars
);
if (err != ASCIICHAT_OK) {
log_error("Failed to apply custom palette");
return err;
}
int apply_palette_config(palette_type_t type, const char *custom_chars)
Custom Palette Validation
Custom palettes are validated to ensure proper formatting:
Validation Rules:
- Non-empty character sequence
- Proper character ordering (if applicable)
- Valid UTF-8 encoding (for multi-byte characters)
- Compatible with client capabilities
Validation Example:
const char *custom_chars = " .:;+=xX$&";
size_t len = strlen(custom_chars);
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
utf8_capabilities_t caps;
if (!caps.utf8_support) {
log_warn("Custom palette requires UTF-8 but client doesn't support it");
return ERROR_PALETTE_INCOMPATIBLE;
}
}
bool palette_requires_utf8_encoding(const char *chars, size_t len)
bool validate_palette_chars(const char *chars, size_t len)
Palette Ordering:
- Palettes should be ordered from darkest to lightest character
- First character = darkest (for low brightness pixels)
- Last character = lightest (for high brightness pixels)
- Ordering affects luminance mapping quality
Luminance Mapping
Luminance mapping is the "secret sauce" that makes video-to-ASCII conversion fast. Instead of calculating which character to use for each pixel every time, we pre-compute a lookup table that maps brightness values directly to character indices. This makes the conversion super fast—we're talking O(1) lookup time!
Luminance Mapping Overview
Luminance mapping converts pixel brightness values (0-255) directly to palette character indices using a pre-computed lookup table. This eliminates the need for searching or calculation during video-to-ASCII conversion, which makes everything significantly faster.
How does it work?
Mapping Process:
- Pixel brightness extracted (0-255 range): We calculate how bright each pixel is (from completely black at 0 to completely white at 255)
- Direct lookup in luminance mapping table (256 entries): We use the brightness value as an index into a lookup table—no calculation needed!
- Character index retrieved (0 to palette_length-1): The lookup table tells us which character index to use
- Character selected from palette: We grab that character from the palette
- Character rendered to terminal: Finally, we render it to the terminal
The key insight is that we pre-compute the lookup table once during initialization, so during rendering we just do a simple array lookup. This is way faster than calculating which character to use for each pixel every time!
Building Luminance Maps
Luminance mapping tables are built from palette character sequences:
char luminance_mapping[256];
palette->chars,
palette->length,
luminance_mapping
);
int build_client_luminance_palette(const char *palette_chars, size_t palette_len, char luminance_mapping[256])
const palette_def_t * get_builtin_palette(palette_type_t type)
Mapping Distribution:
- Brightness 0 → Character index 0 (darkest)
- Brightness 128 → Character index length/2 (mid-tone)
- Brightness 255 → Character index length-1 (lightest)
- Mapping is proportional across the full brightness range
Performance Benefits:
- O(1) lookup time (constant time)
- No calculation required during rendering
- Pre-computed for optimal performance
- Single array access per pixel
Using Luminance Maps
Luminance maps are used during video-to-ASCII conversion:
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t brightness = calculate_pixel_brightness(pixels, x, y);
char char_index = luminance_mapping[brightness];
char ascii_char = palette_chars[char_index];
render_char(ascii_char);
}
}
Initialization and Configuration
Client Palette Initialization
Complete client palette initialization combines palette selection, validation, and luminance mapping creation:
char client_palette_chars[256];
size_t client_palette_len;
char client_luminance_palette[256];
PALETTE_BLOCKS,
NULL,
client_palette_chars,
&client_palette_len,
client_luminance_palette
);
if (err != ASCIICHAT_OK) {
log_error("Failed to initialize client palette");
return err;
}
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
Applying Palette Configuration
Global palette configuration is applied using apply_palette_config():
PALETTE_STANDARD,
NULL
);
PALETTE_CUSTOM,
" .:;+=xX$&"
);
Configuration Steps:
- Validate palette (if custom)
- Check UTF-8 requirements
- Verify client compatibility
- Apply palette globally
- Build luminance mapping
- Make available for rendering
Architecture
Data Structures
Palette Definition:
typedef struct {
const char *name;
const char *chars;
size_t length;
bool requires_utf8;
bool is_validated;
} palette_def_t;
UTF-8 Character Information:
typedef struct {
char bytes[4];
uint8_t byte_len;
} utf8_char_info_t;
int display_width(const char *text)
UTF-8 Palette:
typedef struct {
utf8_char_info_t *chars;
size_t char_count;
size_t total_bytes;
char *raw_string;
} utf8_palette_t;
UTF-8 Capabilities:
typedef struct {
bool utf8_support;
bool forced_utf8;
char terminal_type[32];
char locale_encoding[16];
} utf8_capabilities_t;
API Functions
Palette Management:
UTF-8 Palette Functions:
UTF-8 Detection:
Usage Examples
Basic Palette Usage
Using Built-in Palette:
#include "palette.h"
if (palette->requires_utf8) {
utf8_capabilities_t caps;
log_error("Palette requires UTF-8 but terminal doesn't support it");
return ERROR_PALETTE_INCOMPATIBLE;
}
}
Custom Palette Usage
Defining and Using Custom Palette:
const char *custom_chars = " .:-=+*#%@";
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
PALETTE_CUSTOM,
custom_chars
);
if (err != ASCIICHAT_OK) {
log_error("Failed to apply custom palette");
return err;
}
UTF-8 Palette Usage
Working with UTF-8 Palettes:
size_t utf8_palette_find_char_index(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
Client Palette Initialization
Complete Client Palette Setup:
utf8_capabilities_t caps;
PALETTE_BLOCKS,
caps.utf8_support
);
char client_palette_chars[256];
size_t client_palette_len;
char client_luminance_palette[256];
palette_type,
NULL,
client_palette_chars,
&client_palette_len,
client_luminance_palette
);
Best Practices
Character Ordering
Always Order from Dark to Light:
- First character = darkest (for black/low brightness)
- Last character = lightest (for white/high brightness)
- Proper ordering ensures accurate luminance mapping
- Incorrect ordering produces inverted or incorrect output
Example Ordering:
UTF-8 Compatibility
Always Check UTF-8 Requirements:
- Check if palette requires UTF-8 before using
- Verify client UTF-8 support for UTF-8 palettes
- Provide fallback to ASCII palettes
- Use
select_compatible_palette() for automatic selection
Example:
utf8_capabilities_t caps;
if (!caps.utf8_support) {
log_warn("UTF-8 palette incompatible with client");
}
}
Always Validate Custom Palettes
Validate Before Use:
- Always validate custom palettes
- Check for empty or invalid sequences
- Verify UTF-8 encoding for multi-byte characters
- Ensure proper character ordering
Example:
log_error("Invalid custom palette");
return ERROR_INVALID_PALETTE;
}
Performance Optimization
Use Luminance Mapping:
- Always build luminance mapping for performance
- Use direct lookup instead of calculation
- Pre-compute mappings during initialization
- Cache mappings for repeated use
Example:
char luminance_mapping[256];
char char_index = luminance_mapping[pixel_brightness];
char ascii_char = palette_chars[char_index];
Performance Characteristics
Palette performance is all about speed—we're converting video frames to ASCII in real-time, so every optimization matters. The luminance mapping system makes this super fast, but let's look at what you can expect:
How fast is palette lookup?
Palette Lookup:
- Luminance mapping: O(1) constant time lookup—that means it takes the same amount of time regardless of how many characters are in the palette
- No calculation required during rendering: We just do a simple array lookup—no math, no searching, just grab the value from the table
- Single array access per pixel: One lookup, one character selection, done!
- Optimal for real-time video conversion: Fast enough that you can convert video frames at 60 FPS without any problems
How fast is UTF-8 processing?
UTF-8 Processing:
- UTF-8 palette creation: O(n) where n is byte length—this happens once during initialization, so it's not a bottleneck
- Character index lookup: O(1) with pre-computed structure—once the palette is built, lookups are instant
- UTF-8 detection: O(1) environment variable checks—we just check a few environment variables, which is essentially free
How much memory does it use?
Memory Usage:
- ASCII palette: ~23 bytes (STANDARD palette)—super tiny!
- UTF-8 palette: ~40-100 bytes depending on character count (still very small, even for larger palettes)
- Luminance mapping: 256 bytes (fixed size)—this is the lookup table, and it's always 256 bytes regardless of palette size
- UTF-8 character info: ~8 bytes per character—just a tiny bit of metadata per character to track byte length and display width
Memory usage is minimal—even with UTF-8 palettes, you're looking at well under a kilobyte per palette. The luminance mapping table is always 256 bytes (one byte per possible brightness value), which is nothing by modern standards.
Integration with Video-to-ASCII
The palette module integrates with the video conversion pipeline:
Conversion Pipeline:
- Video frame captured (RGB pixels)
- Pixel brightness calculated (0-255)
- Luminance mapping lookup (brightness → char index)
- Character selected from palette
- Character rendered to terminal
Example Integration:
char palette_chars[256];
size_t palette_len;
char luminance_mapping[256];
palette_chars, &palette_len, luminance_mapping);
for (int y = 0; y < frame_height; y++) {
for (int x = 0; x < frame_width; x++) {
uint8_t brightness = get_pixel_brightness(frame, x, y);
size_t char_index = luminance_mapping[brightness];
char ascii_char = palette_chars[char_index];
render_char(ascii_char);
}
}
- See also
- palette.h
-
Video to ASCII Conversion
-
Video Frame Buffers