From 1377e6d22595e78762b7a9887f5d04cba69cfdc9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 15:33:58 -0600 Subject: [PATCH] font/sprite: rework sprite font drawing This is a fairly large rework of how we handle the sprite font drawing. Drawing routines are now context-less, provided only a canvas and some metrics. There is now a separate file per unicode block / PUA area. Sprites are now drawn on canvases with an extra quarter-cell of padding on each edge, and automatically cropped when sent to the atlas, this allows sprites to extend past cell boundaries which makes it possible to have, for example, diagonal box drawing characters that connect across cell diagonals instead of being pinched in. Most of the sprites the code is just directly ported from the old code, but I've rewritten a handful. Moving forward, I'd like to rewrite more of these since the way they're currently written isn't ideal. This rework, in addition to improving the packing efficiency of sprites on the atlas, and allowing for out-of-cell drawing, will make it a lot easier to add new sprites in the future, since all it takes now is to add a single function and an import (if it's a new file). I reworked the regression/change testing to be more robust as well, it now covers all sprite glyphs (except non-codepoint ones) and does so at 4 different sizes. Addition/removal of glyphs will no longer create diff noise in the generated diff image, since the position in the image of each glyph is now fixed. --- src/font/Atlas.zig | 31 + src/font/sprite.zig | 6 - src/font/sprite/Box.zig | 3397 ----------------- src/font/sprite/Face.zig | 683 ++-- src/font/sprite/Powerline.zig | 564 --- src/font/sprite/canvas.zig | 413 +- src/font/sprite/cursor.zig | 65 - src/font/sprite/draw/README.md | 50 + src/font/sprite/draw/block.zig | 184 + src/font/sprite/draw/box.zig | 947 +++++ src/font/sprite/draw/braille.zig | 148 + src/font/sprite/draw/branch.zig | 505 +++ src/font/sprite/draw/common.zig | 244 ++ src/font/sprite/draw/geometric_shapes.zig | 200 + src/font/sprite/{ => draw}/octants.txt | 0 src/font/sprite/draw/powerline.zig | 396 ++ src/font/sprite/draw/special.zig | 328 ++ .../draw/symbols_for_legacy_computing.zig | 1431 +++++++ ...ymbols_for_legacy_computing_supplement.zig | 193 + src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 0 bytes .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 0 -> 403 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 0 -> 534 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 0 -> 1022 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 0 -> 316 bytes .../testdata/U+1CD00...U+1CDFF-11x21+2.png | Bin 0 -> 1275 bytes .../testdata/U+1CD00...U+1CDFF-12x24+3.png | Bin 0 -> 1870 bytes .../testdata/U+1CD00...U+1CDFF-18x36+4.png | Bin 0 -> 3404 bytes .../testdata/U+1CD00...U+1CDFF-9x17+1.png | Bin 0 -> 1101 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 0 -> 5450 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 0 -> 5724 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 0 -> 9997 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 0 -> 4298 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 0 -> 2223 bytes .../testdata/U+2500...U+25FF-12x24+3.png | Bin 0 -> 2638 bytes .../testdata/U+2500...U+25FF-18x36+4.png | Bin 0 -> 4541 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 0 -> 1848 bytes .../testdata/U+2800...U+28FF-11x21+2.png | Bin 0 -> 1022 bytes .../testdata/U+2800...U+28FF-12x24+3.png | Bin 0 -> 1547 bytes .../testdata/U+2800...U+28FF-18x36+4.png | Bin 0 -> 2490 bytes .../testdata/U+2800...U+28FF-9x17+1.png | Bin 0 -> 917 bytes .../testdata/U+E000...U+E0FF-11x21+2.png | Bin 0 -> 1104 bytes .../testdata/U+E000...U+E0FF-12x24+3.png | Bin 0 -> 1251 bytes .../testdata/U+E000...U+E0FF-18x36+4.png | Bin 0 -> 2228 bytes .../testdata/U+E000...U+E0FF-9x17+1.png | Bin 0 -> 895 bytes .../testdata/U+F500...U+F5FF-11x21+2.png | Bin 0 -> 1114 bytes .../testdata/U+F500...U+F5FF-12x24+3.png | Bin 0 -> 1423 bytes .../testdata/U+F500...U+F5FF-18x36+4.png | Bin 0 -> 2470 bytes .../testdata/U+F500...U+F5FF-9x17+1.png | Bin 0 -> 871 bytes .../testdata/U+F600...U+F6FF-11x21+2.png | Bin 0 -> 493 bytes .../testdata/U+F600...U+F6FF-12x24+3.png | Bin 0 -> 636 bytes .../testdata/U+F600...U+F6FF-18x36+4.png | Bin 0 -> 1218 bytes .../testdata/U+F600...U+F6FF-9x17+1.png | Bin 0 -> 394 bytes src/font/sprite/underline.zig | 312 -- typos.toml | 2 + 54 files changed, 5460 insertions(+), 4639 deletions(-) delete mode 100644 src/font/sprite/Box.zig delete mode 100644 src/font/sprite/Powerline.zig delete mode 100644 src/font/sprite/cursor.zig create mode 100644 src/font/sprite/draw/README.md create mode 100644 src/font/sprite/draw/block.zig create mode 100644 src/font/sprite/draw/box.zig create mode 100644 src/font/sprite/draw/braille.zig create mode 100644 src/font/sprite/draw/branch.zig create mode 100644 src/font/sprite/draw/common.zig create mode 100644 src/font/sprite/draw/geometric_shapes.zig rename src/font/sprite/{ => draw}/octants.txt (100%) create mode 100644 src/font/sprite/draw/powerline.zig create mode 100644 src/font/sprite/draw/special.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig delete mode 100644 src/font/sprite/testdata/Box.ppm create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png delete mode 100644 src/font/sprite/underline.zig diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 969318943..aac2e7e8d 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void { _ = self.modified.fetchAdd(1, .monotonic); } +/// Like `set` but allows specifying a width for the source data and an +/// offset x and y, so that a section of a larger buffer may be copied +/// in to the atlas. +pub fn setFromLarger( + self: *Atlas, + reg: Region, + src: []const u8, + src_width: u32, + src_x: u32, + src_y: u32, +) void { + assert(reg.x < (self.size - 1)); + assert((reg.x + reg.width) <= (self.size - 1)); + assert(reg.y < (self.size - 1)); + assert((reg.y + reg.height) <= (self.size - 1)); + + const depth = self.format.depth(); + var i: u32 = 0; + while (i < reg.height) : (i += 1) { + const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth; + const src_offset = (((src_y + i) * src_width) + src_x) * depth; + fastmem.copy( + u8, + self.data[tex_offset..], + src[src_offset .. src_offset + (reg.width * depth)], + ); + } + + _ = self.modified.fetchAdd(1, .monotonic); +} + // Grow the texture to the new size, preserving all previously written data. pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void { assert(size_new >= self.size); diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 6485d6008..4be06a918 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -33,12 +33,6 @@ pub const Sprite = enum(u32) { cursor_hollow_rect, cursor_bar, - // Note: we don't currently put the box drawing glyphs in here because - // there are a LOT and I'm lazy. What I want to do is spend more time - // studying the patterns to see if we can programmatically build our - // enum perhaps and comptime generate the drawing code at the same time. - // I'm not sure if that's advisable yet though. - test { const testing = std.testing; try testing.expectEqual(start, @intFromEnum(Sprite.underline)); diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig deleted file mode 100644 index f5140091d..000000000 --- a/src/font/sprite/Box.zig +++ /dev/null @@ -1,3397 +0,0 @@ -//! This file contains functions for drawing the box drawing characters -//! (https://en.wikipedia.org/wiki/Box-drawing_character) and related -//! characters that are provided by the terminal. -//! -//! The box drawing logic is based off similar logic in Kitty and Foot. -//! The primary drawing code was originally ported directly and slightly -//! modified from Foot (https://codeberg.org/dnkl/foot/). Foot is licensed -//! under the MIT license and is copyright 2019 Daniel Eklöf. -//! -//! The modifications made were primarily around spacing, DPI calculations, -//! and adapting the code to our atlas model. Further, more extensive changes -//! were made, refactoring the line characters to all share a single unified -//! function (draw_lines), as well as many of the fractional block characters -//! which now use draw_block instead of dedicated separate functions. -//! -//! Additional characters from Unicode 16.0 and beyond are original work. -const Box = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const z2d = @import("z2d"); - -const font = @import("../main.zig"); -const Sprite = @import("../sprite.zig").Sprite; - -const log = std.log.scoped(.box_font); - -/// Grid metrics for the rendering. -metrics: font.Metrics, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -/// Specification of a traditional intersection-style line/box-drawing char, -/// which can have a different style of line from each edge to the center. -const Lines = packed struct(u8) { - up: Style = .none, - right: Style = .none, - down: Style = .none, - left: Style = .none, - - const Style = enum(u2) { - none, - light, - heavy, - double, - }; -}; - -/// Specification of a quadrants char, which has each of the -/// 4 quadrants of the character cell either filled or empty. -const Quads = packed struct(u4) { - tl: bool = false, - tr: bool = false, - bl: bool = false, - br: bool = false, -}; - -/// Specification of a branch drawing node, which consists of a -/// circle which is either empty or filled, and lines connecting -/// optionally between the circle and each of the 4 edges. -const BranchNode = packed struct(u5) { - up: bool = false, - right: bool = false, - down: bool = false, - left: bool = false, - filled: bool = false, -}; - -/// Alignment of a figure within a cell -const Alignment = struct { - horizontal: enum { - left, - right, - center, - } = .center, - - vertical: enum { - top, - bottom, - middle, - } = .middle, - - const upper: Alignment = .{ .vertical = .top }; - const lower: Alignment = .{ .vertical = .bottom }; - const left: Alignment = .{ .horizontal = .left }; - const right: Alignment = .{ .horizontal = .right }; - - const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; - const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; - const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; - const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; - - const center: Alignment = .{}; - - const upper_center = upper; - const lower_center = lower; - const middle_left = left; - const middle_right = right; - const middle_center: Alignment = center; - - const top = upper; - const bottom = lower; - const center_top = top; - const center_bottom = bottom; - - const top_left = upper_left; - const top_right = upper_right; - const bottom_left = lower_left; - const bottom_right = lower_right; -}; - -const Corner = enum(u2) { - tl, - tr, - bl, - br, -}; - -const Edge = enum(u2) { - top, - left, - bottom, - right, -}; - -const SmoothMosaic = packed struct(u10) { - tl: bool, - ul: bool, - ll: bool, - bl: bool, - bc: bool, - br: bool, - lr: bool, - ur: bool, - tr: bool, - tc: bool, - - fn from(comptime pattern: *const [15:0]u8) SmoothMosaic { - return .{ - .tl = pattern[0] == '#', - - .ul = pattern[4] == '#' and - (pattern[0] != '#' or pattern[8] != '#'), - - .ll = pattern[8] == '#' and - (pattern[4] != '#' or pattern[12] != '#'), - - .bl = pattern[12] == '#', - - .bc = pattern[13] == '#' and - (pattern[12] != '#' or pattern[14] != '#'), - - .br = pattern[14] == '#', - - .lr = pattern[10] == '#' and - (pattern[14] != '#' or pattern[6] != '#'), - - .ur = pattern[6] == '#' and - (pattern[10] != '#' or pattern[2] != '#'), - - .tr = pattern[2] == '#', - - .tc = pattern[1] == '#' and - (pattern[2] != '#' or pattern[0] != '#'), - }; - } -}; - -// Octant range, inclusive -const octant_min = 0x1cd00; -const octant_max = 0x1cde5; - -// Utility names for common fractions -const one_eighth: f64 = 0.125; -const one_quarter: f64 = 0.25; -const one_third: f64 = (1.0 / 3.0); -const three_eighths: f64 = 0.375; -const half: f64 = 0.5; -const five_eighths: f64 = 0.625; -const two_thirds: f64 = (2.0 / 3.0); -const three_quarters: f64 = 0.75; -const seven_eighths: f64 = 0.875; - -/// Shades -const Shade = enum(u8) { - off = 0x00, - light = 0x40, - medium = 0x80, - dark = 0xc0, - on = 0xff, - - _, -}; - -pub fn renderGlyph( - self: Box, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - const metrics = self.metrics; - - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init( - alloc, - metrics.cell_width, - metrics.cell_height, - ); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(metrics.cell_height)); - - return font.Glyph{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(metrics.cell_width), - }; -} - -fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - _ = alloc; - switch (cp) { - // '─' - 0x2500 => self.draw_lines(canvas, .{ .left = .light, .right = .light }), - // '━' - 0x2501 => self.draw_lines(canvas, .{ .left = .heavy, .right = .heavy }), - // '│' - 0x2502 => self.draw_lines(canvas, .{ .up = .light, .down = .light }), - // '┃' - 0x2503 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy }), - // '┄' - 0x2504 => self.draw_light_triple_dash_horizontal(canvas), - // '┅' - 0x2505 => self.draw_heavy_triple_dash_horizontal(canvas), - // '┆' - 0x2506 => self.draw_light_triple_dash_vertical(canvas), - // '┇' - 0x2507 => self.draw_heavy_triple_dash_vertical(canvas), - // '┈' - 0x2508 => self.draw_light_quadruple_dash_horizontal(canvas), - // '┉' - 0x2509 => self.draw_heavy_quadruple_dash_horizontal(canvas), - // '┊' - 0x250a => self.draw_light_quadruple_dash_vertical(canvas), - // '┋' - 0x250b => self.draw_heavy_quadruple_dash_vertical(canvas), - // '┌' - 0x250c => self.draw_lines(canvas, .{ .down = .light, .right = .light }), - // '┍' - 0x250d => self.draw_lines(canvas, .{ .down = .light, .right = .heavy }), - // '┎' - 0x250e => self.draw_lines(canvas, .{ .down = .heavy, .right = .light }), - // '┏' - 0x250f => self.draw_lines(canvas, .{ .down = .heavy, .right = .heavy }), - - // '┐' - 0x2510 => self.draw_lines(canvas, .{ .down = .light, .left = .light }), - // '┑' - 0x2511 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy }), - // '┒' - 0x2512 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light }), - // '┓' - 0x2513 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy }), - // '└' - 0x2514 => self.draw_lines(canvas, .{ .up = .light, .right = .light }), - // '┕' - 0x2515 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy }), - // '┖' - 0x2516 => self.draw_lines(canvas, .{ .up = .heavy, .right = .light }), - // '┗' - 0x2517 => self.draw_lines(canvas, .{ .up = .heavy, .right = .heavy }), - // '┘' - 0x2518 => self.draw_lines(canvas, .{ .up = .light, .left = .light }), - // '┙' - 0x2519 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy }), - // '┚' - 0x251a => self.draw_lines(canvas, .{ .up = .heavy, .left = .light }), - // '┛' - 0x251b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy }), - // '├' - 0x251c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .light }), - // '┝' - 0x251d => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .heavy }), - // '┞' - 0x251e => self.draw_lines(canvas, .{ .up = .heavy, .right = .light, .down = .light }), - // '┟' - 0x251f => self.draw_lines(canvas, .{ .down = .heavy, .right = .light, .up = .light }), - - // '┠' - 0x2520 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), - // '┡' - 0x2521 => self.draw_lines(canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), - // '┢' - 0x2522 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), - // '┣' - 0x2523 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), - // '┤' - 0x2524 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light }), - // '┥' - 0x2525 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy }), - // '┦' - 0x2526 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .down = .light }), - // '┧' - 0x2527 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .up = .light }), - // '┨' - 0x2528 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), - // '┩' - 0x2529 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), - // '┪' - 0x252a => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), - // '┫' - 0x252b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), - // '┬' - 0x252c => self.draw_lines(canvas, .{ .down = .light, .left = .light, .right = .light }), - // '┭' - 0x252d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .down = .light }), - // '┮' - 0x252e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .down = .light }), - // '┯' - 0x252f => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), - - // '┰' - 0x2530 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .right = .light }), - // '┱' - 0x2531 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), - // '┲' - 0x2532 => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), - // '┳' - 0x2533 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), - // '┴' - 0x2534 => self.draw_lines(canvas, .{ .up = .light, .left = .light, .right = .light }), - // '┵' - 0x2535 => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light }), - // '┶' - 0x2536 => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light }), - // '┷' - 0x2537 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), - // '┸' - 0x2538 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .right = .light }), - // '┹' - 0x2539 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), - // '┺' - 0x253a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), - // '┻' - 0x253b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), - // '┼' - 0x253c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), - // '┽' - 0x253d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), - // '┾' - 0x253e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), - // '┿' - 0x253f => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), - - // '╀' - 0x2540 => self.draw_lines(canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), - // '╁' - 0x2541 => self.draw_lines(canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), - // '╂' - 0x2542 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - // '╃' - 0x2543 => self.draw_lines(canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), - // '╄' - 0x2544 => self.draw_lines(canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), - // '╅' - 0x2545 => self.draw_lines(canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), - // '╆' - 0x2546 => self.draw_lines(canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), - // '╇' - 0x2547 => self.draw_lines(canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), - // '╈' - 0x2548 => self.draw_lines(canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╉' - 0x2549 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), - // '╊' - 0x254a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), - // '╋' - 0x254b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╌' - 0x254c => self.draw_light_double_dash_horizontal(canvas), - // '╍' - 0x254d => self.draw_heavy_double_dash_horizontal(canvas), - // '╎' - 0x254e => self.draw_light_double_dash_vertical(canvas), - // '╏' - 0x254f => self.draw_heavy_double_dash_vertical(canvas), - - // '═' - 0x2550 => self.draw_lines(canvas, .{ .left = .double, .right = .double }), - // '║' - 0x2551 => self.draw_lines(canvas, .{ .up = .double, .down = .double }), - // '╒' - 0x2552 => self.draw_lines(canvas, .{ .down = .light, .right = .double }), - // '╓' - 0x2553 => self.draw_lines(canvas, .{ .down = .double, .right = .light }), - // '╔' - 0x2554 => self.draw_lines(canvas, .{ .down = .double, .right = .double }), - // '╕' - 0x2555 => self.draw_lines(canvas, .{ .down = .light, .left = .double }), - // '╖' - 0x2556 => self.draw_lines(canvas, .{ .down = .double, .left = .light }), - // '╗' - 0x2557 => self.draw_lines(canvas, .{ .down = .double, .left = .double }), - // '╘' - 0x2558 => self.draw_lines(canvas, .{ .up = .light, .right = .double }), - // '╙' - 0x2559 => self.draw_lines(canvas, .{ .up = .double, .right = .light }), - // '╚' - 0x255a => self.draw_lines(canvas, .{ .up = .double, .right = .double }), - // '╛' - 0x255b => self.draw_lines(canvas, .{ .up = .light, .left = .double }), - // '╜' - 0x255c => self.draw_lines(canvas, .{ .up = .double, .left = .light }), - // '╝' - 0x255d => self.draw_lines(canvas, .{ .up = .double, .left = .double }), - // '╞' - 0x255e => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .double }), - // '╟' - 0x255f => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .light }), - - // '╠' - 0x2560 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .double }), - // '╡' - 0x2561 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double }), - // '╢' - 0x2562 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light }), - // '╣' - 0x2563 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double }), - // '╤' - 0x2564 => self.draw_lines(canvas, .{ .down = .light, .left = .double, .right = .double }), - // '╥' - 0x2565 => self.draw_lines(canvas, .{ .down = .double, .left = .light, .right = .light }), - // '╦' - 0x2566 => self.draw_lines(canvas, .{ .down = .double, .left = .double, .right = .double }), - // '╧' - 0x2567 => self.draw_lines(canvas, .{ .up = .light, .left = .double, .right = .double }), - // '╨' - 0x2568 => self.draw_lines(canvas, .{ .up = .double, .left = .light, .right = .light }), - // '╩' - 0x2569 => self.draw_lines(canvas, .{ .up = .double, .left = .double, .right = .double }), - // '╪' - 0x256a => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), - // '╫' - 0x256b => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), - // '╬' - 0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), - // '╭' - 0x256d => try self.draw_arc(canvas, .br, .light), - // '╮' - 0x256e => try self.draw_arc(canvas, .bl, .light), - // '╯' - 0x256f => try self.draw_arc(canvas, .tl, .light), - - // '╰' - 0x2570 => try self.draw_arc(canvas, .tr, .light), - // '╱' - 0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas), - // '╲' - 0x2572 => self.draw_light_diagonal_upper_left_to_lower_right(canvas), - // '╳' - 0x2573 => self.draw_light_diagonal_cross(canvas), - // '╴' - 0x2574 => self.draw_lines(canvas, .{ .left = .light }), - // '╵' - 0x2575 => self.draw_lines(canvas, .{ .up = .light }), - // '╶' - 0x2576 => self.draw_lines(canvas, .{ .right = .light }), - // '╷' - 0x2577 => self.draw_lines(canvas, .{ .down = .light }), - // '╸' - 0x2578 => self.draw_lines(canvas, .{ .left = .heavy }), - // '╹' - 0x2579 => self.draw_lines(canvas, .{ .up = .heavy }), - // '╺' - 0x257a => self.draw_lines(canvas, .{ .right = .heavy }), - // '╻' - 0x257b => self.draw_lines(canvas, .{ .down = .heavy }), - // '╼' - 0x257c => self.draw_lines(canvas, .{ .left = .light, .right = .heavy }), - // '╽' - 0x257d => self.draw_lines(canvas, .{ .up = .light, .down = .heavy }), - // '╾' - 0x257e => self.draw_lines(canvas, .{ .left = .heavy, .right = .light }), - // '╿' - 0x257f => self.draw_lines(canvas, .{ .up = .heavy, .down = .light }), - - // '▀' UPPER HALF BLOCK - 0x2580 => self.draw_block(canvas, .upper, 1, half), - // '▁' LOWER ONE EIGHTH BLOCK - 0x2581 => self.draw_block(canvas, .lower, 1, one_eighth), - // '▂' LOWER ONE QUARTER BLOCK - 0x2582 => self.draw_block(canvas, .lower, 1, one_quarter), - // '▃' LOWER THREE EIGHTHS BLOCK - 0x2583 => self.draw_block(canvas, .lower, 1, three_eighths), - // '▄' LOWER HALF BLOCK - 0x2584 => self.draw_block(canvas, .lower, 1, half), - // '▅' LOWER FIVE EIGHTHS BLOCK - 0x2585 => self.draw_block(canvas, .lower, 1, five_eighths), - // '▆' LOWER THREE QUARTERS BLOCK - 0x2586 => self.draw_block(canvas, .lower, 1, three_quarters), - // '▇' LOWER SEVEN EIGHTHS BLOCK - 0x2587 => self.draw_block(canvas, .lower, 1, seven_eighths), - // '█' FULL BLOCK - 0x2588 => self.draw_full_block(canvas), - // '▉' LEFT SEVEN EIGHTHS BLOCK - 0x2589 => self.draw_block(canvas, .left, seven_eighths, 1), - // '▊' LEFT THREE QUARTERS BLOCK - 0x258a => self.draw_block(canvas, .left, three_quarters, 1), - // '▋' LEFT FIVE EIGHTHS BLOCK - 0x258b => self.draw_block(canvas, .left, five_eighths, 1), - // '▌' LEFT HALF BLOCK - 0x258c => self.draw_block(canvas, .left, half, 1), - // '▍' LEFT THREE EIGHTHS BLOCK - 0x258d => self.draw_block(canvas, .left, three_eighths, 1), - // '▎' LEFT ONE QUARTER BLOCK - 0x258e => self.draw_block(canvas, .left, one_quarter, 1), - // '▏' LEFT ONE EIGHTH BLOCK - 0x258f => self.draw_block(canvas, .left, one_eighth, 1), - - // '▐' RIGHT HALF BLOCK - 0x2590 => self.draw_block(canvas, .right, half, 1), - // '░' - 0x2591 => self.draw_light_shade(canvas), - // '▒' - 0x2592 => self.draw_medium_shade(canvas), - // '▓' - 0x2593 => self.draw_dark_shade(canvas), - // '▔' UPPER ONE EIGHTH BLOCK - 0x2594 => self.draw_block(canvas, .upper, 1, one_eighth), - // '▕' RIGHT ONE EIGHTH BLOCK - 0x2595 => self.draw_block(canvas, .right, one_eighth, 1), - // '▖' - 0x2596 => self.draw_quadrant(canvas, .{ .bl = true }), - // '▗' - 0x2597 => self.draw_quadrant(canvas, .{ .br = true }), - // '▘' - 0x2598 => self.draw_quadrant(canvas, .{ .tl = true }), - // '▙' - 0x2599 => self.draw_quadrant(canvas, .{ .tl = true, .bl = true, .br = true }), - // '▚' - 0x259a => self.draw_quadrant(canvas, .{ .tl = true, .br = true }), - // '▛' - 0x259b => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '▜' - 0x259c => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .br = true }), - // '▝' - 0x259d => self.draw_quadrant(canvas, .{ .tr = true }), - // '▞' - 0x259e => self.draw_quadrant(canvas, .{ .tr = true, .bl = true }), - // '▟' - 0x259f => self.draw_quadrant(canvas, .{ .tr = true, .bl = true, .br = true }), - - // '◢' - 0x25e2 => self.draw_corner_triangle_shade(canvas, .br, .on), - // '◣' - 0x25e3 => self.draw_corner_triangle_shade(canvas, .bl, .on), - // '◤' - 0x25e4 => self.draw_corner_triangle_shade(canvas, .tl, .on), - // '◥' - 0x25e5 => self.draw_corner_triangle_shade(canvas, .tr, .on), - - // '◸' - 0x25f8 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // left edge - self.rect( - canvas, - 0, - 0, - thickness_px, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - // '◹' - 0x25f9 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 0, - self.metrics.cell_width, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◺' - 0x25fa => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // left edge - self.rect( - canvas, - 0, - 1, - thickness_px, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◿' - 0x25ff => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 1, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - - 0x2800...0x28ff => self.draw_braille(canvas, cp), - - 0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp), - - octant_min...octant_max => self.draw_octant(canvas, cp), - - // '🬼' - 0x1fb3c => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#.. - \\##. - )), - // '🬽' - 0x1fb3d => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#\. - \\### - )), - // '🬾' - 0x1fb3e => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\#\. - \\##. - )), - // '🬿' - 0x1fb3f => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\##. - \\### - )), - // '🭀' - 0x1fb40 => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\#.. - \\##. - \\##. - )), - - // '🭁' - 0x1fb41 => try self.draw_smooth_mosaic(canvas, .from( - \\/## - \\### - \\### - \\### - )), - // '🭂' - 0x1fb42 => try self.draw_smooth_mosaic(canvas, .from( - \\./# - \\### - \\### - \\### - )), - // '🭃' - 0x1fb43 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\### - \\### - )), - // '🭄' - 0x1fb44 => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\.## - \\### - \\### - )), - // '🭅' - 0x1fb45 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\.## - \\### - )), - // '🭆' - 0x1fb46 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\./# - \\### - \\### - )), - - // '🭇' - 0x1fb47 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\..# - \\.## - )), - // '🭈' - 0x1fb48 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\./# - \\### - )), - // '🭉' - 0x1fb49 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\./# - \\.## - )), - // '🭊' - 0x1fb4a => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\.## - \\### - )), - // '🭋' - 0x1fb4b => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\..# - \\.## - \\.## - )), - - // '🭌' - 0x1fb4c => try self.draw_smooth_mosaic(canvas, .from( - \\##\ - \\### - \\### - \\### - )), - // '🭍' - 0x1fb4d => try self.draw_smooth_mosaic(canvas, .from( - \\#\. - \\### - \\### - \\### - )), - // '🭎' - 0x1fb4e => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\### - \\### - )), - // '🭏' - 0x1fb4f => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\##. - \\### - \\### - )), - // '🭐' - 0x1fb50 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\##. - \\### - )), - // '🭑' - 0x1fb51 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#\. - \\### - \\### - )), - - // '🭒' - 0x1fb52 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\\## - )), - // '🭓' - 0x1fb53 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\.\# - )), - // '🭔' - 0x1fb54 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\.## - )), - // '🭕' - 0x1fb55 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\..# - )), - // '🭖' - 0x1fb56 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\.## - \\.## - )), - - // '🭗' - 0x1fb57 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#.. - \\... - \\... - )), - // '🭘' - 0x1fb58 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\#/. - \\... - \\... - )), - // '🭙' - 0x1fb59 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#/. - \\#.. - \\... - )), - // '🭚' - 0x1fb5a => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\#.. - \\... - )), - // '🭛' - 0x1fb5b => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\#.. - \\#.. - )), - - // '🭜' - 0x1fb5c => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\#/. - \\... - )), - // '🭝' - 0x1fb5d => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\##/ - )), - // '🭞' - 0x1fb5e => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\#/. - )), - // '🭟' - 0x1fb5f => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\##. - )), - // '🭠' - 0x1fb60 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\#.. - )), - // '🭡' - 0x1fb61 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\##. - \\##. - )), - - // '🭢' - 0x1fb62 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\..# - \\... - \\... - )), - // '🭣' - 0x1fb63 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.\# - \\... - \\... - )), - // '🭤' - 0x1fb64 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.\# - \\..# - \\... - )), - // '🭥' - 0x1fb65 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\..# - \\... - )), - // '🭦' - 0x1fb66 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\..# - \\..# - )), - // '🭧' - 0x1fb67 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.\# - \\... - )), - - // '🭨' - 0x1fb68 => { - try self.draw_edge_triangle(canvas, .left); - canvas.invert(); - }, - // '🭩' - 0x1fb69 => { - try self.draw_edge_triangle(canvas, .top); - canvas.invert(); - }, - // '🭪' - 0x1fb6a => { - try self.draw_edge_triangle(canvas, .right); - canvas.invert(); - }, - // '🭫' - 0x1fb6b => { - try self.draw_edge_triangle(canvas, .bottom); - canvas.invert(); - }, - // '🭬' - 0x1fb6c => try self.draw_edge_triangle(canvas, .left), - // '🭭' - 0x1fb6d => try self.draw_edge_triangle(canvas, .top), - // '🭮' - 0x1fb6e => try self.draw_edge_triangle(canvas, .right), - // '🭯' - 0x1fb6f => try self.draw_edge_triangle(canvas, .bottom), - - // '🭰' - 0x1fb70 => self.draw_vertical_one_eighth_block_n(canvas, 1), - // '🭱' - 0x1fb71 => self.draw_vertical_one_eighth_block_n(canvas, 2), - // '🭲' - 0x1fb72 => self.draw_vertical_one_eighth_block_n(canvas, 3), - // '🭳' - 0x1fb73 => self.draw_vertical_one_eighth_block_n(canvas, 4), - // '🭴' - 0x1fb74 => self.draw_vertical_one_eighth_block_n(canvas, 5), - // '🭵' - 0x1fb75 => self.draw_vertical_one_eighth_block_n(canvas, 6), - - // '🭶' - 0x1fb76 => self.draw_horizontal_one_eighth_block_n(canvas, 1), - // '🭷' - 0x1fb77 => self.draw_horizontal_one_eighth_block_n(canvas, 2), - // '🭸' - 0x1fb78 => self.draw_horizontal_one_eighth_block_n(canvas, 3), - // '🭹' - 0x1fb79 => self.draw_horizontal_one_eighth_block_n(canvas, 4), - // '🭺' - 0x1fb7a => self.draw_horizontal_one_eighth_block_n(canvas, 5), - // '🭻' - 0x1fb7b => self.draw_horizontal_one_eighth_block_n(canvas, 6), - - // '🮂' UPPER ONE QUARTER BLOCK - 0x1fb82 => self.draw_block(canvas, .upper, 1, one_quarter), - // '🮃' UPPER THREE EIGHTHS BLOCK - 0x1fb83 => self.draw_block(canvas, .upper, 1, three_eighths), - // '🮄' UPPER FIVE EIGHTHS BLOCK - 0x1fb84 => self.draw_block(canvas, .upper, 1, five_eighths), - // '🮅' UPPER THREE QUARTERS BLOCK - 0x1fb85 => self.draw_block(canvas, .upper, 1, three_quarters), - // '🮆' UPPER SEVEN EIGHTHS BLOCK - 0x1fb86 => self.draw_block(canvas, .upper, 1, seven_eighths), - - // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK - 0x1fb7c => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK - 0x1fb7d => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK - 0x1fb7e => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK - 0x1fb7f => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK - 0x1fb80 => { - self.draw_block(canvas, .upper, 1, one_eighth); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮁' - 0x1fb81 => self.draw_horizontal_one_eighth_1358_block(canvas), - - // '🮇' RIGHT ONE QUARTER BLOCK - 0x1fb87 => self.draw_block(canvas, .right, one_quarter, 1), - // '🮈' RIGHT THREE EIGHTHS BLOCK - 0x1fb88 => self.draw_block(canvas, .right, three_eighths, 1), - // '🮉' RIGHT FIVE EIGHTHS BLOCK - 0x1fb89 => self.draw_block(canvas, .right, five_eighths, 1), - // '🮊' RIGHT THREE QUARTERS BLOCK - 0x1fb8a => self.draw_block(canvas, .right, three_quarters, 1), - // '🮋' RIGHT SEVEN EIGHTHS BLOCK - 0x1fb8b => self.draw_block(canvas, .right, seven_eighths, 1), - // '🮌' - 0x1fb8c => self.draw_block_shade(canvas, .left, half, 1, .medium), - // '🮍' - 0x1fb8d => self.draw_block_shade(canvas, .right, half, 1, .medium), - // '🮎' - 0x1fb8e => self.draw_block_shade(canvas, .upper, 1, half, .medium), - // '🮏' - 0x1fb8f => self.draw_block_shade(canvas, .lower, 1, half, .medium), - - // '🮐' - 0x1fb90 => self.draw_medium_shade(canvas), - // '🮑' - 0x1fb91 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .upper, 1, half); - }, - // '🮒' - 0x1fb92 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .lower, 1, half); - }, - // '🮔' - 0x1fb94 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .right, half, 1); - }, - // '🮕' - 0x1fb95 => self.draw_checkerboard_fill(canvas, 0), - // '🮖' - 0x1fb96 => self.draw_checkerboard_fill(canvas, 1), - // '🮗' - 0x1fb97 => { - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 3); - self.draw_horizontal_one_eighth_block_n(canvas, 6); - self.draw_horizontal_one_eighth_block_n(canvas, 7); - }, - // '🮘' - 0x1fb98 => self.draw_upper_left_to_lower_right_fill(canvas), - // '🮙' - 0x1fb99 => self.draw_upper_right_to_lower_left_fill(canvas), - // '🮚' - 0x1fb9a => { - try self.draw_edge_triangle(canvas, .top); - try self.draw_edge_triangle(canvas, .bottom); - }, - // '🮛' - 0x1fb9b => { - try self.draw_edge_triangle(canvas, .left); - try self.draw_edge_triangle(canvas, .right); - }, - // '🮜' - 0x1fb9c => self.draw_corner_triangle_shade(canvas, .tl, .medium), - // '🮝' - 0x1fb9d => self.draw_corner_triangle_shade(canvas, .tr, .medium), - // '🮞' - 0x1fb9e => self.draw_corner_triangle_shade(canvas, .br, .medium), - // '🮟' - 0x1fb9f => self.draw_corner_triangle_shade(canvas, .bl, .medium), - - // '🮠' - 0x1fba0 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true }), - // '🮡' - 0x1fba1 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true }), - // '🮢' - 0x1fba2 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true }), - // '🮣' - 0x1fba3 => self.draw_corner_diagonal_lines(canvas, .{ .br = true }), - // '🮤' - 0x1fba4 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true }), - // '🮥' - 0x1fba5 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .br = true }), - // '🮦' - 0x1fba6 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true, .br = true }), - // '🮧' - 0x1fba7 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true }), - // '🮨' - 0x1fba8 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .br = true }), - // '🮩' - 0x1fba9 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true }), - // '🮪' - 0x1fbaa => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true, .br = true }), - // '🮫' - 0x1fbab => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true, .br = true }), - // '🮬' - 0x1fbac => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .br = true }), - // '🮭' - 0x1fbad => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '🮮' - 0x1fbae => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }), - // '🮯' - 0x1fbaf => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - - // '🮽' - 0x1fbbd => { - self.draw_light_diagonal_cross(canvas); - canvas.invert(); - }, - // '🮾' - 0x1fbbe => { - self.draw_corner_diagonal_lines(canvas, .{ .br = true }); - canvas.invert(); - }, - // '🮿' - 0x1fbbf => { - self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }); - canvas.invert(); - }, - - // '🯎' - 0x1fbce => self.draw_block(canvas, .left, two_thirds, 1), - // '🯏' - 0x1fbcf => self.draw_block(canvas, .left, one_third, 1), - // '🯐' - 0x1fbd0 => self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ), - // '🯑' - 0x1fbd1 => self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ), - // '🯒' - 0x1fbd2 => self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ), - // '🯓' - 0x1fbd3 => self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ), - // '🯔' - 0x1fbd4 => self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ), - // '🯕' - 0x1fbd5 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ), - // '🯖' - 0x1fbd6 => self.draw_cell_diagonal( - canvas, - .upper_right, - .lower_center, - ), - // '🯗' - 0x1fbd7 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_left, - ), - // '🯘' - 0x1fbd8 => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .upper_right, - ); - }, - // '🯙' - 0x1fbd9 => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯚' - 0x1fbda => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯛' - 0x1fbdb => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_left, - ); - }, - // '🯜' - 0x1fbdc => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ); - self.draw_cell_diagonal( - canvas, - .lower_center, - .upper_right, - ); - }, - // '🯝' - 0x1fbdd => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ); - self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ); - }, - // '🯞' - 0x1fbde => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_center, - ); - self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ); - }, - // '🯟' - 0x1fbdf => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ); - self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ); - }, - - // '🯠' - 0x1fbe0 => self.draw_circle(canvas, .top, false), - // '🯡' - 0x1fbe1 => self.draw_circle(canvas, .right, false), - // '🯢' - 0x1fbe2 => self.draw_circle(canvas, .bottom, false), - // '🯣' - 0x1fbe3 => self.draw_circle(canvas, .left, false), - // '🯤' - 0x1fbe4 => self.draw_block(canvas, .upper_center, 0.5, 0.5), - // '🯥' - 0x1fbe5 => self.draw_block(canvas, .lower_center, 0.5, 0.5), - // '🯦' - 0x1fbe6 => self.draw_block(canvas, .middle_left, 0.5, 0.5), - // '🯧' - 0x1fbe7 => self.draw_block(canvas, .middle_right, 0.5, 0.5), - // '🯨' - 0x1fbe8 => self.draw_circle(canvas, .top, true), - // '🯩' - 0x1fbe9 => self.draw_circle(canvas, .right, true), - // '🯪' - 0x1fbea => self.draw_circle(canvas, .bottom, true), - // '🯫' - 0x1fbeb => self.draw_circle(canvas, .left, true), - // '🯬' - 0x1fbec => self.draw_circle(canvas, .top_right, true), - // '🯭' - 0x1fbed => self.draw_circle(canvas, .bottom_left, true), - // '🯮' - 0x1fbee => self.draw_circle(canvas, .bottom_right, true), - // '🯯' - 0x1fbef => self.draw_circle(canvas, .top_left, true), - - // (Below:) - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - - // '' - 0x0f5d0 => self.hline_middle(canvas, .light), - // '' - 0x0f5d1 => self.vline_middle(canvas, .light), - // '' - 0x0f5d2 => self.draw_fading_line(canvas, .right, .light), - // '' - 0x0f5d3 => self.draw_fading_line(canvas, .left, .light), - // '' - 0x0f5d4 => self.draw_fading_line(canvas, .bottom, .light), - // '' - 0x0f5d5 => self.draw_fading_line(canvas, .top, .light), - // '' - 0x0f5d6 => try self.draw_arc(canvas, .br, .light), - // '' - 0x0f5d7 => try self.draw_arc(canvas, .bl, .light), - // '' - 0x0f5d8 => try self.draw_arc(canvas, .tr, .light), - // '' - 0x0f5d9 => try self.draw_arc(canvas, .tl, .light), - // '' - 0x0f5da => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5db => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dc => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dd => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5de => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5df => { - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .bl, .light); - }, - - // '' - 0x0f5e0 => { - try self.draw_arc(canvas, .bl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e1 => { - try self.draw_arc(canvas, .br, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e2 => { - try self.draw_arc(canvas, .br, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5e3 => { - try self.draw_arc(canvas, .tl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e4 => { - try self.draw_arc(canvas, .tr, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e5 => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e6 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5e7 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5e8 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e9 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ea => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5eb => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ec => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ed => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ee => self.draw_branch_node(canvas, .{ .filled = true }, .light), - // '' - 0x0f5ef => self.draw_branch_node(canvas, .{}, .light), - - // '' - 0x0f5f0 => self.draw_branch_node(canvas, .{ - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f1 => self.draw_branch_node(canvas, .{ - .right = true, - }, .light), - // '' - 0x0f5f2 => self.draw_branch_node(canvas, .{ - .left = true, - .filled = true, - }, .light), - // '' - 0x0f5f3 => self.draw_branch_node(canvas, .{ - .left = true, - }, .light), - // '' - 0x0f5f4 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f5 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - }, .light), - // '' - 0x0f5f6 => self.draw_branch_node(canvas, .{ - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5f7 => self.draw_branch_node(canvas, .{ - .down = true, - }, .light), - // '' - 0x0f5f8 => self.draw_branch_node(canvas, .{ - .up = true, - .filled = true, - }, .light), - // '' - 0x0f5f9 => self.draw_branch_node(canvas, .{ - .up = true, - }, .light), - // '' - 0x0f5fa => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fb => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - }, .light), - // '' - 0x0f5fc => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fd => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - }, .light), - // '' - 0x0f5fe => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5ff => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - }, .light), - - // '' - 0x0f600 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f601 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - }, .light), - // '' - 0x0f602 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f603 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - }, .light), - // '' - 0x0f604 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f605 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - }, .light), - // '' - 0x0f606 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f607 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - }, .light), - // '' - 0x0f608 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f609 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60a => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60b => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60c => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60d => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - }, .light), - - // '𜰡' - SEPARATED BLOCK QUADRANT-1 - 0x1cc21 => try self.draw_separated_block_quadrant(canvas, "1"), - // '𜰢' - SEPARATED BLOCK QUADRANT-2 - 0x1cc22 => try self.draw_separated_block_quadrant(canvas, "2"), - // '𜰣' - SEPARATED BLOCK QUADRANT-12 - 0x1cc23 => try self.draw_separated_block_quadrant(canvas, "12"), - // '𜰤' - SEPARATED BLOCK QUADRANT-3 - 0x1cc24 => try self.draw_separated_block_quadrant(canvas, "3"), - // '𜰥' - SEPARATED BLOCK QUADRANT-13 - 0x1cc25 => try self.draw_separated_block_quadrant(canvas, "13"), - // '𜰦' - SEPARATED BLOCK QUADRANT-23 - 0x1cc26 => try self.draw_separated_block_quadrant(canvas, "23"), - // '𜰧' - SEPARATED BLOCK QUADRANT-123 - 0x1cc27 => try self.draw_separated_block_quadrant(canvas, "123"), - // '𜰨' - SEPARATED BLOCK QUADRANT-4 - 0x1cc28 => try self.draw_separated_block_quadrant(canvas, "4"), - // '𜰩' - SEPARATED BLOCK QUADRANT-14 - 0x1cc29 => try self.draw_separated_block_quadrant(canvas, "14"), - // '𜰪' - SEPARATED BLOCK QUADRANT-24 - 0x1cc2a => try self.draw_separated_block_quadrant(canvas, "24"), - // '𜰫' - SEPARATED BLOCK QUADRANT-124 - 0x1cc2b => try self.draw_separated_block_quadrant(canvas, "124"), - // '𜰬' - SEPARATED BLOCK QUADRANT-34 - 0x1cc2c => try self.draw_separated_block_quadrant(canvas, "34"), - // '𜰭' - SEPARATED BLOCK QUADRANT-134 - 0x1cc2d => try self.draw_separated_block_quadrant(canvas, "134"), - // '𜰮' - SEPARATED BLOCK QUADRANT-234 - 0x1cc2e => try self.draw_separated_block_quadrant(canvas, "234"), - // '𜰯' - SEPARATED BLOCK QUADRANT-1234 - 0x1cc2f => try self.draw_separated_block_quadrant(canvas, "1234"), - - else => return error.InvalidCodepoint, - } -} - -fn draw_lines( - self: Box, - canvas: *font.sprite.Canvas, - lines: Lines, -) void { - const light_px = Thickness.light.height(self.metrics.box_thickness); - const heavy_px = Thickness.heavy.height(self.metrics.box_thickness); - - // Top of light horizontal strokes - const h_light_top = (self.metrics.cell_height -| light_px) / 2; - // Bottom of light horizontal strokes - const h_light_bottom = h_light_top +| light_px; - - // Top of heavy horizontal strokes - const h_heavy_top = (self.metrics.cell_height -| heavy_px) / 2; - // Bottom of heavy horizontal strokes - const h_heavy_bottom = h_heavy_top +| heavy_px; - - // Top of the top doubled horizontal stroke (bottom is `h_light_top`) - const h_double_top = h_light_top -| light_px; - // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`) - const h_double_bottom = h_light_bottom +| light_px; - - // Left of light vertical strokes - const v_light_left = (self.metrics.cell_width -| light_px) / 2; - // Right of light vertical strokes - const v_light_right = v_light_left +| light_px; - - // Left of heavy vertical strokes - const v_heavy_left = (self.metrics.cell_width -| heavy_px) / 2; - // Right of heavy vertical strokes - const v_heavy_right = v_heavy_left +| heavy_px; - - // Left of the left doubled vertical stroke (right is `v_light_left`) - const v_double_left = v_light_left -| light_px; - // Right of the right doubled vertical stroke (left is `v_light_right`) - const v_double_right = v_light_right +| light_px; - - // The bottom of the up line - const up_bottom = if (lines.left == .heavy or lines.right == .heavy) - h_heavy_bottom - else if (lines.left != lines.right or lines.down == lines.up) - if (lines.left == .double or lines.right == .double) - h_double_bottom - else - h_light_bottom - else if (lines.left == .none and lines.right == .none) - h_light_bottom - else - h_light_top; - - // The top of the down line - const down_top = if (lines.left == .heavy or lines.right == .heavy) - h_heavy_top - else if (lines.left != lines.right or lines.up == lines.down) - if (lines.left == .double or lines.right == .double) - h_double_top - else - h_light_top - else if (lines.left == .none and lines.right == .none) - h_light_top - else - h_light_bottom; - - // The right of the left line - const left_right = if (lines.up == .heavy or lines.down == .heavy) - v_heavy_right - else if (lines.up != lines.down or lines.left == lines.right) - if (lines.up == .double or lines.down == .double) - v_double_right - else - v_light_right - else if (lines.up == .none and lines.down == .none) - v_light_right - else - v_light_left; - - // The left of the right line - const right_left = if (lines.up == .heavy or lines.down == .heavy) - v_heavy_left - else if (lines.up != lines.down or lines.right == lines.left) - if (lines.up == .double or lines.down == .double) - v_double_left - else - v_light_left - else if (lines.up == .none and lines.down == .none) - v_light_left - else - v_light_right; - - switch (lines.up) { - .none => {}, - .light => self.rect(canvas, v_light_left, 0, v_light_right, up_bottom), - .heavy => self.rect(canvas, v_heavy_left, 0, v_heavy_right, up_bottom), - .double => { - const left_bottom = if (lines.left == .double) h_light_top else up_bottom; - const right_bottom = if (lines.right == .double) h_light_top else up_bottom; - - self.rect(canvas, v_double_left, 0, v_light_left, left_bottom); - self.rect(canvas, v_light_right, 0, v_double_right, right_bottom); - }, - } - - switch (lines.right) { - .none => {}, - .light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom), - .heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom), - .double => { - const top_left = if (lines.up == .double) v_light_right else right_left; - const bottom_left = if (lines.down == .double) v_light_right else right_left; - - self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top); - self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom); - }, - } - - switch (lines.down) { - .none => {}, - .light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height), - .heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height), - .double => { - const left_top = if (lines.left == .double) h_light_bottom else down_top; - const right_top = if (lines.right == .double) h_light_bottom else down_top; - - self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height); - self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height); - }, - } - - switch (lines.left) { - .none => {}, - .light => self.rect(canvas, 0, h_light_top, left_right, h_light_bottom), - .heavy => self.rect(canvas, 0, h_heavy_top, left_right, h_heavy_bottom), - .double => { - const top_right = if (lines.up == .double) v_light_left else left_right; - const bottom_right = if (lines.down == .double) v_light_left else left_right; - - self.rect(canvas, 0, h_double_top, top_right, h_light_top); - self.rect(canvas, 0, h_light_bottom, bottom_right, h_double_bottom); - }, - } -} - -fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.light.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 }, - .p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = @floatFromInt(self.metrics.cell_width), - .y = @floatFromInt(self.metrics.cell_height), - }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_light_diagonal_upper_right_to_lower_left(canvas); - self.draw_light_diagonal_upper_left_to_lower_right(canvas); -} - -fn draw_block( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, -) void { - self.draw_block_shade(canvas, alignment, width, height, .on); -} - -fn draw_block_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, - comptime shade: Shade, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const w: u32 = @intFromFloat(@round(float_width * width)); - const h: u32 = @intFromFloat(@round(float_height * height)); - - const x = switch (alignment.horizontal) { - .left => 0, - .right => self.metrics.cell_width - w, - .center => (self.metrics.cell_width - w) / 2, - }; - const y = switch (alignment.vertical) { - .top => 0, - .bottom => self.metrics.cell_height - h, - .middle => (self.metrics.cell_height - h) / 2, - }; - - canvas.rect(.{ - .x = x, - .y = y, - .width = w, - .height = h, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); -} - -fn draw_corner_triangle_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime shade: Shade, -) void { - const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) { - .tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height }, - .br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - }; - - canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(x0), .y = @floatFromInt(y0) }, - .p1 = .{ .x = @floatFromInt(x1), .y = @floatFromInt(y1) }, - .p2 = .{ .x = @floatFromInt(x2), .y = @floatFromInt(y2) }, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))) catch {}; -} - -fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void { - self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - self.rect(canvas, x, 0, x + w, self.metrics.cell_height); -} - -fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const x_size: usize = 4; - const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width))); - for (0..x_size) |x| { - const x0 = (self.metrics.cell_width * x) / x_size; - const x1 = (self.metrics.cell_width * (x + 1)) / x_size; - for (0..y_size) |y| { - const y0 = (self.metrics.cell_height * y) / y_size; - const y1 = (self.metrics.cell_height * (y + 1)) / y_size; - if ((x + y) % 2 == parity) { - canvas.rect(.{ - .x = @intCast(x0), - .y = @intCast(y0), - .width = @intCast(x1 -| x0), - .height = @intCast(y1 -| y0), - }, .on); - } - } - } -} - -fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); - - for (0..line_count * 2 + 1) |_i| { - const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); - const top_x = @as(f64, @floatFromInt(i)) * stride; - const bottom_x = float_width + top_x; - canvas.line(.{ - .p0 = .{ .x = top_x, .y = 0 }, - .p1 = .{ .x = bottom_x, .y = float_height }, - }, float_thick, .on) catch {}; - } -} - -fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); - - for (0..line_count * 2 + 1) |_i| { - const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); - const bottom_x = @as(f64, @floatFromInt(i)) * stride; - const top_x = float_width + bottom_x; - canvas.line(.{ - .p0 = .{ .x = top_x, .y = 0 }, - .p1 = .{ .x = bottom_x, .y = float_height }, - }, float_thick, .on) catch {}; - } -} - -fn draw_corner_diagonal_lines( - self: Box, - canvas: *font.sprite.Canvas, - comptime corners: Quads, -) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2); - const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2); - - if (corners.tl) canvas.line(.{ - .p0 = .{ .x = center_x, .y = 0 }, - .p1 = .{ .x = 0, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.tr) canvas.line(.{ - .p0 = .{ .x = center_x, .y = 0 }, - .p1 = .{ .x = float_width, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.bl) canvas.line(.{ - .p0 = .{ .x = center_x, .y = float_height }, - .p1 = .{ .x = 0, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.br) canvas.line(.{ - .p0 = .{ .x = center_x, .y = float_height }, - .p1 = .{ .x = float_width, .y = center_y }, - }, float_thick, .on) catch {}; -} - -fn draw_cell_diagonal( - self: Box, - canvas: *font.sprite.Canvas, - comptime from: Alignment, - comptime to: Alignment, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const x0: f64 = switch (from.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y0: f64 = switch (from.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - const x1: f64 = switch (to.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y1: f64 = switch (to.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - - self.draw_line( - canvas, - .{ .x = x0, .y = y0 }, - .{ .x = x1, .y = y1 }, - .light, - ) catch {}; -} - -fn draw_fading_line( - self: Box, - canvas: *font.sprite.Canvas, - comptime to: Edge, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.metrics.cell_width -| thick_px) / 2; - // Right of vertical strokes - const v_right = v_left +| thick_px; - - // If we're fading to the top or left, we start with 0.0 - // and increment up as we progress, otherwise we start - // at 255.0 and increment down (negative). - var color: f64 = switch (to) { - .top, .left => 0.0, - .bottom, .right => 255.0, - }; - const inc: f64 = 255.0 / switch (to) { - .top => float_height, - .bottom => -float_height, - .left => float_width, - .right => -float_width, - }; - - switch (to) { - .top, .bottom => { - for (0..self.metrics.cell_height) |y| { - for (v_left..v_right) |x| { - canvas.pixel( - @intCast(x), - @intCast(y), - @enumFromInt(@as(u8, @intFromFloat(@round(color)))), - ); - } - color += inc; - } - }, - .left, .right => { - for (0..self.metrics.cell_width) |x| { - for (h_top..h_bottom) |y| { - canvas.pixel( - @intCast(x), - @intCast(y), - @enumFromInt(@as(u8, @intFromFloat(@round(color)))), - ); - } - color += inc; - } - }, - } -} - -fn draw_branch_node( - self: Box, - canvas: *font.sprite.Canvas, - node: BranchNode, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.metrics.cell_width -| thick_px) / 2; - // Right of vertical strokes - const v_right = v_left +| thick_px; - - // We calculate the center of the circle this way - // to ensure it aligns with box drawing characters - // since the lines are sometimes off center to - // make sure they aren't split between pixels. - const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; - const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; - // The radius needs to be the smallest distance from the center to an edge. - const r: f64 = @min( - @min(cx, cy), - @min(float_width - cx, float_height - cy), - ); - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth(float_thick); - - // These @intFromFloat casts shouldn't ever fail since r can never - // be greater than cx or cy, so when subtracting it from them the - // result can never be negative. - if (node.up) - self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r))); - if (node.right) - self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom); - if (node.down) - self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height); - if (node.left) - self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom); - - if (node.filled) { - ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.fill() catch return; - } else { - ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.stroke() catch return; - } -} - -fn draw_circle( - self: Box, - canvas: *font.sprite.Canvas, - comptime position: Alignment, - comptime filled: bool, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const x: f64 = switch (position.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y: f64 = switch (position.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - const r: f64 = 0.5 * @min(float_width, float_height); - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth( - @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), - ); - - if (filled) { - ctx.arc(x, y, r, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.fill() catch return; - } else { - ctx.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.stroke() catch return; - } -} - -fn draw_line( - self: Box, - canvas: *font.sprite.Canvas, - p0: font.sprite.Point(f64), - p1: font.sprite.Point(f64), - comptime thickness: Thickness, -) !void { - canvas.line( - .{ .p0 = p0, .p1 = p1 }, - @floatFromInt(thickness.height(self.metrics.box_thickness)), - .on, - ) catch {}; -} - -fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void { - canvas.rect((font.sprite.Box(u32){ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = self.metrics.cell_width, - .y = self.metrics.cell_height, - }, - }).rect(), @as(font.sprite.Color, @enumFromInt(v))); -} - -fn draw_light_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x40); -} - -fn draw_medium_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x80); -} - -fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0xc0); -} - -fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8))); - const y = @min( - self.metrics.cell_height -| h, - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))), - ); - self.rect(canvas, 0, y, self.metrics.cell_width, y + h); -} - -fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_horizontal_one_eighth_block_n(canvas, 0); - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 4); - self.draw_horizontal_one_eighth_block_n(canvas, 7); -} - -fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void { - const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2; - const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2; - - if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y); - if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y); - if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height); - if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8); - var x_spacing: u32 = self.metrics.cell_width / 4; - var y_spacing: u32 = self.metrics.cell_height / 8; - var x_margin: u32 = x_spacing / 2; - var y_margin: u32 = y_spacing / 2; - - var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w; - var y_px_left: u32 = self.metrics.cell_height - 2 * y_margin - 3 * y_spacing - 4 * w; - - // First, try hard to ensure the DOT width is non-zero - if (x_px_left >= 2 and y_px_left >= 4 and w == 0) { - w += 1; - x_px_left -= 2; - y_px_left -= 4; - } - - // Second, prefer a non-zero margin - if (x_px_left >= 2 and x_margin == 0) { - x_margin = 1; - x_px_left -= 2; - } - if (y_px_left >= 2 and y_margin == 0) { - y_margin = 1; - y_px_left -= 2; - } - - // Third, increase spacing - if (x_px_left >= 1) { - x_spacing += 1; - x_px_left -= 1; - } - if (y_px_left >= 3) { - y_spacing += 1; - y_px_left -= 3; - } - - // Fourth, margins (“spacing”, but on the sides) - if (x_px_left >= 2) { - x_margin += 1; - x_px_left -= 2; - } - if (y_px_left >= 2) { - y_margin += 1; - y_px_left -= 2; - } - - // Last - increase dot width - if (x_px_left >= 2 and y_px_left >= 4) { - w += 1; - x_px_left -= 2; - y_px_left -= 4; - } - - assert(x_px_left <= 1 or y_px_left <= 1); - assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width); - assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height); - - const x = [2]u32{ x_margin, x_margin + w + x_spacing }; - const y = y: { - var y: [4]u32 = undefined; - y[0] = y_margin; - y[1] = y[0] + w + y_spacing; - y[2] = y[1] + w + y_spacing; - y[3] = y[2] + w + y_spacing; - break :y y; - }; - - assert(cp >= 0x2800); - assert(cp <= 0x28ff); - const sym = cp - 0x2800; - - // Left side - if (sym & 1 > 0) - self.rect(canvas, x[0], y[0], x[0] + w, y[0] + w); - if (sym & 2 > 0) - self.rect(canvas, x[0], y[1], x[0] + w, y[1] + w); - if (sym & 4 > 0) - self.rect(canvas, x[0], y[2], x[0] + w, y[2] + w); - - // Right side - if (sym & 8 > 0) - self.rect(canvas, x[1], y[0], x[1] + w, y[0] + w); - if (sym & 16 > 0) - self.rect(canvas, x[1], y[1], x[1] + w, y[1] + w); - if (sym & 32 > 0) - self.rect(canvas, x[1], y[2], x[1] + w, y[2] + w); - - // 8-dot patterns - if (sym & 64 > 0) - self.rect(canvas, x[0], y[3], x[0] + w, y[3] + w); - if (sym & 128 > 0) - self.rect(canvas, x[1], y[3], x[1] + w, y[3] + w); -} - -fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - const Sextants = packed struct(u6) { - tl: bool, - tr: bool, - ml: bool, - mr: bool, - bl: bool, - br: bool, - }; - - assert(cp >= 0x1fb00 and cp <= 0x1fb3b); - const idx = cp - 0x1fb00; - const sex: Sextants = @bitCast(@as(u6, @intCast( - idx + (idx / 0x14) + 1, - ))); - - const x_halfs = self.xHalfs(); - const y_thirds = self.yThirds(); - - if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]); - if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]); - if (sex.ml) self.rect(canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); - if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, y_thirds[2]); - if (sex.bl) self.rect(canvas, 0, y_thirds[3], x_halfs[0], self.metrics.cell_height); - if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[3], self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - assert(cp >= octant_min and cp <= octant_max); - - // Octant representation. We use the funny numeric string keys - // so its easier to parse the actual name used in the Symbols for - // Legacy Computing spec. - const Octant = packed struct(u8) { - @"1": bool = false, - @"2": bool = false, - @"3": bool = false, - @"4": bool = false, - @"5": bool = false, - @"6": bool = false, - @"7": bool = false, - @"8": bool = false, - }; - - // Parse the octant data. This is all done at comptime so this is - // static data that is embedded in the binary. - const octants_len = octant_max - octant_min + 1; - const octants: [octants_len]Octant = comptime octants: { - @setEvalBranchQuota(10_000); - - var result: [octants_len]Octant = @splat(.{}); - var i: usize = 0; - - const data = @embedFile("octants.txt"); - var it = std.mem.splitScalar(u8, data, '\n'); - while (it.next()) |line| { - // Skip comments - if (line.len == 0 or line[0] == '#') continue; - - const current = &result[i]; - i += 1; - - // Octants are in the format "BLOCK OCTANT-1235". The numbers - // at the end are keys into our packed struct. Since we're - // at comptime we can metaprogram it all. - const idx = std.mem.indexOfScalar(u8, line, '-').?; - for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; - } - - assert(i == octants_len); - break :octants result; - }; - - const x_halfs = self.xHalfs(); - const y_quads = self.yQuads(); - const oct = octants[cp - octant_min]; - if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]); - if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]); - if (oct.@"3") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]); - if (oct.@"5") self.rect(canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); - if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[3], self.metrics.cell_width, y_quads[4]); - if (oct.@"7") self.rect(canvas, 0, y_quads[5], x_halfs[0], self.metrics.cell_height); - if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[5], self.metrics.cell_width, self.metrics.cell_height); -} - -/// xHalfs[0] should be used as the right edge of a left-aligned half. -/// xHalfs[1] should be used as the left edge of a right-aligned half. -fn xHalfs(self: Box) [2]u32 { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); - return .{ half_width, self.metrics.cell_width - half_width }; -} - -/// Use these values as such: -/// yThirds[0] bottom edge of the first third. -/// yThirds[1] top edge of the second third. -/// yThirds[2] bottom edge of the second third. -/// yThirds[3] top edge of the final third. -fn yThirds(self: Box) [4]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); - const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); - return .{ - one_third_height, - self.metrics.cell_height - two_thirds_height, - two_thirds_height, - self.metrics.cell_height - one_third_height, - }; -} - -/// Use these values as such: -/// yQuads[0] bottom edge of first quarter. -/// yQuads[1] top edge of second quarter. -/// yQuads[2] bottom edge of second quarter. -/// yQuads[3] top edge of third quarter. -/// yQuads[4] bottom edge of third quarter -/// yQuads[5] top edge of fourth quarter. -fn yQuads(self: Box) [6]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); - const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); - const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); - return .{ - quarter_height, - self.metrics.cell_height - three_quarters_height, - half_height, - self.metrics.cell_height - half_height, - three_quarters_height, - self.metrics.cell_height - quarter_height, - }; -} - -fn draw_smooth_mosaic( - self: Box, - canvas: *font.sprite.Canvas, - mosaic: SmoothMosaic, -) !void { - const y_thirds = self.yThirds(); - const top: f64 = 0.0; - // We average the edge positions for the y_thirds boundaries here - // rather than having to deal with varying alignments depending on - // the surrounding pieces. The most this will be off by is half of - // a pixel, so hopefully it's not noticeable. - const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); - const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); - const bottom: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.metrics.cell_width); - - var path: z2d.StaticPath(12) = .{}; - path.init(); // nodes.len = 0 - - if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1 - if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2 - if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3 - if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4 - if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5 - if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6 - if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7 - if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8 - if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9 - if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10 - path.close(); // +2, nodes.len = 12 - - try z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_edge_triangle( - self: Box, - canvas: *font.sprite.Canvas, - comptime edge: Edge, -) !void { - const upper: f64 = 0.0; - const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2); - const lower: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.metrics.cell_width); - - const x0, const y0, const x1, const y1 = switch (edge) { - .top => .{ right, upper, left, upper }, - .left => .{ left, upper, left, lower }, - .bottom => .{ left, lower, right, lower }, - .right => .{ right, lower, right, upper }, - }; - - var path: z2d.StaticPath(5) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(center, middle); // +1, nodes.len = 1 - path.lineTo(x0, y0); // +1, nodes.len = 2 - path.lineTo(x1, y1); // +1, nodes.len = 3 - path.close(); // +2, nodes.len = 5 - - try z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_arc( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime thickness: Thickness, -) !void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2; - const center_y: f64 = @as(f64, @floatFromInt((self.metrics.cell_height -| thick_px) / 2)) + float_thick / 2; - - const r = @min(float_width, float_height) / 2; - - // Fraction away from the center to place the middle control points, - const s: f64 = 0.25; - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth(float_thick); - ctx.setLineCapMode(.round); - - switch (corner) { - .tl => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .tr => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - .bl => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .br => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - } - try ctx.stroke(); -} - -fn draw_dash_horizontal( - self: Box, - canvas: *font.sprite.Canvas, - count: u8, - thick_px: u32, - desired_gap: u32, -) void { - assert(count >= 2 and count <= 4); - - // +------------+ - // | | - // | | - // | | - // | | - // | -- -- -- | - // | | - // | | - // | | - // | | - // +------------+ - // Our dashed line should be made such that when tiled horizontally - // it creates one consistent line with no uneven gap or segment sizes. - // In order to make sure this is the case, we should have half-sized - // gaps on the left and right so that it is centered properly. - - // For N dashes, there are N - 1 gaps between them, but we also have - // half-sized gaps on either side, adding up to N total gaps. - const gap_count = count; - - // We need at least 1 pixel for each gap and each dash, if we don't - // have that then we can't draw our dashed line correctly so we just - // draw a solid line and return. - if (self.metrics.cell_width < count + gap_count) { - self.hline_middle(canvas, .light); - return; - } - - // We never want the gaps to take up more than 50% of the space, - // because if they do the dashes are too small and look wrong. - const gap_width = @min(desired_gap, self.metrics.cell_width / (2 * count)); - const total_gap_width = gap_count * gap_width; - const total_dash_width = self.metrics.cell_width - total_gap_width; - const dash_width = total_dash_width / count; - const remaining = total_dash_width % count; - - assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width); - - // Our dashes should be centered vertically. - const y: u32 = (self.metrics.cell_height -| thick_px) / 2; - - // We start at half a gap from the left edge, in order to center - // our dashes properly. - var x: u32 = gap_width / 2; - - // We'll distribute the extra space in to dash widths, 1px at a - // time. We prefer this to making gaps larger since that is much - // more visually obvious. - var extra: u32 = remaining; - - for (0..count) |_| { - var x1 = x + dash_width; - // We distribute left-over size in to dash widths, - // since it's less obvious there than in the gaps. - if (extra > 0) { - extra -= 1; - x1 += 1; - } - self.hline(canvas, x, x1, y, thick_px); - // Advance by the width of the dash we drew and the width - // of a gap to get the the start of the next dash. - x = x1 + gap_width; - } -} - -fn draw_dash_vertical( - self: Box, - canvas: *font.sprite.Canvas, - comptime count: u8, - thick_px: u32, - desired_gap: u32, -) void { - assert(count >= 2 and count <= 4); - - // +-----------+ - // | | | - // | | | - // | | - // | | | - // | | | - // | | - // | | | - // | | | - // | | - // +-----------+ - // Our dashed line should be made such that when tiled vertically it - // it creates one consistent line with no uneven gap or segment sizes. - // In order to make sure this is the case, we should have an extra gap - // gap at the bottom. - // - // A single full-sized extra gap is preferred to two half-sized ones for - // vertical to allow better joining to solid characters without creating - // visible half-sized gaps. Unlike horizontal, centering is a lot less - // important, visually. - - // Because of the extra gap at the bottom, there are as many gaps as - // there are dashes. - const gap_count = count; - - // We need at least 1 pixel for each gap and each dash, if we don't - // have that then we can't draw our dashed line correctly so we just - // draw a solid line and return. - if (self.metrics.cell_height < count + gap_count) { - self.vline_middle(canvas, .light); - return; - } - - // We never want the gaps to take up more than 50% of the space, - // because if they do the dashes are too small and look wrong. - const gap_height = @min(desired_gap, self.metrics.cell_height / (2 * count)); - const total_gap_height = gap_count * gap_height; - const total_dash_height = self.metrics.cell_height - total_gap_height; - const dash_height = total_dash_height / count; - const remaining = total_dash_height % count; - - assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height); - - // Our dashes should be centered horizontally. - const x: u32 = (self.metrics.cell_width -| thick_px) / 2; - - // We start at the top of the cell. - var y: u32 = 0; - - // We'll distribute the extra space in to dash heights, 1px at a - // time. We prefer this to making gaps larger since that is much - // more visually obvious. - var extra: u32 = remaining; - - inline for (0..count) |_| { - var y1 = y + dash_height; - // We distribute left-over size in to dash widths, - // since it's less obvious there than in the gaps. - if (extra > 0) { - extra -= 1; - y1 += 1; - } - self.vline(canvas, y, y1, x, thick_px); - // Advance by the height of the dash we drew and the height - // of a gap to get the the start of the next dash. - y = y1 + gap_height; - } -} - -fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px); -} - -fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px); -} - -fn vline( - self: Box, - canvas: *font.sprite.Canvas, - y1: u32, - y2: u32, - x: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x + thickness_px, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn hline( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - x2: u32, - y: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y + thickness_px, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn rect( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - y1: u32, - x2: u32, - y2: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -// Separated Block Quadrants from Symbols for Legacy Computing Supplement -// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 -fn draw_separated_block_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime fmt: []const u8) !void { - comptime { - if (fmt.len > 4) @compileError("cannot have more than four quadrants"); - var seen = [_]bool{false} ** (std.math.maxInt(u8) + 1); - for (fmt) |c| { - if (seen[c]) @compileError("repeated quadrants not allowed"); - seen[c] = true; - switch (c) { - '1'...'4' => {}, - else => @compileError("invalid quadrant"), - } - } - } - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - const gap: f64 = @max(1.0, @as(f64, @floatFromInt(self.metrics.cell_width)) * 0.10) / 2.0; - const left: f64 = gap; - const right = @as(f64, @floatFromInt(self.metrics.cell_width)) - gap; - const top: f64 = gap; - const bottom = @as(f64, @floatFromInt(self.metrics.cell_height)) - gap; - const center_x = @as(f64, @floatFromInt(self.metrics.cell_width)) / 2.0; - const center_left = center_x - gap; - const center_right = center_x + gap; - const center_y = @as(f64, @floatFromInt(self.metrics.cell_height)) / 2.0; - const center_top = center_y - gap; - const center_bottom = center_y + gap; - - inline for (fmt) |c| { - const x1, const y1, const x2, const y2 = switch (c) { - '1' => .{ - left, top, - center_left, center_top, - }, - '2' => .{ - center_right, top, - right, center_top, - }, - '3' => .{ - left, center_bottom, - center_left, bottom, - }, - '4' => .{ - center_right, center_bottom, - right, bottom, - }, - else => unreachable, - }; - try ctx.moveTo(x1, y1); - try ctx.lineTo(x2, y1); - try ctx.lineTo(x2, y2); - try ctx.lineTo(x1, y2); - try ctx.closePath(); - } - - try ctx.fill(); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - var cp: u32 = 0x2500; - const end = 0x259f; - while (cp <= end) : (cp += 1) { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Box = .{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - }), - }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width); - try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height); - } -} - -fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { - // Box Drawing and Block Elements. - var cp: u32 = 0x2500; - while (cp <= 0x259f) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Braille - cp = 0x2800; - while (cp <= 0x28ff) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing. - cp = 0x1fb00; - while (cp <= 0x1fbef) : (cp += 1) { - switch (cp) { - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - cp = 0xf5d0; - while (cp <= 0xf60d) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing Supplement: Quadrants - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - cp = 0x1cc21; - while (cp <= 0x1cc2f) : (cp += 1) { - switch (cp) { - 0x1cc21...0x1cc2f => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Symbols for Legacy Computing Supplement: Octants - cp = 0x1CD00; - while (cp <= 0x1CDE5) : (cp += 1) { - switch (cp) { - 0x1CD00...0x1CDE5 => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Geometric Shapes: filled and outlined corners - for ([_]u21{ '◢', '◣', '◤', '◥', '◸', '◹', '◺', '◿' }) |char| { - _ = try self.renderGlyph( - alloc, - atlas, - char, - ); - } -} - -test "render all sprites" { - // Renders all sprites to an atlas and compares - // it to a ground truth for regression testing. - - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // Even cell size and thickness (18 x 36) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - // Odd cell size and thickness (9 x 15) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 9.0, - .ascent = 12.0, - .descent = -3.0, - .line_gap = 0.0, - .underline_thickness = 1.0, - .strikethrough_thickness = 1.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - const ground_truth = @embedFile("./testdata/Box.ppm"); - - var stream = std.io.changeDetectionStream(ground_truth, std.io.null_writer); - try atlas_grayscale.dump(stream.writer()); - - if (stream.changeDetected()) { - log.err( - \\ - \\!! [Box.zig] Change detected from ground truth! - \\!! Dumping ./Box_test.ppm and ./Box_test_diff.ppm - \\!! Please check changes and update Box.ppm in testdata if intended. - , - .{}, - ); - - const ppm = try std.fs.cwd().createFile("Box_test.ppm", .{}); - defer ppm.close(); - try atlas_grayscale.dump(ppm.writer()); - - const diff = try std.fs.cwd().createFile("Box_test_diff.ppm", .{}); - defer diff.close(); - var writer = diff.writer(); - try writer.print( - \\P6 - \\{d} {d} - \\255 - \\ - , .{ atlas_grayscale.size, atlas_grayscale.size }); - for (ground_truth[try diff.getPos()..], atlas_grayscale.data) |a, b| { - if (a == b) { - try writer.writeByteNTimes(a / 3, 3); - } else { - try writer.writeByte(a); - try writer.writeByte(b); - try writer.writeByte(0); - } - } - } -} diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index af0c0af6a..25968e865 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -16,25 +16,154 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const wuffs = @import("wuffs"); +const z2d = @import("z2d"); const font = @import("../main.zig"); const Sprite = font.sprite.Sprite; -const Box = @import("Box.zig"); -const Powerline = @import("Powerline.zig"); -const underline = @import("underline.zig"); -const cursor = @import("cursor.zig"); + +const special = @import("draw/special.zig"); const log = std.log.scoped(.font_sprite); /// Grid metrics for rendering sprites. metrics: font.Metrics, +pub const DrawFnError = + Allocator.Error || + z2d.painter.FillError || + z2d.painter.StrokeError || + error{ + /// Something went wrong while doing math. + MathError, + }; + +/// A function that draws a glyph on the provided canvas. +pub const DrawFn = fn ( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) DrawFnError!void; + +const Range = struct { + min: u32, + max: u32, + draw: DrawFn, +}; + +/// Automatically collect ranges for functions with names +/// in the format `draw` or `draw_`. +const ranges = ranges: { + @setEvalBranchQuota(1_000_000); + + // Structs containing drawing functions for codepoint ranges. + const structs = [_]type{ + @import("draw/block.zig"), + @import("draw/box.zig"), + @import("draw/braille.zig"), + @import("draw/branch.zig"), + @import("draw/geometric_shapes.zig"), + @import("draw/powerline.zig"), + @import("draw/symbols_for_legacy_computing.zig"), + @import("draw/symbols_for_legacy_computing_supplement.zig"), + }; + + // Count how many draw fns we have + var range_count = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + range_count += 1; + } + } + + // Make an array and collect ranges for each function. + var r: [range_count]Range = undefined; + var names: [range_count][:0]const u8 = undefined; + var i = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + + const sep = std.mem.indexOfScalar(u8, decl.name, '_') orelse decl.name.len; + + const min = std.fmt.parseInt(u21, decl.name[4..sep], 16) catch unreachable; + + const max = if (sep == decl.name.len) + min + else + std.fmt.parseInt(u21, decl.name[sep + 1 ..], 16) catch unreachable; + + r[i] = .{ + .min = min, + .max = max, + .draw = @field(s, decl.name), + }; + names[i] = decl.name; + i += 1; + } + } + + // Sort ranges in ascending order + std.mem.sortUnstableContext(0, r.len, struct { + r: []Range, + names: [][:0]const u8, + pub fn lessThan(self: @This(), a: usize, b: usize) bool { + return self.r[a].min < self.r[b].min; + } + pub fn swap(self: @This(), a: usize, b: usize) void { + std.mem.swap(Range, &self.r[a], &self.r[b]); + std.mem.swap([:0]const u8, &self.names[a], &self.names[b]); + } + }{ + .r = &r, + .names = &names, + }); + + // Ensure there's no overlapping ranges + i = 0; + for (r, 0..) |n, k| { + if (n.min <= i) { + @compileError( + std.fmt.comptimePrint( + "Codepoint range for {s}(...) overlaps range for {s}(...), {X} <= {X} <= {X}", + .{ names[k], names[k - 1], r[k - 1].min, n.min, r[k - 1].max }, + ), + ); + } + i = n.max; + } + + break :ranges r; +}; + +fn getDrawFn(cp: u32) ?*const DrawFn { + // For special sprites (cursors, underlines, etc.) all sprites are drawn + // by functions from `Special` that share the name of the enum field. + if (cp >= Sprite.start) switch (@as(Sprite, @enumFromInt(cp))) { + inline else => |sprite| { + return @field(special, @tagName(sprite)); + }, + }; + + // Pray that the compiler is smart enough to + // turn this in to a jump table or something... + inline for (ranges) |range| { + if (cp >= range.min and cp <= range.max) return range.draw; + } + return null; +} + /// Returns true if the codepoint exists in our sprite font. pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool { - // We ignore presentation. No matter what presentation is requested - // we always provide glyphs for our codepoints. + // We ignore presentation. No matter what presentation is + // requested we always provide glyphs for our codepoints. _ = p; _ = self; - return Kind.init(cp) != null; + return getDrawFn(cp) != null; } /// Render the glyph. @@ -52,18 +181,10 @@ pub fn renderGlyph( } } - const metrics = self.metrics; - - // We adjust our sprite width based on the cell width. - const width = switch (opts.cell_width orelse 1) { - 0, 1 => metrics.cell_width, - else => |width| metrics.cell_width * width, - }; - // It should be impossible for this to be null and we assert that // in runtime safety modes but in case it is its not worth memory // corruption so we return a valid, blank glyph. - const kind = Kind.init(cp) orelse return .{ + const draw = getDrawFn(cp) orelse return .{ .width = 0, .height = 0, .offset_x = 0, @@ -73,217 +194,349 @@ pub fn renderGlyph( .advance_x = 0, }; - // Safe to ".?" because of the above assertion. - return switch (kind) { - .box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp), - - .underline => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.underline_position, - metrics.underline_thickness, - ), - - .strikethrough => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.strikethrough_position, - metrics.strikethrough_thickness, - ), - - .overline => overline: { - var g = try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - 0, - metrics.overline_thickness, - ); + const metrics = self.metrics; - // We have to manually subtract the overline position - // on the rendered glyph since it can be negative. - g.offset_y -= metrics.overline_position; + // We adjust our sprite width based on the cell width. + const width = switch (opts.cell_width orelse 1) { + 0, 1 => metrics.cell_width, + else => |width| metrics.cell_width * width, + }; - break :overline g; - }, + const height = metrics.cell_height; - .powerline => powerline: { - const f: Powerline = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.box_thickness, - }; + const padding_x = width / 4; + const padding_y = height / 4; - break :powerline try f.renderGlyph(alloc, atlas, cp); - }, + // Make a canvas of the desired size + var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y); + defer canvas.deinit(); - .cursor => cursor: { - var g = try cursor.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cursor_height, - metrics.cursor_thickness, - ); + try draw(cp, &canvas, width, height, metrics); - // Cursors are drawn at their specified height - // and are centered vertically within the cell. - const cursor_height: i32 = @intCast(metrics.cursor_height); - const cell_height: i32 = @intCast(metrics.cell_height); - g.offset_y += @divTrunc(cell_height - cursor_height, 2); + // Write the drawing to the atlas + const region = try canvas.writeAtlas(alloc, atlas); - break :cursor g; - }, + return font.Glyph{ + .width = region.width, + .height = region.height, + .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)), + .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = @floatFromInt(width), }; } -/// Kind of sprites we have. Drawing is implemented separately for each kind. -const Kind = enum { - box, - underline, - overline, - strikethrough, - powerline, - cursor, - - pub fn init(cp: u32) ?Kind { - return switch (cp) { - Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) { - .underline, - .underline_double, - .underline_dotted, - .underline_dashed, - .underline_curly, - => .underline, - - .overline, - => .overline, - - .strikethrough, - => .strikethrough, - - .cursor_rect, - .cursor_hollow_rect, - .cursor_bar, - => .cursor, - }, - - // == Box fonts == - - // "Box Drawing" block - // ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ - // ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ - // ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ - // ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ - 0x2500...0x257F, - - // "Block Elements" block - // ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ - 0x2580...0x259F, - - // "Geometric Shapes" block - 0x25e2...0x25e5, // ◢◣◤◥ - 0x25f8...0x25fa, // ◸◹◺ - 0x25ff, // ◿ - - // "Braille" block - 0x2800...0x28FF, - - // "Symbols for Legacy Computing" block - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - // (Octants) - 0x1CD00...0x1CDE5, - => .box, - - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - //           - //                     - //                     - //             - 0xF5D0...0xF60D => .box, - - // Separated Block Quadrants from Symbols for Legacy Computing Supplement - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - 0x1CC21...0x1CC2F => .box, - - // Powerline fonts - 0xE0B0, - 0xE0B1, - 0xE0B3, - 0xE0B4, - 0xE0B6, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0D2, - 0xE0D4, - => .powerline, - - else => null, +/// Used in `testDrawRanges`, checks for diff between the provided atlas +/// and the reference file for the range, returns true if there is a diff. +fn testDiffAtlas( + alloc: Allocator, + atlas: *z2d.Surface, + path: []const u8, + i: u32, + width: u32, + height: u32, + thickness: u32, +) !bool { + // Get the file contents, we compare the PNG data first in + // order to ensure that no one smuggles arbitrary binary + // data in to the reference PNGs. + const test_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only }); + defer test_file.close(); + const test_bytes = try test_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(test_bytes); + + const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, "."); + defer alloc.free(cwd_absolute); + + // Get the reference file contents to compare. + const ref_path = try std.fmt.allocPrint( + alloc, + "./src/font/sprite/testdata/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(ref_path); + const ref_file = + std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| { + log.err("Can't open reference file {s}: {}\n", .{ + ref_path, + err, + }); + + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); + + return true; }; + defer ref_file.close(); + const ref_bytes = try ref_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(ref_bytes); + + // Do our PNG bytes comparison, if it's the same then we can + // move on, otherwise we'll decode the reference file and do + // a pixel-for-pixel diff. + if (std.mem.eql(u8, test_bytes, ref_bytes)) return false; + + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); + + // Use wuffs to decode the reference PNG to raw pixels. + // These will be RGBA, so when diffing we can just compare + // every fourth byte. + const ref_rgba = try wuffs.png.decode(alloc, ref_bytes); + defer alloc.free(ref_rgba.data); + + assert(ref_rgba.width == atlas.getWidth()); + assert(ref_rgba.height == atlas.getHeight()); + + // We'll make a visual representation of the diff using + // red for removed pixels and green for added. We make + // a z2d surface for that here. + var diff = try z2d.Surface.init( + .image_surface_rgb, + alloc, + atlas.getWidth(), + atlas.getHeight(), + ); + defer diff.deinit(alloc); + const diff_pix = diff.image_surface_rgb.buf; + + const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); + + var differs: bool = false; + for (0..test_gray.len) |j| { + const t = test_gray[j]; + const r = ref_rgba.data[j * 4]; + if (t == r) { + // If the pixels match, write it as a faded gray. + diff_pix[j].r = t / 3; + diff_pix[j].g = t / 3; + diff_pix[j].b = t / 3; + } else { + differs = true; + // Otherwise put the reference value in the red + // channel and the new value in the green channel. + diff_pix[j].r = r; + diff_pix[j].g = t; + } } -}; + + // If the PNG data differs but not the raw pixels, that's + // a big red flag, since it could mean someone is trying to + // smuggle binary data in to the test files. + if (!differs) { + log.err( + "!!! Test PNG data does not match reference, but pixels do match! " ++ + "Either z2d's PNG exporter changed or someone is " ++ + "trying to smuggle binary data in the test files!\n" ++ + "test={s}, reference={s}", + .{ test_path, ref_path }, + ); + return true; + } + + // Drop the diff image as a PNG in the cwd. + const diff_path = try std.fmt.allocPrint( + alloc, + "./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(diff_path); + try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{}); + log.err( + "One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++ + "test={s}, reference={s}, diff={s}", + .{ i, i + 0xFF, test_path, ref_path, diff_path }, + ); + + return true; +} + +/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions +/// against reference files, logs errors and exposes a diff for any difference +/// between the reference and test atlas. +/// +/// Returns true if there was a diff. +fn testDrawRanges( + width: u32, + ascent: u32, + descent: u32, + thickness: u32, +) !bool { + const testing = std.testing; + const alloc = testing.allocator; + + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(width), + .ascent = @floatFromInt(ascent), + .descent = -@as(f64, @floatFromInt(descent)), + .line_gap = 0.0, + .underline_thickness = @floatFromInt(thickness), + .strikethrough_thickness = @floatFromInt(thickness), + }); + + const height = ascent + descent; + + const padding_x = width / 4; + const padding_y = height / 4; + + // Canvas to draw glyphs on, we'll re-use this for all glyphs. + var canvas = try font.sprite.Canvas.init( + alloc, + width, + height, + padding_x, + padding_y, + ); + defer canvas.deinit(); + + // We render glyphs in batches of 256, which we copy (including padding) to + // a 16 by 16 surface to be compared with the reference file for that range. + const stride_x = width + 2 * padding_x; + const stride_y = height + 2 * padding_y; + var atlas = try z2d.Surface.init( + .image_surface_alpha8, + alloc, + @intCast(stride_x * 16), + @intCast(stride_y * 16), + ); + defer atlas.deinit(alloc); + + var i: u32 = std.mem.alignBackward(u32, ranges[0].min, 0x100); + + // Try to make the sprite_face_test folder if it doesn't already exist. + var dir = testing.tmpDir(.{}); + defer dir.cleanup(); + const tmp_dir = try dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(tmp_dir); + + // We set this to true if we have any fails so we can + // return an error after we're done comparing all glyphs. + var fail: bool = false; + + inline for (ranges) |range| { + for (range.min..range.max + 1) |cp| { + // If we've moved to a new batch of 256, check the + // current one and clear the surface for the next one. + if (cp - i >= 0x100) { + // Export to our tmp dir. + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + i = std.mem.alignBackward(u32, @intCast(cp), 0x100); + @memset(std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf), 0); + } + + try getDrawFn(@intCast(cp)).?( + @intCast(cp), + &canvas, + width, + height, + metrics, + ); + canvas.clearClippingRegions(); + atlas.composite( + &canvas.sfc, + .src, + @intCast(stride_x * ((cp - i) % 16)), + @intCast(stride_y * ((cp - i) / 16)), + .{}, + ); + @memset(std.mem.sliceAsBytes(canvas.sfc.image_surface_alpha8.buf), 0); + canvas.clip_top = 0; + canvas.clip_left = 0; + canvas.clip_right = 0; + canvas.clip_bottom = 0; + } + } + + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + return fail; +} + +test "sprite face render all sprites" { + // Renders all sprites to an atlas and compares + // it to a ground truth for regression testing. + + var diff: bool = false; + + // testDrawRanges(width, ascent, descent, thickness): + // + // We compare 4 different sets of metrics; + // - even cell size / even thickness + // - even cell size / odd thickness + // - odd cell size / even thickness + // - odd cell size / odd thickness + // (Also a decreasing range of sizes.) + if (try testDrawRanges(18, 30, 6, 4)) diff = true; + if (try testDrawRanges(12, 20, 4, 3)) diff = true; + if (try testDrawRanges(11, 19, 2, 2)) diff = true; + if (try testDrawRanges(9, 15, 2, 1)) diff = true; + + try std.testing.expect(!diff); // There should be no diffs from reference. +} + +// test "sprite face print all sprites" { +// std.debug.print("\n\n", .{}); +// inline for (ranges) |range| { +// for (range.min..range.max + 1) |cp| { +// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) }); +// } +// } +// std.debug.print("\n\n", .{}); +// } test { - @import("std").testing.refAllDecls(@This()); + std.testing.refAllDecls(@This()); } diff --git a/src/font/sprite/Powerline.zig b/src/font/sprite/Powerline.zig deleted file mode 100644 index eaa7554b1..000000000 --- a/src/font/sprite/Powerline.zig +++ /dev/null @@ -1,564 +0,0 @@ -//! This file contains functions for drawing certain characters from Powerline -//! Extra (https://github.com/ryanoasis/powerline-extra-symbols). These -//! characters are similarly to box-drawing characters (see Box.zig), so the -//! logic will be mainly the same, just with a much reduced character set. -//! -//! Note that this is not the complete list of Powerline glyphs that may be -//! needed, so this may grow to add other glyphs from the set. -const Powerline = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const font = @import("../main.zig"); -const Quad = @import("canvas.zig").Quad; - -const log = std.log.scoped(.powerline_font); - -/// The cell width and height because the boxes are fit perfectly -/// into a cell so that they all properly connect with zero spacing. -width: u32, -height: u32, - -/// Base thickness value for glyphs that are not completely solid (backslashes, -/// thin half-circles, etc). If you want to do any DPI scaling, it is expected -/// to be done earlier. -/// -/// TODO: this and Thickness are currently unused but will be when the -/// aforementioned glyphs are added. -thickness: u32, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -inline fn sq(x: anytype) @TypeOf(x) { - return x * x; -} - -pub fn renderGlyph( - self: Powerline, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(self.height)); - - return font.Glyph{ - .width = self.width, - .height = self.height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(self.width), - }; -} - -fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - switch (cp) { - // Hard dividers and triangles - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - => try self.draw_wedge_triangle(canvas, cp), - - // Soft Dividers - 0xE0B1, - 0xE0B3, - => try self.draw_chevron(canvas, cp), - - // Half-circles - 0xE0B4, - 0xE0B6, - => try self.draw_half_circle(alloc, canvas, cp), - - // Mirrored top-down trapezoids - 0xE0D2, - 0xE0D4, - => try self.draw_trapezoid_top_bottom(canvas, cp), - - else => return error.InvalidCodepoint, - } -} - -fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p1_y: u32 = 0; - var p2_x: u32 = 0; - var p2_y: u32 = 0; - var p3_x: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B1 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - 0xE0B3 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle_outline(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, @floatFromInt(Thickness.light.height(self.thickness)), .on); -} - -fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p2_x: u32 = 0; - var p3_x: u32 = 0; - var p1_y: u32 = 0; - var p2_y: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B0 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - - 0xE0B2 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - 0xE0B8 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BA => { - p1_x = width; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BC => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = 0; - p3_y = height; - }, - - 0xE0BE => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, .on); -} - -fn draw_half_circle(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - const supersample = 4; - - // We make a canvas big enough for the whole circle, with the supersample - // applied. - const width = self.width * 2 * supersample; - const height = self.height * supersample; - - // We set a minimum super-sampled canvas to assert on. The minimum cell - // size is 1x3px, and this looked safe in empirical testing. - std.debug.assert(width >= 8); // 1 * 2 * 4 - std.debug.assert(height >= 12); // 3 * 4 - - const center_x = width / 2 - 1; - const center_y = height / 2 - 1; - - // Our radii. We're technically drawing an ellipse here to ensure that this - // works for fonts with different aspect ratios than a typical 2:1 H*W, e.g. - // Iosevka (which is around 2.6:1). - const radius_x = width / 2 - 1; // This gives us a small margin for smoothing - const radius_y = height / 2; - - // Pre-allocate a matrix to plot the points on. - const cap = height * width; - var points = try alloc.alloc(u8, cap); - defer alloc.free(points); - @memset(points, 0); - - { - // This is a midpoint ellipse algorithm, similar to a midpoint circle - // algorithm in that we only draw the octants we need and then reflect - // the result across the other axes. Since an ellipse has two radii, we - // need to calculate two octants instead of one. There are variations - // on the algorithm and you can find many examples online. This one - // does use some floating point math in calculating the decision - // parameter, but I've found it clear in its implementation and it does - // not require adjustment for integer error. - // - // This algorithm has undergone some iterations, so the following - // references might be helpful for understanding: - // - // * "Drawing a circle, point by point, without floating point - // support" (Dennis Yurichev, - // https://yurichev.com/news/20220322_circle/), which describes the - // midpoint circle algorithm and implementation we initially adapted - // here. - // - // * "Ellipse-Generating Algorithms" (RTU Latvia, - // https://daugavpils.rtu.lv/wp-content/uploads/sites/34/2020/11/LEC_3.pdf), - // which was used to further adapt the algorithm for ellipses. - // - // * "An Effective Approach to Minimize Error in Midpoint Ellipse - // Drawing Algorithm" (Dr. M. Javed Idrisi, Aayesha Ashraf, - // https://arxiv.org/abs/2103.04033), which includes a synopsis of - // the history of ellipse drawing algorithms, and further references. - - // Declare some casted constants for use in various calculations below - const rx: i32 = @intCast(radius_x); - const ry: i32 = @intCast(radius_y); - const rxf: f64 = @floatFromInt(radius_x); - const ryf: f64 = @floatFromInt(radius_y); - const cx: i32 = @intCast(center_x); - const cy: i32 = @intCast(center_y); - - // Our plotting x and y - var x: i32 = 0; - var y: i32 = @intCast(radius_y); - - // Decision parameter, initialized for region 1 - var dparam: f64 = sq(ryf) - sq(rxf) * ryf + sq(rxf) * 0.25; - - // Region 1 - while (2 * sq(ry) * x < 2 * sq(rx) * y) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - x += 1; - if (dparam < 0) { - const xf: f64 = @floatFromInt(x); - dparam += 2 * sq(ryf) * xf + sq(ryf); - } else { - y -= 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(ryf); - } - } - - // Region 2 - { - // Reset our decision parameter for region 2 - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam = sq(ryf) * sq(xf + 0.5) + sq(rxf) * sq(yf - 1) - sq(rxf) * sq(ryf); - } - while (y >= 0) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - y -= 1; - if (dparam > 0) { - const yf: f64 = @floatFromInt(y); - dparam -= 2 * sq(rxf) * yf + sq(rxf); - } else { - x += 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(rxf); - } - } - } - - // Fill - { - const u_height: u32 = @intCast(height); - const u_width: u32 = @intCast(width); - - for (0..u_height) |yf| { - for (0..u_width) |left| { - // Count forward from the left to the first filled pixel - if (points[yf * u_width + left] != 0) { - // Count back to our left point from the right to the first - // filled pixel on the other side. - var right: usize = u_width - 1; - while (right > left) : (right -= 1) { - if (points[yf * u_width + right] != 0) { - break; - } - } - - // Start filling 1 index after the left and go until we hit - // the right; this will be a no-op if the line length is < - // 3 as both left and right will have already been filled. - const start = yf * u_width + left; - const end = yf * u_width + right; - if (end - start >= 3) { - for (start + 1..end) |idx| { - points[idx] = 0xFF; - } - } - } - } - } - } - - // Now that we have our points, we need to "split" our matrix on the x - // axis for the downsample. - { - // The side of the circle we're drawing - const offset_j: u32 = if (cp == 0xE0B4) center_x + 1 else 0; - - for (0..self.height) |r| { - for (0..self.width) |c| { - var total: u32 = 0; - for (0..supersample) |i| { - for (0..supersample) |j| { - const idx = (r * supersample + i) * width + (c * supersample + j + offset_j); - total += points[idx]; - } - } - - const average = @as(u8, @intCast(@min(total / (supersample * supersample), 0xFF))); - canvas.rect( - .{ - .x = @intCast(c), - .y = @intCast(r), - .width = 1, - .height = 1, - }, - @as(font.sprite.Color, @enumFromInt(average)), - ); - } - } - } -} - -fn draw_trapezoid_top_bottom(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const t_top: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - }; - - const t_bottom: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - }; - - try canvas.quad(t_top, .on); - try canvas.quad(t_bottom, .on); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - const cps = [_]u32{ - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0B4, - 0xE0B6, - 0xE0D2, - 0xE0D4, - 0xE0B1, - 0xE0B3, - }; - for (cps) |cp| { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Powerline = .{ .width = 18, .height = 36, .thickness = 2 }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.width), glyph.width); - try testing.expectEqual(@as(u32, face.height), glyph.height); - } -} diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index a5ca7b290..b981449bc 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -81,19 +81,39 @@ pub const Canvas = struct { /// The underlying z2d surface. sfc: z2d.Surface, + padding_x: u32, + padding_y: u32, + + clip_top: u32 = 0, + clip_left: u32 = 0, + clip_right: u32 = 0, + clip_bottom: u32 = 0, + alloc: Allocator, - pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas { + pub fn init( + alloc: Allocator, + width: u32, + height: u32, + padding_x: u32, + padding_y: u32, + ) !Canvas { // Create the surface we'll be using. + // We add padding to both sides (hence `2 *`) const sfc = try z2d.Surface.initPixel( .{ .alpha8 = .{ .a = 0 } }, alloc, - @intCast(width), - @intCast(height), + @intCast(width + 2 * padding_x), + @intCast(height + 2 * padding_y), ); errdefer sfc.deinit(alloc); - return .{ .sfc = sfc, .alloc = alloc }; + return .{ + .sfc = sfc, + .padding_x = padding_x, + .padding_y = padding_y, + .alloc = alloc, + }; } pub fn deinit(self: *Canvas) void { @@ -109,30 +129,33 @@ pub const Canvas = struct { ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region { assert(atlas.format == .grayscale); - const width = @as(u32, @intCast(self.sfc.getWidth())); - const height = @as(u32, @intCast(self.sfc.getHeight())); + self.trim(); + + const sfc_width: u32 = @intCast(self.sfc.getWidth()); + const sfc_height: u32 = @intCast(self.sfc.getHeight()); + + // Subtract our clip margins from the + // width and height to get region size. + const region_width = sfc_width -| self.clip_left -| self.clip_right; + const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region const region = region: { - // We need to add a 1px padding to the font so that we don't - // get fuzzy issues when blending textures. - const padding = 1; - - // Get the full padded region + // Reserve a region with a 1px margin on the bottom and right edges + // so that we can avoid interpolation between adjacent glyphs during + // texture sampling. var region = try atlas.reserve( alloc, - width + (padding * 2), // * 2 because left+right - height + (padding * 2), // * 2 because top+bottom + region_width + 1, + region_height + 1, ); - // Modify the region so that we remove the padding so that - // we write to the non-zero location. The data in an Altlas - // is always initialized to zero (Atlas.clear) so we don't - // need to worry about zero-ing that. - region.x += padding; - region.y += padding; - region.width -= padding * 2; - region.height -= padding * 2; + // Modify the region to remove the margin so that we write to the + // non-zero location. The data in an Altlas is always initialized + // to zero (Atlas.clear) so we don't need to worry about zero-ing + // that. + region.width -= 1; + region.height -= 1; break :region region; }; @@ -140,38 +163,138 @@ pub const Canvas = struct { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); // Write the glyph information into the atlas - assert(region.width == width); - assert(region.height == height); - atlas.set(region, buffer); + assert(region.width == region_width); + assert(region.height == region_height); + atlas.setFromLarger( + region, + buffer, + sfc_width, + self.clip_left, + self.clip_top, + ); } return region; } + // Adjust clip boundaries to trim off any fully transparent rows or columns. + // This circumvents abstractions from z2d so that it can be performant. + fn trim(self: *Canvas) void { + const width: u32 = @intCast(self.sfc.getWidth()); + const height: u32 = @intCast(self.sfc.getHeight()); + + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + + top: while (self.clip_top < height - self.clip_bottom) { + const y = self.clip_top; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :top; + } + self.clip_top += 1; + } + + bottom: while (self.clip_bottom < height - self.clip_top) { + const y = height - self.clip_bottom -| 1; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :bottom; + } + self.clip_bottom += 1; + } + + left: while (self.clip_left < width - self.clip_right) { + const x = self.clip_left; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :left; + } + self.clip_left += 1; + } + + right: while (self.clip_right < width - self.clip_left) { + const x = width - self.clip_right -| 1; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :right; + } + self.clip_right += 1; + } + } + + /// Only really useful for test purposes, since the clipping region is + /// automatically excluded when writing to an atlas with `writeAtlas`. + pub fn clearClippingRegions(self: *Canvas) void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + + for (0..height) |y| { + for (0..self.clip_left) |x| { + buf[y * width + x] = 0; + } + } + + for (0..height) |y| { + for (width - self.clip_right..width) |x| { + buf[y * width + x] = 0; + } + } + + for (0..self.clip_top) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + + for (height - self.clip_bottom..height) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + } + + /// Return a transformation representing the translation for our padding. + pub fn transformation(self: Canvas) z2d.Transformation { + return .{ + .ax = 1, + .by = 0, + .cx = 0, + .dy = 1, + .tx = @as(f64, @floatFromInt(self.padding_x)), + .ty = @as(f64, @floatFromInt(self.padding_y)), + }; + } + /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) z2d.Context { - return .init(self.alloc, &self.sfc); + var ctx = z2d.Context.init(self.alloc, &self.sfc); + // Offset by our padding to keep + // coordinates relative to the cell. + ctx.setTransformation(self.transformation()); + return ctx; } /// Draw and fill a single pixel - pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void { + pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void { self.sfc.putPixel( - @intCast(x), - @intCast(y), + x + @as(i32, @intCast(self.padding_x)), + y + @as(i32, @intCast(self.padding_y)), .{ .alpha8 = .{ .a = @intFromEnum(color) } }, ); } /// Draw and fill a rectangle. This is the main primitive for drawing /// lines as well (which are just generally skinny rectangles...) - pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void { - const x0 = v.x; - const x1 = v.x + v.width; - const y0 = v.y; - const y1 = v.y + v.height; - - for (y0..y1) |y| { - for (x0..x1) |x| { + pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void { + var y = v.y; + while (y < v.y + v.height) : (y += 1) { + var x = v.x; + while (x < v.x + v.width) : (x += 1) { self.pixel( @intCast(x), @intCast(y), @@ -181,96 +304,226 @@ pub const Canvas = struct { } } + /// Convenience wrapper for `Canvas.rect` + pub fn box( + self: *Canvas, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Color, + ) void { + self.rect((Box(i32){ + .p0 = .{ .x = x0, .y = y0 }, + .p1 = .{ .x = x1, .y = y1 }, + }).rect(), color); + } + /// Draw and fill a quad. pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { - var path: z2d.StaticPath(6) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(6); // nodes.len = 0 path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1 path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2 path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3 path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4 path.close(); // +2, nodes.len = 6 - - try z2d.painter.fill( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); + try self.fillPath(path.wrapped_path, .{}, color); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { - var path: z2d.StaticPath(5) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(5); // nodes.len = 0 path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 path.close(); // +2, nodes.len = 5 + try self.fillPath(path.wrapped_path, .{}, color); + } - try z2d.painter.fill( + /// Stroke a line. + pub fn line( + self: *Canvas, + l: Line(f64), + thickness: f64, + color: Color, + ) !void { + var path = self.staticPath(2); // nodes.len = 0 + path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 + path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 + try self.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = thickness, + }, + color, + ); + } + + /// Create a static path of the provided len and initialize it. + /// Use this function instead of making the path manually since + /// it ensures that the transform is applied. + pub inline fn staticPath( + self: *Canvas, + comptime len: usize, + ) z2d.StaticPath(len) { + var path: z2d.StaticPath(len) = .{}; + path.init(); + path.wrapped_path.transformation = self.transformation(); + return path; + } + + /// Stroke a z2d path. + pub fn strokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) z2d.painter.StrokeError!void { + try z2d.painter.stroke( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, - path.wrapped_path.nodes.items, - .{}, + path.nodes.items, + opts, ); } - pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(3) = .{}; - path.init(); // nodes.len = 0 + /// Do an inner stroke on a z2d path, right now this involves a pretty + /// heavy workaround that uses two extra surfaces; in the future, z2d + /// should add inner and outer strokes natively. + pub fn innerStrokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { + // On one surface we fill the shape, this will be a mask we + // multiply with the double-width stroke so that only the + // part inside is used. + var fill_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer fill_sfc.deinit(self.alloc); - path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 - path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 - path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 + // On the other we'll do the double width stroke. + var stroke_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer stroke_sfc.deinit(self.alloc); + + // Make a closed version of the path for our fill, so + // that we can support open paths for inner stroke. + var closed_path = path; + closed_path.nodes = try path.nodes.clone(self.alloc); + defer closed_path.deinit(self.alloc); + try closed_path.close(self.alloc); + + // Fill the shape in white to the fill surface, we use + // white because this is a mask that we'll multiply with + // the stroke, we want everything inside to be the stroke + // color. + try z2d.painter.fill( + self.alloc, + &fill_sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = 255 } }, + } }, + closed_path.nodes.items, + .{}, + ); + // Stroke the shape with double the desired width. + var mut_opts = opts; + mut_opts.line_width *= 2; try z2d.painter.stroke( self.alloc, - &self.sfc, + &stroke_sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, + path.nodes.items, + mut_opts, ); - } - /// Stroke a line. - pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(2) = .{}; - path.init(); // nodes.len = 0 + // We multiply the stroke sfc on to the fill surface. + // The z2d composite operation doesn't seem to work for + // this with alpha8 surfaces, so we have to do it manually. + for ( + std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf), + std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf), + ) |*d, s| { + d.* = @intFromFloat(@round( + 255.0 * + (@as(f64, @floatFromInt(s)) / 255.0) * + (@as(f64, @floatFromInt(d.*)) / 255.0), + )); + } - path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 - path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 + // Then we composite the result on to the main surface. + self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{}); + } - try z2d.painter.stroke( + /// Fill a z2d path. + pub fn fillPath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.FillOpts, + color: Color, + ) z2d.painter.FillError!void { + try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, + path.nodes.items, + opts, ); } + /// Invert all pixels on the canvas. pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } + + /// Mirror the canvas horizontally. + pub fn flipHorizontal(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[y * width + width - x - 1]; + } + } + std.mem.swap(u32, &self.clip_left, &self.clip_right); + } + + /// Mirror the canvas vertically. + pub fn flipVertical(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[(height - y - 1) * width + x]; + } + } + std.mem.swap(u32, &self.clip_top, &self.clip_bottom); + } }; diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig deleted file mode 100644 index d63db624a..000000000 --- a/src/font/sprite/cursor.zig +++ /dev/null @@ -1,65 +0,0 @@ -//! This file renders cursor sprites. -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const font = @import("../main.zig"); -const Sprite = font.sprite.Sprite; - -/// Draw a cursor. -pub fn renderGlyph( - alloc: Allocator, - atlas: *font.Atlas, - sprite: Sprite, - width: u32, - height: u32, - thickness: u32, -) !font.Glyph { - // Make a canvas of the desired size - var canvas = try font.sprite.Canvas.init(alloc, width, height); - defer canvas.deinit(); - - // Draw the appropriate sprite - switch (sprite) { - Sprite.cursor_rect => canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = height, - }, .on), - Sprite.cursor_hollow_rect => { - // left - canvas.rect(.{ .x = 0, .y = 0, .width = thickness, .height = height }, .on); - // right - canvas.rect(.{ .x = width -| thickness, .y = 0, .width = thickness, .height = height }, .on); - // top - canvas.rect(.{ .x = 0, .y = 0, .width = width, .height = thickness }, .on); - // bottom - canvas.rect(.{ .x = 0, .y = height -| thickness, .width = width, .height = thickness }, .on); - }, - Sprite.cursor_bar => canvas.rect(.{ - .x = 0, - .y = 0, - .width = thickness, - .height = height, - }, .on), - else => unreachable, - } - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - // HACK: Set the width for the bar cursor to just the thickness, - // this is just for the benefit of the custom shader cursor - // uniform code. -- In the future code will be introduced to - // auto-crop the canvas so that this isn't needed. - .width = if (sprite == .cursor_bar) thickness else width, - .height = height, - .offset_x = 0, - .offset_y = @intCast(height), - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} diff --git a/src/font/sprite/draw/README.md b/src/font/sprite/draw/README.md new file mode 100644 index 000000000..c6219b83f --- /dev/null +++ b/src/font/sprite/draw/README.md @@ -0,0 +1,50 @@ +# This is a *special* directory. +The files in this directory are imported by `../Face.zig` and scanned for pub +functions with names matching a specific format, which are then used to handle +drawing specified codepoints. + +## IMPORTANT +When you add a new file here, you need to add the corresponding import in +`../Face.zig` for its draw functions to be picked up. I tried dynamically +listing these files to do this automatically but it was more pain than it +was worth. + +## `draw*` functions +Any function named `draw` or `draw_` will be used to +draw the codepoint or range of codepoints specified in the name. These are +hex-encoded values with upper case letters. + +`draw*` functions are provided with these arguments: +```zig +/// The codepoint being drawn. For single-codepoint draw functions this can +/// just be discarded, but it's needed for range draw functions to determine +/// which value in the range needs to be drawn. +cp: u32, +/// The canvas on which to draw the codepoint. +//// +/// This canvas has been prepared with an extra quarter of the width/height on +/// each edge, and its transform has been set so that [0, 0] is still the upper +/// left of the cell and [width, height] is still the bottom right; in order to +/// draw above or to the left, use negative values, and to draw below or to the +/// right use values greater than the width or the height. +/// +/// Because the canvas has been prepared this way, it's possible to draw glyphs +/// that exit the cell bounds by some amount- an example of when this is useful +/// is in drawing box-drawing diagonals, with enough overlap so that they can +/// seamlessly connect across corners of cells. +canvas: *font.sprite.Canvas, +/// The width of the cell to draw for. +width: u32, +/// The height of the cell to draw for. +height: u32, +/// The font grid metrics. +metrics: font.Metrics, +``` + +`draw*` functions may only return `DrawFnError!void` (defined in `../Face.zig`). + +## `special.zig` +The functions in `special.zig` are not for drawing unicode codepoints, +rather their names match the enum tag names in the `Sprite` enum from +`src/font/sprite.zig`. They are called with the same arguments as the +other `draw*` functions. diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig new file mode 100644 index 000000000..27c6ae516 --- /dev/null +++ b/src/font/sprite/draw/block.zig @@ -0,0 +1,184 @@ +//! Block Elements | U+2580...U+259F +//! https://en.wikipedia.org/wiki/Block_Elements +//! +//! ▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏ +//! ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Shade = common.Shade; +const Quads = common.Quads; +const Alignment = common.Alignment; +const rect = common.rect; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +// Utility names for common fractions +const one_eighth: f64 = 0.125; +const one_quarter: f64 = 0.25; +const one_third: f64 = (1.0 / 3.0); +const three_eighths: f64 = 0.375; +const half: f64 = 0.5; +const five_eighths: f64 = 0.625; +const two_thirds: f64 = (2.0 / 3.0); +const three_quarters: f64 = 0.75; +const seven_eighths: f64 = 0.875; + +pub fn draw2580_259F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '▀' UPPER HALF BLOCK + 0x2580 => block(metrics, canvas, .upper, 1, half), + // '▁' LOWER ONE EIGHTH BLOCK + 0x2581 => block(metrics, canvas, .lower, 1, one_eighth), + // '▂' LOWER ONE QUARTER BLOCK + 0x2582 => block(metrics, canvas, .lower, 1, one_quarter), + // '▃' LOWER THREE EIGHTHS BLOCK + 0x2583 => block(metrics, canvas, .lower, 1, three_eighths), + // '▄' LOWER HALF BLOCK + 0x2584 => block(metrics, canvas, .lower, 1, half), + // '▅' LOWER FIVE EIGHTHS BLOCK + 0x2585 => block(metrics, canvas, .lower, 1, five_eighths), + // '▆' LOWER THREE QUARTERS BLOCK + 0x2586 => block(metrics, canvas, .lower, 1, three_quarters), + // '▇' LOWER SEVEN EIGHTHS BLOCK + 0x2587 => block(metrics, canvas, .lower, 1, seven_eighths), + // '█' FULL BLOCK + 0x2588 => fullBlockShade(metrics, canvas, .on), + // '▉' LEFT SEVEN EIGHTHS BLOCK + 0x2589 => block(metrics, canvas, .left, seven_eighths, 1), + // '▊' LEFT THREE QUARTERS BLOCK + 0x258a => block(metrics, canvas, .left, three_quarters, 1), + // '▋' LEFT FIVE EIGHTHS BLOCK + 0x258b => block(metrics, canvas, .left, five_eighths, 1), + // '▌' LEFT HALF BLOCK + 0x258c => block(metrics, canvas, .left, half, 1), + // '▍' LEFT THREE EIGHTHS BLOCK + 0x258d => block(metrics, canvas, .left, three_eighths, 1), + // '▎' LEFT ONE QUARTER BLOCK + 0x258e => block(metrics, canvas, .left, one_quarter, 1), + // '▏' LEFT ONE EIGHTH BLOCK + 0x258f => block(metrics, canvas, .left, one_eighth, 1), + + // '▐' RIGHT HALF BLOCK + 0x2590 => block(metrics, canvas, .right, half, 1), + // '░' + 0x2591 => fullBlockShade(metrics, canvas, .light), + // '▒' + 0x2592 => fullBlockShade(metrics, canvas, .medium), + // '▓' + 0x2593 => fullBlockShade(metrics, canvas, .dark), + // '▔' UPPER ONE EIGHTH BLOCK + 0x2594 => block(metrics, canvas, .upper, 1, one_eighth), + // '▕' RIGHT ONE EIGHTH BLOCK + 0x2595 => block(metrics, canvas, .right, one_eighth, 1), + // '▖' + 0x2596 => quadrant(metrics, canvas, .{ .bl = true }), + // '▗' + 0x2597 => quadrant(metrics, canvas, .{ .br = true }), + // '▘' + 0x2598 => quadrant(metrics, canvas, .{ .tl = true }), + // '▙' + 0x2599 => quadrant(metrics, canvas, .{ .tl = true, .bl = true, .br = true }), + // '▚' + 0x259a => quadrant(metrics, canvas, .{ .tl = true, .br = true }), + // '▛' + 0x259b => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }), + // '▜' + 0x259c => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .br = true }), + // '▝' + 0x259d => quadrant(metrics, canvas, .{ .tr = true }), + // '▞' + 0x259e => quadrant(metrics, canvas, .{ .tr = true, .bl = true }), + // '▟' + 0x259f => quadrant(metrics, canvas, .{ .tr = true, .bl = true, .br = true }), + + else => unreachable, + } +} + +pub fn block( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, +) void { + blockShade(metrics, canvas, alignment, width, height, .on); +} + +pub fn blockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, + comptime shade: Shade, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const w: u32 = @intFromFloat(@round(float_width * width)); + const h: u32 = @intFromFloat(@round(float_height * height)); + + const x = switch (alignment.horizontal) { + .left => 0, + .right => metrics.cell_width - w, + .center => (metrics.cell_width - w) / 2, + }; + const y = switch (alignment.vertical) { + .top => 0, + .bottom => metrics.cell_height - h, + .middle => (metrics.cell_height - h) / 2, + }; + + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(y), + .width = @intCast(w), + .height = @intCast(h), + }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); +} + +pub fn fullBlockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + shade: Shade, +) void { + canvas.box( + 0, + 0, + @intCast(metrics.cell_width), + @intCast(metrics.cell_height), + @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade))), + ); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} diff --git a/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig new file mode 100644 index 000000000..91d78d2b2 --- /dev/null +++ b/src/font/sprite/draw/box.zig @@ -0,0 +1,947 @@ +//! Box Drawing | U+2500...U+257F +//! https://en.wikipedia.org/wiki/Box_Drawing +//! +//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏ +//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟ +//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯ +//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿ +//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏ +//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟ +//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯ +//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Quads = common.Quads; +const Corner = common.Corner; +const Edge = common.Edge; +const Alignment = common.Alignment; +const rect = common.rect; +const hline = common.hline; +const vline = common.vline; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +/// Specification of a traditional intersection-style line/box-drawing char, +/// which can have a different style of line from each edge to the center. +pub const Lines = packed struct(u8) { + up: Style = .none, + right: Style = .none, + down: Style = .none, + left: Style = .none, + + const Style = enum(u2) { + none, + light, + heavy, + double, + }; +}; + +pub fn draw2500_257F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '─' + 0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }), + // '━' + 0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }), + // '│' + 0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }), + // '┃' + 0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }), + // '┄' + 0x2504 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┅' + 0x2505 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┆' + 0x2506 => dashVertical( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┇' + 0x2507 => dashVertical( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┈' + 0x2508 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┉' + 0x2509 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┊' + 0x250a => dashVertical( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┋' + 0x250b => dashVertical( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┌' + 0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }), + // '┍' + 0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }), + // '┎' + 0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }), + // '┏' + 0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }), + + // '┐' + 0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }), + // '┑' + 0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }), + // '┒' + 0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }), + // '┓' + 0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }), + // '└' + 0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }), + // '┕' + 0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }), + // '┖' + 0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }), + // '┗' + 0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }), + // '┘' + 0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }), + // '┙' + 0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }), + // '┚' + 0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }), + // '┛' + 0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }), + // '├' + 0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }), + // '┝' + 0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }), + // '┞' + 0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }), + // '┟' + 0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }), + + // '┠' + 0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), + // '┡' + 0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), + // '┢' + 0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), + // '┣' + 0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), + // '┤' + 0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }), + // '┥' + 0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }), + // '┦' + 0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }), + // '┧' + 0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }), + // '┨' + 0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), + // '┩' + 0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), + // '┪' + 0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), + // '┫' + 0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), + // '┬' + 0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }), + // '┭' + 0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }), + // '┮' + 0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }), + // '┯' + 0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), + + // '┰' + 0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }), + // '┱' + 0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), + // '┲' + 0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), + // '┳' + 0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), + // '┴' + 0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }), + // '┵' + 0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }), + // '┶' + 0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }), + // '┷' + 0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), + // '┸' + 0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }), + // '┹' + 0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), + // '┺' + 0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), + // '┻' + 0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), + // '┼' + 0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), + // '┽' + 0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), + // '┾' + 0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), + // '┿' + 0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), + + // '╀' + 0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), + // '╁' + 0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), + // '╂' + 0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), + // '╃' + 0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), + // '╄' + 0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), + // '╅' + 0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), + // '╆' + 0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), + // '╇' + 0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), + // '╈' + 0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╉' + 0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), + // '╊' + 0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), + // '╋' + 0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╌' + 0x254c => dashHorizontal( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.light.height(metrics.box_thickness), + ), + // '╍' + 0x254d => dashHorizontal( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╎' + 0x254e => dashVertical( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╏' + 0x254f => dashVertical( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + + // '═' + 0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }), + // '║' + 0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }), + // '╒' + 0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }), + // '╓' + 0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }), + // '╔' + 0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }), + // '╕' + 0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }), + // '╖' + 0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }), + // '╗' + 0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }), + // '╘' + 0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }), + // '╙' + 0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }), + // '╚' + 0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }), + // '╛' + 0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }), + // '╜' + 0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }), + // '╝' + 0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }), + // '╞' + 0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }), + // '╟' + 0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }), + + // '╠' + 0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }), + // '╡' + 0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }), + // '╢' + 0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }), + // '╣' + 0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }), + // '╤' + 0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }), + // '╥' + 0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }), + // '╦' + 0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }), + // '╧' + 0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }), + // '╨' + 0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }), + // '╩' + 0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }), + // '╪' + 0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), + // '╫' + 0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), + // '╬' + 0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), + // '╭' + 0x256d => try arc(metrics, canvas, .br, .light), + // '╮' + 0x256e => try arc(metrics, canvas, .bl, .light), + // '╯' + 0x256f => try arc(metrics, canvas, .tl, .light), + + // '╰' + 0x2570 => try arc(metrics, canvas, .tr, .light), + // '╱' + 0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas), + // '╲' + 0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas), + // '╳' + 0x2573 => lightDiagonalCross(metrics, canvas), + // '╴' + 0x2574 => linesChar(metrics, canvas, .{ .left = .light }), + // '╵' + 0x2575 => linesChar(metrics, canvas, .{ .up = .light }), + // '╶' + 0x2576 => linesChar(metrics, canvas, .{ .right = .light }), + // '╷' + 0x2577 => linesChar(metrics, canvas, .{ .down = .light }), + // '╸' + 0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }), + // '╹' + 0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }), + // '╺' + 0x257a => linesChar(metrics, canvas, .{ .right = .heavy }), + // '╻' + 0x257b => linesChar(metrics, canvas, .{ .down = .heavy }), + // '╼' + 0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }), + // '╽' + 0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }), + // '╾' + 0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }), + // '╿' + 0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }), + + else => unreachable, + } +} + +pub fn linesChar( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + lines: Lines, +) void { + const light_px = Thickness.light.height(metrics.box_thickness); + const heavy_px = Thickness.heavy.height(metrics.box_thickness); + + // Top of light horizontal strokes + const h_light_top = (metrics.cell_height -| light_px) / 2; + // Bottom of light horizontal strokes + const h_light_bottom = h_light_top +| light_px; + + // Top of heavy horizontal strokes + const h_heavy_top = (metrics.cell_height -| heavy_px) / 2; + // Bottom of heavy horizontal strokes + const h_heavy_bottom = h_heavy_top +| heavy_px; + + // Top of the top doubled horizontal stroke (bottom is `h_light_top`) + const h_double_top = h_light_top -| light_px; + // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`) + const h_double_bottom = h_light_bottom +| light_px; + + // Left of light vertical strokes + const v_light_left = (metrics.cell_width -| light_px) / 2; + // Right of light vertical strokes + const v_light_right = v_light_left +| light_px; + + // Left of heavy vertical strokes + const v_heavy_left = (metrics.cell_width -| heavy_px) / 2; + // Right of heavy vertical strokes + const v_heavy_right = v_heavy_left +| heavy_px; + + // Left of the left doubled vertical stroke (right is `v_light_left`) + const v_double_left = v_light_left -| light_px; + // Right of the right doubled vertical stroke (left is `v_light_right`) + const v_double_right = v_light_right +| light_px; + + // The bottom of the up line + const up_bottom = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_bottom + else if (lines.left != lines.right or lines.down == lines.up) + if (lines.left == .double or lines.right == .double) + h_double_bottom + else + h_light_bottom + else if (lines.left == .none and lines.right == .none) + h_light_bottom + else + h_light_top; + + // The top of the down line + const down_top = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_top + else if (lines.left != lines.right or lines.up == lines.down) + if (lines.left == .double or lines.right == .double) + h_double_top + else + h_light_top + else if (lines.left == .none and lines.right == .none) + h_light_top + else + h_light_bottom; + + // The right of the left line + const left_right = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_right + else if (lines.up != lines.down or lines.left == lines.right) + if (lines.up == .double or lines.down == .double) + v_double_right + else + v_light_right + else if (lines.up == .none and lines.down == .none) + v_light_right + else + v_light_left; + + // The left of the right line + const right_left = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_left + else if (lines.up != lines.down or lines.right == lines.left) + if (lines.up == .double or lines.down == .double) + v_double_left + else + v_light_left + else if (lines.up == .none and lines.down == .none) + v_light_left + else + v_light_right; + + switch (lines.up) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + 0, + @intCast(v_light_right), + @intCast(up_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + 0, + @intCast(v_heavy_right), + @intCast(up_bottom), + .on, + ), + .double => { + const left_bottom = if (lines.left == .double) h_light_top else up_bottom; + const right_bottom = if (lines.right == .double) h_light_top else up_bottom; + + canvas.box( + @intCast(v_double_left), + 0, + @intCast(v_light_left), + @intCast(left_bottom), + .on, + ); + canvas.box( + @intCast(v_light_right), + 0, + @intCast(v_double_right), + @intCast(right_bottom), + .on, + ); + }, + } + + switch (lines.right) { + .none => {}, + .light => canvas.box( + @intCast(right_left), + @intCast(h_light_top), + @intCast(metrics.cell_width), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(right_left), + @intCast(h_heavy_top), + @intCast(metrics.cell_width), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_left = if (lines.up == .double) v_light_right else right_left; + const bottom_left = if (lines.down == .double) v_light_right else right_left; + + canvas.box( + @intCast(top_left), + @intCast(h_double_top), + @intCast(metrics.cell_width), + @intCast(h_light_top), + .on, + ); + canvas.box( + @intCast(bottom_left), + @intCast(h_light_bottom), + @intCast(metrics.cell_width), + @intCast(h_double_bottom), + .on, + ); + }, + } + + switch (lines.down) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + @intCast(down_top), + @intCast(v_light_right), + @intCast(metrics.cell_height), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + @intCast(down_top), + @intCast(v_heavy_right), + @intCast(metrics.cell_height), + .on, + ), + .double => { + const left_top = if (lines.left == .double) h_light_bottom else down_top; + const right_top = if (lines.right == .double) h_light_bottom else down_top; + + canvas.box( + @intCast(v_double_left), + @intCast(left_top), + @intCast(v_light_left), + @intCast(metrics.cell_height), + .on, + ); + canvas.box( + @intCast(v_light_right), + @intCast(right_top), + @intCast(v_double_right), + @intCast(metrics.cell_height), + .on, + ); + }, + } + + switch (lines.left) { + .none => {}, + .light => canvas.box( + 0, + @intCast(h_light_top), + @intCast(left_right), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + 0, + @intCast(h_heavy_top), + @intCast(left_right), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_right = if (lines.up == .double) v_light_left else left_right; + const bottom_right = if (lines.down == .double) v_light_left else left_right; + + canvas.box( + 0, + @intCast(h_double_top), + @intCast(top_right), + @intCast(h_light_top), + .on, + ); + canvas.box( + 0, + @intCast(h_light_bottom), + @intCast(bottom_right), + @intCast(h_double_bottom), + .on, + ); + }, + } +} + +pub fn lightDiagonalUpperRightToLowerLeft( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = float_width + 0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = -0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalUpperLeftToLowerRight( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = -0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = float_width + 0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalCross( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + lightDiagonalUpperRightToLowerLeft(metrics, canvas); + lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} + +pub fn arc( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime thickness: Thickness, +) !void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2; + const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2; + + const r = @min(float_width, float_height) / 2; + + // Fraction away from the center to place the middle control points, + const s: f64 = 0.25; + + var path = canvas.staticPath(4); + + switch (corner) { + .tl => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .tr => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + .bl => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .br => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + } + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, + .on, + ); +} + +fn dashHorizontal( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +------------+ + // | | + // | | + // | | + // | | + // | -- -- -- | + // | | + // | | + // | | + // | | + // +------------+ + // Our dashed line should be made such that when tiled horizontally + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have half-sized + // gaps on the left and right so that it is centered properly. + + // For N dashes, there are N - 1 gaps between them, but we also have + // half-sized gaps on either side, adding up to N total gaps. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_width < count + gap_count) { + hlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count))); + const total_gap_width: i32 = gap_count * gap_width; + const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width; + const dash_width: i32 = @divFloor(total_dash_width, count); + const remaining: i32 = @mod(total_dash_width, count); + + assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width); + + // Our dashes should be centered vertically. + const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2); + + // We start at half a gap from the left edge, in order to center + // our dashes properly. + var x: i32 = @divFloor(gap_width, 2); + + // We'll distribute the extra space in to dash widths, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + for (0..count) |_| { + var x1 = x + dash_width; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + x1 += 1; + } + hline(canvas, x, x1, y, thick_px); + // Advance by the width of the dash we drew and the width + // of a gap to get the the start of the next dash. + x = x1 + gap_width; + } +} + +fn dashVertical( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +-----------+ + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // +-----------+ + // Our dashed line should be made such that when tiled vertically it + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have an extra gap + // gap at the bottom. + // + // A single full-sized extra gap is preferred to two half-sized ones for + // vertical to allow better joining to solid characters without creating + // visible half-sized gaps. Unlike horizontal, centering is a lot less + // important, visually. + + // Because of the extra gap at the bottom, there are as many gaps as + // there are dashes. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_height < count + gap_count) { + vlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_height: i32 = @intCast(@min(desired_gap, metrics.cell_height / (2 * count))); + const total_gap_height: i32 = gap_count * gap_height; + const total_dash_height: i32 = @as(i32, @intCast(metrics.cell_height)) - total_gap_height; + const dash_height: i32 = @divFloor(total_dash_height, count); + const remaining: i32 = @mod(total_dash_height, count); + + assert(dash_height * count + gap_height * gap_count + remaining == metrics.cell_height); + + // Our dashes should be centered horizontally. + const x: i32 = @intCast((metrics.cell_width -| thick_px) / 2); + + // We start at the top of the cell. + var y: i32 = 0; + + // We'll distribute the extra space in to dash heights, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + inline for (0..count) |_| { + var y1 = y + dash_height; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + y1 += 1; + } + vline(canvas, y, y1, x, thick_px); + // Advance by the height of the dash we drew and the height + // of a gap to get the the start of the next dash. + y = y1 + gap_height; + } +} diff --git a/src/font/sprite/draw/braille.zig b/src/font/sprite/draw/braille.zig new file mode 100644 index 000000000..c756ff369 --- /dev/null +++ b/src/font/sprite/draw/braille.zig @@ -0,0 +1,148 @@ +//! Braille Patterns | U+2800...U+28FF +//! https://en.wikipedia.org/wiki/Braille_Patterns +//! +//! (6 dot patterns) +//! ⠀ ⠁ ⠂ ⠃ ⠄ ⠅ ⠆ ⠇ ⠈ ⠉ ⠊ ⠋ ⠌ ⠍ ⠎ ⠏ +//! ⠐ ⠑ ⠒ ⠓ ⠔ ⠕ ⠖ ⠗ ⠘ ⠙ ⠚ ⠛ ⠜ ⠝ ⠞ ⠟ +//! ⠠ ⠡ ⠢ ⠣ ⠤ ⠥ ⠦ ⠧ ⠨ ⠩ ⠪ ⠫ ⠬ ⠭ ⠮ ⠯ +//! ⠰ ⠱ ⠲ ⠳ ⠴ ⠵ ⠶ ⠷ ⠸ ⠹ ⠺ ⠻ ⠼ ⠽ ⠾ ⠿ +//! +//! (8 dot patterns) +//! ⡀ ⡁ ⡂ ⡃ ⡄ ⡅ ⡆ ⡇ ⡈ ⡉ ⡊ ⡋ ⡌ ⡍ ⡎ ⡏ +//! ⡐ ⡑ ⡒ ⡓ ⡔ ⡕ ⡖ ⡗ ⡘ ⡙ ⡚ ⡛ ⡜ ⡝ ⡞ ⡟ +//! ⡠ ⡡ ⡢ ⡣ ⡤ ⡥ ⡦ ⡧ ⡨ ⡩ ⡪ ⡫ ⡬ ⡭ ⡮ ⡯ +//! ⡰ ⡱ ⡲ ⡳ ⡴ ⡵ ⡶ ⡷ ⡸ ⡹ ⡺ ⡻ ⡼ ⡽ ⡾ ⡿ +//! ⢀ ⢁ ⢂ ⢃ ⢄ ⢅ ⢆ ⢇ ⢈ ⢉ ⢊ ⢋ ⢌ ⢍ ⢎ ⢏ +//! ⢐ ⢑ ⢒ ⢓ ⢔ ⢕ ⢖ ⢗ ⢘ ⢙ ⢚ ⢛ ⢜ ⢝ ⢞ ⢟ +//! ⢠ ⢡ ⢢ ⢣ ⢤ ⢥ ⢦ ⢧ ⢨ ⢩ ⢪ ⢫ ⢬ ⢭ ⢮ ⢯ +//! ⢰ ⢱ ⢲ ⢳ ⢴ ⢵ ⢶ ⢷ ⢸ ⢹ ⢺ ⢻ ⢼ ⢽ ⢾ ⢿ +//! ⣀ ⣁ ⣂ ⣃ ⣄ ⣅ ⣆ ⣇ ⣈ ⣉ ⣊ ⣋ ⣌ ⣍ ⣎ ⣏ +//! ⣐ ⣑ ⣒ ⣓ ⣔ ⣕ ⣖ ⣗ ⣘ ⣙ ⣚ ⣛ ⣜ ⣝ ⣞ ⣟ +//! ⣠ ⣡ ⣢ ⣣ ⣤ ⣥ ⣦ ⣧ ⣨ ⣩ ⣪ ⣫ ⣬ ⣭ ⣮ ⣯ +//! ⣰ ⣱ ⣲ ⣳ ⣴ ⣵ ⣶ ⣷ ⣸ ⣹ ⣺ ⣻ ⣼ ⣽ ⣾ ⣿ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const font = @import("../../main.zig"); + +/// A braille pattern. +/// +/// Mnemonic: +/// [t]op - . . +/// [u]pper - . . +/// [l]ower - . . +/// [b]ottom - . . +/// | | +/// [l]eft, [r]ight +/// +/// Struct layout matches bit patterns of unicode codepoints. +const Pattern = packed struct(u8) { + tl: bool, + ul: bool, + ll: bool, + tr: bool, + ur: bool, + lr: bool, + bl: bool, + br: bool, + + fn from(cp: u32) Pattern { + return @bitCast(@as(u8, @truncate(cp))); + } +}; + +pub fn draw2800_28FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + var w: i32 = @intCast(@min(width / 4, height / 8)); + var x_spacing: i32 = @intCast(width / 4); + var y_spacing: i32 = @intCast(height / 8); + var x_margin: i32 = @divFloor(x_spacing, 2); + var y_margin: i32 = @divFloor(y_spacing, 2); + + var x_px_left: i32 = + @as(i32, @intCast(width)) - 2 * x_margin - x_spacing - 2 * w; + + var y_px_left: i32 = + @as(i32, @intCast(height)) - 2 * y_margin - 3 * y_spacing - 4 * w; + + // First, try hard to ensure the DOT width is non-zero + if (x_px_left >= 2 and y_px_left >= 4 and w == 0) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + // Second, prefer a non-zero margin + if (x_px_left >= 2 and x_margin == 0) { + x_margin = 1; + x_px_left -= 2; + } + if (y_px_left >= 2 and y_margin == 0) { + y_margin = 1; + y_px_left -= 2; + } + + // Third, increase spacing + if (x_px_left >= 1) { + x_spacing += 1; + x_px_left -= 1; + } + if (y_px_left >= 3) { + y_spacing += 1; + y_px_left -= 3; + } + + // Fourth, margins (“spacing”, but on the sides) + if (x_px_left >= 2) { + x_margin += 1; + x_px_left -= 2; + } + if (y_px_left >= 2) { + y_margin += 1; + y_px_left -= 2; + } + + // Last - increase dot width + if (x_px_left >= 2 and y_px_left >= 4) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + assert(x_px_left <= 1 or y_px_left <= 1); + assert(2 * x_margin + 2 * w + x_spacing <= width); + assert(2 * y_margin + 4 * w + 3 * y_spacing <= height); + + const x = [2]i32{ x_margin, x_margin + w + x_spacing }; + const y = y: { + var y: [4]i32 = undefined; + y[0] = y_margin; + y[1] = y[0] + w + y_spacing; + y[2] = y[1] + w + y_spacing; + y[3] = y[2] + w + y_spacing; + break :y y; + }; + + assert(cp >= 0x2800); + assert(cp <= 0x28ff); + const p: Pattern = .from(cp); + + if (p.tl) canvas.box(x[0], y[0], x[0] + w, y[0] + w, .on); + if (p.ul) canvas.box(x[0], y[1], x[0] + w, y[1] + w, .on); + if (p.ll) canvas.box(x[0], y[2], x[0] + w, y[2] + w, .on); + if (p.bl) canvas.box(x[0], y[3], x[0] + w, y[3] + w, .on); + if (p.tr) canvas.box(x[1], y[0], x[1] + w, y[0] + w, .on); + if (p.ur) canvas.box(x[1], y[1], x[1] + w, y[1] + w, .on); + if (p.lr) canvas.box(x[1], y[2], x[1] + w, y[2] + w, .on); + if (p.br) canvas.box(x[1], y[3], x[1] + w, y[3] + w, .on); +} diff --git a/src/font/sprite/draw/branch.zig b/src/font/sprite/draw/branch.zig new file mode 100644 index 000000000..ac7220390 --- /dev/null +++ b/src/font/sprite/draw/branch.zig @@ -0,0 +1,505 @@ +//! Branch Drawing Characters | U+F5D0...U+F60D +//! +//! Branch drawing character set, used for drawing git-like +//! graphs in the terminal. Originally implemented in Kitty. +//! Ref: +//! - https://github.com/kovidgoyal/kitty/pull/7681 +//! - https://github.com/kovidgoyal/kitty/pull/7805 +//! NOTE: Kitty is GPL licensed, and its code was not referenced +//! for these characters, only the loose specification of +//! the character set in the pull request descriptions. +//! +//!                 +//!                 +//!                 +//!               +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Edge = common.Edge; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const arc = @import("box.zig").arc; + +const font = @import("../../main.zig"); + +/// Specification of a branch drawing node, which consists of a +/// circle which is either empty or filled, and lines connecting +/// optionally between the circle and each of the 4 edges. +const BranchNode = packed struct(u5) { + up: bool = false, + right: bool = false, + down: bool = false, + left: bool = false, + filled: bool = false, +}; + +pub fn drawF5D0_F60D( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '' + 0x0f5d0 => hlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d1 => vlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d2 => fadingLine(metrics, canvas, .right, .light), + // '' + 0x0f5d3 => fadingLine(metrics, canvas, .left, .light), + // '' + 0x0f5d4 => fadingLine(metrics, canvas, .bottom, .light), + // '' + 0x0f5d5 => fadingLine(metrics, canvas, .top, .light), + // '' + 0x0f5d6 => try arc(metrics, canvas, .br, .light), + // '' + 0x0f5d7 => try arc(metrics, canvas, .bl, .light), + // '' + 0x0f5d8 => try arc(metrics, canvas, .tr, .light), + // '' + 0x0f5d9 => try arc(metrics, canvas, .tl, .light), + // '' + 0x0f5da => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5db => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dc => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dd => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5de => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5df => { + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .bl, .light); + }, + + // '' + 0x0f5e0 => { + try arc(metrics, canvas, .bl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e1 => { + try arc(metrics, canvas, .br, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e2 => { + try arc(metrics, canvas, .br, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5e3 => { + try arc(metrics, canvas, .tl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e4 => { + try arc(metrics, canvas, .tr, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e5 => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e6 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5e7 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5e8 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e9 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ea => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5eb => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ec => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ed => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ee => branchNode(metrics, canvas, .{ .filled = true }, .light), + // '' + 0x0f5ef => branchNode(metrics, canvas, .{}, .light), + + // '' + 0x0f5f0 => branchNode(metrics, canvas, .{ + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f1 => branchNode(metrics, canvas, .{ + .right = true, + }, .light), + // '' + 0x0f5f2 => branchNode(metrics, canvas, .{ + .left = true, + .filled = true, + }, .light), + // '' + 0x0f5f3 => branchNode(metrics, canvas, .{ + .left = true, + }, .light), + // '' + 0x0f5f4 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f5 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + }, .light), + // '' + 0x0f5f6 => branchNode(metrics, canvas, .{ + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5f7 => branchNode(metrics, canvas, .{ + .down = true, + }, .light), + // '' + 0x0f5f8 => branchNode(metrics, canvas, .{ + .up = true, + .filled = true, + }, .light), + // '' + 0x0f5f9 => branchNode(metrics, canvas, .{ + .up = true, + }, .light), + // '' + 0x0f5fa => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fb => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + }, .light), + // '' + 0x0f5fc => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fd => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + }, .light), + // '' + 0x0f5fe => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5ff => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + }, .light), + + // '' + 0x0f600 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f601 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + }, .light), + // '' + 0x0f602 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f603 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + }, .light), + // '' + 0x0f604 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f605 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + }, .light), + // '' + 0x0f606 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f607 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + }, .light), + // '' + 0x0f608 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f609 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60a => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60b => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60c => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60d => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + }, .light), + + else => unreachable, + } +} + +fn branchNode( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + node: BranchNode, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // We calculate the center of the circle this way + // to ensure it aligns with box drawing characters + // since the lines are sometimes off center to + // make sure they aren't split between pixels. + const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; + const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; + // The radius needs to be the smallest distance from the center to an edge. + const r: f64 = @min( + @min(cx, cy), + @min(float_width - cx, float_height - cy), + ); + + var ctx = canvas.getContext(); + defer ctx.deinit(); + ctx.setSource(.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + } }); + ctx.setLineWidth(float_thick); + + // These @intFromFloat casts shouldn't ever fail since r can never + // be greater than cx or cy, so when subtracting it from them the + // result can never be negative. + if (node.up) canvas.box( + @intCast(v_left), + 0, + @intCast(v_right), + @intFromFloat(@ceil(cy - r + float_thick / 2)), + .on, + ); + if (node.right) canvas.box( + @intFromFloat(@floor(cx + r - float_thick / 2)), + @intCast(h_top), + @intCast(metrics.cell_width), + @intCast(h_bottom), + .on, + ); + if (node.down) canvas.box( + @intCast(v_left), + @intFromFloat(@floor(cy + r - float_thick / 2)), + @intCast(v_right), + @intCast(metrics.cell_height), + .on, + ); + if (node.left) canvas.box( + 0, + @intCast(h_top), + @intFromFloat(@ceil(cx - r + float_thick / 2)), + @intCast(h_bottom), + .on, + ); + + if (node.filled) { + ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.fill() catch return; + } else { + ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.stroke() catch return; + } +} + +fn fadingLine( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime to: Edge, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // If we're fading to the top or left, we start with 0.0 + // and increment up as we progress, otherwise we start + // at 255.0 and increment down (negative). + var color: f64 = switch (to) { + .top, .left => 0.0, + .bottom, .right => 255.0, + }; + const inc: f64 = 255.0 / switch (to) { + .top => float_height, + .bottom => -float_height, + .left => float_width, + .right => -float_width, + }; + + switch (to) { + .top, .bottom => { + for (0..metrics.cell_height) |y| { + for (v_left..v_right) |x| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + .left, .right => { + for (0..metrics.cell_width) |x| { + for (h_top..h_bottom) |y| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + } +} diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig new file mode 100644 index 000000000..2f608180e --- /dev/null +++ b/src/font/sprite/draw/common.zig @@ -0,0 +1,244 @@ +//! This file contains a set of useful helper functions +//! and types for drawing our sprite font glyphs. These +//! are generally applicable to multiple sets of glyphs +//! rather than being single-use. + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +const log = std.log.scoped(.sprite_font); + +// Utility names for common fractions +pub const one_eighth: f64 = 0.125; +pub const one_quarter: f64 = 0.25; +pub const one_third: f64 = (1.0 / 3.0); +pub const three_eighths: f64 = 0.375; +pub const half: f64 = 0.5; +pub const five_eighths: f64 = 0.625; +pub const two_thirds: f64 = (2.0 / 3.0); +pub const three_quarters: f64 = 0.75; +pub const seven_eighths: f64 = 0.875; + +/// The thickness of a line. +pub const Thickness = enum { + super_light, + light, + heavy, + + /// Calculate the real height of a line based on its + /// thickness and a base thickness value. The base + /// thickness value is expected to be in pixels. + pub fn height(self: Thickness, base: u32) u32 { + return switch (self) { + .super_light => @max(base / 2, 1), + .light => base, + .heavy => base * 2, + }; + } +}; + +/// Shades. +pub const Shade = enum(u8) { + off = 0x00, + light = 0x40, + medium = 0x80, + dark = 0xc0, + on = 0xff, + + _, +}; + +/// Applicable to any set of glyphs with features +/// that may be present or not in each quadrant. +pub const Quads = packed struct(u4) { + tl: bool = false, + tr: bool = false, + bl: bool = false, + br: bool = false, +}; + +/// A corner of a cell. +pub const Corner = enum(u2) { + tl, + tr, + bl, + br, +}; + +/// An edge of a cell. +pub const Edge = enum(u2) { + top, + left, + bottom, + right, +}; + +/// Alignment of a figure within a cell. +pub const Alignment = struct { + horizontal: enum { + left, + right, + center, + } = .center, + + vertical: enum { + top, + bottom, + middle, + } = .middle, + + pub const upper: Alignment = .{ .vertical = .top }; + pub const lower: Alignment = .{ .vertical = .bottom }; + pub const left: Alignment = .{ .horizontal = .left }; + pub const right: Alignment = .{ .horizontal = .right }; + + pub const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; + pub const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; + pub const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; + pub const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; + + pub const center: Alignment = .{}; + + pub const upper_center = upper; + pub const lower_center = lower; + pub const middle_left = left; + pub const middle_right = right; + pub const middle_center: Alignment = center; + + pub const top = upper; + pub const bottom = lower; + pub const center_top = top; + pub const center_bottom = bottom; + + pub const top_left = upper_left; + pub const top_right = upper_right; + pub const bottom_left = lower_left; + pub const bottom_right = lower_right; +}; + +/// Fill a rect, clamped to within the cell boundaries. +/// +/// TODO: Eliminate usages of this, prefer `canvas.box`. +pub fn rect( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + x1: u32, + y1: u32, + x2: u32, + y2: u32, +) void { + canvas.box( + @intCast(@min(@max(x1, 0), metrics.cell_width)), + @intCast(@min(@max(y1, 0), metrics.cell_height)), + @intCast(@min(@max(x2, 0), metrics.cell_width)), + @intCast(@min(@max(y2, 0), metrics.cell_height)), + .on, + ); +} + +/// Centered vertical line of the provided thickness. +pub fn vlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + vline( + canvas, + 0, + @intCast(metrics.cell_height), + @intCast((metrics.cell_width -| thick_px) / 2), + thick_px, + ); +} + +/// Centered horizontal line of the provided thickness. +pub fn hlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + hline( + canvas, + 0, + @intCast(metrics.cell_width), + @intCast((metrics.cell_height -| thick_px) / 2), + thick_px, + ); +} + +/// Vertical line with the left edge at `x`, between `y1` and `y2`. +pub fn vline( + canvas: *font.sprite.Canvas, + y1: i32, + y2: i32, + x: i32, + thickness_px: u32, +) void { + canvas.box(x, y1, x + @as(i32, @intCast(thickness_px)), y2, .on); +} + +/// Horizontal line with the top edge at `y`, between `x1` and `x2`. +pub fn hline( + canvas: *font.sprite.Canvas, + x1: i32, + x2: i32, + y: i32, + thickness_px: u32, +) void { + canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on); +} + +/// xHalfs[0] should be used as the right edge of a left-aligned half. +/// xHalfs[1] should be used as the left edge of a right-aligned half. +pub fn xHalfs(metrics: font.Metrics) [2]u32 { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); + return .{ half_width, metrics.cell_width - half_width }; +} + +/// Use these values as such: +/// yThirds[0] bottom edge of the first third. +/// yThirds[1] top edge of the second third. +/// yThirds[2] bottom edge of the second third. +/// yThirds[3] top edge of the final third. +pub fn yThirds(metrics: font.Metrics) [4]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); + const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); + return .{ + one_third_height, + metrics.cell_height - two_thirds_height, + two_thirds_height, + metrics.cell_height - one_third_height, + }; +} + +/// Use these values as such: +/// yQuads[0] bottom edge of first quarter. +/// yQuads[1] top edge of second quarter. +/// yQuads[2] bottom edge of second quarter. +/// yQuads[3] top edge of third quarter. +/// yQuads[4] bottom edge of third quarter +/// yQuads[5] top edge of fourth quarter. +pub fn yQuads(metrics: font.Metrics) [6]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); + const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); + const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); + return .{ + quarter_height, + metrics.cell_height - three_quarters_height, + half_height, + metrics.cell_height - half_height, + three_quarters_height, + metrics.cell_height - quarter_height, + }; +} diff --git a/src/font/sprite/draw/geometric_shapes.zig b/src/font/sprite/draw/geometric_shapes.zig new file mode 100644 index 000000000..d95a4fd2f --- /dev/null +++ b/src/font/sprite/draw/geometric_shapes.zig @@ -0,0 +1,200 @@ +//! Geometric Shapes | U+25A0...U+25FF +//! https://en.wikipedia.org/wiki/Geometric_Shapes_(Unicode_block) +//! +//! ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ +//! ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ +//! ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● +//! ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ +//! ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯ +//! ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◸ ◹ ◺ ◻ ◼ ◽︎◾︎◿ +//! +//! Only a subset of this block is viable for sprite drawing; filling +//! out this file to have full coverage of this block is not the goal. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; + +const font = @import("../../main.zig"); + +/// ◢ ◣ ◤ ◥ +pub fn draw25E2_25E5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◢ + 0x25e2 => try cornerTriangleShade(metrics, canvas, .br, .on), + // ◣ + 0x25e3 => try cornerTriangleShade(metrics, canvas, .bl, .on), + // ◤ + 0x25e4 => try cornerTriangleShade(metrics, canvas, .tl, .on), + // ◥ + 0x25e5 => try cornerTriangleShade(metrics, canvas, .tr, .on), + + else => unreachable, + } +} + +/// ◸ ◹ ◺ +pub fn draw25F8_25FA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◸ + 0x25f8 => try cornerTriangleOutline(metrics, canvas, .tl), + // ◹ + 0x25f9 => try cornerTriangleOutline(metrics, canvas, .tr), + // ◺ + 0x25fa => try cornerTriangleOutline(metrics, canvas, .bl), + + else => unreachable, + } +} + +/// ◿ +pub fn draw25FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + try cornerTriangleOutline(metrics, canvas, .br); +} + +pub fn cornerTriangleShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime shade: Shade, +) !void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.fillPath( + path.wrapped_path, + .{}, + @enumFromInt(@intFromEnum(shade)), + ); +} + +pub fn cornerTriangleOutline( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, +) !void { + const float_thick: f64 = @floatFromInt(Thickness.light.height(metrics.box_thickness)); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, .on); +} diff --git a/src/font/sprite/octants.txt b/src/font/sprite/draw/octants.txt similarity index 100% rename from src/font/sprite/octants.txt rename to src/font/sprite/draw/octants.txt diff --git a/src/font/sprite/draw/powerline.zig b/src/font/sprite/draw/powerline.zig new file mode 100644 index 000000000..24fce454b --- /dev/null +++ b/src/font/sprite/draw/powerline.zig @@ -0,0 +1,396 @@ +//! Powerline + Powerline Extra Symbols | U+E0B0...U+E0D4 +//! https://github.com/ryanoasis/powerline-extra-symbols +//! +//!                 +//!               +//!     +//! +//! We implement the more geometric glyphs here, but not the stylized ones. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; + +const box = @import("box.zig"); + +const font = @import("../../main.zig"); +const Quad = font.sprite.Canvas.Quad; + +///  +pub fn drawE0B0( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height / 2 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = 0, .y = float_height / 2 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B8( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B9( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0BA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BB( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BC( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BD( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0B1( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + var path = canvas.staticPath(3); + path.moveTo(0, 0); + path.lineTo(float_width, float_height / 2); + path.lineTo(0, float_height); + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = @floatFromInt( + Thickness.light.height(metrics.box_thickness), + ), + }, + .on, + ); +} + +///  +pub fn drawE0B3( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B1(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +///  +pub fn drawE0B5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(4); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_width = @floatFromInt(metrics.box_thickness), + .line_cap_mode = .butt, + }, .on); +} + +///  +pub fn drawE0B6( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B4(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B7( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B5(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0D2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + const float_thick: f64 = @floatFromInt(metrics.box_thickness); + + // Top piece + { + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.lineTo(float_width, 0); + path.lineTo(float_width / 2, float_height / 2 - float_thick / 2); + path.lineTo(0, float_height / 2 - float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } + + // Bottom piece + { + var path = canvas.staticPath(6); + path.moveTo(0, float_height); + path.lineTo(float_width, float_height); + path.lineTo(float_width / 2, float_height / 2 + float_thick / 2); + path.lineTo(0, float_height / 2 + float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } +} + +///  +pub fn drawE0D4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0D2(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} diff --git a/src/font/sprite/draw/special.zig b/src/font/sprite/draw/special.zig new file mode 100644 index 000000000..3d75360e3 --- /dev/null +++ b/src/font/sprite/draw/special.zig @@ -0,0 +1,328 @@ +//! This file contains glyph drawing functions for all of the +//! non-Unicode sprite glyphs, such as cursors and underlines. +//! +//! The naming convention in this file differs from the usual +//! because the draw functions for special sprites are found by +//! having names that exactly match the enum fields in Sprite. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const font = @import("../../main.zig"); +const Sprite = font.sprite.Sprite; + +pub fn underline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_double( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // We place one underline above the underline position, and one below + // by one thickness, creating a "negative" underline where the single + // underline would be placed. + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position -| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position +| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_dotted( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this now that we can go out of bounds, just + // make sure that adjacent versions of this glyph align. + const dot_width = @max(metrics.underline_thickness, 3); + const dot_count = @max((width / dot_width) / 2, 1); + const gap_width = std.math.divCeil( + u32, + width -| (dot_count * dot_width), + dot_count, + ) catch return error.MathError; + var i: u32 = 0; + while (i < dot_count) : (i += 1) { + // Ensure we never go out of bounds for the rect + const x = @min(i * (dot_width + gap_width), width - 1); + const rect_width = @min(width - x, dot_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_dashed( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + const dash_width = width / 3 + 1; + const dash_count = (width / dash_width) + 1; + var i: u32 = 0; + while (i < dash_count) : (i += 2) { + // Ensure we never go out of bounds for the rect + const x = @min(i * dash_width, width - 1); + const rect_width = @min(width - x, dash_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_curly( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this using z2d, this is pretty cool code and all but + // it doesn't need to be highly optimized and z2d path drawing + // code would be clearer and nicer to have. + + const float_width: f64 = @floatFromInt(width); + // Because of we way we draw the undercurl, we end up making it around 1px + // thicker than it should be, to fix this we just reduce the thickness by 1. + // + // We use a minimum thickness of 0.414 because this empirically produces + // the nicest undercurls at 1px underline thickness; thinner tends to look + // too thin compared to straight underlines and has artefacting. + const float_thick: f64 = @max( + 0.414, + @as(f64, @floatFromInt(metrics.underline_thickness -| 1)), + ); + + // Calculate the wave period for a single character + // `2 * pi...` = 1 peak per character + // `4 * pi...` = 2 peaks per character + const wave_period = 2 * std.math.pi / float_width; + + // The full amplitude of the wave can be from the bottom to the + // underline position. We also calculate our mid y point of the wave + const half_amplitude = 1.0 / wave_period; + const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; + + // Offset to move the undercurl up slightly. + const y_off: u32 = @intFromFloat(half_amplitude * 0.5); + + // This is used in calculating the offset curve estimate below. + const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min( + 1.0, + half_amplitude * wave_period, + ); + + // follow Xiaolin Wu's antialias algorithm to draw the curve + var x: u32 = 0; + while (x < width) : (x += 1) { + // We sample the wave function at the *middle* of each + // pixel column, to ensure that it renders symmetrically. + const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; + // Use the slope at this location to add thickness to + // the line on this column, counteracting the thinning + // caused by the slope. + // + // This is not the exact offset curve for a sine wave, + // but it's a decent enough approximation. + // + // How did I derive this? I stared at Desmos and fiddled + // with numbers for an hour until it was good enough. + const t_u: f64 = t + std.math.pi; + const slope_factor_u: f64 = + (@sin(t_u) * @sin(t_u) * offset_factor) / + ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); + const slope_factor_l: f64 = + (@sin(t) * @sin(t) * offset_factor) / + ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); + + const cosx: f64 = @cos(t); + // This will be the center of our stroke. + const y: f64 = y_mid + half_amplitude * cosx; + + // The upper pixel and lower pixel are + // calculated relative to the center. + const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; + const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; + const y_upper: u32 = @intFromFloat(@floor(y_u)); + const y_lower: u32 = @intFromFloat(@ceil(y_l)); + const alpha_u: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_u - @floor(y_u)))), + ); + const alpha_l: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_l - @ceil(y_l)))), + ); + + // upper and lower bounds + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_upper -| y_off), + @enumFromInt(alpha_u), + ); + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_lower -| y_off), + @enumFromInt(alpha_l), + ); + + // fill between upper and lower bound + var y_fill: u32 = y_upper + 1; + while (y_fill < y_lower) : (y_fill += 1) { + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_fill -| y_off), + .on, + ); + } + } +} + +pub fn strikethrough( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.strikethrough_position), + .width = @intCast(width), + .height = @intCast(metrics.strikethrough_thickness), + }, .on); +} + +pub fn overline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.overline_position), + .width = @intCast(width), + .height = @intCast(metrics.overline_thickness), + }, .on); +} + +pub fn cursor_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); +} + +pub fn cursor_hollow_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + // We fill the entire rect and then hollow out the inside, this isn't very + // efficient but it doesn't need to be and it's the easiest way to write it. + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); + canvas.rect(.{ + .x = @intCast(metrics.cursor_thickness), + .y = @intCast(metrics.cursor_thickness), + .width = @intCast(width -| metrics.cursor_thickness * 2), + .height = @intCast(height -| metrics.cursor_thickness * 2), + }, .off); +} + +pub fn cursor_bar( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + + // We place the bar cursor half of its thickness over the left edge of the + // cell, so that it sits centered between characters, not biased to a side. + // + // We round up (add 1 before dividing by 2) because, empirically, having a + // 1px cursor shifted left a pixel looks better than having it not shifted. + canvas.rect(.{ + .x = -@as(i32, @intCast((metrics.cursor_thickness + 1) / 2)), + .y = 0, + .width = @intCast(metrics.cursor_thickness), + .height = @intCast(height), + }, .on); +} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig new file mode 100644 index 000000000..a17ddb494 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -0,0 +1,1431 @@ +//! Symbols for Legacy Computing | U+1FB00...U+1FBFF +//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing +//! +//! 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 +//! 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 +//! 🬠 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 +//! 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 🬼 🬽 🬾 🬿 +//! 🭀 🭁 🭂 🭃 🭄 🭅 🭆 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 +//! 🭐 🭑 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 🭝 🭞 🭟 +//! 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 +//! 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 🭼 🭽 🭾 🭿 +//! 🮀 🮁 🮂 🮃 🮄 🮅 🮆 🮇 🮈 🮉 🮊 🮋 🮌 🮍 🮎 🮏 +//! 🮐 🮑 🮒 🮔 🮕 🮖 🮗 🮘 🮙 🮚 🮛 🮜 🮝 🮞 🮟 +//! 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 🮯 +//! 🮰 🮱 🮲 🮳 🮴 🮵 🮶 🮷 🮸 🮹 🮺 🮻 🮼 🮽 🮾 🮿 +//! 🯀 🯁 🯂 🯃 🯄 🯅 🯆 🯇 🯈 🯉 🯊 🯋 🯌 🯍 🯎 🯏 +//! 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 +//! 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 +//! 🯰 🯱 🯲 🯳 🯴 🯵 🯶 🯷 🯸 🯹 +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Alignment = common.Alignment; +const Corner = common.Corner; +const Quads = common.Quads; +const Edge = common.Edge; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yThirds = common.yThirds; +const rect = common.rect; + +const box = @import("box.zig"); +const block = @import("block.zig"); +const geo = @import("geometric_shapes.zig"); + +const font = @import("../../main.zig"); + +// Utility names for common fractions +const one_eighth: f64 = 0.125; +const one_quarter: f64 = 0.25; +const one_third: f64 = (1.0 / 3.0); +const three_eighths: f64 = 0.375; +const half: f64 = 0.5; +const five_eighths: f64 = 0.625; +const two_thirds: f64 = (2.0 / 3.0); +const three_quarters: f64 = 0.75; +const seven_eighths: f64 = 0.875; + +const SmoothMosaic = packed struct(u10) { + tl: bool, + ul: bool, + ll: bool, + bl: bool, + bc: bool, + br: bool, + lr: bool, + ur: bool, + tr: bool, + tc: bool, + + fn from(comptime pattern: *const [15:0]u8) SmoothMosaic { + return .{ + .tl = pattern[0] == '#', + + .ul = pattern[4] == '#' and + (pattern[0] != '#' or pattern[8] != '#'), + + .ll = pattern[8] == '#' and + (pattern[4] != '#' or pattern[12] != '#'), + + .bl = pattern[12] == '#', + + .bc = pattern[13] == '#' and + (pattern[12] != '#' or pattern[14] != '#'), + + .br = pattern[14] == '#', + + .lr = pattern[10] == '#' and + (pattern[14] != '#' or pattern[6] != '#'), + + .ur = pattern[6] == '#' and + (pattern[10] != '#' or pattern[2] != '#'), + + .tr = pattern[2] == '#', + + .tc = pattern[1] == '#' and + (pattern[2] != '#' or pattern[0] != '#'), + }; + } +}; + +/// Sextants +pub fn draw1FB00_1FB3B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const Sextants = packed struct(u6) { + tl: bool, + tr: bool, + ml: bool, + mr: bool, + bl: bool, + br: bool, + }; + + assert(cp >= 0x1fb00 and cp <= 0x1fb3b); + const idx = cp - 0x1fb00; + const sex: Sextants = @bitCast(@as(u6, @intCast( + idx + (idx / 0x14) + 1, + ))); + + const x_halfs = xHalfs(metrics); + const y_thirds = yThirds(metrics); + + if (sex.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_thirds[0]); + if (sex.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_thirds[0]); + if (sex.ml) rect(metrics, canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); + if (sex.mr) rect(metrics, canvas, x_halfs[1], y_thirds[1], metrics.cell_width, y_thirds[2]); + if (sex.bl) rect(metrics, canvas, 0, y_thirds[3], x_halfs[0], metrics.cell_height); + if (sex.br) rect(metrics, canvas, x_halfs[1], y_thirds[3], metrics.cell_width, metrics.cell_height); +} + +/// Smooth Mosaics +pub fn draw1FB3C_1FB67( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + // Hand written lookup table for these shapes since I couldn't + // determine any sort of mathematical pattern in the codepoints. + const mosaic: SmoothMosaic = switch (cp) { + // '🬼' + 0x1fb3c => SmoothMosaic.from( + \\... + \\... + \\#.. + \\##. + ), + // '🬽' + 0x1fb3d => SmoothMosaic.from( + \\... + \\... + \\#\. + \\### + ), + // '🬾' + 0x1fb3e => SmoothMosaic.from( + \\... + \\#.. + \\#\. + \\##. + ), + // '🬿' + 0x1fb3f => SmoothMosaic.from( + \\... + \\#.. + \\##. + \\### + ), + // '🭀' + 0x1fb40 => SmoothMosaic.from( + \\#.. + \\#.. + \\##. + \\##. + ), + + // '🭁' + 0x1fb41 => SmoothMosaic.from( + \\/## + \\### + \\### + \\### + ), + // '🭂' + 0x1fb42 => SmoothMosaic.from( + \\./# + \\### + \\### + \\### + ), + // '🭃' + 0x1fb43 => SmoothMosaic.from( + \\.## + \\.## + \\### + \\### + ), + // '🭄' + 0x1fb44 => SmoothMosaic.from( + \\..# + \\.## + \\### + \\### + ), + // '🭅' + 0x1fb45 => SmoothMosaic.from( + \\.## + \\.## + \\.## + \\### + ), + // '🭆' + 0x1fb46 => SmoothMosaic.from( + \\... + \\./# + \\### + \\### + ), + + // '🭇' + 0x1fb47 => SmoothMosaic.from( + \\... + \\... + \\..# + \\.## + ), + // '🭈' + 0x1fb48 => SmoothMosaic.from( + \\... + \\... + \\./# + \\### + ), + // '🭉' + 0x1fb49 => SmoothMosaic.from( + \\... + \\..# + \\./# + \\.## + ), + // '🭊' + 0x1fb4a => SmoothMosaic.from( + \\... + \\..# + \\.## + \\### + ), + // '🭋' + 0x1fb4b => SmoothMosaic.from( + \\..# + \\..# + \\.## + \\.## + ), + + // '🭌' + 0x1fb4c => SmoothMosaic.from( + \\##\ + \\### + \\### + \\### + ), + // '🭍' + 0x1fb4d => SmoothMosaic.from( + \\#\. + \\### + \\### + \\### + ), + // '🭎' + 0x1fb4e => SmoothMosaic.from( + \\##. + \\##. + \\### + \\### + ), + // '🭏' + 0x1fb4f => SmoothMosaic.from( + \\#.. + \\##. + \\### + \\### + ), + // '🭐' + 0x1fb50 => SmoothMosaic.from( + \\##. + \\##. + \\##. + \\### + ), + // '🭑' + 0x1fb51 => SmoothMosaic.from( + \\... + \\#\. + \\### + \\### + ), + + // '🭒' + 0x1fb52 => SmoothMosaic.from( + \\### + \\### + \\### + \\\## + ), + // '🭓' + 0x1fb53 => SmoothMosaic.from( + \\### + \\### + \\### + \\.\# + ), + // '🭔' + 0x1fb54 => SmoothMosaic.from( + \\### + \\### + \\.## + \\.## + ), + // '🭕' + 0x1fb55 => SmoothMosaic.from( + \\### + \\### + \\.## + \\..# + ), + // '🭖' + 0x1fb56 => SmoothMosaic.from( + \\### + \\.## + \\.## + \\.## + ), + + // '🭗' + 0x1fb57 => SmoothMosaic.from( + \\##. + \\#.. + \\... + \\... + ), + // '🭘' + 0x1fb58 => SmoothMosaic.from( + \\### + \\#/. + \\... + \\... + ), + // '🭙' + 0x1fb59 => SmoothMosaic.from( + \\##. + \\#/. + \\#.. + \\... + ), + // '🭚' + 0x1fb5a => SmoothMosaic.from( + \\### + \\##. + \\#.. + \\... + ), + // '🭛' + 0x1fb5b => SmoothMosaic.from( + \\##. + \\##. + \\#.. + \\#.. + ), + + // '🭜' + 0x1fb5c => SmoothMosaic.from( + \\### + \\### + \\#/. + \\... + ), + // '🭝' + 0x1fb5d => SmoothMosaic.from( + \\### + \\### + \\### + \\##/ + ), + // '🭞' + 0x1fb5e => SmoothMosaic.from( + \\### + \\### + \\### + \\#/. + ), + // '🭟' + 0x1fb5f => SmoothMosaic.from( + \\### + \\### + \\##. + \\##. + ), + // '🭠' + 0x1fb60 => SmoothMosaic.from( + \\### + \\### + \\##. + \\#.. + ), + // '🭡' + 0x1fb61 => SmoothMosaic.from( + \\### + \\##. + \\##. + \\##. + ), + + // '🭢' + 0x1fb62 => SmoothMosaic.from( + \\.## + \\..# + \\... + \\... + ), + // '🭣' + 0x1fb63 => SmoothMosaic.from( + \\### + \\.\# + \\... + \\... + ), + // '🭤' + 0x1fb64 => SmoothMosaic.from( + \\.## + \\.\# + \\..# + \\... + ), + // '🭥' + 0x1fb65 => SmoothMosaic.from( + \\### + \\.## + \\..# + \\... + ), + // '🭦' + 0x1fb66 => SmoothMosaic.from( + \\.## + \\.## + \\..# + \\..# + ), + // '🭧' + 0x1fb67 => SmoothMosaic.from( + \\### + \\### + \\.\# + \\... + ), + else => unreachable, + }; + + const y_thirds = yThirds(metrics); + const top: f64 = 0.0; + // We average the edge positions for the y_thirds boundaries here + // rather than having to deal with varying alignments depending on + // the surrounding pieces. The most this will be off by is half of + // a pixel, so hopefully it's not noticeable. + const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); + const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); + const bottom: f64 = @floatFromInt(metrics.cell_height); + const left: f64 = 0.0; + const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(metrics.cell_width); + + var path = canvas.staticPath(12); // nodes.len = 0 + if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1 + if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2 + if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3 + if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4 + if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5 + if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6 + if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7 + if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8 + if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9 + if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10 + path.close(); // +2, nodes.len = 12 + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +pub fn draw1FB68_1FB6F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🭨' + 0x1fb68 => { + try edgeTriangle(metrics, canvas, .left); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭩' + 0x1fb69 => { + try edgeTriangle(metrics, canvas, .top); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭪' + 0x1fb6a => { + try edgeTriangle(metrics, canvas, .right); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭫' + 0x1fb6b => { + try edgeTriangle(metrics, canvas, .bottom); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭬' + 0x1fb6c => try edgeTriangle(metrics, canvas, .left), + // '🭭' + 0x1fb6d => try edgeTriangle(metrics, canvas, .top), + // '🭮' + 0x1fb6e => try edgeTriangle(metrics, canvas, .right), + // '🭯' + 0x1fb6f => try edgeTriangle(metrics, canvas, .bottom), + + else => unreachable, + } +} + +/// Vertical one eighth blocks +pub fn draw1FB70_1FB75( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const n = cp + 1 - 0x1fb70; + + const x: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + const w: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + rect(metrics, canvas, x, 0, x + w, metrics.cell_height); +} + +/// Horizontal one eighth blocks +pub fn draw1FB76_1FB7B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const n = cp + 1 - 0x1fb76; + + const h = @as( + u32, + @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + ); + const y = @min( + metrics.cell_height -| h, + @as( + u32, + @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * + @as(f64, @floatFromInt(metrics.cell_height)) / 8), + ), + ), + ); + rect(metrics, canvas, 0, y, metrics.cell_width, y + h); +} + +pub fn draw1FB7C_1FB97( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + switch (cp) { + + // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK + 0x1fb7c => { + block.block(metrics, canvas, .left, one_eighth, 1); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK + 0x1fb7d => { + block.block(metrics, canvas, .left, one_eighth, 1); + block.block(metrics, canvas, .upper, 1, one_eighth); + }, + // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK + 0x1fb7e => { + block.block(metrics, canvas, .right, one_eighth, 1); + block.block(metrics, canvas, .upper, 1, one_eighth); + }, + // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK + 0x1fb7f => { + block.block(metrics, canvas, .right, one_eighth, 1); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK + 0x1fb80 => { + block.block(metrics, canvas, .upper, 1, one_eighth); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🮁' Horizontal One Eighth Block 1358 + 0x1fb81 => { + // We just call the draw function for each of the relevant codepoints. + // The first codepoint is actually a lie, it's before the range, but + // we need it to get the first (0th) block position. This might be a + // bit brittle, oh well, if it breaks we can fix it. + try draw1FB76_1FB7B(0x1fb74 + 1, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 3, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 5, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 8, canvas, width, height, metrics); + }, + + // '🮂' UPPER ONE QUARTER BLOCK + 0x1fb82 => block.block(metrics, canvas, .upper, 1, one_quarter), + // '🮃' UPPER THREE EIGHTHS BLOCK + 0x1fb83 => block.block(metrics, canvas, .upper, 1, three_eighths), + // '🮄' UPPER FIVE EIGHTHS BLOCK + 0x1fb84 => block.block(metrics, canvas, .upper, 1, five_eighths), + // '🮅' UPPER THREE QUARTERS BLOCK + 0x1fb85 => block.block(metrics, canvas, .upper, 1, three_quarters), + // '🮆' UPPER SEVEN EIGHTHS BLOCK + 0x1fb86 => block.block(metrics, canvas, .upper, 1, seven_eighths), + + // '🮇' RIGHT ONE QUARTER BLOCK + 0x1fb87 => block.block(metrics, canvas, .right, one_quarter, 1), + // '🮈' RIGHT THREE EIGHTHS BLOCK + 0x1fb88 => block.block(metrics, canvas, .right, three_eighths, 1), + // '🮉' RIGHT FIVE EIGHTHS BLOCK + 0x1fb89 => block.block(metrics, canvas, .right, five_eighths, 1), + // '🮊' RIGHT THREE QUARTERS BLOCK + 0x1fb8a => block.block(metrics, canvas, .right, three_quarters, 1), + // '🮋' RIGHT SEVEN EIGHTHS BLOCK/ + 0x1fb8b => block.block(metrics, canvas, .right, seven_eighths, 1), + + // '🮌' + 0x1fb8c => block.blockShade(metrics, canvas, .left, half, 1, .medium), + // '🮍' + 0x1fb8d => block.blockShade(metrics, canvas, .right, half, 1, .medium), + // '🮎' + 0x1fb8e => block.blockShade(metrics, canvas, .upper, 1, half, .medium), + // '🮏' + 0x1fb8f => block.blockShade(metrics, canvas, .lower, 1, half, .medium), + + // '🮐' + 0x1fb90 => block.fullBlockShade(metrics, canvas, .medium), + // '🮑' + 0x1fb91 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .upper, 1, half); + }, + // '🮒' + 0x1fb92 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .lower, 1, half); + }, + 0x1fb93 => { + // NOTE: This codepoint is currently un-allocated, it's a hole + // in the unicode block, so it's safe to just render it + // as an empty glyph, probably. + }, + // '🮔' + 0x1fb94 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .right, half, 1); + }, + // '🮕' + 0x1fb95 => checkerboardFill(metrics, canvas, 0), + // '🮖' + 0x1fb96 => checkerboardFill(metrics, canvas, 1), + // '🮗' + 0x1fb97 => { + canvas.box( + 0, + @intCast(height / 4), + @intCast(width), + @intCast(2 * height / 4), + .on, + ); + canvas.box( + 0, + @intCast(3 * height / 4), + @intCast(width), + @intCast(height), + .on, + ); + }, + + else => unreachable, + } +} + +/// Upper Left to Lower Right Fill +/// 🮘 +pub fn draw1FB98( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + // TODO: This doesn't align properly for most cell sizes, fix that. + + const thick_px = Thickness.light.height(metrics.box_thickness); + const line_count = metrics.cell_width / (2 * thick_px); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); + + for (0..line_count * 2 + 1) |_i| { + const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); + const top_x = @as(f64, @floatFromInt(i)) * stride; + const bottom_x = float_width + top_x; + canvas.line(.{ + .p0 = .{ .x = top_x, .y = 0 }, + .p1 = .{ .x = bottom_x, .y = float_height }, + }, float_thick, .on) catch {}; + } +} + +/// Upper Right to Lower Left Fill +/// 🮙 +pub fn draw1FB99( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + // TODO: This doesn't align properly for most cell sizes, fix that. + + const thick_px = Thickness.light.height(metrics.box_thickness); + const line_count = metrics.cell_width / (2 * thick_px); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); + + for (0..line_count * 2 + 1) |_i| { + const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); + const bottom_x = @as(f64, @floatFromInt(i)) * stride; + const top_x = float_width + bottom_x; + canvas.line(.{ + .p0 = .{ .x = top_x, .y = 0 }, + .p1 = .{ .x = bottom_x, .y = float_height }, + }, float_thick, .on) catch {}; + } +} + +pub fn draw1FB9A_1FB9F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🮚' + 0x1fb9a => { + try edgeTriangle(metrics, canvas, .top); + try edgeTriangle(metrics, canvas, .bottom); + }, + // '🮛' + 0x1fb9b => { + try edgeTriangle(metrics, canvas, .left); + try edgeTriangle(metrics, canvas, .right); + }, + // '🮜' + 0x1fb9c => try geo.cornerTriangleShade(metrics, canvas, .tl, .medium), + // '🮝' + 0x1fb9d => try geo.cornerTriangleShade(metrics, canvas, .tr, .medium), + // '🮞' + 0x1fb9e => try geo.cornerTriangleShade(metrics, canvas, .br, .medium), + // '🮟' + 0x1fb9f => try geo.cornerTriangleShade(metrics, canvas, .bl, .medium), + + else => unreachable, + } +} + +pub fn draw1FBA0_1FBAE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🮠' + 0x1fba0 => cornerDiagonalLines(metrics, canvas, .{ .tl = true }), + // '🮡' + 0x1fba1 => cornerDiagonalLines(metrics, canvas, .{ .tr = true }), + // '🮢' + 0x1fba2 => cornerDiagonalLines(metrics, canvas, .{ .bl = true }), + // '🮣' + 0x1fba3 => cornerDiagonalLines(metrics, canvas, .{ .br = true }), + // '🮤' + 0x1fba4 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true }), + // '🮥' + 0x1fba5 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .br = true }), + // '🮦' + 0x1fba6 => cornerDiagonalLines(metrics, canvas, .{ .bl = true, .br = true }), + // '🮧' + 0x1fba7 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true }), + // '🮨' + 0x1fba8 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .br = true }), + // '🮩' + 0x1fba9 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true }), + // '🮪' + 0x1fbaa => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true, .br = true }), + // '🮫' + 0x1fbab => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true, .br = true }), + // '🮬' + 0x1fbac => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .br = true }), + // '🮭' + 0x1fbad => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }), + // '🮮' + 0x1fbae => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }), + + else => unreachable, + } +} + +/// 🮯 +pub fn draw1FBAF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + box.linesChar(metrics, canvas, .{ + .up = .heavy, + .down = .heavy, + .left = .light, + .right = .light, + }); +} + +/// 🮽 +pub fn draw1FBBD( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + box.lightDiagonalCross(metrics, canvas); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🮾 +pub fn draw1FBBE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + cornerDiagonalLines(metrics, canvas, .{ .br = true }); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🮿 +pub fn draw1FBBF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + cornerDiagonalLines(metrics, canvas, .{ + .tl = true, + .tr = true, + .bl = true, + .br = true, + }); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🯎 +pub fn draw1FBCE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + block.block(metrics, canvas, .left, two_thirds, 1); +} + +// 🯏 +pub fn draw1FBCF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + block.block(metrics, canvas, .left, one_third, 1); +} + +/// Cell diagonals. +pub fn draw1FBD0_1FBDF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🯐' + 0x1fbd0 => cellDiagonal( + metrics, + canvas, + .middle_right, + .lower_left, + ), + // '🯑' + 0x1fbd1 => cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_left, + ), + // '🯒' + 0x1fbd2 => cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_right, + ), + // '🯓' + 0x1fbd3 => cellDiagonal( + metrics, + canvas, + .middle_left, + .lower_right, + ), + // '🯔' + 0x1fbd4 => cellDiagonal( + metrics, + canvas, + .upper_left, + .lower_center, + ), + // '🯕' + 0x1fbd5 => cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_right, + ), + // '🯖' + 0x1fbd6 => cellDiagonal( + metrics, + canvas, + .upper_right, + .lower_center, + ), + // '🯗' + 0x1fbd7 => cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_left, + ), + // '🯘' + 0x1fbd8 => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .upper_right, + ); + }, + // '🯙' + 0x1fbd9 => { + cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_right, + ); + }, + // '🯚' + 0x1fbda => { + cellDiagonal( + metrics, + canvas, + .lower_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_right, + ); + }, + // '🯛' + 0x1fbdb => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_left, + ); + }, + // '🯜' + 0x1fbdc => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .lower_center, + ); + cellDiagonal( + metrics, + canvas, + .lower_center, + .upper_right, + ); + }, + // '🯝' + 0x1fbdd => { + cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_left, + ); + cellDiagonal( + metrics, + canvas, + .middle_left, + .lower_right, + ); + }, + // '🯞' + 0x1fbde => { + cellDiagonal( + metrics, + canvas, + .lower_left, + .upper_center, + ); + cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_right, + ); + }, + // '🯟' + 0x1fbdf => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_right, + ); + cellDiagonal( + metrics, + canvas, + .middle_right, + .lower_left, + ); + }, + + else => unreachable, + } +} + +pub fn draw1FBE0_1FBEF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🯠' + 0x1fbe0 => circle(metrics, canvas, .top, false), + // '🯡' + 0x1fbe1 => circle(metrics, canvas, .right, false), + // '🯢' + 0x1fbe2 => circle(metrics, canvas, .bottom, false), + // '🯣' + 0x1fbe3 => circle(metrics, canvas, .left, false), + // '🯤' + 0x1fbe4 => block.block(metrics, canvas, .upper_center, 0.5, 0.5), + // '🯥' + 0x1fbe5 => block.block(metrics, canvas, .lower_center, 0.5, 0.5), + // '🯦' + 0x1fbe6 => block.block(metrics, canvas, .middle_left, 0.5, 0.5), + // '🯧' + 0x1fbe7 => block.block(metrics, canvas, .middle_right, 0.5, 0.5), + // '🯨' + 0x1fbe8 => circle(metrics, canvas, .top, true), + // '🯩' + 0x1fbe9 => circle(metrics, canvas, .right, true), + // '🯪' + 0x1fbea => circle(metrics, canvas, .bottom, true), + // '🯫' + 0x1fbeb => circle(metrics, canvas, .left, true), + // '🯬' + 0x1fbec => circle(metrics, canvas, .top_right, true), + // '🯭' + 0x1fbed => circle(metrics, canvas, .bottom_left, true), + // '🯮' + 0x1fbee => circle(metrics, canvas, .bottom_right, true), + // '🯯' + 0x1fbef => circle(metrics, canvas, .top_left, true), + + else => unreachable, + } +} + +fn edgeTriangle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime edge: Edge, +) !void { + const upper: f64 = 0.0; + const middle: f64 = @round(@as(f64, @floatFromInt(metrics.cell_height)) / 2); + const lower: f64 = @floatFromInt(metrics.cell_height); + const left: f64 = 0.0; + const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(metrics.cell_width); + + const x0, const y0, const x1, const y1 = switch (edge) { + .top => .{ right, upper, left, upper }, + .left => .{ left, upper, left, lower }, + .bottom => .{ left, lower, right, lower }, + .right => .{ right, lower, right, upper }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(center, middle); // +1, nodes.len = 1 + path.lineTo(x0, y0); // +1, nodes.len = 2 + path.lineTo(x1, y1); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +fn cornerDiagonalLines( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corners: Quads, +) void { + const thick_px = Thickness.light.height(metrics.box_thickness); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const center_x: f64 = @floatFromInt(metrics.cell_width / 2 + metrics.cell_width % 2); + const center_y: f64 = @floatFromInt(metrics.cell_height / 2 + metrics.cell_height % 2); + + if (corners.tl) canvas.line(.{ + .p0 = .{ .x = center_x, .y = 0 }, + .p1 = .{ .x = 0, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.tr) canvas.line(.{ + .p0 = .{ .x = center_x, .y = 0 }, + .p1 = .{ .x = float_width, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.bl) canvas.line(.{ + .p0 = .{ .x = center_x, .y = float_height }, + .p1 = .{ .x = 0, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.br) canvas.line(.{ + .p0 = .{ .x = center_x, .y = float_height }, + .p1 = .{ .x = float_width, .y = center_y }, + }, float_thick, .on) catch {}; +} + +fn cellDiagonal( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime from: Alignment, + comptime to: Alignment, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0: f64 = switch (from.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y0: f64 = switch (from.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + const x1: f64 = switch (to.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y1: f64 = switch (to.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + + canvas.line( + .{ + .p0 = .{ .x = x0, .y = y0 }, + .p1 = .{ .x = x1, .y = y1 }, + }, + @floatFromInt(Thickness.light.height(metrics.box_thickness)), + .on, + ) catch {}; +} + +fn checkerboardFill( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + parity: u1, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const x_size: usize = 4; + const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width))); + for (0..x_size) |x| { + const x0 = (metrics.cell_width * x) / x_size; + const x1 = (metrics.cell_width * (x + 1)) / x_size; + for (0..y_size) |y| { + const y0 = (metrics.cell_height * y) / y_size; + const y1 = (metrics.cell_height * (y + 1)) / y_size; + if ((x + y) % 2 == parity) { + canvas.rect(.{ + .x = @intCast(x0), + .y = @intCast(y0), + .width = @intCast(x1 -| x0), + .height = @intCast(y1 -| y0), + }, .on); + } + } + } +} + +fn circle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime position: Alignment, + comptime filled: bool, +) void { + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x: f64 = switch (position.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y: f64 = switch (position.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + const r: f64 = 0.5 * @min(float_width, float_height); + + var ctx = canvas.getContext(); + defer ctx.deinit(); + ctx.setSource(.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + } }); + ctx.setLineWidth( + @floatFromInt(Thickness.light.height(metrics.box_thickness)), + ); + + if (filled) { + ctx.arc(x, y, r, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.fill() catch return; + } else { + ctx.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.stroke() catch return; + } +} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig new file mode 100644 index 000000000..0a57a0439 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -0,0 +1,193 @@ +//! Symbols for Legacy Computing Supplement | U+1CC00...U+1CEBF +//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement +//! +//! 𜰀 𜰁 𜰂 𜰃 𜰄 𜰅 𜰆 𜰇 𜰈 𜰉 𜰊 𜰋 𜰌 𜰍 𜰎 𜰏 +//! 𜰐 𜰑 𜰒 𜰓 𜰔 𜰕 𜰖 𜰗 𜰘 𜰙 𜰚 𜰛 𜰜 𜰝 𜰞 𜰟 +//! 𜰠 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +//! 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿 +//! 𜱀 𜱁 𜱂 𜱃 𜱄 𜱅 𜱆 𜱇 𜱈 𜱉 𜱊 𜱋 𜱌 𜱍 𜱎 𜱏 +//! 𜱐 𜱑 𜱒 𜱓 𜱔 𜱕 𜱖 𜱗 𜱘 𜱙 𜱚 𜱛 𜱜 𜱝 𜱞 𜱟 +//! 𜱠 𜱡 𜱢 𜱣 𜱤 𜱥 𜱦 𜱧 𜱨 𜱩 𜱪 𜱫 𜱬 𜱭 𜱮 𜱯 +//! 𜱰 𜱱 𜱲 𜱳 𜱴 𜱵 𜱶 𜱷 𜱸 𜱹 𜱺 𜱻 𜱼 𜱽 𜱾 𜱿 +//! 𜲀 𜲁 𜲂 𜲃 𜲄 𜲅 𜲆 𜲇 𜲈 𜲉 𜲊 𜲋 𜲌 𜲍 𜲎 𜲏 +//! 𜲐 𜲑 𜲒 𜲓 𜲔 𜲕 𜲖 𜲗 𜲘 𜲙 𜲚 𜲛 𜲜 𜲝 𜲞 𜲟 +//! 𜲠 𜲡 𜲢 𜲣 𜲤 𜲥 𜲦 𜲧 𜲨 𜲩 𜲪 𜲫 𜲬 𜲭 𜲮 𜲯 +//! 𜲰 𜲱 𜲲 𜲳 𜲴 𜲵 𜲶 𜲷 𜲸 𜲹 𜲺 𜲻 𜲼 𜲽 𜲾 𜲿 +//! 𜳀 𜳁 𜳂 𜳃 𜳄 𜳅 𜳆 𜳇 𜳈 𜳉 𜳊 𜳋 𜳌 𜳍 𜳎 𜳏 +//! 𜳐 𜳑 𜳒 𜳓 𜳔 𜳕 𜳖 𜳗 𜳘 𜳙 𜳚 𜳛 𜳜 𜳝 𜳞 𜳟 +//! 𜳠 𜳡 𜳢 𜳣 𜳤 𜳥 𜳦 𜳧 𜳨 𜳩 𜳪 𜳫 𜳬 𜳭 𜳮 𜳯 +//! 𜳰 𜳱 𜳲 𜳳 𜳴 𜳵 𜳶 𜳷 𜳸 𜳹 +//! 𜴀 𜴁 𜴂 𜴃 𜴄 𜴅 𜴆 𜴇 𜴈 𜴉 𜴊 𜴋 𜴌 𜴍 𜴎 𜴏 +//! 𜴐 𜴑 𜴒 𜴓 𜴔 𜴕 𜴖 𜴗 𜴘 𜴙 𜴚 𜴛 𜴜 𜴝 𜴞 𜴟 +//! 𜴠 𜴡 𜴢 𜴣 𜴤 𜴥 𜴦 𜴧 𜴨 𜴩 𜴪 𜴫 𜴬 𜴭 𜴮 𜴯 +//! 𜴰 𜴱 𜴲 𜴳 𜴴 𜴵 𜴶 𜴷 𜴸 𜴹 𜴺 𜴻 𜴼 𜴽 𜴾 𜴿 +//! 𜵀 𜵁 𜵂 𜵃 𜵄 𜵅 𜵆 𜵇 𜵈 𜵉 𜵊 𜵋 𜵌 𜵍 𜵎 𜵏 +//! 𜵐 𜵑 𜵒 𜵓 𜵔 𜵕 𜵖 𜵗 𜵘 𜵙 𜵚 𜵛 𜵜 𜵝 𜵞 𜵟 +//! 𜵠 𜵡 𜵢 𜵣 𜵤 𜵥 𜵦 𜵧 𜵨 𜵩 𜵪 𜵫 𜵬 𜵭 𜵮 𜵯 +//! 𜵰 𜵱 𜵲 𜵳 𜵴 𜵵 𜵶 𜵷 𜵸 𜵹 𜵺 𜵻 𜵼 𜵽 𜵾 𜵿 +//! 𜶀 𜶁 𜶂 𜶃 𜶄 𜶅 𜶆 𜶇 𜶈 𜶉 𜶊 𜶋 𜶌 𜶍 𜶎 𜶏 +//! 𜶐 𜶑 𜶒 𜶓 𜶔 𜶕 𜶖 𜶗 𜶘 𜶙 𜶚 𜶛 𜶜 𜶝 𜶞 𜶟 +//! 𜶠 𜶡 𜶢 𜶣 𜶤 𜶥 𜶦 𜶧 𜶨 𜶩 𜶪 𜶫 𜶬 𜶭 𜶮 𜶯 +//! 𜶰 𜶱 𜶲 𜶳 𜶴 𜶵 𜶶 𜶷 𜶸 𜶹 𜶺 𜶻 𜶼 𜶽 𜶾 𜶿 +//! 𜷀 𜷁 𜷂 𜷃 𜷄 𜷅 𜷆 𜷇 𜷈 𜷉 𜷊 𜷋 𜷌 𜷍 𜷎 𜷏 +//! 𜷐 𜷑 𜷒 𜷓 𜷔 𜷕 𜷖 𜷗 𜷘 𜷙 𜷚 𜷛 𜷜 𜷝 𜷞 𜷟 +//! 𜷠 𜷡 𜷢 𜷣 𜷤 𜷥 𜷦 𜷧 𜷨 𜷩 𜷪 𜷫 𜷬 𜷭 𜷮 𜷯 +//! 𜷰 𜷱 𜷲 𜷳 𜷴 𜷵 𜷶 𜷷 𜷸 𜷹 𜷺 𜷻 𜷼 𜷽 𜷾 𜷿 +//! 𜸀 𜸁 𜸂 𜸃 𜸄 𜸅 𜸆 𜸇 𜸈 𜸉 𜸊 𜸋 𜸌 𜸍 𜸎 𜸏 +//! 𜸐 𜸑 𜸒 𜸓 𜸔 𜸕 𜸖 𜸗 𜸘 𜸙 𜸚 𜸛 𜸜 𜸝 𜸞 𜸟 +//! 𜸠 𜸡 𜸢 𜸣 𜸤 𜸥 𜸦 𜸧 𜸨 𜸩 𜸪 𜸫 𜸬 𜸭 𜸮 𜸯 +//! 𜸰 𜸱 𜸲 𜸳 𜸴 𜸵 𜸶 𜸷 𜸸 𜸹 𜸺 𜸻 𜸼 𜸽 𜸾 𜸿 +//! 𜹀 𜹁 𜹂 𜹃 𜹄 𜹅 𜹆 𜹇 𜹈 𜹉 𜹊 𜹋 𜹌 𜹍 𜹎 𜹏 +//! 𜹐 𜹑 𜹒 𜹓 𜹔 𜹕 𜹖 𜹗 𜹘 𜹙 𜹚 𜹛 𜹜 𜹝 𜹞 𜹟 +//! 𜹠 𜹡 𜹢 𜹣 𜹤 𜹥 𜹦 𜹧 𜹨 𜹩 𜹪 𜹫 𜹬 𜹭 𜹮 𜹯 +//! 𜹰 𜹱 𜹲 𜹳 𜹴 𜹵 𜹶 𜹷 𜹸 𜹹 𜹺 𜹻 𜹼 𜹽 𜹾 𜹿 +//! 𜺀 𜺁 𜺂 𜺃 𜺄 𜺅 𜺆 𜺇 𜺈 𜺉 𜺊 𜺋 𜺌 𜺍 𜺎 𜺏 +//! 𜺐 𜺑 𜺒 𜺓 𜺔 𜺕 𜺖 𜺗 𜺘 𜺙 𜺚 𜺛 𜺜 𜺝 𜺞 𜺟 +//! 𜺠 𜺡 𜺢 𜺣 𜺤 𜺥 𜺦 𜺧 𜺨 𜺩 𜺪 𜺫 𜺬 𜺭 𜺮 𜺯 +//! 𜺰 𜺱 𜺲 𜺳 +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yQuads = common.yQuads; +const rect = common.rect; + +const font = @import("../../main.zig"); + +const octant_min = 0x1cd00; +const octant_max = 0x1cde5; + +/// Octants +pub fn draw1CD00_1CDE5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + // Octant representation. We use the funny numeric string keys + // so its easier to parse the actual name used in the Symbols for + // Legacy Computing spec. + const Octant = packed struct(u8) { + @"1": bool = false, + @"2": bool = false, + @"3": bool = false, + @"4": bool = false, + @"5": bool = false, + @"6": bool = false, + @"7": bool = false, + @"8": bool = false, + }; + + // Parse the octant data. This is all done at comptime so + // that this is static data that is embedded in the binary. + const octants_len = octant_max - octant_min + 1; + const octants: [octants_len]Octant = comptime octants: { + @setEvalBranchQuota(10_000); + + var result: [octants_len]Octant = @splat(.{}); + var i: usize = 0; + + const data = @embedFile("octants.txt"); + var it = std.mem.splitScalar(u8, data, '\n'); + while (it.next()) |line| { + // Skip comments + if (line.len == 0 or line[0] == '#') continue; + + const current = &result[i]; + i += 1; + + // Octants are in the format "BLOCK OCTANT-1235". The numbers + // at the end are keys into our packed struct. Since we're + // at comptime we can metaprogram it all. + const idx = std.mem.indexOfScalar(u8, line, '-').?; + for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; + } + + assert(i == octants_len); + break :octants result; + }; + + const x_halfs = xHalfs(metrics); + const y_quads = yQuads(metrics); + const oct = octants[cp - octant_min]; + if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]); + if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]); + if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); + if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]); + if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); + if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]); + if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height); + if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height); +} + +// Separated Block Quadrants +// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +pub fn draw1CC21_1CC2F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + // Struct laid out to match the codepoint order so we can cast from it. + const Quads = packed struct(u4) { + tl: bool, + tr: bool, + bl: bool, + br: bool, + }; + + const quad: Quads = @bitCast(@as(u4, @truncate(cp - 0x1CC20))); + + const gap: i32 = @intCast(@max(1, width / 12)); + + const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2)); + const mid_gap_y: i32 = gap * 2 + @as(i32, @intCast(height % 2)); + + const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2); + const h: i32 = @divExact(@as(i32, @intCast(height)) - gap * 2 - mid_gap_y, 2); + + if (quad.tl) canvas.box( + gap, + gap, + gap + w, + gap + h, + .on, + ); + if (quad.tr) canvas.box( + gap + w + mid_gap_x, + gap, + gap + w + mid_gap_x + w, + gap + h, + .on, + ); + if (quad.bl) canvas.box( + gap, + gap + h + mid_gap_y, + gap + w, + gap + h + mid_gap_y + h, + .on, + ); + if (quad.br) canvas.box( + gap + w + mid_gap_x, + gap + h + mid_gap_y, + gap + w + mid_gap_x + w, + gap + h + mid_gap_y + h, + .on, + ); +} diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm deleted file mode 100644 index 6082475af7d3e2264cf04061e9f63d7c6e6fdc9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048593 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

L8;Oac5oI4fOy(zgfHN>KeESBIh12o>f5O=y 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd28ba0d428b099b2f709b64e4458d7770942d6 GIT binary patch literal 1022 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfr0s`r;B4q#hf<>3>g_1 z7!ED?>YvN4uK`kkgm*|XFfjc8|K_kEF9QQZ!-9Wnbsus{Ga;#9U|=xdf~aZaVPIf5 zpiuu>IcC8&7F0D4Six$pIC3=yFdPYZU;nZ!@P*%s5H?Qz2gla8D6Zf>P`Jk~rs_%c zgD?X_M)ap6Z?69m z?p%L3fu;Yrp^Oq&t7B^rSL?y^T#O&qb2Bjf|NrK&Auj_1L&Ji9YyX~QkY<9I1S1O= zA!-;o!D{}mm!8ywri6zTP04RTE|8aCMldiid=O?pQ*&ETLjy?x0|UbWcd(kbhZ}i7 kJcat#+A<5au`nuK5HopJq&DcFp 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 Iol`;+0H&lvzW@LL literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..ecbb650a1c0b08f8038ff65ecd1a70d91f5b8e37 GIT binary patch literal 3404 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq_@p)5S5QV$Pe3hJA+( zcvu7P|G%_5R7<%<^O6$NUQOjBf&V-8K*|~FChX_mE}y5*dGhblqth%`-q`p5b=+-X z79kIX##y;cy0?tfMQ1y7Z#(08RPDkd-Qu8)oLe$Z&k4zaNZsD%z~7erbHUWsxvum&Fo~9|jcR16Vr*p5dJ(miiSr8+(_GOPGF9!jzZMC6PUaRdvft*Qh?~J+ z_KBZ?;s5_Pw+?b1HV|;R_;0lzmjILelB56juv_LP*vwn~7wmix@xWBBMt{$rr`snK zYpaK^m^N8E-g!2)s=7~grx3^sL^BNwxy0Mq~fubwc@bjUYe ztrR*Tn0Lq8>mQg`B!AX_exCoyo~gm*0nvVU9dj*9*|$EuB70>ThJdkWO~SwWL$Qk= zd1*2mR~~X;m*{9b<}dMrf!XlF4^C#;1O+xJ`@{vzJVzWJtFOx6ne^`5+#IG;_ay2I zwS)t{9sAK8JhNew`LXypr&y-!HT?G|L~X&F-jCwTjTlq&d-hvQ<`^5T5~5?@D~Z45dII3iX85bJ zapr@L)|3}-di)}qjSdQOpS*BWa_$DU)Fx%INeed{TP5bBsuvFS z+D|@xOj_`N?;ePwE*|7;aS&j=_-FAo2A3taq5SV-Pp4M>l)ul`7tAw7pRG%r`Dl0J zqoWQVlNMBH-dnQ7FMwyu4i*d&6738hUR6o_N&C=Q{%>#b9r;;%iXnOU;znK-jX4}5ek@$-Os$ju>v@Q9Rxl}DND}(M;xk)>@9YpS#sAiy zcDlL${QA8WN6!6CZah@Z!gXIzqsD!~2U%p!+*A7lHrDU^Ze+US|J^EmCQ*GR&Jvrg zLLLh)IySOc&5Pnxx$t6v1Jhqn)$#xTn~NJc859Iq4*uJ{jp?FG#Lt;u^lo2vX>=%L zI?5@c9xw;SxZ^tg@6k_nf*SicMB-Vx^qG!|H$Lii_~^RehtdUgZipj&wnB2%OhdlI z20Tapfy&B8@x%LGaB#gi(m3g+-`B@)&YYY7@O=80JD(o#EvhVFzi6|gIYaJ_qY3{z zg<0(70%x18JDwesb@?naZ|hGTPtNQ9Rnv|fb71S@;!RobM&g7+;KD|pDYDFx8d^Q9 zUbb^mK68Kn&n3hxI+;=1gD1p?ZHg~QwLf(=oU-qudLerxyC%q;wTVRm8>CzHS4WHR-OftE+E~{z# z&T2XYk_wA0NqSh&eDdCn_wF8Q^ZyJ_WYwb>8NScA=d%?{^sEz6sg({@|L z=dQ_3p6!}5c4cmo%nLRsy_{lvXIa9lWhc{i_#V9CJNazE?534x!k0Z@aa)iWT)?D! zVS}bcqhQ7jkvRuAOmZMm_#wOo;+~m-e9Zx$u5-w(zPC2lZnot$BL>wdfLsEdh>P zw;Gy6N}^drwLBc9112=(buTzn%zAWLnVQ7Um2)}W>ax2Y zM=qv79_9UT4g=?resl2Kxw*>wX+}Bs&_tV=_foLug*RBhR0HK#V+HSa~0JgVZz{I zzh`5g&4pc`pCA6V)#>`XIoc&}&+SAo?%Vx>6l1p>xf%?3STBOgRt1sPW&c{tZ#SwX z#@s4Ax1)L4S;_JXTPz>*%`!{>tbfM!e4Rz;>k}*l<1-#~vIJ~)$^9Z=*wR<1m zk;pY>KlZTqiP`W7pMn%B8{YSuUz^AtWjUSy_EuN}D?k@p`>TDy?9+?y*R8OszFEX! zHla~2h9_k~D4WrR%lD;DIIv0WJ#*#*w2WOSc$fi{j+f`LYiM+TE%py_N&j!||4@N_ z?`4*tZ^~1A6;kb;ZW6(?vo}x`_DGN^xxpeN!7gjo$a2iF-B+Qp@IoRpG-iyU>0stT z&Z7Z5hriwb1A(>;M1xclKeBEmzZb z4UT(@6)O6h9w0dUQ335bbKm+{Ii+2W3UTC$Qs4@@;3TqS!SQ12bkQXWti3|4ni-1o z#qQ4gqBisDUBw2$6Dms=s*ytddcqE=h7WQ!K;mXwQ-_XI+Kun)lsA3oj}(0Ik5jC% zYkGsQ4wI-pqk063RveSx29A(DEOQbnd7%x{11Fzs{QBo*yZhaf`R2kJa~vMB{JIZ~ z0BtT7Mgix8zwcLF-KBV@-_yl5u(EcXn9z`Gz66lm)`yllJ<10+&TN}{PW{tue_zEh5p|W5hf!W^#Qdro;T(M*XJrIBwbVIkRAj34Gzu&{x5p1;&-da#%dG1P&h1HmRn(HEe|^(( zkH&yG0z8sY)q+YUP8z{_+E-Ew;_u!t+8I|asB|R~r0M+AAKXQEE3V92d0KGR-Wacp z?HlE~gqswd7o6IurRbzDBqekyd1fg`3rDX269a?AV}1sP|Nq}ibL2a$z{B$Y|Eq6T zyiOS2GwIBI(R1an5|8yxk-A#L^;Qqhil4btee0xTi@+%{qn8#u>AxLab{MR)z1bz; z)ajHI+;Yl+W8qTCGhTmmuq~jLwM8Jxih+S)O7WlbDPjA$*YoqH3pi;+ zpO6=lY&-hl^qqh!{2YoQdS5QMMt+a^Uv!tr>F(isPSP!Gq5{8+6&V&w9&QlK^5}fAve5Wj9= zi3<|W3DM_0{ze>0ULKV4aQ;k>)Q7u%)~(xr)^FeQ+_S9<7jY8NBcq_JQ z+FZq=b7r-Cey{#=D3&;7Ji99H9Ja7+ULUj5fBld=L7s>9jwcuw$~ZAFFf2C^K54jq z%6X6J&K`U3>3GM#t(rwQ?AYrLsgS@X$9`sBv-Q_`25R$LONr=QZ^yZHQi zE9aI%3B?wZR;8zloL0)`RsSkD5r64@!-Za_`(M}{7ceN* literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..58afe3de7ab7579b86fd68a5e0dfff03db55dd65 GIT binary patch literal 5450 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..9be9501ba18ef61d9715e67027bf5b9ef23e861e GIT binary patch literal 4298 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 Yeu2<7t$)kg`$4J5)78&qol`;+08(TlvH$=8 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png b/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9a9d9053b66f4852f4c3d176f42e1cead1a9cb GIT binary patch literal 2638 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 L>gTe~DWM4fXbX?> literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png b/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..5485a4927561ee9e486b17bedf3f5f8d33b16086 GIT binary patch literal 4541 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

kHKf7th$X` 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 B@)ZC8 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png b/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..012a5b952848f82e54e56fbb0568b14e15cbd914 GIT binary patch literal 2490 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq}Ep)5S5QV$PduhFlE> zJggV){l8RLEO^lC;FsU`G?qP7_dW(v{6K%zDzkMv{}k2j(0>>6`N7n^SyueFn`VJ= z_1Pu{1~uO~>c46(sOtqp&EMgWS^J1-=~qUEi05ZnFaLbyd0JRpL)SlM!4{js#!H_V z7~VW$U|{(F|IM9^oD2pWEC>E>KgZ>$bMHA{5L=-1>hB;cAy`9DB;WGqWSM-;%_cQB z#6;|JxVhxFU5RH%IDhJ_;m(9BKX*U6C6%>0`1*yNR{UTyuWjUHP~c%W_;-65i>}MV z;_GZe`%b4t>#ZnbXL!)9oqyrrlc`budMmC?=3e#nNmG_B#3nF#SQqLpN3K=_0p<(8 z@81&cP}(}V?fZlEcBU!2gk|g*%6@+e47@N?HQKr9+@6Y4k?E7#-!>frUtV$VVE6iEtv8M+Nm04a=GZs){JKlrkiq3He!B;3T+pt~-M8RTzZL^S!7+BQM{aH8YBk_t4fy|m_1zej zPSqO~mhAUFZE2gWBl!yAypzK$bq|E!%wuF^ zkcqWEeu{0eUv9TI_mnVDik-W|A+wZ$VOvIIGnfCqNz*`L^L997R+_O`F@kNr!I4p@`hY_#E(8{J3~C|! zu5!J~{F&_$aK3-{n^&H4+ZqnF>n`|^_?_j-Q%DruQRHhiU;rgJsa^$z8`nzTtHy_x zoHch~s$(eViPTqm$*F%yIBtph9uIzo1FH|tKUYz=;9q{w?&Wy_x99D0%&mUKw)HI| z!;Z)@NtK^dSY#X@@!zuGv}ii=SCfH(;mb})D!UC&*T3Q)aHmR`@V;-ux8C>X83U5%fGVDIMX11>uT*!S6H)72aB)J^^aY!&8FrpB<^lIf(jT=BD|%| zqWPBlOv7sbB`VUb$Cw??Y(B4WQiD^*G0CBO@&Q4WfBx{W%llCrt{qaG@=`m*xZ?D> zSw)+9HSXmwcbVVr%eo0M_|8V&1_J@-i+{JLv3DK$S>?ZI*P)FWeN7b%9M)fzC(Ze& zVENdw=vea;28KIfyJs%GrXIcmj@cP1uC3g9J3+v0P##KO5lR!@dvW`++P%GnnVP2PRw(-Wqv zP|SPkGb<$3d4n=8s8Fy9=a_W9^Zp$Ob}y1Q6a#5;Q#+y{2E$UOnHs3 zyI#7{%~ked)|1(L)0!C=;%5E&@8MT5Va^8yP*KLfkXKuibnGkp*R7Lpfy=HdqFJv& zg&9A?ft~l~wfo5VGEGZJYBrn4qI2*_w-y7#6iZ0xL86@nV%HRbl-*|<)H@Ylrt(7) z9K(dvPu*bwXHP9%zhGAL`a7YM_vEmP>;vQS5XJ^u?U~7w624e^7v?f={kl?m#ajQn z4!N}q4ENSPneyIzGRx=iU8m+ehh)<03waq71z0cs+r5mDrEP=Hz0LREJUJ7{4=Lt% zE{Ni1IB;+Cf5p=mjUKRc^MCmWqT01UK?cGFm*K(FYQO^u^;^OkN=a&ut3QM?qz;oMR?o zxmkDJ+J)QZ>}tDJ^hzr0^uzU#GW*6sP6kCDmy7>)Z(|nZRFu5eYuQ(-{408l(G$9Se@0ZcvU`C(Z(Kh_x&ve(!*bt9>n zUn{os<7b(IH4pu#D>(8qFfhMelfi4Xd2_Yd&ohUj@0F$%ZH~S0A#L}9hiA9_;%1O3 zOtcAn?l3oIK@<$jGCT+_bL)w=TVekty$@bBfT{^Oh6elBlaE4ta?6m5!4O>gRIPs7 z?E0x?=C@>lwM<@dDve?)Vp}XYwna>GFw9vKr9A1N==Z<3 qz@YFF9Bk4@7bl4HGzA)@FfeRqIlReAV%8gwV?AB{T-G@yGywqlJY~TE literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..563f23d093b48bd455c244ca49e3b3448efbab10 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=GUMo-U3d6?5L)aO67V zz{7gs-v3LYEaFXzC6=lF+_PQNglDq2eUu#+4iaGbIrb~-^0wd?_GCCt|QQ{ zul&QzYiFl28D6h?P;KA3``t-y76t}^?bExtUBdUP@06=JZoGE3Hmj1)oj5%`0R{#K zFugeB-)-ypnRn-?o-C=?J8*6G(^K<)HC2CNVq`G5&dk8@|3An%%?<)A2mb%J(x31^ zWruC9XvGHui?fdsZy#1H-5V3D5OjCX#pCB)^v$_kcI}E=rzgO`aKINV6Si~T=l63u zmU?;Q<+RIrfE z%bLl{GQRLL9>|ktfVd-&>yUu}%Z1-_MmN{!mM3dDP0la4+lc`W4Tfln1S{)Re2Py|hoj{pLxTljV!r zg+IhC(|*0ZlMlE2hFJCwEP(QLD1pefMSD zySLxZep<5p-{In4OcNmrjbI8VzIy5=Uk^6v(%M(2!)%x93%uO-GghUJQ{v(AMh1p? zweX;0Hsok|`1}4Yb`7C0wz)q4_s3`t5eG1v`F#1M}_lja&|f9L)d!ufP9?ZKBr%=XVR5 zTQ0FoKepNTui`0<+Tc?fiMO|IlJi($V^;M?cFWE{g@u>j|1j^_{qLIfcP7pmC! gA8@jm&~YNxPU+HzM^n?aib46!)78&qol`;+07f&vDgXcg literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1569bfaba5e3e5faea55947ed3bdf3456e6736 GIT binary patch literal 1104 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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png b/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..5397cdfd8707207442abb955618f11e819a62a7e GIT binary patch literal 895 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=GU}o-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE60VSBU|{(F|IJ}TUIqq+h6VrD>OSO@W+GimQ0ochU(7No=sb3 zC(D%7gf0@#xf9r?@M3M(ME@v5zjwa>)}2^(x38*3f&KEb3=2lbJg>q{E5uc!HVTV% zC|BhD?Re!7@Gkd#;2Vkb0}%^qeRP`3*@Um&mS$s?yb*oU>q@@BrxPMpj>+mNzh&K$ zI=%8;e`lm$_P>!kTkYo(^NuT($LAYsViIU%+uh&D-))B(I`iqa1D=f=-=NFxyA7Sc$tnK~6&z^hhmtWbl z`_^5NQ$lz5ea+4jfBUY8z5iXR>G%C<+uwfJQc+aAZs$Vt-*Q}$_CL3+{MmLY^UKj$ zpLYKI^(XVmZgWs{PjLUk2u?S*4ssrH5MjAclC*N#q@Wz%g3gZL{yWcpkgNDKNipT~ zVR?H{@-%yV?(O69=VDKGy{gUp-oaPGS3mb)0r!^#n4kXYd)xNDGc*x@-vf5W+r#i| fko%AolnoAOT{Sk5SL$3B15)Yf>gTe~DWM4f<@1Ek literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png b/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d09df5e457b30258dda2c21cd7dcd49516ef62a5 GIT binary patch literal 1114 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=Axao-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE65b)jz`*eT|C_^xybKHs4GaFQ)qTh*&4i?afq}t*6QZV(hk=3N zfI|IiZJ7nzSWwj@a6!~Ca)QnIzg~J$6Phu1SkaXH7UTl?0Lh>Naj0|P#@rUvKy&kf zP>7nDhJK$N1X_z3RJ<%0OvTt8UQSrCLSy1lMgd;lsn$|fd=08zd<`zCoy-Y=#SAZk z3K|?$w#}*BZT(u&FDd%-lKB1e%F>PIN4{NW`|W&+(M%t;%N{OU)}1VRb1Y{GU!=YL znRjnAlGr)_d_DT+-#(?X2?yrSKO-jt7W#MY&Bcizq5S+aa`M_BRi6&MdAU+)+me)% zsv%C2oZFMuxOB`6)OzqdJ?;E?(T$d=-g=8=PMz2xnyi{|rYDHe-p;7Ha%IO{q13PU zHBWXd3ve@j71p{jG2w(yug&I{zaM{n`S01ICk6TMzVrF(<-MC9XTR^YBtQR~>Fd@B zXz4%yX{7a8v&Z++uk!y|%ck&Kg;? zn~dh~|F1YFrCe@Wk+F+6{b)|u97_e=*83H6w=GRrcq&87tZU}sjXxLNob-Eb*bmYB znbBNg4U1Ra&`9Guwybo$&e}I`s>;7tiDeu7eEKbO&bHJGOagF$)}}utjXE+ zAnpdwuJ$`&zI&G)SNH!{z3Rr%7PkkQ3TlEMeB_FUm zyo~RPa|@9v)p;C52+s9pv;qS{)gJC z{4+P&YY8qAs2{cy`JMT0rhw6i1_6ecsz+hAiaU~T^%lmR_m{an^L HB{Ts5m8aTm literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png b/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..f88688c1409204a3158c8c30902d1e1ead9afce1 GIT binary patch literal 1423 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|RTAr;B4q#hf<>3>g_1 z7!ED?>YvN4uK`kkgnvjfFfjc8|K_kEF9QQZ!-9Wnbsus{Ga;#9U|=ZVf~aZaVPIf5 zpiuu>TV}yF7F0C}91t~(oM3bQuU{3hbq?H^+kzTsZaxqN zRReeO+J}tjPS#`s+~5%{R{|pUdFQx*ewJC{S@ka;>B9WsTIGF;yUDX{@#?k8{7pV$&Bz zJ2g`G##MtvQzNxhyOhA9HFnymn{QSeS?LmHo2a^|-27s4&#bi;&1DihdH(R6`W&NK zC{ukz(}mCWp+e9K!$rp>Ors0gc#VoAe@K>C&7HF8_FJ1rIqx^u|9cz$y)t*>p?4CS z)3nbuIL`34+Vjij)~+*~O*$8UNRJk737oSeGVa>kYwD+srrLQ5CI55%to+Aumect^ zuYdnPw!TVk^CT~R!|Xb@I??67U%P*s7bLaw-x9~;j{|*^_uqd#^Q?Agi79)t%&{4! z+E23!fA4jSs+1^+{$tvF$t3i$!_m~z9czqY^xGW&##HUyV{l^4agX-~Ud^vruTMGs z?@`mv*(+i*t+)6;T=yiZf4<|6#}C#$DRSDGAhqP2>50^Nk@+@9Kb$#Kyw(dal5} ze-F4XF5x}&-MJ=t&KAcXOug%`%+I}G4oOh69_Aia5NMsbL{d>XrKRUWpjDUqOAfhK z!I+c18_&+IW#<&l-Tyv*-Ur1vzlNvbW*!Rr6HmOCsXw9sN#rjk-!qc9ySL-pwh*4I zMJ7wOFHH&%UaGiPSF~A5W4fYW;f^;`3Z=@ubiJnepJj>aifP+kWac*e^oru{;(Mz1 zCVl!Bkj11f>BZ#)k~KLIZ7IF@Y-!wcA$iV)J4()&rY_x7zfr=}eQ_GodC|-X`$3vt z?>RT&jYL@Lp7!56wB#?hl`fjQDP_;LmHOv{i|6@jEc&FU{q5ztJ+)aYi|wYbkh%O` zVp7rHyd|!M`lU6ei<|W;ytGu=Y*(`=F3F3V8veCu{rgXKD@|Xlh(D(lUKa2+FQ|9h z<0S+1(h|MF(D)7Q$7&a$+r^XwizICF}3)29jdf_*%nW_li4?!Qm+?X~3P zVpq2;tTZ89^-q0&{g-;e(#&IUf&oT=3n~VN0}bMV%sfU16q@sReu1;& z+gX8JhYfg~%NO7MUm}^D!d;=@9HTtz`vUuw1{*V0dOxX3oN(aGf-^6Z)<2u8wjyHX zP5);#zfyeF9u{m;n>x`la`Tck)9zd?p7k^Gr)Ju_tQM7P9Hnk6CVQ4#&Hnc5*Zmvc z{Vv!vJ=oo^n}vgo)4-#_ lk%>nrVFE)l0|P@;1tS}KUU28;@Bct??&<31vd$@?2>_FVT&w^9 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png b/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..feb4c4f8b0f4d013b581e94a49982f8ca577de80 GIT binary patch literal 2470 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq^sE)5S5QV$PcbhKvjh z42Kqc_0Q$j*8nL%!aF1x7#RNle{c9J62>3#yt2tPnMfoDg%?zw2m1GbVr;Malp5zXiEKK0q>PgBa8~P-FhDzb&YN z=H>&DP&IHTuYJgf?qnILbKq)vA2MU8VS=~^t_F+O=b)=O`+y(A97c#aa5pcO$M6@_ z$#5molh8wl8{HJl(6M2GSOj-47LR8`je)Dd;_>xR*TB{IK4eGtR5!Ysw-0#H)p(<; z+546OU5zkA&BcXXtO^3H4>LC3%yFCj{y&%5;y7iTylnh-FWN82 z%X9tlx}fFkQQ5kdCoR+W%Ki47E0=N3^68Q(K~PPG$0kgU%DxKGRP{dAFDl!2{qed> z{&Fwa!J!+i*!S2$V2}76?r*G-0y$!7VrgP059plHIdl5ZnL}p|`Gh5=9sbkA&38lm z4)?E;zAdZozSWxlJgQT<^xzh?*maw38^4|TU!LXS_uBRDlTFOOG>sX5f8C}Dm@(N)2#~&-MD9=8dmOOpA(8O@v z4_Zo7;U+*-EX$O#4H9upoSx1p*DiB?<3q2n#~HI7KKqz-6e@2peNkW`^W{X@?z^|1 zN@n%%c{YDt`Aq)rrmK{im&G67T)6~xU%jdqB z?|@P+6)b z?E`=1l>L$s%`6SKdwFlY|6UzBSAO^1yyfoyP6?O@GHupyKlJi*Ltt~0lw#!s-bRTf z1-myohK4p?SLi#^A)BnVjd3;ePDANuIRbTlA2y2yuUq$c*;LoY)*gu^h0?ky!CrSX zuH06W`r+Gr-_KD0kHiD*y#Ch!tHiQR>A`xtAKnwCT@tmK{^fP; z-@40DWSi21x$G6&o4|tib~k_jy#pkuBi7w2XF31;^OZbz6p95u{0rRk{0i@)4Wf+i z4;wBQFxwwiYb!GiOd4ym`_0Sj7M)qFe0TVx@_*M9h60f=%wEDy9ke%tQPh?;F-N z-g~#+_*&b?1%G^h{dY8ct~G;i*%PiQqMMfX%Un=BF*{-IPT324!cXlh2yWZ~P zb-o?`+B=SU+;lUN;`O*36!-A>-9(-(%rTemI<_-LYW`u~e7U)&tRb66W`$hgwg1f)`f7~))4)akEp<4q@YmQup3`AV^&io!w|dAzV+nw2G%} z{;Hy7xisX%V)f@MLsDLztn^YXt}2?hZCT+>t=+SZg=XsXuPussd!u67y7cdyeb--x zd-#3+lX3rE&YfS|eitm<_dLZR_?l;>-7dLD$0RS9DdZP^h?e=u9mM}fec3$eKmN1i zjwH67+VORIsmzK?+x{`}T?sw5c7KcD?eg~gCyMt=SN#tG?X}3L;NeKbLh*2~7agruc9G literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png b/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..04e2e8a5bde82e2f0e1ad9238e67e42f29c81999 GIT binary patch literal 871 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=GVAo-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE60VSBU|{(F|IJ}TUIqq+h6VrD>OSO@Wm-Ad{mqoFfx~yFm3@bCt4@vCV|Iwm3<&4xn|E@zDtT+GL`{vEN z5B&m89EvR<;)!3u+Oo;D>oQDwv_3zT-gr(u!*Tw;kKWIPw=Cel|6}cE&ZrB!^Z#7@ z$)fw>togrJHET5OrMa7>?hDkq@JjFoa#elpt+$%5qiL@$$usLm+Y4o`Lm3@G7rOKR zT>m+vG-yx$`xaM#4f9Qm#}Nr@j$Mo zc*#Hio=jaNBO(@}wszf;r=~2G3`Q+;PEToeI<-0IpRsPb<%ug34sZS4d~W&XT8?i) zH&Uy$IQ*;&_+G74u;@SJxpjHdgJTTkoflXHoj4Q|FNO9l-u(9SHz|!3GjC__NOmmD zn(NywTx7HP+_@>N--Px&JLFo;e%0aQB^`Ew^$(i-b3S`b?d5k%`^LJ|TQpIT$xBgt z1zT4tuVq7oieSyAMc<}vUzYjg@8uv}QOAv0OjjK?eo$c-I2Ezz{5F9tXL}R>-7Tp8 z@Qu4SL&P}pvQqhNafa8u#~8$aH#F+||9tiJz!Hsn-)=71@hh*cp-u9Ve&1=lIUfU1I9=ow#vPR#I`$cB2FQk?Zcx)oG|*(cTnQ*;2MR@Pab8Qj36- zT;uNBcA6R`y98C@r>?v3i5HaM7Ps@9JNM?ol!L~13`|l#@Nn?@O4>M=IR~c+v}jIE z&Ug@PJN5I01Jk`j^q#UgpLGnCe8pUp_1Do>&g9j?K>msO=QSi)7czopr0FDA|LjV8( literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..3a60a59d5b294d62b39cf4f00c0a75aa1c9d8981 GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E80JY5_^D(1Yoc96H( zfTuOlEWyO>14qvhjh+dCMop`?WzLFo;E7ykHBtkdoKbyIeKxa5!Ux$Boc){cGNfBfU}(9@AcH#e44Fd9Vj|G6dhK2?I*WVV@(11&W83%l! zY7BY7YW}TV`;bwZ2`mRE?yx}2fvd@V$cnBeh6zoL?n6#oY8rVM7#I#H)W24aS+I=- zZVi|r!3Z%2?q=ypO=!-k1}k}s@YZiZE>Jvz^!Koeu~qPP-In_g;(5CIxvX}%A&u)r7#QC9g$Aq2O)L)# zxRNR&GNmG5LSqw8V>xeV=<5j@f>Yx?FMpf1+eYnTzR}l|o0Fb?G?qL3Ys#uOXZvocFCI?-%66Pb-PvShCx5o9&6+JCEgVpLn}$gY~~SZdXS(E+ep0 zL{b(gfcyx-9`Css827T9Vs!qCbSfNG0i9@kPz-hvx zclRbY9hlR*;L;DF8Jh(|q~e*1&6>O(dncT83&^bDJadynt5?6#QrdCpM^%G#;Kc|(uiLvL|{TmujqgWGbew`|wQna*b!uqgv>QDEJ za(rf5Ei$_;anpT~O22A>N#RSJDr@%E<%m}LRVzHJx-duk^}Fb`CyMVeZ@kiM6TWP| zdGNX%9q)A_a@n@&k3Ytq|2L=GMqoYrT6yo|hehW#AKiO)j(w$qmFBwpyqA>w{s&z5_^`_XpAU^@l|hX4QHTr}imP+&OX@PA$52jPW0<({HnoY*A4gzZ1L zVCDUU3s>?DGG3WWn0%GyVFE<~6gTM0K(rrb2qVKi{YkV`uq$uaNwT8 z^!NcEs7-J+xer;w=A_qMhSy0;&2flPUO7;Z}MLuN?m aZQyph*z zL4eJ{YC+3>mgz5qG#V#q990$gx%A1AgSU=&%-k6=e z)wF!}_JuJ~`Ks}WxqH`E{yVi`{r yz}56VWX4d#gr>&#Av?O78nBwThvDWx)f~_?QQr4tYV@2Cke!~celF{r5}E*~R*?h% literal 0 HcmV?d00001 diff --git a/src/font/sprite/underline.zig b/src/font/sprite/underline.zig deleted file mode 100644 index d2e439e6a..000000000 --- a/src/font/sprite/underline.zig +++ /dev/null @@ -1,312 +0,0 @@ -//! This file renders underline sprites. To draw underlines, we render the -//! full cell-width as a sprite and then draw it as a separate pass to the -//! text. -//! -//! We used to render the underlines directly in the GPU shaders but its -//! annoying to support multiple types of underlines and its also annoying -//! to maintain and debug another set of shaders for each renderer instead of -//! just relying on the glyph system we already need to support for text -//! anyways. -//! -//! This also renders strikethrough, so its really more generally a -//! "horizontal line" renderer. -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const font = @import("../main.zig"); -const Sprite = font.sprite.Sprite; - -/// Draw an underline. -pub fn renderGlyph( - alloc: Allocator, - atlas: *font.Atlas, - sprite: Sprite, - width: u32, - height: u32, - line_pos: u32, - line_thickness: u32, -) !font.Glyph { - // Draw the appropriate sprite - var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) { - .underline => try drawSingle(alloc, width, line_thickness), - .underline_double => try drawDouble(alloc, width, line_thickness), - .underline_dotted => try drawDotted(alloc, width, line_thickness), - .underline_dashed => try drawDashed(alloc, width, line_thickness), - .underline_curly => try drawCurly(alloc, width, line_thickness), - .overline => try drawSingle(alloc, width, line_thickness), - .strikethrough => try drawSingle(alloc, width, line_thickness), - else => unreachable, - }; - defer canvas.deinit(); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - .width = width, - .height = @intCast(region.height), - .offset_x = 0, - // Glyph.offset_y is the distance between the top of the glyph and the - // bottom of the cell. We want the top of the glyph to be at line_pos - // from the TOP of the cell, and then offset by the offset_y from the - // draw function. - .offset_y = @as(i32, @intCast(height -| line_pos)) - offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} - -/// A tuple with the canvas that the desired sprite was drawn on and -/// a recommended offset (+Y = down) to shift its Y position by, to -/// correct for underline styles with additional thickness. -const CanvasAndOffset = struct { font.sprite.Canvas, i32 }; - -/// Draw a single underline. -fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a double underline. -fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - // Our gap between lines will be at least 2px. - // (i.e. if our thickness is 1, we still have a gap of 2) - const gap = @max(2, thickness); - - const height: u32 = thickness * 2 * gap; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - canvas.rect(.{ - .x = 0, - .y = thickness * 2, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = -@as(i32, @intCast(thickness)); - - return .{ canvas, offset_y }; -} - -/// Draw a dotted underline. -fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dot_width = @max(thickness, 3); - const dot_count = @max((width / dot_width) / 2, 1); - const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count); - var i: u32 = 0; - while (i < dot_count) : (i += 1) { - // Ensure we never go out of bounds for the rect - const x = @min(i * (dot_width + gap_width), width - 1); - const rect_width = @min(width - x, dot_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a dashed underline. -fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dash_width = width / 3 + 1; - const dash_count = (width / dash_width) + 1; - var i: u32 = 0; - while (i < dash_count) : (i += 2) { - // Ensure we never go out of bounds for the rect - const x = @min(i * dash_width, width - 1); - const rect_width = @min(width - x, dash_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a curly underline. Thanks to Wez Furlong for providing -/// the basic math structure for this since I was lazy with the -/// geometry. -fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const float_width: f64 = @floatFromInt(width); - // Because of we way we draw the undercurl, we end up making it around 1px - // thicker than it should be, to fix this we just reduce the thickness by 1. - // - // We use a minimum thickness of 0.414 because this empirically produces - // the nicest undercurls at 1px underline thickness; thinner tends to look - // too thin compared to straight underlines and has artefacting. - const float_thick: f64 = @max(0.414, @as(f64, @floatFromInt(thickness -| 1))); - - // Calculate the wave period for a single character - // `2 * pi...` = 1 peak per character - // `4 * pi...` = 2 peaks per character - const wave_period = 2 * std.math.pi / float_width; - - // The full amplitude of the wave can be from the bottom to the - // underline position. We also calculate our mid y point of the wave - const half_amplitude = 1.0 / wave_period; - const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; - - // This is used in calculating the offset curve estimate below. - const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(1.0, half_amplitude * wave_period); - - const height: u32 = @intFromFloat(@ceil(half_amplitude + float_thick + 1) * 2); - - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - // follow Xiaolin Wu's antialias algorithm to draw the curve - var x: u32 = 0; - while (x < width) : (x += 1) { - // We sample the wave function at the *middle* of each - // pixel column, to ensure that it renders symmetrically. - const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; - // Use the slope at this location to add thickness to - // the line on this column, counteracting the thinning - // caused by the slope. - // - // This is not the exact offset curve for a sine wave, - // but it's a decent enough approximation. - // - // How did I derive this? I stared at Desmos and fiddled - // with numbers for an hour until it was good enough. - const t_u: f64 = t + std.math.pi; - const slope_factor_u: f64 = (@sin(t_u) * @sin(t_u) * offset_factor) / ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); - const slope_factor_l: f64 = (@sin(t) * @sin(t) * offset_factor) / ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); - - const cosx: f64 = @cos(t); - // This will be the center of our stroke. - const y: f64 = y_mid + half_amplitude * cosx; - - // The upper pixel and lower pixel are - // calculated relative to the center. - const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; - const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; - const y_upper: u32 = @intFromFloat(@floor(y_u)); - const y_lower: u32 = @intFromFloat(@ceil(y_l)); - const alpha_u: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_u - @floor(y_u))))); - const alpha_l: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_l - @ceil(y_l))))); - - // upper and lower bounds - canvas.pixel(x, @min(y_upper, height - 1), @enumFromInt(alpha_u)); - canvas.pixel(x, @min(y_lower, height - 1), @enumFromInt(alpha_l)); - - // fill between upper and lower bound - var y_fill: u32 = y_upper + 1; - while (y_fill < y_lower) : (y_fill += 1) { - canvas.pixel(x, @min(y_fill, height - 1), .on); - } - } - - const offset_y: i32 = @intFromFloat(-@round(half_amplitude)); - - return .{ canvas, offset_y }; -} - -test "single" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 2, - ); -} - -test "strikethrough" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .strikethrough, - 36, - 18, - 9, - 2, - ); -} - -test "single large thickness" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // unrealistic thickness but used to cause a crash - // https://github.com/mitchellh/ghostty/pull/1548 - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 200, - ); -} - -test "curly" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline_curly, - 36, - 18, - 9, - 2, - ); -} diff --git a/typos.toml b/typos.toml index fafc38858..a8b296755 100644 --- a/typos.toml +++ b/typos.toml @@ -32,6 +32,8 @@ extend-ignore-re = [ # Ignore typos in test expectations "testing\\.expect[^;]*;", "kHOM\\d*", + # Ignore "typos" in sprite font draw fn names + "draw[0-9A-F]+(_[0-9A-F]+)?\\(", ] [default.extend-words] -- 2.51.2