/* * decorations.c * Copyright (C) 2024 Kovid Goyal * * 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)); } 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) { \ 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); } 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, 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); } } 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 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); 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; } 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 }