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 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 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 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfkCjv)5S5QV$Pds8`m9j z;9>dv-~Mg5&8!JGo+Q+63Nsecd}q*Iu3^Ck(!s#Mpsb{{>GPYuIlVhX+ZBqwJqnX) zI&qWxC@;I?VaE#v8hI9ZO?SR+j6RVozq|F$w~fCiHz*V>Inp}YJfxZ1G5`FrIoBWm zzv_AKc5|n@F~={?sS9G7BLdbrYHM7-ptvIM`8&0I5l0Ry{y4vO?x@q2_Z2gAk9!34 zMFdEOa0Ojd)$r9eog&OpB=ALW>H^IdtZQp$?J+lZyx*|kp*;(WqR0QI0wH%5GP3!a z+MX+{Fn-R@!0`Y7o0}Us4;Y9zAN;%A&nzHSNAle*e!~}2EkE=*Jbuq%@~_X~F^iP? z1&hXGzU)j8Z$4NOy}u*AM!oj!Eb(?>jksTjIYspUK4j^d{;z=PsCT_Z9f=*WqPvjq?^}{_cs+xMlZPY}U5( z-yc4{X@7ic>$TsdkOpIW^7RdZ(06TJ~W8Yx|9_Ii5U)^O_nH21%mR-W}Wyy+%6?%+k z`UQ%_-~Ni)xohrTj(L0g{y$os!LpLq$8o}~10p0dBAH{4pL+65;8nZ$T|1jgnNze* z_s(Yjr&r0arIpKZJxlJARoV8la%|sRpWIcFkhECqey;*c?);D?%)-*4Kl4~*|4lUb zW@y{{sf*olzf#7V3bFsoeA1+!RL(j%(}5#v`X@4o<6OTWQFYUitHprF;o|T4yYg5w zmE24kzfVh>VfH7Lb=FRQ6G)ITJSe|!BWPc_@7t5PF^&q2ET=eCCOC*PajFEgIxumD zC{A!-3grL`?G?|Mdsk+C_aCl@nzQ9qp0~)eznlE;{Fc^3{fEEZ?Dw(qk&0P({|O`p zZXV=nQIKf8_`UvhB=>%+DaS(3MV2Ld?K|t>>3u*ug2k#L^ZTcy!us-gwkwSGR{kya z`!)H(o8SCj?*HCrci~C;JB}@hJI`Aj-sHMqIdW8^$Nsy;4&bO~VEEEw7bJeY9%{Ye zIwnJ&mWRLROIfj8s$xohY;mjp)br;mi`Or>akTHd;h*pP2ZiI_9(iHBP|JaV@rgeJ z(+mCeWlRDNRqP6~4Gfp88D285Ot{Cm!bW%%lZw$oEe9nKBPi_a$9WacljqzkRC`$+ z68Asi8P|oF1tnK!`-wQ)7;f7CYNl{Qo4=65^m?B9jBnw##h2Jp^ke;+^opQb7?@sk ztCq&xS@K-&xygH0KL)0ZQW*y=2F5K`db~j7b;v-3CE$I1W-O=E857oxlRwGrSat4y+OBSXNN8->_}%q=o%p`b zr(eJK|7^e4OgA9FIcmXzwk-~hyqS$nQkR%mO_#88WqELmT$v!Iu|g3^!S&^~?917D z_s^Vt#!^~%QD0fK^`)zK)iUMp|38|?B{fgO!;x#3zhnFJ3G7EdP8R*NX6cHhy5DZU zHQc8(v+3Wa+uUGRUUcL;WWeKk@ZSGR8(0Ia4ZBwDpAo6_$f`1aKbOd!+o}QI@~-#4u{H(a@C?K`1xMHc*v}I$GLksYChg>T3C48UggL#*ZkK{4F1lm zz4>lu+vr?+i(XY@N~-uOM?&4q7sLN0t|DSD^*A#v7ZYi`dA61R0fSN*GydGS_5 zyf?5T2&g(@-GUC*dMNydi(Qq=I^xlZLF~;FSswNHQ==R=TW(oSzv0Ywy|k>{^ymxS1x(@ zsK)(#%ifx6CMy@Po}XI1PqV%?X6m0EiFM`nC4a4)j!(?(jhtt>wW*FZ_w4mt`@W2+ zs-h+fqaAIr5#*>`CHmyv?mgCto~*quLMHrw z{r-(=N#(ycK~|zq-p`+VcZc5j`LSN}o6gx?5>v2f0 zi zQLTllJ^#0y>)v0f!YguKN|o{X+KFc8okab(mI}X!ZmP>$^UUe`lcx^?KGs*?uY1?x zb3=2Ap8{tQ%UWrRf6hvKM5?|NzQ31uQ;_9VmfV|n#hY^)s}9OpwD#@4EVFU<{~yeG z6Bm>UKjgl4;FZb>*9*nc?}W4MlI{L{>BtuN(quY+V;lcZN2k_33h#4n9tdS^eSiF5 zoPYt_gP)0Itel&~St_`qXRHWYk+C9HwR-8TewDk2qXQqiF8CAV-}

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]