Files
kitty-mirror/kitty/decorations.c

1808 lines
72 KiB
C
Raw Permalink 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;
for (unsigned x = offset; x < offset + width; x++) ans[x] = 0xff;
}
}
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;
for (unsigned x = 0; x < fcm.cell_width; x++) ans[offset + x] = 0xff;
}
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 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 uint
thickness(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 (uint)ceil(self->supersample_factor * self->scale * pts * dpi / 72.0);
}
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 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
uint sz = thickness(self, level, false);
uint start = minus(y, sz / 2);
for (uint y = start; y < min(start + sz, self->height); y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + x1, 255, minus(min(x2, self->width), x1));
}
}
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
uint sz = thickness(self, level, true);
uint start = minus(x, sz / 2), end = min(start + sz, self->width), xsz = minus(end, start);
for (uint y = y1; y < min(y2, self->height); y++) {
uint8_t *py = self->mask + y * self->width;
memset(py + 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 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));
}
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, thickness(self, level, true), 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, thickness(self, level, true), p1, p2);
}
typedef struct CubicBezier {
Point start, c1, c2, end;
} CubicBezier;
#define bezier_eq(which) { \
double tm1 = 1 - t; \
double tm1_3 = tm1 * tm1 * tm1; \
double t_3 = t * t * t; \
return tm1_3 * cb.start.which + 3 * t * tm1 * (tm1 * cb.c1.which + t * cb.c2.which) + t_3 * cb.end.which; \
}
static double
bezier_x(CubicBezier cb, double t) { bezier_eq(x); }
static double
bezier_y(CubicBezier cb, double t) { bezier_eq(y); }
#undef bezier_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(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, 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));
}
#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; }
#define draw_parametrized_curve(self, level, xfunc, yfunc) { \
div_t d = div(thickness(self, level, true), 2u); \
int delta = d.quot, extra = d.rem; \
uint num_samples = self->height * 8; \
position_set seen; vt_init(&seen); \
for (uint i = 0; i < num_samples + 1; i++) { \
double t = i / (double)num_samples; \
Point p = {.x=(int32_t)xfunc, .y=(int32_t)yfunc}; \
position_set_itr q = vt_get(&seen, p); \
if (!vt_is_end(q)) continue; \
if (vt_is_end(vt_insert(&seen, p))) fatal("Out of memory"); \
for (int y = MAX(0, p.y - delta); y < MIN(p.y + delta + extra, (int)self->height); y++) { \
uint offset = y * self->width, start = MAX(0, p.x - delta); \
memset(self->mask + offset + start, 255, minus((uint)MIN(p.x + delta + extra, (int)self->width), start)); \
} \
} \
vt_cleanup(&seen); \
}
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), self->height);
CubicBezier cb = {.end={.y=self->height - 1}, .c1={.x=c1x}, .c2={.x=c1x, .y=self->height - 1}};
if (left) { draw_parametrized_curve(self, level, bezier_x(cb, t), bezier_y(cb, t)); }
else { mirror_horizontally(draw_parametrized_curve(self, level, bezier_x(cb, t), bezier_y(cb, t))); }
}
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 {
Point origin;
double radius;
double start, end, amt;
} Circle;
static Circle
circle(Point origin, double radius, double start_at, double end_at) {
double conv = M_PI / 180.;
Circle ans = {.origin=origin, .radius=radius, .start=start_at*conv, .end=end_at*conv};
ans.amt = ans.end - ans.start;
return ans;
}
static double
circle_x(Circle c, double t) { return c.origin.x + c.radius * cos(c.start + c.amt * t); }
static double
circle_y(Circle c, double t) { return c.origin.y + c.radius * sin(c.start + c.amt * t); }
static void
spinner(Canvas *self, uint level, double start_degrees, double end_degrees) {
uint w = self->width / 2, h = self->height / 2;
uint radius = minus(min(w, h), thickness(self, level, true) / 2);
Circle c = circle((Point){.x=w, .y=h}, radius, start_degrees, end_degrees);
draw_parametrized_curve(self, level, circle_x(c, t), circle_y(c, t));
}
static void
draw_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;
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 - w, yh = (double)y - h;
if (xw * xw + yh * yh <= limit) self->mask[y * self->width + x] = fill;
}
}
}
static void
draw_fish_eye(Canvas *self, uint level) {
uint w = self->width / 2, h = self->height / 2;
uint line_width = thickness(self, level, true) / 2;
uint radius = minus(min(w, h), line_width);
Circle c = circle((Point){.x=w, .y=h}, radius, 0, 360);
draw_parametrized_curve(self, level, circle_x(c, t), circle_y(c, t));
uint gap = minus(radius, radius / 10);
draw_circle(self, 1.0, gap, false);
}
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) {
x2 = minus(self->width / 2 + vthick + 1, hgap); xd = -1;
} 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, th, 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);
}
}
typedef struct Rectircle Rectircle;
typedef double (*Rectircle_equation)(Rectircle r, double t);
typedef struct Rectircle {
uint a, b;
double yexp, xexp, adjust_x;
uint cell_width;
Rectircle_equation x, y;
} Rectircle;
static double
rectircle_lower_quadrant_y(Rectircle r, double t) {
return r.b * t; // 0 -> top of cell, 1 -> middle of cell
}
static double
rectircle_upper_quadrant_y(Rectircle r, double t) {
return r.b * (2. - t); // 0 -> bottom of cell, 1 -> middle of cell
}
// x(t). To get this we first need |y(t)|/b. This is just t since as t goes
// from 0 to 1 y goes from either 0 to b or 0 to -b
static double
rectircle_left_quadrant_x(Rectircle r, double t) {
double xterm = 1 - pow(t, r.yexp);
return floor(r.cell_width - fabs(r.a * pow(xterm, r.xexp)) - r.adjust_x);
}
static double
rectircle_right_quadrant_x(Rectircle r, double t) {
double xterm = 1 - pow(t, r.yexp);
return ceil(fabs(r.a * pow(xterm, r.xexp)));
}
static Rectircle
rectcircle(Canvas *self, Corner which) {
/*
Return two functions, x(t) and y(t) that map the parameter t which must be
in the range [0, 1] to x and y coordinates in the cell. The rectircle equation
we use is:
(|x| / a) ^ (2a / r) + (|y| / a) ^ (2b / r) = 1
where 2a = width, 2b = height and r is radius
The entire rectircle fits in four cells, each cell being one quadrant
of the full rectircle and the origin being the center of the rectircle.
The functions we return do the mapping for the specified cell.
╭╮ ╭─╮
╰╯ │ │
╰─╯
See https://math.stackexchange.com/questions/1649714
*/
double radius = self->width / 2.;
uint cell_width_is_odd = (self->width / self->supersample_factor) & 1;
Rectircle ans = {
.a = half_width(self), .b = half_height(self),
.yexp = self->height / radius,
.xexp = radius / self->width,
.cell_width = self->width,
.adjust_x = cell_width_is_odd * self->supersample_factor,
.x = which & LEFT_EDGE ? rectircle_left_quadrant_x : rectircle_right_quadrant_x,
.y = which & TOP_EDGE ? rectircle_upper_quadrant_y : rectircle_lower_quadrant_y,
};
return ans;
}
static void
rounded_corner(Canvas *self, uint level, Corner which) {
Rectircle r = rectcircle(self, which);
draw_parametrized_curve(self, level, r.x(r, t), r.y(r, t));
}
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);
draw_circle(self, scale, 0, false);
if (!solid) draw_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) {
half_hline(self, hlevel, which & RIGHT_EDGE, thickness(self, vlevel, true) / 2);
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) {
half_dhline(self, level, which & LEFT_EDGE, TOP_EDGE | BOTTOM_EDGE);
uint gap = thickness(self, level + 1, false);
half_vline(self, level, which & TOP_EDGE, gap / 2 + thickness(self, level, false));
}
static void
dhcorner(Canvas *self, uint level, Corner which) {
half_dvline(self, level, which & TOP_EDGE, LEFT_EDGE | RIGHT_EDGE);
uint gap = thickness(self, level + 1, true);
half_hline(self, level, which & LEFT_EDGE, gap / 2 + thickness(self, level, true));
}
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 y1 = self->height / 2, y2 = self->height / 2;
if (which & BOTTOM_EDGE) y1 = 0; else y2 = self->height;
uint xpos = self->width / 2;
int xdelta = (which & LEFT_EDGE) ? vgap : -vgap;
uint yd = thickness(self, level, true) / 2;
if (which & BOTTOM_EDGE) y2 += hgap + yd; else y1 -= hgap + yd;
draw_vline(self, y1, y2, xpos - xdelta, level);
if (which & BOTTOM_EDGE) y2 -= 2 * hgap; else y1 += 2 * hgap;
draw_vline(self, y1, y2, 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
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
}
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);
S(L'', rounded_separator, 1, true);
S(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);
S(L'', spinner, 1, 235, 305);
S(L'', spinner, 1, 270, 390);
S(L'', spinner, 1, 315, 470);
S(L'', spinner, 1, 360, 540);
S(L'', spinner, 1, 80, 220);
S(L'', spinner, 1, 170, 270);
S(L'', spinner, 0, 0, 360);
S(L'', spinner, 1, 180, 270);
S(L'', spinner, 1, 270, 360);
S(L'', spinner, 1, 360, 450);
S(L'', spinner, 1, 450, 540);
S(L'', spinner, 1, 180, 360);
S(L'', spinner, 1, 0, 180);
S(L'', draw_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);
S(L'', rounded_corner, 1, TOP_LEFT);
S(L'', rounded_corner, 1, TOP_RIGHT);
S(L'', rounded_corner, 1, BOTTOM_LEFT);
S(L'', rounded_corner, 1, BOTTOM_RIGHT);
SS(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT));
SS(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT));
SS(L'', rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, TOP_LEFT));
SS(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', vline(c, 1); rounded_corner(c, 1, TOP_RIGHT));
SS(L'', rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, TOP_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, TOP_LEFT));
SS(L'', rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, TOP_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT));
SS(L'', rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', vline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, TOP_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', hline(c, 1); rounded_corner(c, 1, BOTTOM_LEFT), rounded_corner(c, 1, TOP_LEFT));
SS(L'', vline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(L'', vline(c, 1); rounded_corner(c, 1, TOP_RIGHT), rounded_corner(c, 1, BOTTOM_LEFT));
SS(L'', hline(c, 1); rounded_corner(c, 1, TOP_LEFT), rounded_corner(c, 1, BOTTOM_RIGHT));
SS(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
S(L'', rounded_corner, 1, TOP_LEFT);
S(L'', rounded_corner, 1, TOP_RIGHT);
S(L'', rounded_corner, 1, BOTTOM_LEFT);
S(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;
}
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
}