Files
kitty/kitty/decorations.c
copilot-swe-agent[bot] 74c3bbe06d Implement software rendering for Unicode 16 legacy computing symbols
Add rendering for codepoints U+1FBCE-U+1FBEF, U+1CC1B-U+1CC3F,
and U+1CE16-U+1CE19 in decorations.c with registration in fonts.c
and test coverage in kitty_tests/fonts.py.

Characters implemented:
- U+1FBCE-1FBCF: Left two-thirds and one-third blocks
- U+1FBD0-1FBDF: 16 diagonal box drawing characters
- U+1FBE0-1FBE3: Justified half white circles (outlines)
- U+1FBE4-1FBE5: Upper/lower centre quarter blocks
- U+1FBE8-1FBEB: Justified half black circles (filled)
- U+1FBEC-1FBEF: Justified quarter black circles (filled)
- U+1CC1B-1CC1E: Box drawing variants with offset junctions
- U+1CC1F-1CC20: Double diagonal lines
- U+1CC30-1CC3F: Twelfth and quarter circle arcs
- U+1CE16-1CE19: Box drawings light vertical T-junctions

Fixes #9851
2026-04-11 14:17:21 +05:30

2321 lines
97 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* decorations.c
* Copyright (C) 2024 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "decorations.h"
#include "state.h"
typedef uint32_t uint;
static uint max(uint a, uint b) { return a > b ? a : b; }
static uint min(uint a, uint b) { return a < b ? a : b; }
// Decorations {{{
#define STRAIGHT_UNDERLINE_LOOP \
unsigned half = fcm.underline_thickness / 2; \
DecorationGeometry ans = {.top = half > fcm.underline_position ? 0 : fcm.underline_position - half}; \
for (unsigned y = ans.top; fcm.underline_thickness > 0 && y < fcm.cell_height; fcm.underline_thickness--, y++, ans.height++)
DecorationGeometry
add_straight_underline(uint8_t *buf, FontCellMetrics fcm) {
STRAIGHT_UNDERLINE_LOOP {
memset(buf + fcm.cell_width * y, 0xff, fcm.cell_width * sizeof(buf[0]));
}
return ans;
}
DecorationGeometry
add_strikethrough(uint8_t *buf, FontCellMetrics fcm) {
unsigned half = fcm.strikethrough_thickness / 2;
DecorationGeometry ans = {.top = half > fcm.strikethrough_position ? 0 : fcm.strikethrough_position - half};
for (unsigned y = ans.top; fcm.strikethrough_thickness > 0 && y < fcm.cell_height; fcm.strikethrough_thickness--, y++, ans.height++) {
memset(buf + fcm.cell_width * y, 0xff, fcm.cell_width * sizeof(buf[0]));
}
return ans;
}
DecorationGeometry
add_missing_glyph(uint8_t *buf, FontCellMetrics fcm) {
DecorationGeometry ans = {.height=fcm.cell_height};
unsigned thickness = min(fcm.underline_thickness, fcm.strikethrough_thickness);
thickness = min(thickness, fcm.cell_width);
for (unsigned y = 0; y < ans.height; y++) {
uint8_t *line = buf + fcm.cell_width * y;
if (y < thickness || y >= ans.height - thickness) memset(line, 0xff, fcm.cell_width);
else {
memset(line, 0xff, thickness);
memset(line + fcm.cell_width - thickness, 0xff, thickness);
}
}
return ans;
}
DecorationGeometry
add_double_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned a = fcm.underline_position > fcm.underline_thickness ? fcm.underline_position - fcm.underline_thickness : 0;
a = min(a, fcm.cell_height - 1);
unsigned b = min(fcm.underline_position, fcm.cell_height - 1);
unsigned top = min(a, b), bottom = max(a, b);
int deficit = 2 - (bottom - top);
if (deficit > 0) {
if (bottom + deficit < fcm.cell_height) bottom += deficit;
else if (bottom < fcm.cell_height - 1) {
bottom += 1;
if (deficit > 1) top -= deficit - 1;
} else top -= deficit;
}
top = max(0u, min(top, fcm.cell_height - 1u));
bottom = max(0u, min(bottom, fcm.cell_height - 1u));
memset(buf + fcm.cell_width * top, 0xff, fcm.cell_width);
memset(buf + fcm.cell_width * bottom, 0xff, fcm.cell_width);
DecorationGeometry ans = {.top=top, .height = bottom + 1 - top};
return ans;
}
static unsigned
distribute_dots(unsigned available_space, unsigned num_of_dots, unsigned *summed_gaps, unsigned *gaps) {
unsigned dot_size = max(1u, available_space / (2u * num_of_dots));
unsigned extra = 2 * num_of_dots * dot_size;
extra = available_space > extra ? available_space - extra : 0;
for (unsigned i = 0; i < num_of_dots; i++) gaps[i] = dot_size;
if (extra > 0) {
unsigned idx = 0;
while (extra > 0) {
gaps[idx] += 1;
idx = (idx + 1) % num_of_dots;
extra--;
}
}
gaps[0] /= 2;
for (unsigned i = 0; i < num_of_dots; i++) {
summed_gaps[i] = 0;
for (unsigned g = 0; g <= i; g++) summed_gaps[i] += gaps[g];
}
return dot_size;
}
DecorationGeometry
add_dotted_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned num_of_dots = MAX(1u, fcm.cell_width / (2 * MAX(1u, fcm.underline_thickness)));
RAII_ALLOC(unsigned, spacing, malloc(num_of_dots * 2 * sizeof(unsigned)));
if (!spacing) fatal("Out of memory");
unsigned size = distribute_dots(fcm.cell_width, num_of_dots, spacing, spacing + num_of_dots);
STRAIGHT_UNDERLINE_LOOP {
uint8_t *offset = buf + fcm.cell_width * y;
for (unsigned j = 0; j < num_of_dots; j++) {
unsigned s = spacing[j];
memset(offset + j * size + s, 0xff, size);
}
}
return ans;
}
DecorationGeometry
add_dashed_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned quarter_width = fcm.cell_width / 4;
unsigned dash_width = fcm.cell_width - 3 * quarter_width;
unsigned second_dash_start = 3 * quarter_width;
STRAIGHT_UNDERLINE_LOOP {
uint8_t *offset = buf + fcm.cell_width * y;
memset(offset, 0xff, dash_width);
memset(offset + second_dash_start, 0xff, dash_width);
}
return ans;
}
static unsigned
add_intensity(uint8_t *buf, unsigned x, int y, uint8_t val, unsigned max_y, unsigned position, unsigned cell_width) {
y += position;
y = min(MAX(0, y), max_y);
unsigned idx = cell_width * y + x;
buf[idx] = min(255, buf[idx] + val);
return y;
}
static uint
minus(uint a, uint b) { // saturating subtraction (a > b ? a - b : 0)
uint res = a - b;
res &= -(res <= a);
return res;
}
DecorationGeometry
add_curl_underline(uint8_t *buf, FontCellMetrics fcm) {
unsigned max_x = fcm.cell_width - 1, max_y = fcm.cell_height - 1;
double xfactor = ((OPT(undercurl_style) & 1) ? 4.0 : 2.0) * M_PI / max_x;
div_t d = div(fcm.underline_thickness, 2);
/*printf("cell_width: %u cell_height: %u underline_position: %u underline_thickness: %u\n",*/
/* fcm.cell_width, fcm.cell_height, fcm.underline_position, fcm.underline_thickness);*/
unsigned position = min(fcm.underline_position, minus(fcm.cell_height, d.quot + d.rem));
unsigned thickness = max(1u, min(fcm.underline_thickness, minus(fcm.cell_height, position + 1)));
unsigned max_height = fcm.cell_height - minus(position, thickness / 2); // descender from the font
unsigned half_height = max(1u, max_height / 4u); // 4 so as to be not too large
if (OPT(undercurl_style) & 2) thickness = max(half_height, thickness);
else thickness = max(1u, thickness) - (thickness < 3u ? 1u : 2u);
position += half_height * 2;
if (position + half_height > max_y) position = max_y - half_height;
/*printf("position: %u half_height: %u thickness: %u\n", position, half_height, thickness);*/
unsigned miny = fcm.cell_height, maxy = 0;
// Use the Wu antialias algorithm to draw the curve
// cosine waves always have slope <= 1 so are never steep
for (unsigned x = 0; x < fcm.cell_width; x++) {
double y = half_height * cos(x * xfactor);
int y1 = (int)(floor(y - thickness)), y2 = (int)(ceil(y));
unsigned intensity = (unsigned)((255. * fabs(y - floor(y))));
unsigned i1 = 255 - intensity, i2 = intensity;
unsigned yc = add_intensity(buf, x, y1, i1, max_y, position, fcm.cell_width); // upper bound
if (i1) { if (yc < miny) miny = yc; if (yc > maxy) maxy = yc; }
yc = add_intensity(buf, x, y2, i2, max_y, position, fcm.cell_width); // lower bound
if (i2) { if (yc < miny) miny = yc; if (yc > maxy) maxy = yc; }
// fill between upper and lower bound
for (unsigned t = 1; t <= thickness; t++) add_intensity(buf, x, y1 + t, 255, max_y, position, fcm.cell_width);
}
DecorationGeometry ans = {.top=miny, .height=maxy-miny + 1};
return ans;
}
static void
vert(uint8_t *ans, bool is_left_edge, double width_pt, double dpi_x, FontCellMetrics fcm) {
unsigned width = max(1u, min((unsigned)(round(width_pt * dpi_x / 72.0)), fcm.cell_width));
const unsigned left = is_left_edge ? 0 : (fcm.cell_width > width ? fcm.cell_width - width : 0);
for (unsigned y = 0; y < fcm.cell_height; y++) {
const unsigned offset = y * fcm.cell_width + left;
memset(ans + offset, 0xff, width);
}
}
static unsigned
horz(uint8_t *ans, bool is_top_edge, double height_pt, double dpi_y, FontCellMetrics fcm) {
unsigned height = max(1u, min((unsigned)(round(height_pt * dpi_y / 72.0)), fcm.cell_height));
const unsigned top = is_top_edge ? 0 : (fcm.cell_height > height ? fcm.cell_height - height : 0);
for (unsigned y = top; y < top + height; y++) {
const unsigned offset = y * fcm.cell_width;
memset(ans + offset, 0xff, fcm.cell_width);
}
return top;
}
DecorationGeometry
add_beam_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x) {
vert(buf, true, OPT(cursor_beam_thickness), dpi_x, fcm);
DecorationGeometry ans = {.height=fcm.cell_height};
return ans;
}
DecorationGeometry
add_underline_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_y) {
DecorationGeometry ans = {0};
ans.top = horz(buf, false, OPT(cursor_underline_thickness), dpi_y, fcm);
ans.height = fcm.cell_height - ans.top;
return ans;
}
DecorationGeometry
add_hollow_cursor(uint8_t *buf, FontCellMetrics fcm, double dpi_x, double dpi_y) {
vert(buf, true, 1.0, dpi_x, fcm); vert(buf, false, 1.0, dpi_x, fcm);
horz(buf, true, 1.0, dpi_y, fcm); horz(buf, false, 1.0, dpi_y, fcm);
DecorationGeometry ans = {.height=fcm.cell_height};
return ans;
}
// }}}
typedef struct Range {
uint start, end;
} Range;
typedef struct Limit { double upper, lower; } Limit;
typedef struct FloatPoint { double x, y; } FloatPoint;
typedef struct Canvas {
uint8_t *mask;
uint width, height, supersample_factor;
struct { double x, y; } dpi;
double scale; // used to scale line thickness with font size for multicell rendering
Range *holes; uint holes_count, holes_capacity;
Limit *y_limits; uint y_limits_count, y_limits_capacity;
} Canvas;
static void
fill_canvas(Canvas *self, int byte) { memset(self->mask, byte, sizeof(self->mask[0]) * self->width * self->height); }
static void
append_hole(Canvas *self, Range hole) {
ensure_space_for(self, holes, self->holes[0], self->holes_count + 1, holes_capacity, self->width, false);
self->holes[self->holes_count++] = hole;
}
static void
append_limit(Canvas *self, double upper, double lower) {
ensure_space_for(self, y_limits, self->y_limits[0], self->y_limits_count + 1, y_limits_capacity, self->width, false);
self->y_limits[self->y_limits_count].upper = upper;
self->y_limits[self->y_limits_count++].lower = lower;
}
static double
thickness_as_float(Canvas *self, uint level, bool horizontal) {
level = min(level, arraysz(OPT(box_drawing_scale)));
double pts = OPT(box_drawing_scale)[level];
double dpi = horizontal ? self->dpi.x : self->dpi.y;
return self->supersample_factor * self->scale * pts * dpi / 72.0;
}
static uint
thickness(Canvas *self, uint level, bool horizontal) {
return (uint)ceil(thickness_as_float(self, level, horizontal));
}
static const uint hole_factor = 8;
static void
get_holes(Canvas *self, uint sz, uint hole_sz, uint num) {
uint all_holes_use = (num + 1) * hole_sz;
uint individual_block_size = max(1u, minus(sz, all_holes_use) / (num + 1));
uint half_hole_sz = hole_sz / 2;
int pos = - half_hole_sz;
while (pos < (int)sz) {
uint left = pos > 0 ? pos : 0;
uint right = min(sz, pos + hole_sz);
if (right > left) append_hole(self, (Range){left, right});
pos = right + individual_block_size;
}
}
static void
add_hholes(Canvas *self, uint level, uint num) {
uint line_sz = thickness(self, level, true);
uint hole_sz = self->width / hole_factor;
uint start = minus(self->height / 2, line_sz / 2);
get_holes(self, self->width, hole_sz, num);
for (uint y = 0; y < start + line_sz; y++) {
uint offset = y * self->width;
for (uint i = 0; i < self->holes_count; i++) memset(self->mask + offset + self->holes[i].start, 0, self->holes[i].end - self->holes[i].start);
}
}
static void
add_vholes(Canvas *self, uint level, uint num) {
uint line_sz = thickness(self, level, false);
uint hole_sz = self->height / hole_factor;
uint start = minus(self->width / 2, line_sz / 2);
get_holes(self, self->height, hole_sz, num);
for (uint i = 0; i < self->holes_count; i++) {
for (uint y = self->holes[i].start; y < self->holes[i].end; y++) {
uint offset = y * self->width;
memset(self->mask + offset + start, 0, line_sz);
}
}
}
static Range
hline_limits(Canvas *self, uint y, uint level) {
uint sz = thickness(self, level, false);
Range r = {.start=minus(y, sz / 2)};
r.end = min(r.start + sz, self->height);
return r;
}
static void
draw_hline(Canvas *self, uint x1, uint x2, uint y, uint level) {
// Draw a horizontal line between [x1, x2) centered at y with the thickness given by level and self->supersample_factor
Range r = hline_limits(self, y, level);
for (uint y = r.start; y < r.end; y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + x1, 255, minus(min(x2, self->width), x1));
}
}
static Range
vline_limits(Canvas *self, uint x, uint level) {
uint sz = thickness(self, level, true);
Range r = {.start = minus(x, sz / 2)};
r.end = min(r.start + sz, self->width);
return r;
}
static void
draw_vline(Canvas *self, uint y1, uint y2, uint x, uint level) {
// Draw a vertical line between [y1, y2) centered at x with the thickness given by level and self->supersample_factor
Range r = vline_limits(self, x, level);
uint xsz = minus(r.end, r.start);
for (uint y = y1; y < min(y2, self->height); y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + r.start, 255, xsz);
}
}
static uint
half_width(Canvas *self) { // align with non-supersampled co-ords
return self->supersample_factor * (self->width / 2 / self->supersample_factor);
}
static uint
half_height(Canvas *self) { // align with non-supersampled co-ords
return self->supersample_factor * (self->height / 2 / self->supersample_factor);
}
static double
unit_double(double x) {
return x < 0.0 ? 0.0 : (x > 1.0 ? 1.0 : x);
}
static double
smoothstep(double edge0, double edge1, double x) {
if (edge0 == edge1) return x < edge0 ? 0.0 : 1.0;
double t = unit_double((x - edge0) / (edge1 - edge0));
return t * t * (3.0 - 2.0 * t);
}
static void
half_hline(Canvas *self, uint level, bool right_half, uint extend_by) {
uint x1, x2;
if (right_half) {
x1 = minus(half_width(self), extend_by); x2 = self->width;
} else {
x1 = 0; x2 = half_width(self) + extend_by;
}
draw_hline(self, x1, x2, half_height(self), level);
}
typedef union Point {
struct {
int32_t x: 32, y: 32;
};
int64_t val;
} Point;
static Point
half_dhline(Canvas *self, uint level, bool right_half, Edge which) {
uint x1 = 0, x2 = 0;
if (right_half) { x1 = self->width / 2; x2 = self->width; } else x2 = self->width / 2;
uint gap = thickness(self, level + 1, false);
Point ans = {.x=self->height / 2 - gap, .y=self->height / 2 + gap};
if (which & TOP_EDGE) draw_hline(self, x1, x2, ans.x, level);
if (which & BOTTOM_EDGE) draw_hline(self, x1, x2, ans.y, level);
return ans;
}
static Point
half_dvline(Canvas *self, uint level, bool bottom_half, Edge which) {
uint y1 = 0, y2 = 0;
if (bottom_half) { y1 = self->height / 2; y2 = self->height; } else y2 = self->height / 2;
uint gap = thickness(self, level + 1, true);
Point ans = {.x=self->width / 2 - gap, .y=self->width / 2 + gap};
if (which & LEFT_EDGE) draw_vline(self, y1, y2, ans.x, level);
if (which & RIGHT_EDGE) draw_vline(self, y1, y2, ans.y, level);
return ans;
}
static Point
dhline(Canvas *self, uint level, Edge which) {
half_dhline(self, level, false, which);
return half_dhline(self, level, true, which);
}
static Point
dvline(Canvas *self, uint level, Edge which) {
half_dvline(self, level, false, which);
return half_dvline(self, level, true, which);
}
static void
half_vline(Canvas *self, uint level, bool bottom_half, uint extend_by) {
uint y1, y2;
if (bottom_half) {
y1 = minus(half_height(self), extend_by); y2 = self->height;
} else {
y1 = 0; y2 = half_height(self) + extend_by;
}
draw_vline(self, y1, y2, half_width(self), level);
}
static void
hline(Canvas *self, uint level) {
half_hline(self, level, false, 0);
half_hline(self, level, true, 0);
}
static void
vline(Canvas *self, uint level) {
half_vline(self, level, false, 0);
half_vline(self, level, true, 0);
}
static void
hholes(Canvas *self, uint level, uint num) {
hline(self, level);
add_hholes(self, level, num);
}
static void
vholes(Canvas *self, uint level, uint num) {
vline(self, level);
add_vholes(self, level, num);
}
static uint8_t
plus(uint8_t a, uint8_t b) {
uint8_t res = a + b;
res |= -(res < a);
return res;
}
static uint8_t
average_intensity(const Canvas *src, uint dest_x, uint dest_y) {
uint src_x = dest_x * src->supersample_factor, src_y = dest_y * src->supersample_factor;
uint total = 0;
for (uint y = src_y; y < src_y + src->supersample_factor; y++) {
uint offset = src->width * y;
for (uint x = src_x; x < src_x + src->supersample_factor; x++) total += src->mask[offset + x];
}
return (total / (src->supersample_factor * src->supersample_factor)) & 0xff;
}
static void
downsample(const Canvas *src, Canvas *dest) {
for (uint y = 0; y < dest->height; y++) {
uint offset = dest->width * y;
for (uint x = 0; x < dest->width; x++) {
dest->mask[offset + x] = plus(dest->mask[offset + x], average_intensity(src, x, y));
}
}
}
typedef struct StraightLine {
double m, c;
} StraightLine;
static StraightLine
line_from_points(double x1, double y1, double x2, double y2) {
StraightLine ans = {.m = (y2 - y1) / (x2 - x1)};
ans.c = y1 - ans.m * x1;
return ans;
}
static double
line_y(StraightLine l, int x) {
return l.m * x + l.c;
}
#define calc_limits(self, lower_y, upper_y) { \
if (!self->y_limits) { \
self->y_limits_count = self->width; self->y_limits = malloc(sizeof(self->y_limits[0]) * self->y_limits_count); \
if (!self->y_limits) fatal("Out of memory"); \
} \
for (uint x = 0; x < self->width; x++) { self->y_limits[x].lower = lower_y; self->y_limits[x].upper = upper_y; } \
}
static void
fill_region(Canvas *self, bool inverted) {
uint8_t full = 0, empty = 0; if (inverted) empty = 255; else full = 255;
for (uint y = 0; y < self->height; y++) {
uint offset = y * self->width;
for (uint x = 0; x < self->width && x < self->y_limits_count; x++) {
self->mask[offset + x] = self->y_limits[x].lower <= y && y <= self->y_limits[x].upper ? full : empty;
}
}
}
static void
triangle(Canvas *self, bool left, bool inverted) {
int ay1 = 0, by1 = self->height - 1, y2 = self->height / 2, x1 = 0, x2 = 0;
if (left) x2 = self->width - 1; else x1 = self->width - 1;
StraightLine uppery = line_from_points(x1, ay1, x2, y2);
StraightLine lowery = line_from_points(x1, by1, x2, y2);
calc_limits(self, line_y(uppery, x), line_y(lowery, x));
fill_region(self, inverted);
}
typedef enum Corner {
TOP_LEFT = LEFT_EDGE | TOP_EDGE, TOP_RIGHT = TOP_EDGE | RIGHT_EDGE,
BOTTOM_LEFT = BOTTOM_EDGE | LEFT_EDGE, BOTTOM_RIGHT = BOTTOM_EDGE | RIGHT_EDGE,
} Corner;
static void
thick_line(Canvas *self, uint thickness_in_pixels, Point p1, Point p2) {
if (p1.x > p2.x) SWAP(p1, p2);
StraightLine l = line_from_points(p1.x, p1.y, p2.x, p2.y);
div_t d = div(thickness_in_pixels, 2);
int delta = d.quot, extra = d.rem;
for (int x = p1.x > 0 ? p1.x : 0; x < (int)self->width && x < p2.x + 1; x++) {
int y_p = (int)line_y(l, x);
for (int y = MAX(0, y_p - delta); y < MIN(y_p + delta + extra, (int)self->height); y++) {
self->mask[x + y * self->width] = 255;
}
}
}
static void
frame(Canvas *self, uint level, Edge edges) {
uint h = thickness(self, level, true), v = thickness(self, level, false);
#define line(x1, x2, y1, y2) { \
for (uint y=y1; y < min(y2, self->height); y++) memset(self->mask + y * self->width + x1, 255, minus(min(x2, self->width), x1)); }
#define hline(y1, y2) line(0, self->width, y1, y2)
#define vline(x1, x2) line(x1, x2, 0, self->height)
if (edges & TOP_EDGE) hline(0, h + 1);
if (edges & BOTTOM_EDGE) hline(self->height - h - 1, self->height);
if (edges & LEFT_EDGE) vline(0, v + 1);
if (edges & RIGHT_EDGE) vline(self->width - v - 1, self->width);
#undef hline
#undef vline
#undef line
}
typedef enum Segment { LEFT, MIDDLE, RIGHT } Segment;
static void
progress_bar(Canvas *self, Segment which, bool filled) {
const Edge edges = TOP_EDGE | BOTTOM_EDGE;
switch(which) {
case LEFT: frame(self, 1, LEFT_EDGE | edges); break;
case MIDDLE: frame(self, 1, edges); break;
case RIGHT: frame(self, 1, RIGHT_EDGE | edges); break;
}
if (!filled) return;
uint h = thickness(self, 1, true), v = thickness(self, 1, false);
static const uint gap_factor = 3;
uint y1 = gap_factor * h, y2 = minus(self->height, gap_factor*h), x1 = 0, x2 = 0;
switch(which) {
case LEFT: x1 = gap_factor * v; x2 = self->width; break;
case MIDDLE: x2 = self->width; break;
case RIGHT: x2 = minus(self->width, gap_factor * v); break;
}
for (uint y = y1; y < y2; y++) memset(self->mask + y * self->width + x1, 255, minus(min(x2, self->width), x1));
}
// thick_line extends by thickness/2 pixels in the y-direction for each x
// column. For a diagonal line with slope m = dy/dx the perceived perpendicular
// thickness is therefore (thickness/2) / sqrt(1 + m²). Multiply the nominal
// thickness by sqrt(1 + m²) so the visual weight matches that of horizontal
// and vertical lines of the same specified thickness.
static uint
diagonal_thickness(uint base, Point p1, Point p2) {
int dx = (int)p2.x - (int)p1.x;
if (dx == 0) return base;
// m is squared below so its sign does not matter; no need for abs(dx).
double m = ((double)((int)p2.y - (int)p1.y)) / (double)dx;
uint ans = (uint)round(base * sqrt(1.0 + m * m));
return ans > 0 ? ans : 1;
}
static void
half_cross_line(Canvas *self, uint level, Corner corner) {
uint my = minus(self->height, 1) / 2; Point p1 = {0}, p2 = {0};
switch (corner) {
case TOP_LEFT: p2.x = minus(self->width, 1); p2.y = my; break;
case BOTTOM_LEFT: p1.x = minus(self->width, 1); p1.y = my; p2.y = self->height -1; break;
case TOP_RIGHT: p1.x = minus(self->width, 1); p2.y = my; break;
case BOTTOM_RIGHT: p2.x = minus(self->width, 1), p2.y = minus(self->height, 1); p1.y = my; break;
}
thick_line(self, diagonal_thickness(thickness(self, level, true), p1, p2), p1, p2);
}
static void
cross_line(Canvas *self, uint level, bool left) {
uint w = minus(self->width, 1), h = minus(self->height, 1);
Point p1 = {0}, p2 = {0};
if (left) p2 = (Point){.x=w, .y=h}; else { p1.x = w; p2.y = h; }
thick_line(self, diagonal_thickness(thickness(self, level, true), p1, p2), p1, p2);
}
typedef struct CubicBezier {
Point start, c1, c2, end;
} CubicBezier;
#define bezier_eq(which) { \
const CubicBezier *cb = v; \
const double u = 1. - t; \
const double u_3 = u * u * u; \
const double t_3 = t * t * t; \
return u_3 * cb->start.which + 3 * t * u * (u * cb->c1.which + t * cb->c2.which) + t_3 * cb->end.which; \
}
#define bezier_prime_eq(which) { \
const CubicBezier *cb = v; \
const double u = 1. - t; \
const double u_2 = u * u; \
const double t_2 = t * t; \
return 3 * u_2 * (cb->c1.which - cb->start.which) + 6 * t * u * (cb->c2.which - cb->c1.which) + 3 * t_2 * (cb->end.which - cb->c2.which); \
}
static double bezier_x(const void *v, double t) { bezier_eq(x); }
static double bezier_y(const void *v, double t) { bezier_eq(y); }
static double bezier_prime_x(const void *v, double t) { bezier_prime_eq(x); }
static double bezier_prime_y(const void *v, double t) { bezier_prime_eq(y); }
#undef bezier_eq
#undef bezier_prime_eq
static int
find_bezier_for_D(int width, int height) {
int cx = width - 1, last_cx = cx;
CubicBezier cb = {.end={.x=0, .y=height - 1}, .c2={.x=0, .y=height - 1}};
while (true) {
cb.c1.x = cx; cb.c2.x = cx;
if (bezier_x(&cb, 0.5) > width - 1) return last_cx;
last_cx = cx++;
}
}
static double
find_t_for_x(const CubicBezier *cb, int x, double start_t) {
if (fabs(bezier_x(cb, start_t) - x) < 0.1) return start_t;
static const double t_limit = 0.5;
double increment = t_limit - start_t;
if (increment <= 0) return start_t;
while (true) {
double q = bezier_x(cb, start_t + increment);
if (fabs(q - x) < 0.1) return start_t + increment;
if (q > x) {
increment /= 2.0;
if (increment < 1e-6) {
log_error("Failed to find cubic bezier t for x=%d\n", x);
return start_t;
}
} else {
start_t += increment;
increment = t_limit - start_t;
if (increment <= 0) return start_t;
}
}
}
static void
get_bezier_limits(Canvas *self, const CubicBezier *cb) {
int start_x = (int)bezier_x(cb, 0), max_x = (int)bezier_x(cb, 0.5);
double last_t = 0.;
for (int x = start_x; x < max_x + 1; x++) {
if (x > start_x) last_t = find_t_for_x(cb, x, last_t);
double upper = bezier_y(cb, last_t), lower = bezier_y(cb, 1.0 - last_t);
if (fabs(upper - lower) <= 2.0) break; // avoid pip on end of D
append_limit(self, lower, upper);
}
}
#define mirror_horizontally(expr) { \
RAII_ALLOC(uint8_t, mbuf, calloc(self->width, self->height)); \
if (!mbuf) fatal("Out of memory"); \
uint8_t *buf = self->mask; \
self->mask = mbuf; \
expr; \
self->mask = buf; \
for (uint y = 0; y < self->height; y++) { \
uint offset = y * self->width; \
for (uint src_x = 0; src_x < self->width; src_x++) { \
uint dest_x = self->width - 1 - src_x; \
buf[offset + dest_x] = mbuf[offset + src_x]; \
} \
} \
}
static void
filled_D(Canvas *self, bool left) {
int c1x = find_bezier_for_D(self->width, self->height);
CubicBezier cb = {.end={.y=self->height-1}, .c1 = {.x=c1x}, .c2 = {.x=c1x, .y=self->height - 1}};
get_bezier_limits(self, &cb);
if (left) fill_region(self, false);
else mirror_horizontally(fill_region(self, false));
}
typedef double(*curve_func)(const void *, double t);
#define NAME position_set
#define KEY_TY Point
#define HASH_FN hash_point
#define CMPR_FN cmpr_point
static uint64_t hash_point(Point p);
static bool cmpr_point(Point, Point);
#include "kitty-verstable.h"
static uint64_t hash_point(Point p) { return vt_hash_integer(p.val); }
static bool cmpr_point(Point a, Point b) { return a.val == b.val; }
typedef struct ClipRect { uint left, top, x_end, y_end; } ClipRect;
static void
draw_parametrized_curve_with_derivative_and_antialiasing(
Canvas *self, void *curve_data, double line_width, curve_func xfunc, curve_func yfunc,
curve_func x_prime, curve_func y_prime, double x_offset, double y_offset, const ClipRect *clip_to
) {
line_width = fmax(1., line_width);
double half_thickness = line_width / 2.0;
uint i=0, larger_dim = MAX(self->height, self->width);
double t = 0;
double step = 1.0 / larger_dim;
uint cap = 2 * larger_dim;
const double min_step = step / 1000., max_step = step;
RAII_ALLOC(FloatPoint, samples, malloc(sizeof(FloatPoint) * cap));
if (!samples) fatal("Out of memory");
ClipRect cr = clip_to ? *clip_to : (ClipRect){.x_end=self->width, .y_end=self->height};
while (true) {
samples[i] = (FloatPoint){xfunc(curve_data, t) + x_offset, yfunc(curve_data, t) + y_offset};
if (t >= 1.0) break;
// Dynamically adjust step size based on curve's derivative
double dx = x_prime(curve_data, t), dy = y_prime(curve_data, t);
double d = sqrt(dx * dx + dy * dy);
step = 1.0 / fmax(1e-6, d);
step = fmax(min_step, fmin(step, max_step));
t = fmin(t + step, 1.0);
i++;
if (i >= cap) {
cap *= 2;
samples = realloc(samples, sizeof(samples[0]) * cap);
if (!samples) fatal("Out of memory");
}
}
const uint num_samples = i;
for (uint py = cr.top; py < cr.y_end; py++) {
uint ypos = self->width * py;
for (uint px = cr.left; px < cr.x_end; px++) {
// Center of the current pixel
double pixel_center_x = (double)px + 0.5;
double pixel_center_y = (double)py + 0.5;
double min_dist_sq = -1.0;
// Find the closest point on the curve to the pixel center by sampling the curve.
for (uint i = 0; i < num_samples; ++i) {
double dx = samples[i].x - pixel_center_x;
double dy = samples[i].y - pixel_center_y;
double dist_sq = dx * dx + dy * dy;
if (min_dist_sq < 0 || dist_sq < min_dist_sq) min_dist_sq = dist_sq;
}
double distance = sqrt(min_dist_sq);
// Calculate alpha based on the distance from the curve.
// This creates the anti-aliased edge. The distance from the center
// of the pixel to the edge of the stroke is used.
// We assume a pixel has a width of 1.0 for this calculation.
double alpha_unclamped = half_thickness - distance + 0.5;
uint offset = ypos + px;
uint8_t old_alpha = self->mask[offset];
double alpha = MAX(0.0, MIN(alpha_unclamped, 1.0));
self->mask[offset] = (uint8_t)(alpha * 255 + (1 - alpha) * old_alpha); // alpha blend
}
}
}
static void
rounded_separator(Canvas *self, uint level, bool left) {
uint gap = thickness(self, level, true);
int c1x = find_bezier_for_D(minus(self->width, gap), minus(self->height, gap));
uint half_gap = gap / 2;
CubicBezier cb = {.end={.y=minus(self->height, 1 + half_gap)}, .c1={.x=c1x},
.c2={.x=c1x, .y=minus(self->height, 1 + half_gap)}};
double line_width = thickness_as_float(self, level, true);
#define d draw_parametrized_curve_with_derivative_and_antialiasing(\
self, &cb, line_width, bezier_x, bezier_y, bezier_prime_x, bezier_prime_y, 0, half_gap, NULL)
if (left) { d; } else { mirror_horizontally(d); }
#undef d
}
static void
corner_triangle(Canvas *self, const Corner corner) {
StraightLine diag;
const uint w = minus(self->width, 1), h = minus(self->height, 1);
bool top = corner == TOP_RIGHT || corner == TOP_LEFT;
if (corner == TOP_RIGHT || corner == BOTTOM_LEFT) diag = line_from_points(0, 0, w, h);
else diag = line_from_points(w, 0, 0, h);
for (uint x = 0; x < self->width; x++) {
if (top) append_limit(self, line_y(diag, x), 0);
else append_limit(self, h, line_y(diag, x));
}
fill_region(self, false);
}
typedef struct Circle {
double x, y, radius;
double start, end, amt;
} Circle;
static Circle
circle(double x, double y, double radius, double start_at, double end_at) {
double conv = M_PI / 180.;
Circle ans = {.x=x, .y=y, .radius=radius, .start=start_at*conv, .end=end_at*conv};
ans.amt = ans.end - ans.start;
return ans;
}
static double circle_x(const void *v, double t) { const Circle *c=v; return c->x + c->radius * cos(c->start + c->amt * t); }
static double circle_y(const void *v, double t) { const Circle *c=v; return c->y + c->radius * sin(c->start + c->amt * t); }
static double circle_prime_x(const void *v, double t) { const Circle *c=v; return -c->radius * sin(c->start + c->amt * t); }
static double circle_prime_y(const void *v, double t) { const Circle *c=v; return c->radius * cos(c->start + c->amt * t); }
typedef struct Ellipse {
double x, y, rx, ry;
double start, end, amt;
} Ellipse;
static Ellipse
ellipse(double x, double y, double rx, double ry, double start_at, double end_at) {
double conv = M_PI / 180.;
Ellipse ans = {.x=x, .y=y, .rx=rx, .ry=ry, .start=start_at*conv, .end=end_at*conv};
ans.amt = ans.end - ans.start;
return ans;
}
static double ellipse_x(const void *v, double t) { const Ellipse *e=v; return e->x + e->rx * cos(e->start + e->amt * t); }
static double ellipse_y(const void *v, double t) { const Ellipse *e=v; return e->y + e->ry * sin(e->start + e->amt * t); }
static double ellipse_prime_x(const void *v, double t) { const Ellipse *e=v; return -e->rx * sin(e->start + e->amt * t); }
static double ellipse_prime_y(const void *v, double t) { const Ellipse *e=v; return e->ry * cos(e->start + e->amt * t); }
static void
spinner(Canvas *self, uint level, double start_degrees, double end_degrees) {
double x = self->width / 2.0, y = self->height / 2.0;
double line_width = thickness_as_float(self, level, true);
double half_real_line_width = fmax(0.5, line_width / 2.0);
double radius = fmax(0, fmin(x, y) - half_real_line_width);
Circle c = circle(x, y, radius, start_degrees, end_degrees);
uint leftover = minus(self->height, 2*(uint)(ceil(radius) + half_real_line_width) + 1) / 2;
ClipRect cr = {.top=leftover, .y_end=self->height - leftover, .x_end=self->width};
draw_parametrized_curve_with_derivative_and_antialiasing(
self, &c, line_width, circle_x, circle_y, circle_prime_x, circle_prime_y, 0, 0, &cr);
}
static void
fill_circle_of_radius(Canvas *self, double origin_x, double origin_y, double radius, uint8_t alpha) {
const double limit = radius * radius;
for (uint y = 0; y < self->height; y++) {
for (uint x = 0; x < self->width; x++) {
double xw = (double)x - origin_x, yh = (double)y - origin_y;
if (xw * xw + yh * yh <= limit) self->mask[y * self->width + x] = alpha;
}
}
}
static void
fill_circle(Canvas *self, double scale, double gap, bool invert) {
const uint w = self->width / 2, h = self->height / 2;
const double radius = (int)(scale * min(w, h) - gap / 2);
const uint8_t fill = invert ? 0 : 255;
fill_circle_of_radius(self, w, h, radius, fill);
}
static void
draw_fish_eye(Canvas *self, uint level UNUSED) {
double x = self->width / 2., y = self->height / 2.;
double radius = fmin(x, y);
uint leftover = minus(self->height, 2*(uint)ceil(radius) + 1) / 2;
double central_radius = (2./3.) * radius;
fill_circle_of_radius(self, x, y, central_radius, 255);
double line_width = fmax(1. * self->supersample_factor, (radius - central_radius) / 2.5);
radius = fmax(0, fmin(x, y) - line_width / 2.);
Circle c = circle(x, y, radius, 0, 360);
ClipRect cr = {.top=leftover, .y_end=self->height - leftover, .x_end=self->width};
draw_parametrized_curve_with_derivative_and_antialiasing(
self, &c, line_width, circle_x, circle_y, circle_prime_x, circle_prime_y, 0, 0, &cr);
}
static void
inner_corner(Canvas *self, uint level, Corner corner) {
uint hgap = thickness(self, level + 1, true), vgap = thickness(self, level + 1, false);
uint vthick = thickness(self, level, true) / 2;
uint x1 = 0, x2 = self->width, y1 = 0, y2 = self->height; int xd = 1, yd = 1;
if (corner & LEFT_EDGE) {
xd = -1;
Range vlinelimit = vline_limits(self, self->width / 2 + (xd * hgap), level);
x2 = vlinelimit.end;
} else x1 = minus(self->width / 2 + hgap, vthick);
if (corner & TOP_EDGE) {
y2 = minus(self->height / 2, vgap); yd = -1;
} else y1 = self->height / 2 + vgap;
draw_hline(self, x1, x2, self->height / 2 + (yd * vgap), level);
draw_vline(self, y1, y2, self->width / 2 + (xd * hgap), level);
}
static Range
fourth_range(uint size, uint which) {
uint thickness = max(1, size / 4);
uint block = thickness * 4;
if (block == size) return (Range){.start=thickness * which, .end=thickness * (which + 1)};
if (block > size) {
uint start = min(which * thickness, minus(size, thickness));
return (Range){.start=start, .end=start + thickness};
}
uint extra = minus(size, block);
uint thicknesses[4] = {thickness, thickness, thickness, thickness};
uint pos = 0;
if (extra) {
#define d(i) thicknesses[i]++; if (!--extra) goto done;
// ensures the thickness of first and last are least likely to be changed
d(1); d(2); d(3); d(0);
#undef d
}
done:
for (uint i = 0; i < which; i++) pos += thicknesses[i];
return (Range){.start=pos, .end=pos + thicknesses[which]};
}
static Range
eight_range(uint size, uint which) {
uint thickness = max(1, size / 8);
uint block = thickness * 8;
if (block == size) return (Range){.start=thickness * which, .end=thickness * (which + 1)};
if (block > size) {
uint start = min(which * thickness, minus(size, thickness));
return (Range){.start=start, .end=start + thickness};
}
uint extra = minus(size, block);
uint thicknesses[8] = {thickness, thickness, thickness, thickness, thickness, thickness, thickness, thickness};
uint pos = 0;
if (extra) {
#define d(i) thicknesses[i]++; if (!--extra) goto done;
// ensures the thickness of first and last are least likely to be changed
d(3); d(4); d(2); d(5); d(6); d(1); d(7); d(0);
#undef d
}
done:
for (uint i = 0; i < which; i++) pos += thicknesses[i];
return (Range){.start=pos, .end=pos + thicknesses[which]};
}
static void
eight_bar(Canvas *self, uint which, bool horizontal) {
Range x_range, y_range;
if (horizontal) {
x_range = (Range){0, self->width};
y_range = eight_range(self->height, which);
} else {
y_range = (Range){0, self->height};
x_range = eight_range(self->width, which);
}
for (uint y = y_range.start; y < y_range.end; y++) {
uint offset = y * self->width;
memset(self->mask + offset + x_range.start, 255, minus(x_range.end, x_range.start));
}
}
static void
octant_segment(Canvas *self, uint8_t which, bool left) {
Range x_range = left ? (Range){0, self->width / 2} : (Range){self->width/2, self->width};
Range y_range = fourth_range(self->height, which);
for (uint y = y_range.start; y < y_range.end; y++) {
uint offset = y * self->width;
memset(self->mask + offset + x_range.start, 255, minus(x_range.end, x_range.start));
}
}
static void
octant(Canvas *self, uint8_t which) {
enum flags { a = 1, b = 2, c = 4, d = 8, m = 16, n = 32, o = 64, p = 128 };
static const enum flags mapping[232] = {
// 00 - 0f
b, b|m, a|b|m, n, a|n, a|m|n, b|n, a|b|n, b|m|n, c, a|c, c|m, a|c|m, a|b|c, b|c|m, a|b|c|m,
// 10 - 1f
c|n, a|c|n, c|m|n, a|c|m|n, b|c|n, a|b|c|n, b|c|m|n, a|b|c|m|n, o, a|o, m|o, a|m|o, b|o, a|b|o, b|m|o, a|b|m|o,
// 20 - 2f
a|n|o, m|n|o, a|m|n|o, b|n|o, a|b|n|o, b|m|n|o, a|b|m|n|o, c|o, a|c|o, c|m|o, a|c|m|o, b|c|o, a|b|c|o, b|c|m|o, a|b|c|m|o, c|n|o,
// 30 - 3f
a|c|n|o, c|m|n|o, a|c|m|n|o, b|c|n|o, a|b|c|n|o, b|c|m|n|o, a|d, d|m, a|d|m, b|d, a|b|d, b|d|m, a|b|d|m, d|n, a|d|n, d|m|n,
// 40 - 4f
a|d|m|n, b|d|n, a|b|d|n, b|d|m|n, a|b|d|m|n, a|c|d, c|d|m, a|c|d|m, b|c|d, b|c|d|m, a|b|c|d|m, c|d|n, a|c|d|n, a|c|d|m|n, b|c|d|n, a|b|c|d|n,
// 50 - 5f
b|c|d|m|n, d|o, a|d|o, d|m|o, a|d|m|o, b|d|o, a|b|d|o, b|d|m|o, a|b|d|m|o, d|n|o, a|d|n|o, d|m|n|o, a|d|m|n|o, b|d|n|o, a|b|d|n|o, b|d|m|n|o,
// 60 - 6f
~(c|p), c|d|o, a|c|d|o, c|d|m|o, a|c|d|m|o, b|c|d|o, ~(m|n|p), b|c|d|m|o, ~(n|p), c|d|n|o, a|c|d|n|o, c|d|m|n|o, ~(b|p), b|c|d|n|o, ~(m|p), ~(a|p),
// 70 - 7f
~p, a|p, m|p, a|m|p, b|p, a|b|p, b|m|p, a|b|m|p, n|p, a|n|p, m|n|p, a|m|n|p, b|n|p, a|b|n|p, b|m|n|p, ~(c|d|o),
// 80 - 8f
c|p, a|c|p, c|m|p, a|c|m|p, b|c|p, a|b|c|p, b|c|m|p, ~(d|n|o), c|n|p, a|c|n|p, c|m|n|p, ~(b|d|o), b|c|n|p, ~(d|m|o), ~(a|d|o), ~(d|o),
// 90 - 9f
a|o|p, m|o|p, a|m|o|p, b|o|p, b|m|o|p, a|b|m|o|p, n|o|p, a|n|o|p, a|m|n|o|p, b|n|o|p, a|b|n|o|p, b|m|n|o|p, c|o|p, a|c|o|p, c|m|o|p, a|c|m|o|p,
// a0 - af
b|c|o|p, a|b|c|o|p, b|c|m|o|p, ~(n|d), c|n|o|p, a|c|n|o|p, c|m|n|o|p, ~(b|d), b|c|n|o|p, ~(d|m), ~(a|d), ~d, a|d|p, d|m|p, a|d|m|p, b|d|p,
// b0 - bf
a|b|d|p, b|d|m|p, a|b|d|m|p, d|n|p, a|d|n|p, d|m|n|p, a|d|m|n|p, b|d|n|p, a|b|d|n|p, b|d|m|n|p, ~(c|o), c|d|p, a|c|d|p, c|d|m|p, a|c|d|m|p, b|c|d|p,
// c0 -cf
a|b|c|d|p, b|c|d|m|p, ~(n|o), c|d|n|p, a|c|d|n|p, c|d|m|n|p, ~(b|o), b|c|d|n|p, ~(m|o), ~(a|o), ~o, d|o|p, a|d|o|p, d|m|o|p, a|d|m|o|p, b|d|o|p,
// d0 - df
a|b|d|o|p, b|d|m|o|p, ~(c|n), d|n|o|p, a|d|n|o|p, d|m|n|o|p, ~(b|c), b|d|n|o|p, ~(c|m), ~(a|c), ~c, a|c|d|o|p, c|d|m|o|p, ~(b|n), b|c|d|o|p, ~(a|n),
// e0 - e7
~n, c|d|n|o|p, ~(b|m), ~b, ~m, ~a, b|c, n|o,
};
which = mapping[which];
if (which & a) octant_segment(self, 0, true);
if (which & b) octant_segment(self, 1, true);
if (which & c) octant_segment(self, 2, true);
if (which & d) octant_segment(self, 3, true);
if (which & m) octant_segment(self, 0, false);
if (which & n) octant_segment(self, 1, false);
if (which & o) octant_segment(self, 2, false);
if (which & p) octant_segment(self, 3, false);
}
static void
eight_block(Canvas *self, int horizontal, ...) {
va_list args; va_start(args, horizontal);
int which;
while ((which = va_arg(args, int)) >= 0) eight_bar(self, which, horizontal);
va_end(args);
}
typedef struct Shade {
bool light, invert, fill_blank;
Edge which_half;
uint xnum, ynum;
} Shade;
#define is_odd(x) ((x) & 1u)
static void
shade(Canvas *self, Shade s) {
const uint square_width = max(1, self->width / s.xnum);
const uint square_height = max(1, s.ynum ? (self->height / s.ynum) : square_width);
uint number_of_rows = self->height / square_height;
uint number_of_cols = self->width / square_width;
// Make sure the parity is correct
// (except when that would cause division by zero)
if (number_of_cols > 1 && is_odd(number_of_cols) != is_odd(s.xnum)) number_of_cols--;
if (number_of_rows > 1 && is_odd(number_of_rows) != is_odd(s.ynum)) number_of_rows--;
// Calculate how much space remains unused, and how frequently
// to insert an extra column/row to fill all of it
uint excess_cols = minus(self->width, square_width * number_of_cols);
double square_width_extension = (double)excess_cols / number_of_cols;
uint excess_rows = minus(self->height, square_height * number_of_rows);
double square_height_extension = (double)excess_rows / number_of_rows;
Range rows = {.end=number_of_rows}, cols = {.end=number_of_cols};
switch(s.which_half) {
// this is to remove gaps between half-filled characters
case TOP_EDGE: rows.end /= 2; square_height_extension *= 2; break;
case BOTTOM_EDGE: rows.start = number_of_rows / 2; square_height_extension *= 2; break;
case LEFT_EDGE: cols.end /= 2; square_width_extension *= 2; break;
case RIGHT_EDGE: cols.start = number_of_cols / 2; square_width_extension *= 2; break;
}
bool extra_row = false;
uint ey = 0, old_ey = 0, drawn_rows = 0;
for (uint r = rows.start; r < rows.end; r++) {
// Keep track of how much extra height has accumulated, and add an extra row at every passed integer, including 0
old_ey = ey;
ey = (uint)ceil(drawn_rows * square_height_extension);
extra_row = ey != old_ey;
drawn_rows += 1;
bool extra_col = false;
uint ex = 0, old_ex = 0, drawn_cols = 0;
for (uint c = cols.start; c < cols.end; c++) {
old_ex = ex;
ex = (uint)ceil(drawn_cols * square_width_extension);
extra_col = ex != old_ex;
drawn_cols += 1;
// Fill extra rows with semi-transparent pixels that match the pattern
if (extra_row) {
uint y = r * square_height + old_ey;
uint offset = self->width * y;
for (uint xc = 0; xc < square_width; xc++) {
uint x = c * square_width + xc + ex;
if (s.light) {
if (s.invert) self->mask[offset + x] = is_odd(c) ? 255 : 70;
else self->mask[offset + x] = is_odd(c) ? 0 : 70;
} else self->mask[offset + x] = is_odd(c) == s.invert ? 120 : 30;
}
}
// Do the same for the extra columns
if (extra_col) {
uint x = c * square_width + old_ex;
for (uint yr = 0; yr < square_height; yr++) {
uint y = r * square_height + yr + ey;
uint offset = self->width * y;
if (s.light) {
if (s.invert) self->mask[offset + x] = is_odd(r) ? 255 : 70;
else self->mask[offset + x] = is_odd(r) ? 0 : 70;
} else self->mask[offset + x] = is_odd(r) == s.invert ? 120 : 30;
}
}
// And in case they intersect, set the corner pixel too
if (extra_row && extra_col) {
uint x = c * square_width + old_ex;
uint y = r * square_height + old_ey;
uint offset = self->width * y;
self->mask[offset + x] = 50;
}
const bool is_blank = s.invert ^ (is_odd(r) != is_odd(c) || (s.light && is_odd(r)));
if (!is_blank) {
// Fill the square
for (uint yr = 0; yr < square_height; yr++) {
uint y = r * square_height + yr + ey;
uint offset = self->width * y;
for (uint xc = 0; xc < square_width; xc++) {
uint x = c * square_width + xc + ex;
self->mask[offset + x] = 255;
}
}
}
}
}
if (!s.fill_blank) return;
cols = (Range){.end=self->width}; rows = (Range){.end=self->height};
switch(s.which_half) {
case BOTTOM_EDGE: rows.end = self->height / 2; break;
case TOP_EDGE: rows.start = minus(self->height / 2, 1); break;
case RIGHT_EDGE: cols.end = self->width / 2; break;
case LEFT_EDGE: cols.start = minus(self->width / 2, 1); break;
}
for (uint r = rows.start; r < rows.end; r++) memset(self->mask + r * self->width + cols.start, 255, cols.end - cols.start);
}
static void
apply_mask(Canvas *self, uint8_t *mask) {
for (uint y = 0; y < self->height; y++) {
uint offset = y * self->width;
for (uint x = 0; x < self->width; x++) {
uint p = offset + x;
self->mask[p] = (uint8_t)round((mask[p] / 255.0) * self->mask[p]);
}
}
}
static void
cross_shade(Canvas *self, bool rotate) {
static const uint num_of_lines = 7;
uint line_thickness = max(self->supersample_factor, self->width / num_of_lines);
uint delta = 2 * line_thickness;
uint y1 = 0, y2 = self->height;
if (rotate) SWAP(y1, y2);
for (uint x = 0; x < self->width; x += delta) {
thick_line(self, line_thickness, (Point){.x=0 + x, .y=y1}, (Point){.x=self->width + x, .y=y2});
thick_line(self, line_thickness, (Point){.x=0 - x, .y=y1}, (Point){.x=self->width - x, .y=y2});
}
}
static void
quad(Canvas *self, Corner which) {
uint x = which & LEFT_EDGE ? 0 : 1, y = which & TOP_EDGE ? 0 : 1;
uint num_cols = self->width / 2;
uint left = x * num_cols;
uint right = x ? self->width : num_cols;
uint num_rows = self->height / 2;
uint top = y * num_rows;
uint bottom = y ? self->height : num_rows;
for (uint r = top; r < bottom; r++) {
uint off = r * self->width;
memset(self->mask + off + left, 255, right - left);
}
}
static void
quads(Canvas *self, ...) {
va_list args; va_start(args, self);
int which;
while ((which = va_arg(args, int))) quad(self, which);
va_end(args);
}
static void
smooth_mosaic(Canvas *self, bool lower, double ax, double ay, double bx, double by) {
StraightLine l = line_from_points(
ax * minus(self->width, 1), ay * minus(self->height, 1), bx * minus(self->width, 1), by * minus(self->height, 1));
for (uint y = 0; y < self->height; y++) {
uint offset = y * self->width;
for (uint x = 0; x < self->width; x++) {
double edge = line_y(l, x);
if ((lower && y >= edge) || (!lower && y <= edge)) self->mask[offset + x] = 255;
}
}
}
static void
half_triangle(Canvas *self, Edge which, bool inverted) {
uint mid_x = self->width / 2, mid_y = self->height / 2;
StraightLine u, l;
append_limit(self, 0, 0); // ensure space for limits
#define set_limits(startx, endx, a, b) for (uint x = startx; x < endx; x++) self->y_limits[x] = (Limit){.upper=b, .lower=a};
switch (which) {
case LEFT_EDGE:
u = line_from_points(0, 0, mid_x, mid_y);
l = line_from_points(0, minus(self->height, 1), mid_x, mid_y);
set_limits(0, self->width, line_y(u, x), line_y(l, x));
break;
case TOP_EDGE:
l = line_from_points(0, 0, mid_x, mid_y);
set_limits(0, mid_x, 0, line_y(l, x));
l = line_from_points(mid_x, mid_y, minus(self->width, 1), 0);
set_limits(mid_x, self->width, 0, line_y(l, x));
break;
case RIGHT_EDGE:
u = line_from_points(mid_x, mid_y, minus(self->width, 1), 0);
l = line_from_points(mid_x, mid_y, minus(self->width, 1), minus(self->height, 1));
set_limits(0, self->width, line_y(u, x), line_y(l, x));
break;
case BOTTOM_EDGE:
l = line_from_points(0, minus(self->height, 1), mid_x, mid_y);
set_limits(0, mid_x, line_y(l, x), minus(self->height, 1));
l = line_from_points(mid_x, mid_y, minus(self->width, 1), minus(self->height, 1));
set_limits(mid_x, self->width, line_y(l, x), minus(self->height, 1));
break;
}
self->y_limits_count = self->width;
fill_region(self, inverted);
#undef set_limits
}
static void
mid_lines(Canvas *self, uint level, ...) {
uint mid_x = self->width / 2, mid_y = self->height / 2;
const uint th = thickness(self, level, true);
const Point l = {.x=0, .y=mid_y}, t={.x=mid_x, .y=0}, r={.x=minus(self->width, 1), .y=mid_y}, b={.x=mid_x, .y=minus(self->height, 1)};
va_list args; va_start(args, level);
Corner which;
while ((which = va_arg(args, int)) > 0) {
Point p1, p2;
switch(which) {
case TOP_LEFT: p1 = l; p2 = t; break;
case TOP_RIGHT: p1 = r; p2 = t; break;
case BOTTOM_LEFT: p1 = l; p2 = b; break;
case BOTTOM_RIGHT: p1 = r; p2 = b; break;
}
thick_line(self, diagonal_thickness(th, p1, p2), p1, p2);
}
va_end(args);
}
static Point*
get_fading_lines(uint total_length, uint num, Edge fade) {
uint step = total_length / num, d1 = 0; int dir = 1;
if (fade == LEFT_EDGE || fade == TOP_EDGE) { dir = -1; d1 = total_length; }
Point *ans = malloc(num * sizeof(Point));
if (!ans) fatal("Out of memory");
for (uint i = 0; i < num; i++) {
uint sz = step * (num - i) / (num + 1);
if (step > 2 && sz >= step - 1) sz = step - 2;
int d2 = d1 + dir * sz; if (d2 < 0) d2 = 0;
if (d1 <= (uint)d2) { ans[i].x = d1; ans[i].y = d2; }
else { ans[i].x = d2; ans[i].y = d1; }
d1 += step * dir;
}
return ans;
}
static void
fading_hline(Canvas *self, uint level, uint num, Edge fade) {
uint y = self->height / 2;
RAII_ALLOC(Point, pts, get_fading_lines(self->width, num, fade));
for (uint i = 0; i < num; i++) {
uint x1 = pts[i].x, x2 = pts[i].y;
draw_hline(self, x1, x2, y, level);
}
}
static void
fading_vline(Canvas *self, uint level, uint num, Edge fade) {
uint x = self->width / 2;
RAII_ALLOC(Point, pts, get_fading_lines(self->height, num, fade));
for (uint i = 0; i < num; i++) {
uint y1 = pts[i].x, y2 = pts[i].y;
draw_vline(self, y1, y2, x, level);
}
}
static void
rounded_corner(Canvas *self, uint level, Corner which) {
// Render a rounded box corner.
const Range hori_line_range = hline_limits(self, half_height(self), level);
const Range vert_line_range = vline_limits(self, half_width(self), level);
const uint hori_line_height = hori_line_range.end - hori_line_range.start;
const uint vert_line_width = vert_line_range.end - vert_line_range.start;
const double adjusted_Hx = (double)vert_line_range.start + (double)vert_line_width / 2.0;
const double adjusted_Hy = (double)hori_line_range.start + (double)hori_line_height / 2.0;
const double stroke = (double)max(hori_line_height, vert_line_width);
const double corner_radius = fmin(adjusted_Hx, adjusted_Hy);
const double bx = adjusted_Hx - corner_radius;
const double by = adjusted_Hy - corner_radius;
// Anti-aliasing on corner
const double aa_corner = (double)self->supersample_factor * 0.5;
const double half_stroke = 0.5 * stroke;
const double x_shift = (which & RIGHT_EDGE) ? adjusted_Hx : -adjusted_Hx;
const double y_shift = (which & TOP_EDGE) ? -adjusted_Hy : adjusted_Hy;
for (uint y = 0; y < self->height; y++) {
const double sample_y = (double)y + y_shift + 0.5;
const double pos_y = sample_y - adjusted_Hy;
const uint row_off = y * self->width;
for (uint x = 0; x < self->width; x++) {
const double sample_x = (double)x + x_shift + 0.5;
const double pos_x = sample_x - adjusted_Hx;
const double qx = fabs(pos_x) - bx;
const double qy = fabs(pos_y) - by;
const double dx = qx > 0.0 ? qx : 0.0;
const double dy = qy > 0.0 ? qy : 0.0;
const double dist = hypot(dx, dy) + fmin(fmax(qx, qy), 0.0) - corner_radius;
const double aa = (qx > 1e-7 && qy > 1e-7) ? aa_corner : 0.0;
const double outer = half_stroke - dist;
const double inner = -half_stroke - dist;
const double alpha = smoothstep(-aa, aa, outer) - smoothstep(-aa, aa, inner);
if (alpha <= 0.0) continue;
const uint8_t value = (uint8_t)lrint(unit_double(alpha) * 255.0);
uint8_t *p = &self->mask[row_off + x];
if (value > *p) *p = value;
}
}
}
static void
commit(Canvas *self, Edge lines, bool solid) {
static const uint level = 1; static const double scale = 0.9;
uint hw = half_width(self), hh = half_height(self);
if (lines & RIGHT_EDGE) draw_hline(self, hw, self->width, hh, level);
if (lines & LEFT_EDGE) draw_hline(self, 0, hw, hh, level);
if (lines & TOP_EDGE) draw_vline(self, 0, hh, hw, level);
if (lines & BOTTOM_EDGE) draw_vline(self, hh, self->height, hw, level);
fill_circle(self, scale, 0, false);
if (!solid) fill_circle(self, scale, thickness(self, level, true), true);
}
// thin and fat line levels
#define t 1u
#define f 3u
static void
corner(Canvas *self, uint hlevel, uint vlevel, Corner which) {
const uint v_thickness = thickness(self, vlevel, true);
uint v_half_tickness;
if (which & LEFT_EDGE && v_thickness % 2 != 0) {
v_half_tickness = v_thickness / 2 + 1;
} else {
v_half_tickness = v_thickness / 2;
}
half_hline(self, hlevel, which & RIGHT_EDGE, v_half_tickness);
half_vline(self, vlevel, which & BOTTOM_EDGE, 0);
}
static void
cross(Canvas *self, uint which) {
static const uint level_map[16][4] = {
{t, t, t, t}, {f, t, t, t}, {t, f, t, t}, {f, f, t, t}, {t, t, f, t}, {t, t, t, f}, {t, t, f, f},
{f, t, f, t}, {t, f, f, t}, {f, t, t, f}, {t, f, t, f}, {f, f, f, t}, {f, f, t, f}, {f, t, f, f},
{t, f, f, f}, {f, f, f, f}
};
const uint *m = level_map[which];
half_hline(self, m[0], false, 0); half_hline(self, m[1], true, 0);
half_vline(self, m[2], false, 0); half_vline(self, m[3], true, 0);
}
static void
vert_t(Canvas *self, uint base_char, uint variation) {
static const uint level_map[8][3] = {
{t, t, t}, {t, f, t}, {f, t, t}, {t, t, f}, {f, t, f}, {f, f, t}, {t, f, f}, {f, f, f}
};
const uint *m = level_map[variation];
half_vline(self, m[0], false, 0);
half_hline(self, m[1], base_char != L'', 0);
half_vline(self, m[2], true, 0);
}
static void
horz_t(Canvas *self, uint base_char, uint variation) {
static const uint level_map[8][3] = {
{t, t, t}, {f, t, t}, {t, f, t}, {f, f, t}, {t, t, f}, {f, t, f}, {t, f, f}, {f, f, f}
};
const uint *m = level_map[variation];
half_hline(self, m[0], false, 0);
half_hline(self, m[1], true, 0);
half_vline(self, m[2], base_char != L'', 0);
}
static void
dvcorner(Canvas *self, uint level, Corner which) {
Point dline_position = half_dhline(self, level, which & LEFT_EDGE, TOP_EDGE | BOTTOM_EDGE);
if (which & BOTTOM_EDGE) {
Range bottom_limit = hline_limits(self, dline_position.y, level);
draw_vline(self, 0, bottom_limit.end, half_width(self), level);
} else {
Range top_limit = hline_limits(self, dline_position.x, level);
draw_vline(self, top_limit.start, self->height, half_width(self), level);
}
}
static void
dhcorner(Canvas *self, uint level, Corner which) {
Point dline_position = half_dvline(self, level, which & TOP_EDGE, LEFT_EDGE | RIGHT_EDGE);
if (which & RIGHT_EDGE) {
Range right_limit = vline_limits(self, dline_position.y, level);
draw_hline(self, 0, right_limit.end, half_height(self), level);
} else {
Range left_limit = vline_limits(self, dline_position.x, level);
draw_hline(self, left_limit.start, self->width, half_height(self), level);
}
}
static void
dcorner(Canvas *self, uint level, Corner which) {
uint hgap = thickness(self, level + 1, false);
uint vgap = thickness(self, level + 1, true);
uint x1 = self->width / 2, x2 = self->width / 2;
if (which & RIGHT_EDGE) x1 = 0; else x2 = self->width;
uint ypos = self->height / 2;
int ydelta = which & BOTTOM_EDGE ? hgap : -hgap;
if (which & RIGHT_EDGE) x2 += vgap; else x1 = minus(x1, vgap);
draw_hline(self, x1, x2, ypos + ydelta, level);
if (which & RIGHT_EDGE) x2 = minus(x2, 2 * vgap); else x1 += 2 * vgap;
draw_hline(self, x1, x2, ypos - ydelta, level);
uint xpos = self->width / 2;
int xdelta = (which & LEFT_EDGE) ? vgap : -vgap;
Range top_hline_limit = hline_limits(self, ypos + ydelta, level);
Range bottom_hline_limit = hline_limits(self, ypos - ydelta, level);
if (which & TOP_EDGE) {
draw_vline(self, top_hline_limit.start, self->height, xpos - xdelta, level);
draw_vline(self, bottom_hline_limit.start, self->height, xpos + xdelta, level);
} else {
draw_vline(self, 0, bottom_hline_limit.end, xpos + xdelta, level);
draw_vline(self, 0, top_hline_limit.end, xpos - xdelta, level);
}
}
static void
dpip(Canvas *self, uint level, Edge which) {
uint x1, x2, y1, y2;
if (which & (LEFT_EDGE | RIGHT_EDGE)) {
Point p = dvline(self, level, LEFT_EDGE | RIGHT_EDGE);
if (which & LEFT_EDGE) { x1 = 0; x2 = p.x; } else { x1 = p.y; x2 =self->width; }
draw_hline(self, x1, x2, self->height / 2, level);
} else {
Point p = dhline(self, level, TOP_EDGE | BOTTOM_EDGE);
if (which & TOP_EDGE) { y1 = 0; y2 = p.x; } else { y1 = p.y; y2 = self->height; }
draw_vline(self, y1, y2, self->width / 2, level);
}
}
static void
braille_dot(Canvas *self, uint col, uint row) {
static const uint num_x_dots = 2, num_y_dots = 4;
unsigned x_gaps[num_x_dots * 2], y_gaps[num_y_dots * 2];
unsigned dot_width = distribute_dots(self->width, num_x_dots, x_gaps, x_gaps + num_x_dots);
unsigned dot_height = distribute_dots(self->height, num_y_dots, y_gaps, y_gaps + num_y_dots);
uint x_start = x_gaps[col] + col * dot_width;
uint y_start = y_gaps[row] + row * dot_height;
if (y_start < self->height && x_start < self->width) {
for (uint y = y_start; y < min(self->height, y_start + dot_height); y++) {
uint offset = y * self->width;
memset(self->mask + offset + x_start, 255, minus(min(self->width, x_start + dot_width), x_start));
}
}
}
static void
braille(Canvas *self, uint8_t which) {
if (!which) return;
for (uint8_t i = 0, mask = 1; i < 8; i++, mask <<= 1) {
if (which & mask) {
uint q = i + 1, col, row;
switch(q) { case 1: case 2: case 3: case 7: col = 0; break; default: col = 1; break; }
switch(q) { case 1: case 4: row = 0; break; case 2: case 5: row = 1; break; case 3: case 6: row = 2; break; default: row = 3; }
braille_dot(self, col, row);
}
}
}
static void
fill_rect(Canvas *self, uint x_start, uint y_start, uint x_end, uint y_end) {
if (x_end > self->width) x_end = self->width;
if (y_end > self->height) y_end = self->height;
for (uint y = y_start; y < y_end; y++) {
uint off = y * self->width;
memset(self->mask + off + x_start, 255, x_end - x_start);
}
}
static void
draw_separated_block(Canvas *self, uint num_cols, uint num_rows, uint which) {
// Each "separated" block is drawn with a small gap around it.
// Use 1/8 of the cell dimensions as the gap (at least 1 pixel).
uint gap_x = max(1u, self->width / 8u);
uint gap_y = max(1u, self->height / 8u);
uint total_gap_x = (num_cols + 1) * gap_x;
uint total_gap_y = (num_rows + 1) * gap_y;
uint block_w = total_gap_x < self->width ? (self->width - total_gap_x) / num_cols : 0;
uint block_h = total_gap_y < self->height ? (self->height - total_gap_y) / num_rows : 0;
for (uint row = 0; row < num_rows; row++) {
for (uint col = 0; col < num_cols; col++) {
if (which & (1u << (row * num_cols + col))) {
uint x_start = gap_x + col * (block_w + gap_x);
uint y_start = gap_y + row * (block_h + gap_y);
fill_rect(self, x_start, y_start, x_start + block_w, y_start + block_h);
}
}
}
}
static void
sixteenth_block(Canvas *self, uint pos) {
// Fill one cell in a 4x4 grid (no gaps, just a 1/16 solid fill).
uint row = pos / 4u, col = pos % 4u;
uint block_w = self->width / 4u;
uint block_h = self->height / 4u;
uint x_start = col * block_w;
uint y_start = row * block_h;
uint x_end = col == 3u ? self->width : x_start + block_w;
uint y_end = row == 3u ? self->height : y_start + block_h;
fill_rect(self, x_start, y_start, x_end, y_end);
}
static void
draw_sextant(Canvas *self, uint row, uint col) {
Point start = {0}, end = {.x=self->width, .y = self->height};
switch(row) {
case 0: end.y = self->height / 3; break;
case 1: start.y = self->height / 3; end.y = 2 * self->height / 3; break;
case 2: start.y = 2 * self->height / 3; break;
}
switch(col) {
case 0: end.x = self->width / 2; break;
default: start.x = self->width / 2; break;
}
for (int r = start.y; r < end.y; r++) {
uint off = r * self->width;
memset(self->mask + off + start.x, 255, end.x - start.x);
}
}
static void
sextant(Canvas *self, uint which) {
#define add_row(q, r) if (q & 1) { draw_sextant(self, r, 0); } if (q & 2) { draw_sextant(self, r, 1); }
add_row(which % 4, 0)
add_row(which / 4, 1)
add_row(which / 16, 2)
#undef add_row
}
// Double diagonal lines - two parallel diagonal lines with a gap between them.
// Similar to how dhline/dvline draw double horizontal/vertical lines.
static void
double_cross_line(Canvas *self, uint level, bool left) {
uint w = minus(self->width, 1), h = minus(self->height, 1);
// The gap is perpendicular to the line direction
uint gap = thickness(self, level + 1, true);
// Offset endpoints perpendicular to the diagonal to create two parallel lines
// For a line from (x1,y1) to (x2,y2), we shift in the x-direction
Point p1a = {0}, p2a = {0}, p1b = {0}, p2b = {0};
if (left) {
// upper-left to lower-right direction
p1a = (Point){.x=0, .y=gap}; p2a = (Point){.x=minus(w, gap), .y=h};
p1b = (Point){.x=gap, .y=0}; p2b = (Point){.x=w, .y=minus(h, gap)};
} else {
// upper-right to lower-left direction
p1a = (Point){.x=w, .y=gap}; p2a = (Point){.x=gap, .y=h};
p1b = (Point){.x=minus(w, gap), .y=0}; p2b = (Point){.x=0, .y=minus(h, gap)};
}
uint th = thickness(self, level, true);
thick_line(self, diagonal_thickness(th, p1a, p2a), p1a, p2a);
thick_line(self, diagonal_thickness(th, p1b, p2b), p1b, p2b);
}
// Diagonal box drawings for U+1FBD0-U+1FBDF
// These draw light diagonal lines between specific points on the cell boundary.
// The naming convention uses: upper/lower for top/bottom edges, left/right for side edges,
// centre for the midpoint of top or bottom edges, middle for the midpoint of left or right edges.
static void
diagonal_line(Canvas *self, uint level, int x1, int y1, int x2, int y2) {
Point p1 = {.x=x1, .y=y1}, p2 = {.x=x2, .y=y2};
thick_line(self, diagonal_thickness(thickness(self, level, true), p1, p2), p1, p2);
}
// Justified half/quarter circle (filled)
// For top/bottom: diameter = cell width, circle centered horizontally.
// Top: flat edge at top, arc going down. Bottom: flat edge at bottom, arc going up.
// For left/right: diameter = cell width, circle vertically centered.
// Left: flat edge at left, arc going right. Right: flat edge at right, arc going left.
static void
justified_half_circle(Canvas *self, Edge edge) {
double cx, cy, radius;
double w = self->width, h = self->height;
radius = w / 2.0;
switch (edge) {
case TOP_EDGE:
cx = w / 2.0; cy = 0; break;
case BOTTOM_EDGE:
cx = w / 2.0; cy = h; break;
case LEFT_EDGE:
cx = 0; cy = h / 2.0; break;
case RIGHT_EDGE:
cx = w; cy = h / 2.0; break;
default: return;
}
fill_circle_of_radius(self, cx, cy, radius, 255);
}
// Justified quarter circle (filled)
// Diameter = cell width. Circle center at corner of cell.
static void
justified_quarter_circle(Canvas *self, Corner corner) {
double cx, cy;
double radius = self->width / 2.0;
switch (corner) {
case TOP_RIGHT: cx = self->width; cy = 0; break;
case BOTTOM_LEFT: cx = 0; cy = self->height; break;
case BOTTOM_RIGHT: cx = self->width; cy = self->height; break;
case TOP_LEFT: cx = 0; cy = 0; break;
default: return;
}
fill_circle_of_radius(self, cx, cy, radius, 255);
}
// Justified half circle outline (white/unfilled circle arc)
// Diameter = cell width for all edges.
// For left/right, center is at the cell edge so that two adjacent arcs
// (e.g. RIGHT then LEFT) share the diameter line and join seamlessly.
static void
justified_half_circle_outline(Canvas *self, uint level, Edge edge) {
double cx, cy, radius;
double line_width = thickness_as_float(self, level, true);
double half_lw = fmax(0.5, line_width / 2.0);
double w = self->width, h = self->height;
double start_deg, end_deg;
radius = w / 2.0 - half_lw;
switch (edge) {
case TOP_EDGE:
cx = w / 2.0; cy = half_lw;
start_deg = 0; end_deg = 180; break;
case BOTTOM_EDGE:
cx = w / 2.0; cy = h - half_lw;
start_deg = 180; end_deg = 360; break;
case LEFT_EDGE:
cx = 0; cy = h / 2.0;
start_deg = 270; end_deg = 450; break;
case RIGHT_EDGE:
cx = w; cy = h / 2.0;
start_deg = 90; end_deg = 270; break;
default: return;
}
if (radius < 1) radius = 1;
Circle c = circle(cx, cy, radius, start_deg, end_deg);
draw_parametrized_curve_with_derivative_and_antialiasing(
self, &c, line_width, circle_x, circle_y, circle_prime_x, circle_prime_y, 0, 0, NULL);
}
// Twelfth/quarter circle arcs for U+1CC30-U+1CC3F
// The 12 twelfth circles form a continuous circle when printed in a 4x4 grid:
// printf '\U1cc30\U1cc31\U1cc32\U1cc33\n\U1cc34 \U1cc37\n\U1cc38 \U1cc3b\n\U1cc3c\U1cc3d\U1cc3e\U1cc3f'
// The 4 quarter circles form a continuous circle when printed in a 2x2 grid:
// printf '\U1cc35\U1cc36\n\U1cc39\U1cc3a'
//
// For the twelfth circles: in a 4x4 grid of cells (total size 4w × 4h), an
// ellipse is centered at (2w, 2h) with semi-axes rx=2w and ry=2h so it
// exactly inscribes the grid. Each cell draws its 30° arc segment.
// For each cell at grid position (col, row), the local center is at
// (2w - col*w, 2h - row*h).
//
// For the quarter circles: in a 2x2 grid, the ellipse center is at (w, h)
// with semi-axes rx=w, ry=h.
static void
twelfth_circle(Canvas *self, uint level, uint pos) {
double line_width = thickness_as_float(self, level, true);
double half_lw = fmax(0.5, line_width / 2.0);
double w = self->width, h = self->height;
double cx, cy, rx, ry, start_deg, end_deg;
switch (pos) {
// Top row (row=0): arcs from the upper part of the ellipse
case 0: // upper left twelfth (col=0, row=0): 210° to 240°
cx = 2*w; cy = 2*h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 210; end_deg = 240; break;
case 1: // upper centre left twelfth (col=1, row=0): 240° to 270°
cx = w; cy = 2*h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 240; end_deg = 270; break;
case 2: // upper centre right twelfth (col=2, row=0): 270° to 300°
cx = 0; cy = 2*h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 270; end_deg = 300; break;
case 3: // upper right twelfth (col=3, row=0): 300° to 330°
cx = -w; cy = 2*h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 300; end_deg = 330; break;
// Side cells row=1
case 4: // upper middle left twelfth (col=0, row=1): 180° to 210°
cx = 2*w; cy = h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 180; end_deg = 210; break;
case 5: // upper left quarter circle (2x2 grid: col=0, row=0)
// Center at bottom-right corner of cell
cx = w; cy = h; rx = w - half_lw; ry = h - half_lw;
start_deg = 180; end_deg = 270; break;
case 6: // upper right quarter circle (2x2 grid: col=1, row=0)
// Center at bottom-left corner of cell
cx = 0; cy = h; rx = w - half_lw; ry = h - half_lw;
start_deg = 270; end_deg = 360; break;
case 7: // upper middle right twelfth (col=3, row=1): 330° to 360°
cx = -w; cy = h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 330; end_deg = 360; break;
// Side cells row=2
case 8: // lower middle left twelfth (col=0, row=2): 150° to 180°
cx = 2*w; cy = 0; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 150; end_deg = 180; break;
case 9: // lower left quarter circle (2x2 grid: col=0, row=1)
// Center at top-right corner of cell
cx = w; cy = 0; rx = w - half_lw; ry = h - half_lw;
start_deg = 90; end_deg = 180; break;
case 10: // lower right quarter circle (2x2 grid: col=1, row=1)
// Center at top-left corner of cell
cx = 0; cy = 0; rx = w - half_lw; ry = h - half_lw;
start_deg = 0; end_deg = 90; break;
case 11: // lower middle right twelfth (col=3, row=2): 0° to 30°
cx = -w; cy = 0; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 0; end_deg = 30; break;
// Bottom row (row=3): arcs from the lower part of the ellipse
case 12: // lower left twelfth (col=0, row=3): 120° to 150°
cx = 2*w; cy = -h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 120; end_deg = 150; break;
case 13: // lower centre left twelfth (col=1, row=3): 90° to 120°
cx = w; cy = -h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 90; end_deg = 120; break;
case 14: // lower centre right twelfth (col=2, row=3): 60° to 90°
cx = 0; cy = -h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 60; end_deg = 90; break;
case 15: // lower right twelfth (col=3, row=3): 30° to 60°
cx = -w; cy = -h; rx = 2*w - half_lw; ry = 2*h - half_lw;
start_deg = 30; end_deg = 60; break;
default: return;
}
if (rx < 1) rx = 1;
if (ry < 1) ry = 1;
Ellipse e = ellipse(cx, cy, rx, ry, start_deg, end_deg);
draw_parametrized_curve_with_derivative_and_antialiasing(
self, &e, line_width, ellipse_x, ellipse_y, ellipse_prime_x, ellipse_prime_y, 0, 0, NULL);
}
void
render_box_char(char_type ch, uint8_t *buf, unsigned width, unsigned height, double dpi_x, double dpi_y, double scale) {
Canvas canvas = {.mask=buf, .width = width, .height = height, .dpi={.x=dpi_x, .y=dpi_y}, .supersample_factor=1u, .scale=scale}, ss = canvas;
ss.mask = buf + width*height; ss.supersample_factor = SUPERSAMPLE_FACTOR;
ss.width *= SUPERSAMPLE_FACTOR; ss.height *= SUPERSAMPLE_FACTOR;
fill_canvas(&canvas, 0);
Canvas *c = &canvas;
#define SB(ch, ...) case ch: fill_canvas(&ss, 0); c = &ss, __VA_ARGS__; downsample(&ss, &canvas);
#define CC(ch, ...) case ch: __VA_ARGS__; break
#define SS(ch, ...) SB(ch, __VA_ARGS__); break
#define C(ch, func, ...) CC(ch, func(c, __VA_ARGS__))
#define S(ch, func, ...) SS(ch, func(c, __VA_ARGS__))
START_ALLOW_CASE_RANGE
switch(ch) {
default: log_error("Unknown box drawing character: U+%x rendered as blank", ch); break;
case L'': fill_canvas(c, 255); break;
C(L'', hline, 1);
C(L'', hline, 3);
C(L'', vline, 1);
C(L'', vline, 3);
C(L'', hholes, 1, 1);
C(L'', hholes, 3, 1);
C(L'', hholes, 1, 2);
C(L'', hholes, 3, 2);
C(L'', hholes, 1, 3);
C(L'', hholes, 3, 3);
C(L'', vholes, 1, 1);
C(L'', vholes, 3, 1);
C(L'', vholes, 1, 2);
C(L'', vholes, 3, 2);
C(L'', vholes, 1, 3);
C(L'', vholes, 3, 3);
C(L'', half_hline, 1, false, 0);
C(L'', half_vline, 1, false, 0);
C(L'', half_hline, 1, true, 0);
C(L'', half_vline, 1, true, 0);
C(L'', half_hline, 3, false, 0);
C(L'', half_vline, 3, false, 0);
C(L'', half_hline, 3, true, 0);
C(L'', half_vline, 3, true, 0);
CC(L'', half_hline(c, 3, false, 0); half_hline(c, 1, true, 0));
CC(L'', half_hline(c, 1, false, 0); half_hline(c, 3, true, 0));
CC(L'', half_vline(c, 3, false, 0); half_vline(c, 1, true, 0));
CC(L'', half_vline(c, 1, false, 0); half_vline(c, 3, true, 0));
S(L'', triangle, true, false);
S(L'', triangle, true, true);
SS(L'', half_cross_line(c, 1, TOP_LEFT); half_cross_line(c, 1, BOTTOM_LEFT));
S(L'', triangle, false, false);
S(L'', triangle, false, true);
SS(L'', half_cross_line(c, 1, TOP_RIGHT); half_cross_line(c, 1, BOTTOM_RIGHT));
S(L'', filled_D, true);
S(L'', filled_D, true);
S(L'', filled_D, false);
S(L'', filled_D, false);
C(L'', rounded_separator, 1, true);
C(L'', rounded_separator, 1, false);
S(L'', cross_line, 1, true);
S(L'', cross_line, 1, true);
S(L'', cross_line, 1, true);
S(L'', cross_line, 1, false);
S(L'', cross_line, 1, false);
S(L'', cross_line, 1, false);
SS(L'', cross_line(c, 1, false); cross_line(c, 1, true));
S(L'', corner_triangle, BOTTOM_LEFT);
S(L'', corner_triangle, BOTTOM_LEFT);
S(L'', corner_triangle, BOTTOM_RIGHT);
S(L'', corner_triangle, BOTTOM_RIGHT);
S(L'', corner_triangle, TOP_LEFT);
S(L'', corner_triangle, TOP_LEFT);
S(L'', corner_triangle, TOP_RIGHT);
S(L'', corner_triangle, TOP_RIGHT);
C(L'', progress_bar, LEFT, false);
C(L'', progress_bar, MIDDLE, false);
C(L'', progress_bar, RIGHT, false);
C(L'', progress_bar, LEFT, true);
C(L'', progress_bar, MIDDLE, true);
C(L'', progress_bar, RIGHT, true);
C(L'', spinner, 1, 235, 305);
C(L'', spinner, 1, 270, 390);
C(L'', spinner, 1, 315, 470);
C(L'', spinner, 1, 360, 540);
C(L'', spinner, 1, 80, 220);
C(L'', spinner, 1, 170, 270);
C(L'', spinner, 0, 0, 360);
C(L'', spinner, 1, 180, 270);
C(L'', spinner, 1, 270, 360);
C(L'', spinner, 1, 360, 450);
C(L'', spinner, 1, 450, 540);
C(L'', spinner, 1, 180, 360);
C(L'', spinner, 1, 0, 180);
S(L'', fill_circle, 1.0, 0, false);
S(L'', draw_fish_eye, 0);
C(L'', dhline, 1, TOP_EDGE | BOTTOM_EDGE);
C(L'', dvline, 1, LEFT_EDGE | RIGHT_EDGE);
CC(L'', vline(c, 1); half_dhline(c, 1, true, TOP_EDGE | BOTTOM_EDGE));
CC(L'', vline(c, 1); half_dhline(c, 1, false, TOP_EDGE | BOTTOM_EDGE));
CC(L'', hline(c, 1); half_dvline(c, 1, true, LEFT_EDGE | RIGHT_EDGE));
CC(L'', hline(c, 1); half_dvline(c, 1, false, LEFT_EDGE | RIGHT_EDGE));
CC(L'', vline(c, 1); dhline(c, 1, TOP_EDGE | BOTTOM_EDGE));
CC(L'', hline(c, 1), dvline(c, 1, LEFT_EDGE | RIGHT_EDGE));
CC(L'', inner_corner(c, 1, TOP_LEFT); inner_corner(c, 1, TOP_RIGHT); inner_corner(c, 1, BOTTOM_LEFT); inner_corner(c, 1, BOTTOM_RIGHT));
CC(L'', inner_corner(c, 1, TOP_RIGHT); inner_corner(c, 1, BOTTOM_RIGHT); dvline(c, 1, LEFT_EDGE));
CC(L'', inner_corner(c, 1, TOP_LEFT); inner_corner(c, 1, BOTTOM_LEFT); dvline(c, 1, RIGHT_EDGE));
CC(L'', inner_corner(c, 1, BOTTOM_LEFT); inner_corner(c, 1, BOTTOM_RIGHT); dhline(c, 1, TOP_EDGE));
CC(L'', inner_corner(c, 1, TOP_LEFT); inner_corner(c, 1, TOP_RIGHT); dhline(c, 1, BOTTOM_EDGE));
#define EH(ch, ...) C(ch, eight_block, true, __VA_ARGS__, -1);
EH(L'', 0);
EH(L'', 0, 1, 2, 3);
EH(L'', 7);
EH(L'', 6, 7);
EH(L'', 5, 6, 7);
EH(L'', 4, 5, 6, 7);
EH(L'', 3, 4, 5, 6, 7);
EH(L'', 2, 3, 4, 5, 6, 7);
EH(L'', 1, 2, 3, 4, 5, 6, 7);
#undef EH
#define EV(ch, ...) C(ch, eight_block, false, __VA_ARGS__, -1);
EV(L'', 0, 1, 2, 3, 4, 5, 6);
EV(L'', 0, 1, 2, 3, 4, 5);
EV(L'', 0, 1, 2, 3, 4);
EV(L'', 0, 1, 2, 3);
EV(L'', 0, 1, 2);
EV(L'', 0, 1);
EV(L'', 0);
EV(L'', 7);
EV(L'', 4, 5, 6, 7);
#undef EV
#define SH(ch, ...) C(ch, shade, (Shade){ __VA_ARGS__ });
SH(L'', .xnum=12, .light=true);
SH(L'', .xnum=12);
SH(L'', .xnum=12, .light=true, .invert=true);
SH(L'🮌', .xnum=12, .which_half=LEFT_EDGE);
SH(L'🮍', .xnum=12, .which_half=RIGHT_EDGE);
SH(L'🮎', .xnum=12, .which_half=TOP_EDGE);
SH(L'🮏', .xnum=12, .which_half=BOTTOM_EDGE);
SH(L'🮐', .xnum=12, .invert=true);
SH(L'🮑', .xnum=12, .invert=true, .fill_blank=true, .which_half=BOTTOM_EDGE);
SH(L'🮒', .xnum=12, .invert=true, .fill_blank=true, .which_half=TOP_EDGE);
SH(L'🮓', .xnum=12, .invert=true, .fill_blank=true, .which_half=RIGHT_EDGE);
SH(L'🮔', .xnum=12, .invert=true, .fill_blank=true, .which_half=LEFT_EDGE);
SH(L'🮕', .xnum=4, .ynum=4);
SH(L'🮖', .xnum=4, .ynum=4, .invert=true);
SH(L'🮗', .xnum=1, .ynum=4, .invert=true);
#define M(ch, corner) SB(ch, corner_triangle(c, corner)); \
memcpy(ss.mask, canvas.mask, sizeof(canvas.mask[0]) * canvas.width * canvas.height); \
fill_canvas(&canvas, 0); shade(&canvas, (Shade){.xnum=12}); \
apply_mask(&canvas, ss.mask); break;
M(L'🮜', TOP_LEFT);
M(L'🮝', TOP_RIGHT);
M(L'🮞', BOTTOM_RIGHT);
M(L'🮟', BOTTOM_LEFT);
#undef M
#undef SH
S(L'🮘', cross_shade, false);
S(L'🮙', cross_shade, true);
C(L'', quad, BOTTOM_LEFT);
C(L'', quad, BOTTOM_RIGHT);
C(L'', quad, TOP_LEFT);
C(L'', quad, TOP_RIGHT);
C(L'', quads, TOP_LEFT, BOTTOM_LEFT, BOTTOM_RIGHT, 0);
C(L'', quads, TOP_LEFT, BOTTOM_RIGHT, 0);
C(L'', quads, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, 0);
C(L'', quads, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, 0);
C(L'', quads, TOP_RIGHT, BOTTOM_LEFT, 0);
C(L'', quads, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, 0);
S(L'🬼', smooth_mosaic, true, 0, 2. / 3, 0.5, 1);
S(L'🬽', smooth_mosaic, true, 0, 2. / 3, 1, 1);
S(L'🬾', smooth_mosaic, true, 0, 1. / 3, 0.5, 1);
S(L'🬿', smooth_mosaic, true, 0, 1. / 3, 1, 1);
S(L'🭀', smooth_mosaic, true, 0, 0, 0.5, 1);
S(L'🭁', smooth_mosaic, true, 0, 1. / 3, 0.5, 0);
S(L'🭂', smooth_mosaic, true, 0, 1. / 3, 1, 0);
S(L'🭃', smooth_mosaic, true, 0, 2. / 3, 0.5, 0);
S(L'🭄', smooth_mosaic, true, 0, 2. / 3, 1, 0);
S(L'🭅', smooth_mosaic, true, 0, 1, 0.5, 0);
S(L'🭆', smooth_mosaic, true, 0, 2. / 3, 1, 1. / 3);
S(L'🭇', smooth_mosaic, true, 0.5, 1, 1, 2. / 3);
S(L'🭈', smooth_mosaic, true, 0, 1, 1, 2. / 3);
S(L'🭉', smooth_mosaic, true, 0.5, 1, 1, 1. / 3);
S(L'🭊', smooth_mosaic, true, 0, 1, 1, 1. / 3);
S(L'🭋', smooth_mosaic, true, 0.5, 1, 1, 0);
S(L'🭌', smooth_mosaic, true, 0.5, 0, 1, 1. / 3);
S(L'🭍', smooth_mosaic, true, 0, 0, 1, 1. / 3);
S(L'🭎', smooth_mosaic, true, 0.5, 0, 1, 2. / 3);
S(L'🭏', smooth_mosaic, true, 0, 0, 1, 2. / 3);
S(L'🭐', smooth_mosaic, true, 0.5, 0, 1, 1);
S(L'🭑', smooth_mosaic, true, 0, 1. / 3, 1, 2. / 3);
S(L'🭒', smooth_mosaic, false, 0, 2. / 3, 0.5, 1);
S(L'🭓', smooth_mosaic, false, 0, 2. / 3, 1, 1);
S(L'🭔', smooth_mosaic, false, 0, 1. / 3, 0.5, 1);
S(L'🭕', smooth_mosaic, false, 0, 1. / 3, 1, 1);
S(L'🭖', smooth_mosaic, false, 0, 0, 0.5, 1);
S(L'🭗', smooth_mosaic, false, 0, 1. / 3, 0.5, 0);
S(L'🭘', smooth_mosaic, false, 0, 1. / 3, 1, 0);
S(L'🭙', smooth_mosaic, false, 0, 2. / 3, 0.5, 0);
S(L'🭚', smooth_mosaic, false, 0, 2. / 3, 1, 0);
S(L'🭛', smooth_mosaic, false, 0, 1, 0.5, 0);
S(L'🭜', smooth_mosaic, false, 0, 2. / 3, 1, 1. / 3);
S(L'🭝', smooth_mosaic, false, 0.5, 1, 1, 2. / 3);
S(L'🭞', smooth_mosaic, false, 0, 1, 1, 2. / 3);
S(L'🭟', smooth_mosaic, false, 0.5, 1, 1, 1. / 3);
S(L'🭠', smooth_mosaic, false, 0, 1, 1, 1. / 3);
S(L'🭡', smooth_mosaic, false, 0.5, 1, 1, 0);
S(L'🭢', smooth_mosaic, false, 0.5, 0, 1, 1. / 3);
S(L'🭣', smooth_mosaic, false, 0, 0, 1, 1. / 3);
S(L'🭤', smooth_mosaic, false, 0.5, 0, 1, 2. / 3);
S(L'🭥', smooth_mosaic, false, 0, 0, 1, 2. / 3);
S(L'🭦', smooth_mosaic, false, 0.5, 0, 1, 1);
S(L'🭧', smooth_mosaic, false, 0, 1. / 3, 1, 2. / 3);
S(L'🭨', half_triangle, LEFT_EDGE, true);
S(L'🭩', half_triangle, TOP_EDGE, true);
S(L'🭪', half_triangle, RIGHT_EDGE, true);
S(L'🭫', half_triangle, BOTTOM_EDGE, true);
S(L'🭬', half_triangle, LEFT_EDGE, false);
SS(L'🮛', half_triangle(c, LEFT_EDGE, false), half_triangle(c, RIGHT_EDGE, false));
S(L'🭭', half_triangle, TOP_EDGE, false);
S(L'🭮', half_triangle, RIGHT_EDGE, false);
S(L'🭯', half_triangle, BOTTOM_EDGE, false);
SS(L'🮚', half_triangle(c, BOTTOM_EDGE, false), half_triangle(c, TOP_EDGE, false));
CC(L'🭼', eight_bar(c, 0, false); eight_bar(c, 7, true));
CC(L'🭽', eight_bar(c, 0, false); eight_bar(c, 0, true));
CC(L'🭾', eight_bar(c, 7, false); eight_bar(c, 0, true));
CC(L'🭿', eight_bar(c, 7, false); eight_bar(c, 7, true));
CC(L'🮀', eight_bar(c, 0, true); eight_bar(c, 7, true));
CC(L'🮁', eight_bar(c, 0, true); eight_bar(c, 2, true); eight_bar(c, 4, true); eight_bar(c, 7, true));
C(L'🮂', eight_block, true, 0, 1, -1);
C(L'🮃', eight_block, true, 0, 1, 2, -1);
C(L'🮄', eight_block, true, 0, 1, 2, 3, 4, -1);
C(L'🮅', eight_block, true, 0, 1, 2, 3, 4, 5, -1);
C(L'🮆', eight_block, true, 0, 1, 2, 3, 4, 5, 6, -1);
C(L'🮇', eight_block, false, 6, 7, -1);
C(L'🮈', eight_block, false, 5, 6, 7, -1);
C(L'🮉', eight_block, false, 3, 4, 5, 6, 7, -1);
C(L'🮊', eight_block, false, 2, 3, 4, 5, 6, 7, -1);
C(L'🮋', eight_block, false, 1, 2, 3, 4, 5, 6, 7, -1);
S(L'🮠', mid_lines, 1, TOP_LEFT, 0);
S(L'🮡', mid_lines, 1, TOP_RIGHT, 0);
S(L'🮢', mid_lines, 1, BOTTOM_LEFT, 0);
S(L'🮣', mid_lines, 1, BOTTOM_RIGHT, 0);
S(L'🮤', mid_lines, 1, TOP_LEFT, BOTTOM_LEFT, 0);
S(L'🮥', mid_lines, 1, TOP_RIGHT, BOTTOM_RIGHT, 0);
S(L'🮦', mid_lines, 1, BOTTOM_RIGHT, BOTTOM_LEFT, 0);
S(L'🮧', mid_lines, 1, TOP_RIGHT, TOP_LEFT, 0);
S(L'🮨', mid_lines, 1, BOTTOM_RIGHT, TOP_LEFT, 0);
S(L'🮩', mid_lines, 1, BOTTOM_LEFT, TOP_RIGHT, 0);
S(L'🮪', mid_lines, 1, BOTTOM_LEFT, TOP_RIGHT, BOTTOM_RIGHT, 0);
S(L'🮫', mid_lines, 1, BOTTOM_LEFT, TOP_LEFT, BOTTOM_RIGHT, 0);
S(L'🮬', mid_lines, 1, TOP_RIGHT, TOP_LEFT, BOTTOM_RIGHT, 0);
S(L'🮭', mid_lines, 1, TOP_RIGHT, TOP_LEFT, BOTTOM_LEFT, 0);
S(L'🮮', mid_lines, 1, TOP_RIGHT, BOTTOM_RIGHT, TOP_LEFT, BOTTOM_LEFT, 0);
C(L'', hline, 1);
C(L'', vline, 1);
C(L'', fading_hline, 1, 4, RIGHT_EDGE);
C(L'', fading_hline, 1, 4, LEFT_EDGE);
C(L'', fading_vline, 1, 5, BOTTOM_EDGE);
C(L'', fading_vline, 1, 5, TOP_EDGE);
C(L'', rounded_corner, 1, TOP_LEFT);
C(L'', rounded_corner, 1, TOP_RIGHT);
C(L'', rounded_corner, 1, BOTTOM_LEFT);
C(L'', rounded_corner, 1, BOTTOM_RIGHT);
CC(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT));
CC(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT));
CC(L'', rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, TOP_LEFT));
CC(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', vline(c, 1); rounded_corner(c, 1, TOP_RIGHT));
CC(L'', rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, TOP_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, TOP_LEFT));
CC(L'', rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, TOP_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT));
CC(L'', rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, TOP_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, TOP_LEFT));
CC(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', vline(c, 1); rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_LEFT));
CC(L'', hline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
CC(L'', hline(c, 1); rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_LEFT));
#define P(ch, lines) S(ch, commit, lines, true); S(ch+1, commit, lines, false);
P(L'', 0);
P(L'', RIGHT_EDGE);
P(L'', LEFT_EDGE);
P(L'', LEFT_EDGE | RIGHT_EDGE);
P(L'', BOTTOM_EDGE);
P(L'', TOP_EDGE);
P(L'', BOTTOM_EDGE | TOP_EDGE);
P(L'', RIGHT_EDGE | BOTTOM_EDGE);
P(L'', LEFT_EDGE | BOTTOM_EDGE);
P(L'', RIGHT_EDGE | TOP_EDGE);
P(L'', LEFT_EDGE | TOP_EDGE);
P(L'', TOP_EDGE | BOTTOM_EDGE | RIGHT_EDGE);
P(L'', TOP_EDGE | BOTTOM_EDGE | LEFT_EDGE);
P(L'', LEFT_EDGE | RIGHT_EDGE | BOTTOM_EDGE);
P(L'', LEFT_EDGE | RIGHT_EDGE | TOP_EDGE);
P(L'', LEFT_EDGE | RIGHT_EDGE | TOP_EDGE | BOTTOM_EDGE);
#undef P
#define Q(ch, which) C(ch, corner, t, t, which); C(ch + 1, corner, f, t, which); C(ch + 2, corner, t, f, which); C(ch + 3, corner, f, f, which);
Q(L'', BOTTOM_RIGHT); Q(L'', BOTTOM_LEFT); Q(L'', TOP_RIGHT); Q(L'', TOP_LEFT);
#undef Q
C(L'', rounded_corner, 1, TOP_LEFT);
C(L'', rounded_corner, 1, TOP_RIGHT);
C(L'', rounded_corner, 1, BOTTOM_LEFT);
C(L'', rounded_corner, 1, BOTTOM_RIGHT);
case L'' ... L'' + 15: cross(c, ch - L''); break;
#define T(q, func) case q ... q + 7: func(c, q, ch - q); break;
T(L'', vert_t); T(L'', vert_t);
T(L'', horz_t); T(L'', horz_t);
#undef T
C(L'', dvcorner, 1, TOP_LEFT);
C(L'', dvcorner, 1, TOP_RIGHT);
C(L'', dvcorner, 1, BOTTOM_LEFT);
C(L'', dvcorner, 1, BOTTOM_RIGHT);
C(L'', dhcorner, 1, TOP_LEFT);
C(L'', dhcorner, 1, TOP_RIGHT);
C(L'', dhcorner, 1, BOTTOM_LEFT);
C(L'', dhcorner, 1, BOTTOM_RIGHT);
C(L'', dcorner, 1, TOP_LEFT);
C(L'', dcorner, 1, TOP_RIGHT);
C(L'', dcorner, 1, BOTTOM_LEFT);
C(L'', dcorner, 1, BOTTOM_RIGHT);
C(L'', dpip, 1, RIGHT_EDGE);
C(L'', dpip, 1, LEFT_EDGE);
C(L'', dpip, 1, BOTTOM_EDGE);
C(L'', dpip, 1, TOP_EDGE);
case 0x2800 ... 0x2800 + 255: braille(c, ch - 0x2800); break;
case 0x1fb00 ... 0x1fb00 + 19: sextant(c, ch - 0x1fb00 + 1); break;
case 0x1fb14 ... 0x1fb14 + 19: sextant(c, ch - 0x1fb00 + 2); break;
case 0x1fb28 ... 0x1fb28 + 19: sextant(c, ch - 0x1fb00 + 3); break;
case 0x1fb70 ... 0x1fb70 + 5: eight_bar(c, ch - 0x1fb6f, false); break;
case 0x1fb76 ... 0x1fb76 + 5: eight_bar(c, ch - 0x1fb75, true); break;
case 0x1fbe6: octant(c, 0xe6); break;
case 0x1fbe7: octant(c, 0xe7); break;
case 0x1cd00 ... 0x1cde5: octant(c, ch - 0x1cd00); break;
// U+1FBCE LEFT TWO THIRDS BLOCK
case 0x1fbce: fill_rect(c, 0, 0, 2 * c->width / 3, c->height); break;
// U+1FBCF LEFT ONE THIRD BLOCK
case 0x1fbcf: fill_rect(c, 0, 0, c->width / 3, c->height); break;
// U+1FBE4 UPPER CENTRE ONE QUARTER BLOCK
case 0x1fbe4: fill_rect(c, c->width / 4, 0, 3 * c->width / 4, c->height / 2); break;
// U+1FBE5 LOWER CENTRE ONE QUARTER BLOCK
case 0x1fbe5: fill_rect(c, c->width / 4, c->height / 2, 3 * c->width / 4, c->height); break;
// U+1FBD0-1FBDF Diagonal box drawings (supersampled for anti-aliasing)
// Key points used: UL=(0,0), UC=(w/2,0), UR=(w,0), ML=(0,h/2), MC=(w/2,h/2), MR=(w,h/2),
// LL=(0,h), LC=(w/2,h), LR=(w,h)
#define DL(x1,y1,x2,y2) diagonal_line(c, 1, x1, y1, x2, y2)
#define W (int)minus(c->width,1)
#define H (int)minus(c->height,1)
#define HW ((int)(c->width/2))
#define HH ((int)(c->height/2))
// 1FBD0: middle right to lower left
SS(0x1fbd0, DL(W, HH, 0, H));
// 1FBD1: upper right to middle left
SS(0x1fbd1, DL(W, 0, 0, HH));
// 1FBD2: upper left to middle right
SS(0x1fbd2, DL(0, 0, W, HH));
// 1FBD3: middle left to lower right
SS(0x1fbd3, DL(0, HH, W, H));
// 1FBD4: upper left to lower centre
SS(0x1fbd4, DL(0, 0, HW, H));
// 1FBD5: upper centre to lower right
SS(0x1fbd5, DL(HW, 0, W, H));
// 1FBD6: upper right to lower centre
SS(0x1fbd6, DL(W, 0, HW, H));
// 1FBD7: upper centre to lower left
SS(0x1fbd7, DL(HW, 0, 0, H));
// 1FBD8: upper left to middle centre to upper right (V open down)
SS(0x1fbd8, DL(0, 0, HW, HH); DL(HW, HH, W, 0));
// 1FBD9: upper right to middle centre to lower right (> shape)
SS(0x1fbd9, DL(W, 0, HW, HH); DL(HW, HH, W, H));
// 1FBDA: lower left to middle centre to lower right (^ shape)
SS(0x1fbda, DL(0, H, HW, HH); DL(HW, HH, W, H));
// 1FBDB: upper left to middle centre to lower left (< shape)
SS(0x1fbdb, DL(0, 0, HW, HH); DL(HW, HH, 0, H));
// 1FBDC: upper left to lower centre to upper right (V with apex at bottom-center)
SS(0x1fbdc, DL(0, 0, HW, H); DL(HW, H, W, 0));
// 1FBDD: upper right to middle left to lower right (> with apex at middle-left)
SS(0x1fbdd, DL(W, 0, 0, HH); DL(0, HH, W, H));
// 1FBDE: lower left to upper centre to lower right (^ with apex at upper-center)
SS(0x1fbde, DL(0, H, HW, 0); DL(HW, 0, W, H));
// 1FBDF: upper left to middle right to lower left (< with apex at middle-right)
SS(0x1fbdf, DL(0, 0, W, HH); DL(W, HH, 0, H));
#undef DL
#undef W
#undef H
#undef HW
#undef HH
// U+1FBE0-1FBE3 Justified half white circles (outlines)
S(0x1fbe0, justified_half_circle_outline, 1, TOP_EDGE);
S(0x1fbe1, justified_half_circle_outline, 1, RIGHT_EDGE);
S(0x1fbe2, justified_half_circle_outline, 1, BOTTOM_EDGE);
S(0x1fbe3, justified_half_circle_outline, 1, LEFT_EDGE);
// U+1FBE8-1FBEB Justified half black circles (filled)
S(0x1fbe8, justified_half_circle, TOP_EDGE);
S(0x1fbe9, justified_half_circle, RIGHT_EDGE);
S(0x1fbea, justified_half_circle, BOTTOM_EDGE);
S(0x1fbeb, justified_half_circle, LEFT_EDGE);
// U+1FBEC-1FBEF Justified quarter black circles (filled)
S(0x1fbec, justified_quarter_circle, TOP_RIGHT);
S(0x1fbed, justified_quarter_circle, BOTTOM_LEFT);
S(0x1fbee, justified_quarter_circle, BOTTOM_RIGHT);
S(0x1fbef, justified_quarter_circle, TOP_LEFT);
// U+1CC1B-1CC1E Box drawing variants
// 1CC1B: HORIZONTAL AND UPPER RIGHT - full hline + half vline going up from right quarter
CC(0x1cc1b, hline(c, 1); draw_vline(c, 0, c->height / 2, 3 * c->width / 4, 1));
// 1CC1C: HORIZONTAL AND LOWER RIGHT - full hline + half vline going down from right quarter
CC(0x1cc1c, hline(c, 1); draw_vline(c, c->height / 2, c->height, 3 * c->width / 4, 1));
// 1CC1D: TOP AND UPPER LEFT - half vline from top to 1/4 + half hline going left from that point
CC(0x1cc1d, draw_vline(c, 0, c->height / 4, c->width / 2, 1); draw_hline(c, 0, c->width / 2, c->height / 4, 1));
// 1CC1E: BOTTOM AND LOWER LEFT - half vline from bottom to 3/4 + half hline going left from that point
CC(0x1cc1e, draw_vline(c, 3 * c->height / 4, c->height, c->width / 2, 1); draw_hline(c, 0, c->width / 2, 3 * c->height / 4, 1));
// U+1CC1F-1CC20 Double diagonal lines
S(0x1cc1f, double_cross_line, 1, false);
S(0x1cc20, double_cross_line, 1, true);
// Symbols for Legacy Computing Supplement (U+1CC00U+1CEBF)
// Separated Block Quadrant (bit 0=TL, 1=TR, 2=BL, 3=BR)
case 0x1cc21 ... 0x1cc21 + 14: draw_separated_block(c, 2, 2, ch - 0x1cc21 + 1); break;
// U+1CC30-1CC3F Twelfth and quarter circle arcs
case 0x1cc30 ... 0x1cc3f: twelfth_circle(c, 1, ch - 0x1cc30); break;
// U+1CE16-1CE19 Box drawings light vertical with offset horizontal
// 1CE16: VERTICAL AND TOP RIGHT - full vline + half hline going right from 1/4 height
CC(0x1ce16, vline(c, 1); draw_hline(c, c->width / 2, c->width, c->height / 4, 1));
// 1CE17: VERTICAL AND BOTTOM RIGHT - full vline + half hline going right from 3/4 height
CC(0x1ce17, vline(c, 1); draw_hline(c, c->width / 2, c->width, 3 * c->height / 4, 1));
// 1CE18: VERTICAL AND TOP LEFT - full vline + half hline going left from 1/4 height
CC(0x1ce18, vline(c, 1); draw_hline(c, 0, c->width / 2, c->height / 4, 1));
// 1CE19: VERTICAL AND BOTTOM LEFT - full vline + half hline going left from 3/4 height
CC(0x1ce19, vline(c, 1); draw_hline(c, 0, c->width / 2, 3 * c->height / 4, 1));
// Separated Block Sextant (same bit encoding as regular sextants)
case 0x1ce51 ... 0x1ce51 + 62: draw_separated_block(c, 2, 3, ch - 0x1ce51 + 1); break;
// One Sixteenth Block: individual 1/16 cells in a 4x4 grid, row-major
case 0x1ce90 ... 0x1ce90 + 15: sixteenth_block(c, ch - 0x1ce90); break;
// One Quarter Block partial fills: each is a sub-rectangle of one of the four quarter strips
// Lower quarter (y: 3H/4 to H)
case 0x1cea0: fill_rect(c, c->width/2, 3*c->height/4, c->width, c->height); break;
case 0x1cea1: fill_rect(c, c->width/4, 3*c->height/4, c->width, c->height); break;
case 0x1cea2: fill_rect(c, 0, 3*c->height/4, 3*c->width/4, c->height); break;
case 0x1cea3: fill_rect(c, 0, 3*c->height/4, c->width/2, c->height); break;
// Left quarter (x: 0 to W/4)
case 0x1cea4: fill_rect(c, 0, c->height/2, c->width/4, c->height); break;
case 0x1cea5: fill_rect(c, 0, c->height/4, c->width/4, c->height); break;
case 0x1cea6: fill_rect(c, 0, 0, c->width/4, 3*c->height/4); break;
case 0x1cea7: fill_rect(c, 0, 0, c->width/4, c->height/2); break;
// Upper quarter (y: 0 to H/4)
case 0x1cea8: fill_rect(c, 0, 0, c->width/2, c->height/4); break;
case 0x1cea9: fill_rect(c, 0, 0, 3*c->width/4, c->height/4); break;
case 0x1ceaa: fill_rect(c, c->width/4, 0, c->width, c->height/4); break;
case 0x1ceab: fill_rect(c, c->width/2, 0, c->width, c->height/4); break;
// Right quarter (x: 3W/4 to W)
case 0x1ceac: fill_rect(c, 3*c->width/4, 0, c->width, c->height/2); break;
case 0x1cead: fill_rect(c, 3*c->width/4, 0, c->width, 3*c->height/4); break;
case 0x1ceae: fill_rect(c, 3*c->width/4, c->height/4, c->width, c->height); break;
case 0x1ceaf: fill_rect(c, 3*c->width/4, c->height/2, c->width, c->height); break;
}
free(canvas.holes); free(canvas.y_limits);
free(ss.holes); free(ss.y_limits);
END_ALLOW_CASE_RANGE
#undef CC
#undef SS
#undef C
#undef S
#undef SB
#undef t
#undef f
}