AsciiCam/C/src/ascii.c

792 lines
24 KiB
C

// NOTE:
// SIMD paths:
// - x86_64 Linux/macOS: SSE2 via <immintrin.h>
// - ARM64 macOS: NEON via <arm_neon.h>
#include "nolibc.h"
#include "ascii.h"
#include "platform.h"
#include <stdint.h>
#include <sys/inotify.h>
// Helpers
static inline uint8_t clamp_u8(int v) {
return (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v;
}
static inline int my_abs(int x) { return x < 0 ? -x : x; }
static inline double my_sqrt(double x) {
if (x <= 0.0)
return 0.0;
double guess = x;
// initial halve the exponent by repeated division
while (guess > 1.0) {
guess /= 2.0;
}
if (guess <= 0.0)
guess = 1.0;
for (int i = 0; i < 12; i++) {
guess = 0.5 * (guess + x / guess);
}
return guess;
}
// YUYV to grayscale
#if defined(ARCH_X86_64)
#include <immintrin.h>
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
__m128i mask = _mm_set1_epi16(0x00FF);
int i = 0;
for (; i + 16 <= total; i += 16) {
__m128i lo = _mm_loadu_si128((__m128i *)(yuyv + i * 2));
__m128i hi = _mm_loadu_si128((__m128i *)(yuyv + i * 2 + 16));
lo = _mm_and_si128(lo, mask);
hi = _mm_and_si128(hi, mask);
_mm_storeu_si128((__m128i *)(gray + i), _mm_packus_epi16(lo, hi));
}
for (; i < total; i++)
gray[i] = yuyv[i * 2];
}
#elif defined(ARCH_ARM64)
#include <arm_neon.h>
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
// NEON: process 8 YUYV pairs (= 16 px) per iteration
int i = 0;
for (; i + 16 <= total; i += 16) {
// Load 32 bytes: [Y0 U0 Y1 V0 Y2 U1 Y3 V1 ...]
uint8x16x2_t yuv = vld2q_u8(yuyv + i * 2);
// yuv.val[0] = all Y bytes (even bytes = luma)
vst1q_u8(gray + i, yuv.val[0]);
}
for (; i < total; i++)
gray[i] = yuyv[i * 2];
}
#else
// fallback
void yuyv_to_gray_simd(const uint8_t *yuyv, uint8_t *gray, int width,
int height) {
int total = width * height;
for (int i = 0; i < total; i++)
gray[i] = yuyv[i * 2];
}
#endif
void yuyv_to_rgb(const uint8_t *yuyv, uint8_t *rgb, int width, int height) {
int pairs = (width * height) / 2;
for (int i = 0; i < pairs; i++) {
int y0 = yuyv[i * 4 + 0], u = yuyv[i * 4 + 1];
int y1 = yuyv[i * 4 + 2], v = yuyv[i * 4 + 3];
int d = u - 128, e = v - 128;
for (int p = 0; p < 2; p++) {
int c = ((p == 0) ? y0 : y1) - 16;
uint8_t *px = rgb + (i * 2 + p) * 3;
px[0] = clamp_u8((298 * c + 409 * e + 128) >> 8);
px[1] = clamp_u8((298 * c - 100 * d - 208 * e + 128) >> 8);
px[2] = clamp_u8((298 * c + 516 * d + 128) >> 8);
}
}
}
// Buffer sizing for multi-byte UTF-8 Braille (3 bytes per block) + color codes
size_t ascii_out_size(int dst_w, int dst_h, int color) {
return ascii_out_size_for_mode(dst_w, dst_h, color, RENDER_BRAILLE);
}
size_t ascii_out_size_for_mode(int dst_w, int dst_h, int color,
render_mode_t mode) {
int braille_term_w = dst_w / 2;
int braille_term_h = dst_h / 4;
switch (mode) {
case RENDER_HALF_BLOCK: {
int tw = dst_w;
int th = dst_h / 2;
size_t per_cell = color ? 41 : 3;
return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1;
}
case RENDER_BLOCKS:
case RENDER_ASCII_RAMP:
case RENDER_DOTS: {
int tw = braille_term_w;
int th = braille_term_h;
size_t per_cell = color ? 22 : 3; // worst case: 3-byte glyph
return 3 + (size_t)th * ((size_t)tw * per_cell + 5) + 1;
}
case RENDER_BRAILLE:
default: {
int tw = braille_term_w;
int th = braille_term_h;
if (color)
return 3 + (size_t)th * ((size_t)tw * 22 + 5) + 1;
return 3 + (size_t)th * ((size_t)tw * 3 + 1) + 1;
}
}
}
// Sobel edge detection
static void sobel(const uint8_t *in, uint8_t *out, int w, int h) {
static const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
static const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
int gx = 0, gy = 0;
for (int ky = -1; ky <= 1; ky++)
for (int kx = -1; kx <= 1; kx++) {
int p = in[(y + ky) * w + (x + kx)];
gx += Gx[ky + 1][kx + 1] * p;
gy += Gy[ky + 1][kx + 1] * p;
}
int mag = my_abs(gx) + my_abs(gy);
out[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag);
}
}
}
static void sobel_dir(const uint8_t *in, uint8_t *out_mag, uint8_t *out_dir,
int w, int h) {
static const int Gx[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
static const int Gy[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
int gx = 0, gy = 0;
for (int ky = -1; ky <= 1; ky++)
for (int kx = -1; kx <= 1; kx++) {
int p = in[(y + ky) * w + (x + kx)];
gx += Gx[ky + 1][kx + 1] * p;
gy += Gy[ky + 1][kx + 1] * p;
}
int mag = my_abs(gx) + my_abs(gy);
out_mag[y * w + x] = (uint8_t)(mag > 255 ? 255 : mag);
uint8_t dir;
int agx = my_abs(gx), agy = my_abs(gy);
if (agx == 0 && agy == 0) {
dir = 0;
} else if (agy * 5 < agx * 2) {
dir = 2;
} else if (agx * 5 < agy * 2) {
dir = 0;
} else {
dir = ((gx > 0) == (gy > 0)) ? 3 : 1;
}
out_dir[y * w + x] = dir;
}
}
}
static void laplacian(const uint8_t *in, uint8_t *out, int w, int h) {
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
int center = in[y * w + x];
int lap = 4 * center - in[(y - 1) * w + x] - in[(y + 1) * w + x] -
in[y * w + (x - 1)] - in[y * w + (x + 1)];
out[y * w + x] = (uint8_t)(my_abs(lap) > 255 ? 255 : my_abs(lap));
}
}
}
static inline uint8_t get_braille_bitmask(int dx, int dy) {
static const uint8_t braille_dots[2][4] = {
{0x01, 0x02, 0x04, 0x40}, // Left layout column: dots 1, 2, 3, 7
{0x08, 0x10, 0x20, 0x80} // Right layout column: dots 4, 5, 6, 8
};
return braille_dots[dx][dy];
}
// Charset registry: hot-reloadable character ramps from a directory.
static void _trim_line(char *s) {
// Cut at first newline/CR, then trim trailing whitespace.
for (char *p = s; *p; p++) {
if (*p == '\n' || *p == '\r') {
*p = '\0';
break;
}
}
size_t len = nl_strlen(s);
while (len > 0 && (s[len - 1] == ' ' || s[len - 1] == '\t')) {
s[--len] = '\0';
}
}
static void _name_from_path(const char *path, char *out, size_t out_sz) {
char tmp[256];
nl_strncpy_safe(tmp, path, sizeof(tmp));
const char *base = basename(tmp);
nl_strncpy_safe(out, base, out_sz);
// strip extension
for (size_t i = 0; out[i]; i++) {
if (out[i] == '.') {
out[i] = '\0';
break;
}
}
}
int charset_registry_init(charset_registry_t *reg, const char *dir) {
nl_memset(reg, 0, sizeof(*reg));
reg->inotify_fd = -1;
reg->inotify_wd = -1;
nl_strncpy_safe(reg->dir_path, dir, sizeof(reg->dir_path));
nl_strncpy_safe(reg->sets[0].name, "default", CHARSET_NAME_LEN);
nl_strncpy_safe(reg->sets[0].ramp, ASCII_CHARS_DEFAULT, CHARSET_RAMP_LEN);
reg->count = 1;
reg->active = 0;
charset_registry_scan(reg);
reg->inotify_fd = inotify_init1(IN_NONBLOCK);
if (reg->inotify_fd >= 0) {
reg->inotify_wd =
inotify_add_watch(reg->inotify_fd, dir,
IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_DELETE);
}
return 0;
}
// TEST:
void charset_registry_scan(charset_registry_t *reg) {
static const char *known[] = {"blocks", "ascii", "dots", "shades",
"minimal", "emoji", "lines", NULL};
for (int i = 0; known[i] && reg->count < MAX_CHARSETS; i++) {
char path[256];
nl_snprintf(path, sizeof(path), "%s/%s.txt", reg->dir_path, known[i]);
int fd = open(path, O_RDONLY, 0);
if (fd < 0)
continue;
char buf[CHARSET_RAMP_LEN];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0)
continue;
buf[n] = '\0';
_trim_line(buf);
if (buf[0] == '\0')
continue;
// Skip if charset with name is already loaded
int dup = 0;
for (int j = 0; j < reg->count; j++) {
char nm[CHARSET_NAME_LEN];
_name_from_path(path, nm, sizeof(nm));
if (nl_strcmp(reg->sets[j].name, nm) == 0) {
nl_strncpy_safe(reg->sets[j].ramp, buf, CHARSET_RAMP_LEN);
dup = 1;
break;
}
}
if (dup)
continue;
_name_from_path(path, reg->sets[reg->count].name, CHARSET_NAME_LEN);
nl_strncpy_safe(reg->sets[reg->count].ramp, buf, CHARSET_RAMP_LEN);
reg->count++;
}
}
void charset_registry_check_reload(charset_registry_t *reg) {
if (reg->inotify_fd < 0)
return;
char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
ssize_t n = read(reg->inotify_fd, buf, sizeof(buf));
if (n <= 0)
return;
charset_registry_scan(reg);
}
const char *charset_registry_active_ramp(const charset_registry_t *reg) {
if (reg->count == 0)
return ASCII_CHARS_DEFAULT;
int idx = reg->active;
if (idx < 0 || idx >= reg->count)
idx = 0;
return reg->sets[idx].ramp;
}
void charset_registry_cleanup(charset_registry_t *reg) {
if (reg->inotify_fd >= 0) {
close(reg->inotify_fd);
reg->inotify_fd = -1;
}
}
// Grayscale to ASCII
// Block-shade ramp
static const char *BLOCK_SHADE_UTF8[] = {" ", "\xe2\x96\x91", "\xe2\x96\x92",
"\xe2\x96\x93", "\xe2\x96\x88"};
#define BLOCK_SHADE_COUNT 5
// Dot ramp
static const char *DOT_UTF8[] = {" ", "\xc2\xb7", "\xe2\x80\xa2",
"\xe2\x97\x8f"};
#define DOT_COUNT 4
// Half block glyph
static const char HALF_BLOCK_UTF8[] = "\xe2\x96\x80";
const char *render_mode_name(render_mode_t m) {
switch (m) {
case RENDER_BRAILLE:
return "braille";
case RENDER_BLOCKS:
return "blocks";
case RENDER_ASCII_RAMP:
return "ascii";
case RENDER_HALF_BLOCK:
return "halfblock";
case RENDER_DOTS:
return "dots";
default:
return "?";
}
}
const char *edge_mode_name(edge_mode_t m) {
switch (m) {
case EDGE_OFF:
return "off";
case EDGE_SOBEL:
return "sobel";
case EDGE_SOBEL_DIR:
return "sobel-dir";
case EDGE_LAPLACIAN:
return "laplacian";
default:
return "?";
}
}
static int emit_glyph(char *out, size_t out_size, int out_idx,
const char *glyph, int do_color, int r, int g, int b) {
if (do_color) {
int written = snprintf(out + out_idx, out_size - (size_t)out_idx,
"\033[38;2;%d;%d;%dm%s", r, g, b, glyph);
if (written > 0 && (size_t)(out_idx + written) < out_size)
out_idx += written;
} else {
size_t glen = nl_strlen(glyph);
if ((size_t)(out_idx + (int)glen) < out_size) {
nl_memcpy(out + out_idx, glyph, glen);
out_idx += (int)glen;
}
}
return out_idx;
}
int grayscale_to_ascii(const uint8_t *gray, const uint8_t *rgb, int src_w,
int src_h, int dst_w, int dst_h, char *out,
size_t out_size, const ascii_opts_t *opts) {
int safe_dst_w = dst_w - (dst_w % 2);
int safe_dst_h = dst_h - (dst_h % 4);
int brightness = opts ? opts->brightness : 0;
int contrast = opts ? opts->contrast : 100;
int invert = opts ? opts->invert : 0;
int do_color = opts && opts->color && rgb;
edge_mode_t edge_mode = opts ? opts->edges : EDGE_OFF;
int do_dither = opts ? opts->dither : 0;
int thresh_limit = opts ? opts->threshold_val : 35;
render_mode_t render_mode = opts ? opts->render_mode : RENDER_BRAILLE;
const char *ramp = (opts && opts->charset && opts->charset[0])
? opts->charset
: ASCII_CHARS_DEFAULT;
int ramp_len = (int)nl_strlen(ramp);
if (ramp_len < 1) {
ramp = ASCII_CHARS_DEFAULT;
ramp_len = (int)nl_strlen(ramp);
}
int depth_pop = opts ? opts->depth_pop : 0;
int depth_invert = opts ? opts->depth_invert : 0;
double bw = (double)src_w / safe_dst_w;
double bh = (double)src_h / safe_dst_h;
uint8_t *subpixel_g = malloc((size_t)(safe_dst_w * safe_dst_h));
uint8_t *subpixel_rgb =
do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL;
if (!subpixel_g || (do_color && !subpixel_rgb)) {
free(subpixel_g);
free(subpixel_rgb);
return -1;
}
// Downscale and interpolate the high-res frames to subpixel boundaries
for (int y = 0; y < safe_dst_h; y++) {
int ys = (int)(y * bh), ye = (int)((y + 1) * bh);
if (ye <= ys)
ye = ys + 1;
for (int x = 0; x < safe_dst_w; x++) {
int xs = (int)(x * bw), xe = (int)((x + 1) * bw);
if (xe <= xs)
xe = xs + 1;
long tg = 0, tr = 0, tgv = 0, tb = 0;
int count = 0;
for (int sy = ys; sy < ye && sy < src_h; sy++) {
for (int sx = xs; sx < xe && sx < src_w; sx++) {
tg += gray[sy * src_w + sx];
if (do_color) {
const uint8_t *px = rgb + (sy * src_w + sx) * 3;
tr += px[0];
tgv += px[1];
tb += px[2];
}
count++;
}
}
if (!count)
count = 1;
int gv = (int)(tg / count);
if (contrast != 100)
gv = 128 + (gv - 128) * contrast / 100;
gv += brightness;
subpixel_g[y * safe_dst_w + x] = clamp_u8(gv);
if (do_color) {
uint8_t *op = subpixel_rgb + (y * safe_dst_w + x) * 3;
op[0] = clamp_u8((int)(tr / count));
op[1] = clamp_u8((int)(tgv / count));
op[2] = clamp_u8((int)(tb / count));
}
}
}
// TEST:
// Pseudo-3D "pop out" parallax effect.
if (depth_pop > 0) {
uint8_t *warped_g = malloc((size_t)(safe_dst_w * safe_dst_h));
uint8_t *warped_rgb =
do_color ? malloc((size_t)(safe_dst_w * safe_dst_h * 3)) : NULL;
if (warped_g && (!do_color || warped_rgb)) {
double cx = safe_dst_w / 2.0;
double cy = safe_dst_h / 2.0;
double max_r = (cx > cy ? cx : cy);
double strength = (depth_pop / 100.0) * (max_r * 0.35);
for (int y = 0; y < safe_dst_h; y++) {
for (int x = 0; x < safe_dst_w; x++) {
uint8_t luma = subpixel_g[y * safe_dst_w + x];
double depth01 = depth_invert ? (255 - luma) / 255.0 : luma / 255.0;
double dx = x - cx, dy = y - cy;
double r = (dx * dx + dy * dy);
double rlen = r > 0.0001 ? my_sqrt(r) : 0.0001;
double nx = dx / rlen, ny = dy / rlen;
double rnorm = rlen / (max_r > 0.0001 ? max_r : 0.0001);
double disp = strength * depth01 * rnorm;
int sx = (int)(x + nx * disp);
int sy = (int)(y + ny * disp);
if (sx < 0)
sx = 0;
if (sx >= safe_dst_w)
sx = safe_dst_w - 1;
if (sy < 0)
sy = 0;
if (sy >= safe_dst_h)
sy = safe_dst_h - 1;
warped_g[y * safe_dst_w + x] = subpixel_g[sy * safe_dst_w + sx];
if (do_color) {
const uint8_t *src_px = subpixel_rgb + (sy * safe_dst_w + sx) * 3;
uint8_t *dst_px = warped_rgb + (y * safe_dst_w + x) * 3;
dst_px[0] = src_px[0];
dst_px[1] = src_px[1];
dst_px[2] = src_px[2];
}
}
}
nl_memcpy(subpixel_g, warped_g, (size_t)(safe_dst_w * safe_dst_h));
if (do_color)
nl_memcpy(subpixel_rgb, warped_rgb,
(size_t)(safe_dst_w * safe_dst_h * 3));
}
free(warped_g);
free(warped_rgb);
}
// Edge detection dispatch
uint8_t *dir_buf = NULL;
if (edge_mode != EDGE_OFF) {
uint8_t *eb = calloc((size_t)(safe_dst_w * safe_dst_h), 1);
if (eb) {
switch (edge_mode) {
case EDGE_SOBEL:
sobel(subpixel_g, eb, safe_dst_w, safe_dst_h);
break;
case EDGE_SOBEL_DIR:
dir_buf = calloc((size_t)(safe_dst_w * safe_dst_h), 1);
if (dir_buf)
sobel_dir(subpixel_g, eb, dir_buf, safe_dst_w, safe_dst_h);
else
sobel(subpixel_g, eb, safe_dst_w, safe_dst_h);
break;
case EDGE_LAPLACIAN:
laplacian(subpixel_g, eb, safe_dst_w, safe_dst_h);
break;
default:
break;
}
nl_memcpy(subpixel_g, eb, (size_t)(safe_dst_w * safe_dst_h));
free(eb);
}
}
// Floyd-Steinberg dithering on the subpixel grayscale buffer
if (do_dither) {
int16_t *err = calloc((size_t)(safe_dst_w * safe_dst_h), sizeof(int16_t));
if (err) {
for (int i = 0; i < safe_dst_w * safe_dst_h; i++)
err[i] = (int16_t)subpixel_g[i];
for (int y = 0; y < safe_dst_h; y++) {
for (int x = 0; x < safe_dst_w; x++) {
int idx = y * safe_dst_w + x;
int16_t old = err[idx];
int16_t nval = (old > (int16_t)thresh_limit) ? 255 : 0;
int16_t qerr = old - nval;
err[idx] = nval;
if (x + 1 < safe_dst_w)
err[idx + 1] += (qerr * 7) >> 4;
if (y + 1 < safe_dst_h) {
if (x > 0)
err[idx + safe_dst_w - 1] += (qerr * 3) >> 4;
err[idx + safe_dst_w] += (qerr * 5) >> 4;
if (x + 1 < safe_dst_w)
err[idx + safe_dst_w + 1] += (qerr * 1) >> 4;
}
}
}
for (int i = 0; i < safe_dst_w * safe_dst_h; i++) {
int16_t v = err[i];
subpixel_g[i] = (v < 0) ? 0 : (v > 255) ? 255 : (uint8_t)v;
}
free(err);
}
}
int out_idx = 0;
static const char HOME[] = "\033[H";
if (out_size > sizeof(HOME)) {
nl_memcpy(out, HOME, sizeof(HOME) - 1);
out_idx = sizeof(HOME) - 1;
}
if (render_mode == RENDER_HALF_BLOCK) {
int term_w = safe_dst_w;
int term_h = safe_dst_h / 2;
for (int ty = 0; ty < term_h; ty++) {
for (int tx = 0; tx < term_w; tx++) {
int top_idx = (ty * 2) * safe_dst_w + tx;
int bot_idx = (ty * 2 + 1) * safe_dst_w + tx;
uint8_t top_l = subpixel_g[top_idx];
uint8_t bot_l = subpixel_g[bot_idx];
if (do_color) {
const uint8_t *tp = subpixel_rgb + top_idx * 3;
const uint8_t *bp = subpixel_rgb + bot_idx * 3;
int written =
snprintf(out + out_idx, out_size - (size_t)out_idx,
"\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm%s", tp[0], tp[1],
tp[2], bp[0], bp[1], bp[2], HALF_BLOCK_UTF8);
if (written > 0 && (size_t)(out_idx + written) < out_size)
out_idx += written;
} else {
int active = (top_l > thresh_limit) || (bot_l > thresh_limit);
if (invert)
active = !active;
const char *glyph = active ? "\xe2\x96\x88" : " ";
out_idx = emit_glyph(out, out_size, out_idx, glyph, 0, 0, 0, 0);
}
}
if (do_color) {
int written =
snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
if (written > 0 && (size_t)(out_idx + written) < out_size)
out_idx += written;
} else {
if ((size_t)(out_idx + 1) < out_size)
out[out_idx++] = '\n';
}
}
out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0';
free(subpixel_g);
free(subpixel_rgb);
free(dir_buf);
return out_idx;
}
// BRAILLE / BLOCKS / ASCII_RAMP / DOTS
int term_w = safe_dst_w / 2;
int term_h = safe_dst_h / 4;
for (int ty = 0; ty < term_h; ty++) {
for (int tx = 0; tx < term_w; tx++) {
uint8_t braille_offset = 0;
long sum_r = 0, sum_g = 0, sum_b = 0;
long sum_luma = 0;
int colored_subpixels = 0;
int active_count = 0;
uint8_t dom_dir = 0;
int dom_dir_votes[4] = {0, 0, 0, 0};
for (int dy = 0; dy < 4; dy++) {
for (int dx = 0; dx < 2; dx++) {
int sub_x = tx * 2 + dx;
int sub_y = ty * 4 + dy;
int pixel_idx = sub_y * safe_dst_w + sub_x;
uint8_t luma = subpixel_g[pixel_idx];
sum_luma += luma;
int is_active = (luma > thresh_limit);
if (invert)
is_active = !is_active;
if (is_active) {
braille_offset |= get_braille_bitmask(dx, dy);
active_count++;
if (dir_buf)
dom_dir_votes[dir_buf[pixel_idx]]++;
}
if (do_color) {
const uint8_t *px = subpixel_rgb + pixel_idx * 3;
sum_r += px[0];
sum_g += px[1];
sum_b += px[2];
colored_subpixels++;
}
}
}
if (dir_buf) {
int best = 0;
for (int d = 1; d < 4; d++)
if (dom_dir_votes[d] > dom_dir_votes[best])
best = d;
dom_dir = (uint8_t)best;
}
int avg_r = 0, avg_g = 0, avg_b = 0;
if (do_color && colored_subpixels > 0) {
avg_r = (int)(sum_r / colored_subpixels);
avg_g = (int)(sum_g / colored_subpixels);
avg_b = (int)(sum_b / colored_subpixels);
}
int avg_luma = (int)(sum_luma / 8);
switch (render_mode) {
case RENDER_BRAILLE: {
uint32_t unicode_val = 0x2800 + braille_offset;
char utf8_seq[4];
utf8_seq[0] = (char)(0xE0 | ((unicode_val >> 12) & 0x0F));
utf8_seq[1] = (char)(0x80 | ((unicode_val >> 6) & 0x3F));
utf8_seq[2] = (char)(0x80 | (unicode_val & 0x3F));
utf8_seq[3] = '\0';
out_idx = emit_glyph(out, out_size, out_idx, utf8_seq, do_color, avg_r,
avg_g, avg_b);
break;
}
case RENDER_BLOCKS: {
int shade_idx = avg_luma * BLOCK_SHADE_COUNT / 256;
if (shade_idx >= BLOCK_SHADE_COUNT)
shade_idx = BLOCK_SHADE_COUNT - 1;
if (invert)
shade_idx = BLOCK_SHADE_COUNT - 1 - shade_idx;
out_idx =
emit_glyph(out, out_size, out_idx, BLOCK_SHADE_UTF8[shade_idx],
do_color, avg_r, avg_g, avg_b);
break;
}
case RENDER_DOTS: {
int dot_idx = avg_luma * DOT_COUNT / 256;
if (dot_idx >= DOT_COUNT)
dot_idx = DOT_COUNT - 1;
if (invert)
dot_idx = DOT_COUNT - 1 - dot_idx;
out_idx = emit_glyph(out, out_size, out_idx, DOT_UTF8[dot_idx],
do_color, avg_r, avg_g, avg_b);
break;
}
case RENDER_ASCII_RAMP:
default: {
char glyph[2] = {0, 0};
if (dir_buf && active_count > 0) {
static const char dirs[4] = {'-', '/', '|', '\\'};
glyph[0] = dirs[dom_dir];
} else {
int ramp_idx = avg_luma * ramp_len / 256;
if (ramp_idx >= ramp_len)
ramp_idx = ramp_len - 1;
if (invert)
ramp_idx = ramp_len - 1 - ramp_idx;
glyph[0] = ramp[ramp_idx];
}
out_idx = emit_glyph(out, out_size, out_idx, glyph, do_color, avg_r,
avg_g, avg_b);
break;
}
}
}
if (do_color) {
int written =
snprintf(out + out_idx, out_size - (size_t)out_idx, "\033[0m\n");
if (written > 0 && (size_t)(out_idx + written) < out_size)
out_idx += written;
} else {
if ((size_t)(out_idx + 1) < out_size)
out[out_idx++] = '\n';
}
}
out[(size_t)out_idx < out_size ? (size_t)out_idx : out_size - 1] = '\0';
free(subpixel_g);
free(subpixel_rgb);
free(dir_buf);
return out_idx;
}
// FPS Overlay Configuration
void overlay_fps_box(int dst_w, double fps, int color_enabled) {
char buf[80];
int term_w = dst_w / 2;
int col = (term_w - 13) / 2 + 1;
if (col < 1)
col = 1;
char fpsbuf[10];
nl_fmt_fps(fpsbuf, sizeof(fpsbuf), fps);
int n;
if (color_enabled)
n = snprintf(
buf, sizeof(buf),
"\033[1;%dH\033[38;2;0;255;0m\033[48;2;30;30;30m[ FPS: %s ]\033[0m",
col, fpsbuf);
else
n = snprintf(buf, sizeof(buf), "\033[1;%dH[ FPS: %s ]", col, fpsbuf);
if (n > 0 && n < (int)sizeof(buf))
(void)write(STDOUT_FILENO, buf, (size_t)n);
}