From ac871543622f8b4da7f93108110ead96ba16deba Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 15:04:21 -0600 Subject: [PATCH 1/5] font/sprite: introduce `Fraction` enum for cell fractions I've included a compatibility test here to make sure that the numbers from this are in line with the numbers produced by xHalfs, yThirds, etc. After this commit I'll introduce a helper function that fills based on a span specified with this enum to replace any uses of xHalfs and friends. Once I do that I'll remove them and the compatibility test, this should be a much cleaner interface for this and make it easier to consistently align block elements with each other. --- src/font/sprite/draw/common.zig | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index d10128cdf..ad9788b94 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -122,6 +122,162 @@ pub const Alignment = struct { pub const bottom_right = lower_right; }; +/// A value that indicates some fraction across +/// the cell either horizontally or vertically. +/// +/// This has some redundant names in it so that you can +/// use whichever one feels most semantically appropriate. +pub const Fraction = enum { + // Names for the min edge + start, + left, + top, + zero, + + // Names based on eighths + one_eighth, + two_eighths, + three_eighths, + four_eighths, + five_eighths, + six_eighths, + seven_eighths, + + // Names based on quarters + one_quarter, + two_quarters, + three_quarters, + + // Names based on thirds + one_third, + two_thirds, + + // Names based on halves + one_half, + half, + + // Alternative names for 1/2 + center, + middle, + + // Names for the max edge + end, + right, + bottom, + one, + full, + + /// Get the x position for this fraction across a particular + /// size (width or height), assuming it will be used as the + /// min (left/top) coordinate for a block. + /// + /// `size` can be any integer type, since it will be coerced + pub inline fn min(self: Fraction, size: anytype) i32 { + const s: f64 = @as(f64, @floatFromInt(size)); + // For min coordinates, we want to align with the complementary + // fraction taken from the end, this ensures that rounding evens + // out, so that for example, if `size` is `7`, and we're looking + // at the `half` line, `size - round((1 - 0.5) * size)` => `3`; + // whereas the max coordinate directly rounds, which means that + // both `start` -> `half` and `half` -> `end` will be 4px, from + // `0` -> `4` and `3` -> `7`. + return @intFromFloat(s - @round((1.0 - self.fraction()) * s)); + } + + /// Get the x position for this fraction across a particular + /// size (width or height), assuming it will be used as the + /// max (right/bottom) coordinate for a block. + /// + /// `size` can be any integer type, since it will be coerced + /// with `@floatFromInt`. + pub inline fn max(self: Fraction, size: anytype) i32 { + const s: f64 = @as(f64, @floatFromInt(size)); + // See explanation of why these are different in `min`. + return @intFromFloat(@round(self.fraction() * s)); + } + + pub inline fn fraction(self: Fraction) f64 { + return switch (self) { + .start, + .left, + .top, + .zero, + => 0.0, + + .one_eighth, + => 0.125, + + .one_quarter, + .two_eighths, + => 0.25, + + .one_third, + => 1.0 / 3.0, + + .three_eighths, + => 0.375, + + .one_half, + .two_quarters, + .four_eighths, + .half, + .center, + .middle, + => 0.5, + + .five_eighths, + => 0.625, + + .two_thirds, + => 2.0 / 3.0, + + .three_quarters, + .six_eighths, + => 0.75, + + .seven_eighths, + => 0.875, + + .end, + .right, + .bottom, + .one, + .full, + => 1.0, + }; + } +}; + +test "sprite font fraction" { + const testing = std.testing; + + for (4..64) |s| { + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(s), + .ascent = @floatFromInt(s), + .descent = 0.0, + .line_gap = 0.0, + .underline_thickness = 2.0, + .strikethrough_thickness = 2.0, + }); + + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); + } +} + /// Fill a rect, clamped to within the cell boundaries. /// /// TODO: Eliminate usages of this, prefer `canvas.box`. From c838d3d7d251f1420d7bdaa1c6428caf9f6416d6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:00:35 -0600 Subject: [PATCH 2/5] font/sprite: remove `yHalfs` and friends, use `Fraction` Introduces `fill`, which fills between two `Fraction`s, use this instead of `yHalfs` and friends wherever they're used, which also means we can remove `rect`. This commit does change alignment of the vertical/horizontal eighths in certain cell sizes, but the change is for the better IMO. Also changes the center-point alignment of smooth mosaics for odd cell widths, but the change is no more than half a pixel at worst and is probably an improvement ultimately. --- src/font/sprite/draw/block.zig | 15 +- src/font/sprite/draw/box.zig | 15 -- src/font/sprite/draw/common.zig | 168 +++++++----------- .../draw/symbols_for_legacy_computing.zig | 66 +++---- ...ymbols_for_legacy_computing_supplement.zig | 22 +-- typos.toml | 2 - 6 files changed, 108 insertions(+), 180 deletions(-) diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig index f7faacea7..571f25a79 100644 --- a/src/font/sprite/draw/block.zig +++ b/src/font/sprite/draw/block.zig @@ -15,9 +15,7 @@ const common = @import("common.zig"); const Shade = common.Shade; const Quads = common.Quads; const Alignment = common.Alignment; -const xHalfs = common.xHalfs; -const yHalfs = common.yHalfs; -const rect = common.rect; +const fill = common.fill; const font = @import("../../main.zig"); const Sprite = @import("../../sprite.zig").Sprite; @@ -176,11 +174,8 @@ fn quadrant( canvas: *font.sprite.Canvas, comptime quads: Quads, ) void { - const x_halfs = xHalfs(metrics); - const y_halfs = yHalfs(metrics); - - if (quads.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_halfs[0]); - if (quads.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_halfs[0]); - if (quads.bl) rect(metrics, canvas, 0, y_halfs[1], x_halfs[0], metrics.cell_height); - if (quads.br) rect(metrics, canvas, x_halfs[1], y_halfs[1], metrics.cell_width, metrics.cell_height); + if (quads.tl) fill(metrics, canvas, .zero, .half, .zero, .half); + if (quads.tr) fill(metrics, canvas, .half, .full, .zero, .half); + if (quads.bl) fill(metrics, canvas, .zero, .half, .half, .full); + if (quads.br) fill(metrics, canvas, .half, .full, .half, .full); } diff --git a/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig index 91d78d2b2..f14e5a3f9 100644 --- a/src/font/sprite/draw/box.zig +++ b/src/font/sprite/draw/box.zig @@ -24,7 +24,6 @@ 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; @@ -695,20 +694,6 @@ pub fn lightDiagonalCross( 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, diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index ad9788b94..67b9dc778 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -135,6 +135,7 @@ pub const Fraction = enum { zero, // Names based on eighths + eighth, one_eighth, two_eighths, three_eighths, @@ -144,17 +145,19 @@ pub const Fraction = enum { seven_eighths, // Names based on quarters + quarter, one_quarter, two_quarters, three_quarters, // Names based on thirds + third, one_third, two_thirds, // Names based on halves - one_half, half, + one_half, // Alternative names for 1/2 center, @@ -167,6 +170,43 @@ pub const Fraction = enum { one, full, + /// This can be indexed to get the fraction for `i/8`. + pub const eighths: [9]Fraction = .{ + .zero, + .one_eighth, + .two_eighths, + .three_eighths, + .four_eighths, + .five_eighths, + .six_eighths, + .seven_eighths, + .one, + }; + + /// This can be indexed to get the fraction for `i/4`. + pub const quarters: [5]Fraction = .{ + .zero, + .one_quarter, + .two_quarters, + .three_quarters, + .one, + }; + + /// This can be indexed to get the fraction for `i/3`. + pub const thirds: [4]Fraction = .{ + .zero, + .one_third, + .two_thirds, + .one, + }; + + /// This can be indexed to get the fraction for `i/2`. + pub const halves: [3]Fraction = .{ + .zero, + .one_half, + .one, + }; + /// Get the x position for this fraction across a particular /// size (width or height), assuming it will be used as the /// min (left/top) coordinate for a block. @@ -196,6 +236,19 @@ pub const Fraction = enum { return @intFromFloat(@round(self.fraction() * s)); } + /// Get this fraction across a particular size (width/height). + /// If you need an integer, use `min` or `max` instead, since + /// they contain special logic for consistent alignment. This + /// is for when you're drawing with paths and don't care about + /// pixel alignment. + /// + /// `size` can be any integer type, since it will be coerced + /// with `@floatFromInt`. + pub inline fn float(self: Fraction, size: anytype) f64 { + return self.fraction() * @as(f64, @floatFromInt(size)); + } + + /// Get a float for the fraction this represents. pub inline fn fraction(self: Fraction) f64 { return switch (self) { .start, @@ -204,23 +257,26 @@ pub const Fraction = enum { .zero, => 0.0, + .eighth, .one_eighth, => 0.125, + .quarter, .one_quarter, .two_eighths, => 0.25, + .third, .one_third, => 1.0 / 3.0, .three_eighths, => 0.375, + .half, .one_half, .two_quarters, .four_eighths, - .half, .center, .middle, => 0.5, @@ -248,52 +304,21 @@ pub const Fraction = enum { } }; -test "sprite font fraction" { - const testing = std.testing; - - for (4..64) |s| { - const metrics: font.Metrics = .calc(.{ - .cell_width = @floatFromInt(s), - .ascent = @floatFromInt(s), - .descent = 0.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }); - - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); - } -} - -/// Fill a rect, clamped to within the cell boundaries. -/// -/// TODO: Eliminate usages of this, prefer `canvas.box`. -pub fn rect( +/// Fill a section of the cell, specified by a +/// horizontal and vertical pair of fraction lines. +pub fn fill( metrics: font.Metrics, canvas: *font.sprite.Canvas, - x1: u32, - y1: u32, - x2: u32, - y2: u32, + x0: Fraction, + x1: Fraction, + y0: Fraction, + y1: Fraction, ) 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)), + x0.min(metrics.cell_width), + y0.min(metrics.cell_height), + x1.max(metrics.cell_width), + y1.max(metrics.cell_height), .on, ); } @@ -351,58 +376,3 @@ pub fn hline( ) 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 }; -} - -/// yHalfs[0] should be used as the bottom edge of a top-aligned half. -/// yHalfs[1] should be used as the top edge of a bottom-aligned half. -pub fn yHalfs(metrics: font.Metrics) [2]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const half_height: u32 = @intFromFloat(@round(0.5 * float_height)); - return .{ half_height, metrics.cell_height - half_height }; -} - -/// 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/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index a17ddb494..19e62cf4b 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -28,13 +28,12 @@ const z2d = @import("z2d"); const common = @import("common.zig"); const Thickness = common.Thickness; const Alignment = common.Alignment; +const Fraction = common.Fraction; 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 fill = common.fill; const box = @import("box.zig"); const block = @import("block.zig"); @@ -121,16 +120,12 @@ pub fn draw1FB00_1FB3B( 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); + if (sex.tl) fill(metrics, canvas, .zero, .half, .zero, .one_third); + if (sex.tr) fill(metrics, canvas, .half, .full, .zero, .one_third); + if (sex.ml) fill(metrics, canvas, .zero, .half, .one_third, .two_thirds); + if (sex.mr) fill(metrics, canvas, .half, .full, .one_third, .two_thirds); + if (sex.bl) fill(metrics, canvas, .zero, .half, .two_thirds, .end); + if (sex.br) fill(metrics, canvas, .half, .full, .two_thirds, .end); } /// Smooth Mosaics @@ -465,17 +460,12 @@ pub fn draw1FB3C_1FB67( 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 upper: f64 = Fraction.one_third.float(metrics.cell_height); + const lower: f64 = Fraction.two_thirds.float(metrics.cell_height); const bottom: f64 = @floatFromInt(metrics.cell_height); const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const center: f64 = Fraction.half.float(metrics.cell_width); const right: f64 = @floatFromInt(metrics.cell_width); var path = canvas.staticPath(12); // nodes.len = 0 @@ -571,13 +561,14 @@ pub fn draw1FB70_1FB75( const n = cp + 1 - 0x1fb70; - const x: u32 = @intFromFloat( - @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + fill( + metrics, + canvas, + Fraction.eighths[n], + Fraction.eighths[n + 1], + .top, + .bottom, ); - 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 @@ -593,21 +584,14 @@ pub fn draw1FB76_1FB7B( const n = cp + 1 - 0x1fb76; - const h = @as( - u32, - @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + fill( + metrics, + canvas, + .left, + .right, + Fraction.eighths[n], + Fraction.eighths[n + 1], ); - 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( diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 9ae92cc72..fd193a0d5 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -57,9 +57,7 @@ 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 fill = common.fill; const box = @import("box.zig"); @@ -122,17 +120,15 @@ pub fn draw1CD00_1CDE5( 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); + if (oct.@"1") fill(metrics, canvas, .zero, .half, .zero, .one_quarter); + if (oct.@"2") fill(metrics, canvas, .half, .full, .zero, .one_quarter); + if (oct.@"3") fill(metrics, canvas, .zero, .half, .one_quarter, .two_quarters); + if (oct.@"4") fill(metrics, canvas, .half, .full, .one_quarter, .two_quarters); + if (oct.@"5") fill(metrics, canvas, .zero, .half, .two_quarters, .three_quarters); + if (oct.@"6") fill(metrics, canvas, .half, .full, .two_quarters, .three_quarters); + if (oct.@"7") fill(metrics, canvas, .zero, .half, .three_quarters, .end); + if (oct.@"8") fill(metrics, canvas, .half, .full, .three_quarters, .end); } // Separated Block Quadrants diff --git a/typos.toml b/typos.toml index a8b296755..1fb54ecc6 100644 --- a/typos.toml +++ b/typos.toml @@ -39,8 +39,6 @@ extend-ignore-re = [ [default.extend-words] Pn = "Pn" thr = "thr" -# Should be "halves", but for now skip it as it would make diff huge -halfs = "halfs" # Swift oddities Requestor = "Requestor" iterm = "iterm" From ffe06f1ccdc82a4f0effa13729ab066640537615 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:26:57 -0600 Subject: [PATCH 3/5] font/sprite: add sixteenth blocks from slfc supplement --- ...ymbols_for_legacy_computing_supplement.zig | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index fd193a0d5..40c330d2c 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -55,6 +55,7 @@ const z2d = @import("z2d"); const common = @import("common.zig"); const Thickness = common.Thickness; +const Fraction = common.Fraction; const Corner = common.Corner; const Shade = common.Shade; const fill = common.fill; @@ -399,6 +400,88 @@ pub fn draw1CE51_1CE8F( ); } +/// Sixteenth Blocks +pub fn draw1CE90_1CEAF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + const q = Fraction.quarters; + switch (cp) { + // 𜺐 UPPER LEFT ONE SIXTEENTH BLOCK + 0x1CE90 => fill(metrics, canvas, q[0], q[1], q[0], q[1]), + // 𜺑 UPPER CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE91 => fill(metrics, canvas, q[1], q[2], q[0], q[1]), + // 𜺒 UPPER CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE92 => fill(metrics, canvas, q[2], q[3], q[0], q[1]), + // 𜺓 UPPER RIGHT ONE SIXTEENTH BLOCK + 0x1CE93 => fill(metrics, canvas, q[3], q[4], q[0], q[1]), + // 𜺔 UPPER MIDDLE LEFT ONE SIXTEENTH BLOCK + 0x1CE94 => fill(metrics, canvas, q[0], q[1], q[1], q[2]), + // 𜺕 UPPER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE95 => fill(metrics, canvas, q[1], q[2], q[1], q[2]), + // 𜺖 UPPER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE96 => fill(metrics, canvas, q[2], q[3], q[1], q[2]), + // 𜺗 UPPER MIDDLE RIGHT ONE SIXTEENTH BLOCK + 0x1CE97 => fill(metrics, canvas, q[3], q[4], q[1], q[2]), + // 𜺘 LOWER MIDDLE LEFT ONE SIXTEENTH BLOCK + 0x1CE98 => fill(metrics, canvas, q[0], q[1], q[2], q[3]), + // 𜺙 LOWER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE99 => fill(metrics, canvas, q[1], q[2], q[2], q[3]), + // 𜺚 LOWER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9A => fill(metrics, canvas, q[2], q[3], q[2], q[3]), + // 𜺛 LOWER MIDDLE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9B => fill(metrics, canvas, q[3], q[4], q[2], q[3]), + // 𜺜 LOWER LEFT ONE SIXTEENTH BLOCK + 0x1CE9C => fill(metrics, canvas, q[0], q[1], q[3], q[4]), + // 𜺝 LOWER CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE9D => fill(metrics, canvas, q[1], q[2], q[3], q[4]), + // 𜺞 LOWER CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9E => fill(metrics, canvas, q[2], q[3], q[3], q[4]), + // 𜺟 LOWER RIGHT ONE SIXTEENTH BLOCK + 0x1CE9F => fill(metrics, canvas, q[3], q[4], q[3], q[4]), + + // 𜺠 RIGHT HALF LOWER ONE QUARTER BLOCK + 0x1CEA0 => fill(metrics, canvas, q[2], q[4], q[3], q[4]), + // 𜺡 RIGHT THREE QUARTERS LOWER ONE QUARTER BLOCK + 0x1CEA1 => fill(metrics, canvas, q[1], q[4], q[3], q[4]), + // 𜺢 LEFT THREE QUARTERS LOWER ONE QUARTER BLOCK + 0x1CEA2 => fill(metrics, canvas, q[0], q[3], q[3], q[4]), + // 𜺣 LEFT HALF LOWER ONE QUARTER BLOCK + 0x1CEA3 => fill(metrics, canvas, q[0], q[2], q[3], q[4]), + // 𜺤 LOWER HALF LEFT ONE QUARTER BLOCK + 0x1CEA4 => fill(metrics, canvas, q[0], q[1], q[2], q[4]), + // 𜺥 LOWER THREE QUARTERS LEFT ONE QUARTER BLOCK + 0x1CEA5 => fill(metrics, canvas, q[0], q[1], q[1], q[4]), + // 𜺦 UPPER THREE QUARTERS LEFT ONE QUARTER BLOCK + 0x1CEA6 => fill(metrics, canvas, q[0], q[1], q[0], q[3]), + // 𜺧 UPPER HALF LEFT ONE QUARTER BLOCK + 0x1CEA7 => fill(metrics, canvas, q[0], q[1], q[0], q[2]), + // 𜺨 LEFT HALF UPPER ONE QUARTER BLOCK + 0x1CEA8 => fill(metrics, canvas, q[0], q[2], q[0], q[1]), + // 𜺩 LEFT THREE QUARTERS UPPER ONE QUARTER BLOCK + 0x1CEA9 => fill(metrics, canvas, q[0], q[3], q[0], q[1]), + // 𜺪 RIGHT THREE QUARTERS UPPER ONE QUARTER BLOCK + 0x1CEAA => fill(metrics, canvas, q[1], q[4], q[0], q[1]), + // 𜺫 RIGHT HALF UPPER ONE QUARTER BLOCK + 0x1CEAB => fill(metrics, canvas, q[2], q[4], q[0], q[1]), + // 𜺬 UPPER HALF RIGHT ONE QUARTER BLOCK + 0x1CEAC => fill(metrics, canvas, q[3], q[4], q[0], q[2]), + // 𜺭 UPPER THREE QUARTERS RIGHT ONE QUARTER BLOCK + 0x1CEAD => fill(metrics, canvas, q[3], q[4], q[0], q[3]), + // 𜺮 LOWER THREE QUARTERS RIGHT ONE QUARTER BLOCK + 0x1CEAE => fill(metrics, canvas, q[3], q[4], q[1], q[4]), + // 𜺯 LOWER HALF RIGHT ONE QUARTER BLOCK + 0x1CEAF => fill(metrics, canvas, q[3], q[4], q[2], q[4]), + + else => unreachable, + } +} + fn circlePiece( canvas: *font.sprite.Canvas, width: u32, From 0cd95a791f15dfc17d257c5d40b92a1fe1a26ec9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:52:17 -0600 Subject: [PATCH 4/5] font/sprite: add sflc supplement circle/ellipse glyphs --- .../draw/symbols_for_legacy_computing.zig | 2 +- ...ymbols_for_legacy_computing_supplement.zig | 129 +++++++++++++----- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index 19e62cf4b..164aa1ac3 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -1367,7 +1367,7 @@ fn checkerboardFill( } } -fn circle( +pub fn circle( metrics: font.Metrics, canvas: *font.sprite.Canvas, comptime position: Alignment, diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 40c330d2c..f43949eb9 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -61,6 +61,7 @@ const Shade = common.Shade; const fill = common.fill; const box = @import("box.zig"); +const sflc = @import("symbols_for_legacy_computing.zig"); const font = @import("../../main.zig"); @@ -210,37 +211,37 @@ pub fn draw1CC30_1CC3F( ) !void { switch (cp) { // 𜰰 UPPER LEFT TWELFTH CIRCLE - 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2), + 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2, .tl), // 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE - 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2), + 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2, .tl), // 𜰲 UPPER CENTRE RIGHT TWELFTH CIRCLE - 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2), + 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2, .tr), // 𜰳 UPPER RIGHT TWELFTH CIRCLE - 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2), + 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2, .tr), // 𜰴 UPPER MIDDLE LEFT TWELFTH CIRCLE - 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2), + 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2, .tl), // 𜰵 UPPER LEFT QUARTER CIRCLE - 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1), + 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1, .tl), // 𜰶 UPPER RIGHT QUARTER CIRCLE - 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1), + 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1, .tr), // 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE - 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2), + 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2, .tr), // 𜰸 LOWER MIDDLE LEFT TWELFTH CIRCLE - 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2), + 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2, .bl), // 𜰹 LOWER LEFT QUARTER CIRCLE - 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1), + 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1, .bl), // 𜰺 LOWER RIGHT QUARTER CIRCLE - 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1), + 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1, .br), // 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE - 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2), + 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2, .br), // 𜰼 LOWER LEFT TWELFTH CIRCLE - 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2), + 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2, .bl), // 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE - 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2), + 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2, .bl), // 𜰾 LOWER CENTRE RIGHT TWELFTH CIRCLE - 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2), + 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2, .br), // 𜰿 LOWER RIGHT TWELFTH CIRCLE - 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2), + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2, .br), else => unreachable, } } @@ -285,6 +286,62 @@ pub fn draw1CC1B_1CC1E( } } +/// 𜸀 RIGHT HALF AND LEFT HALF WHITE CIRCLE +pub fn draw1CE00( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + sflc.circle(metrics, canvas, .left, false); + sflc.circle(metrics, canvas, .right, false); +} + +/// 𜸁 LOWER HALF AND UPPER HALF WHITE CIRCLE +pub fn draw1CE01( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + sflc.circle(metrics, canvas, .top, false); + sflc.circle(metrics, canvas, .bottom, false); +} + +/// 𜸋 LEFT HALF WHITE ELLIPSE +pub fn draw1CE0B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .tl); + try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .bl); +} + +/// 𜸌 RIGHT HALF WHITE ELLIPSE +pub fn draw1CE0C( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .tr); + try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .br); +} + pub fn draw1CE16_1CE19( cp: u32, canvas: *font.sprite.Canvas, @@ -491,6 +548,7 @@ fn circlePiece( y: f64, w: f64, h: f64, + corner: Corner, ) !void { // Radius in pixels of the arc we need to draw. const wdth: f64 = @as(f64, @floatFromInt(width)) * w; @@ -516,9 +574,8 @@ fn circlePiece( var path = canvas.staticPath(2); - if (xp < wdth) { - if (yp < hght) { - // Upper left arc. + switch (corner) { + .tl => { path.moveTo(wdth - xp, ht - yp); path.curveTo( wdth - cw - xp, @@ -528,8 +585,19 @@ fn circlePiece( ht - xp, hght - yp, ); - } else { - // Lower left arc. + }, + .tr => { + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth + cw - xp, + ht - yp, + wdth * 2 - ht - xp, + hght - ch - yp, + wdth * 2 - ht - xp, + hght - yp, + ); + }, + .bl => { path.moveTo(ht - xp, hght - yp); path.curveTo( ht - xp, @@ -539,21 +607,8 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } - } else { - if (yp < hght) { - // Upper right arc. - path.moveTo(wdth - xp, ht - yp); - path.curveTo( - wdth + cw - xp, - ht - yp, - wdth * 2 - ht - xp, - hght - ch - yp, - wdth * 2 - ht - xp, - hght - yp, - ); - } else { - // Lower right arc. + }, + .br => { path.moveTo(wdth * 2 - ht - xp, hght - yp); path.curveTo( wdth * 2 - ht - xp, @@ -563,7 +618,7 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } + }, } try canvas.strokePath(path.wrapped_path, .{ From cff6860fd936b3265adf01db068f8b86a7458aa6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 17:03:10 -0600 Subject: [PATCH 5/5] font/sprite: update reference images --- .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 632 -> 1006 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 819 -> 1255 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1492 -> 2247 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 443 -> 751 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 5448 -> 5427 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 5724 -> 5718 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 9973 -> 9974 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 4295 -> 4334 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index 03305c81c24482574aa134051787ecb75293f0ab..b3cda82d1391bf8a59367dae98436e1ed243ef7f 100644 GIT binary patch delta 943 zcmeyt@{WCiay|1_PZ!6KiaBp?T+BVJAi$PjmcZh6f}!>VN6!R7BPQhvmi&PJ0~~UV zf)Wqczgiaf_Li4}$)l?O-yg~J`R!z2OXd-rm~r5nv%R0+bLQWCT3u%%7hsUxzRPAk;`l>)(sdyhX0T|fT${5#IBV>wSRaIxP! zwNR9AQ^EE*lQS%DCR#4NSmct}`u+4tKCb#u380P!`>Z{Bh1*3^wA;fhPK5E|;IDasHe^5lMOFqcbZr`4F;oS$A)}KDd(wNVx=J-5P zZCda*frtmrPp8OC+n4@V`?Kd#&`qTc z2VQS7c6<=sc=BT9oKB!F7O@`zmZ|;Kjf@Np%l>RXux!D+2lmboo|NqTlMoyq94GaFSm!8xF zGZbtx149lgSjpXkY)1_QSTFql|9X2xh^zDZg;if=ywrWpsP5Kz#0*yZVBhKIs(nZQ zpMS^Mb?n}$g{*v4+U;T%FIqQ0o#XN}rTys?x2GKknBJd0$;V_bDgm<5JZq0$;dYTP zVSKs;7osh{Xc*l81~d%lq?;SLSQU9(E&}bluGi$$({b2ZeA^7AM7@aA_V@4WwhE}( z^v~sO$>&sVJh$1WXZ;ripC17=OOM+og52UB5kK$R_k~App19P)y?NqNPVuxx{lB*_ z-D`U|by^blp_b`>RoX1*QYJ}_*KujO;d}R z(W~RT!+^WyW&G4{W<2Y+h(tpy4qhq`R0?$GrHz~n20Y9M|876Wrs&e!`rfLv?!)1p zHQdV_KgsnLpOS52<^IRR267Y(Ja`0TfkNGo7szN>@NaGJLuP5TV4sby#`hsRb~SGi d<{+yn;C6n=uyXZB!($+OJzf1=);T3K0RW(Q_2~cr diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index c17e08c39e013a569c43f4e933fd0280ecbf106a..e076da7c52f1c09e553d278e5025d464d17985db 100644 GIT binary patch literal 1255 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GK00NFLPi8YPu&ncRaSW-L^X6uLVT*wP zYk`b{qR@l}78QkuDlSZulo&ZDZV-x7XSLFOdc$w}+%tvmzb~EpIL%V-r+bE!lG37T z#tpxpr}6|(dVAb^7Tf&IXAVUC>pSqKm`y%cW+^DJx1MplQ@2B?Yi&?y(S^EMOL-;rv)_8nzk4cd0h--xhhvM#S=74PD*%lgACA9lWQ_hT9JGa`42LJKa(9lUPm0tR2w8vbVQ zn1BArem?n{=hq}AG%;%aEBeUuqRBt6n^6~hMrEN#72^g!@+_Rez4soUyws;4LOh)8G{1SBjt z-~iHnV6Qxo1ln@zASaWffXl@{o6Q(n<4Owb&Q?EI%I_NZAoCjkgZEkUXJ08QYR$6T z>9loD%)%(Ybpg8S;VY&whyVDKny{d;k&R7CA|pZJ$BQfR$*)zzSCpL;T@?$|Fg<4B zEfALxXp30ICy~!jDpRsyx+iOgoCd0U`h@Fh(Sxqd=XLfk-}^su-)@;?h>1B8FIXXt zbL2YY0Ce2%`?r`SUM%4KDS0pY+9D09%~G-_>P0F~U1KlGnj{_KUDTZAnHQjYl6RHr zPRFe*Kszh8TKgN8&vW;C3l#kNgzah-P>n56a^9TGqUfW?q?$ z2M)~aQWgR+w|;5Pn*~%o38)ImU`B=qhaeGm-H;0yT&x#=&yP}O-nvk%b>qbE36uT4 z1>gL^P?ue^y{__)HZ%b!Tu^jk2PXxE0D%_*3!8z70?5+fDCxKWWr?sbF)%!s^j-Wx zMWy9EIXyZ1A1}q=+9#_W>I({yJVMr_JC*>kWdwHy+E*EjMu z81S%O{JVV_e}{8s!IJM{AFh|mnIxa-%xiE(cmO`=-id3XQSf|{!ah#L*v=i zjfiymr2=RY6f}s716iO1YRC&@G%WbH_U~B+X`m~SKmjmPQPQyVq$X4qe}GZ&|NmQr gA!{EpA{%lbGLdbuqQJeqLLg&3UHx3vIVCg!0E+Sig8%>k literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GK00NFLPi6x-zdT(WLn`LHxv-I|L4mvK0)g|M02jI+r z_B^W5eXkdt`TTfMd1Uf}0}hRiOw6ooOkmBf_7F>M9pq$k6mYrtXR{f@(YP&%c89AU zEai6%+_3y&KkwH4v#%5twPsoFblN&6W?_`yx&U4E@DUzM|};=&D$thUqa2Z-KasKuh#KJkfl1^7Dzc=%!584msVZTsA-QpMUlL zh`QZt41q>L%oM2E__cA)-aQaE-b8X^tK62O??2qz$me?T&AeyTo_PVfCwW(??sVL$ zV!mS96Sk{DKs^uE&$T%^$EN?zt;y0MuYvMkff7|k%~`fUb#gyC@3K!je|uj2wC|Hl z`AiBHG&1wbWIS;AaOv9eitI_!A>KfhNCpF=oFU8>;*`UNyg)|7f`4oGzGaXG2PKdX z1>!&&C2plBHK8gw;DfFNGy3naLQH|%wDut*noS2{z{b2qxTg0ZGeXUDhJe-1Cq8#1 QyMr9&>FVdQ&MBb@01+}Y3jhEB diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png index 6f4a004d497fbb023e9bfb3d94b4268f823015e4..366be38671868fa0eeefedb0a19356ef4d3c2b59 100644 GIT binary patch literal 2247 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 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png index a822c4f58a76d4becfb6991ca4974fe3c528e2d2..5cd7a4efeb2fac99fcaf19e9b0493909aaecfce6 100644 GIT binary patch delta 728 zcmdnZ{GN4!ay`>FPZ!6KiaBqtU(7wMAkdJQe}JQB0^_d-f)WqiPH@Pz@}A<5YZR1d z6pXQ4yXx4&pp$_!Fa57B?3r?Tu4T>)H3r?>_Scp@EhVQDyd8VALQ1xGcjp>a_nnS= z^V?tBu3PQeoNpdxXM2mP+xn-k-#-0?=|z=x)faMIla4q~o*7X;-H-2g$r;7^KX-Ng zu5YtayZv~=*-%bNPb1GDy?trxTG>8d-z8zTcS-y4qG?xc`*vZlK0yL*|}+ti}e z38}GBPZx*H>G^Nx+`M+-!+S-CYun%PZV29Cd?zk%$K7uQ&lMRM{_V;AUKuPi*YAJD z^&fV*T(6i}zxFj%xjXJ%yrBMGV8DBg73Iz1Up5~WWPpLXqtZa;|Nn399Aslq;9-04 zz5aCv-)g?i&SLl7H%a`xQYr>h%E0jE+(q@*DetbI)RmD6 zu|vvxK|#R96XGi07yb;`(B$y?x(LIA#%Eq$`q%fxh;Uf-uHMMP#oo`_6#IU=|EZnx zS(z9jvezy7QuOw;YvY2;VErE=6mCrTv;1s(lPJrlQ;Y@2SixSp>f3eLfT!W&@A*-8 zZ`E@yeA(Zsw4Z&m0dtt0zE=HmR_V`{e*e$?TqV!3YF+FF^INt}3tq8QvN--^DZTPe zkR|lG9tVS+vF`T6i4iS=`Tg7VV*M{X{djHZ>r##=d+VKk0S7rxK0f7Bac)Q1e#fSN z8Rr)yn(}*B-@4x(T34EBQ4c1=rzRd4TJ>CVMb0MqhZ1S_0p4?n81=S;(#Mq p$=kz*ykI5&)}DR9FAY+2Ky2os(8bril^8*4Jzf1=);T3K0RTc4NEHA8 literal 443 zcmeAS@N?(olHy`uVBq!ia0vp^7Z?~ACvX5648I<1EC6zvJY5_^D(1YoZphVOz~gZ7 z-v3Ko-3l5zMC=~!U3+$teVQ&vF+;+<$3^yW_41SVyx+=d%CSl~M16%{z}y8^j-`j2 z|H-<86*1%#GBGgx|NrKWAs2%J2kV93_pLaj*02?7w0~DPZ9K7Y;R zo$>06OWtMHS~f0N{~>O&>r%s-zLY?vfQ=s`6&M&E>}CX;cf*nEsDc19(7a#r2mN)- zWbZfY#qjtn*wyhq^X$|qaqDU=U6q@AR&QkCVi)bLaV(Ob#m}nEv8B$Jk>THk7|)ka zZ%yxJa$H_$I?dtXgN+UwUjO~(X;}`mqG9VZFE9P;`(i{mta`!v`&pZKPuAb6(TH$V zSTDnH=OH`T35N}NfsSri@Neze2mI1NZz6*lh%*m2@&H8-DAd1Zj#;>k1z80H!wewz r|Npm#899NBh6VrE{}$u|#SAy2p~?A7_3NUGLFzqS{an^LB{Ts51a+Sc 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 index ecf6eae26b9a2cfcd2d027d2c15f9373eb6c0d9e..023396feb4c6f9c643c7874fcaa14f51f5fe8420 100644 GIT binary patch delta 5237 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 delta 1912 zcmV-;2Z#99EZi)RBmpC_C2I$N-<8}f48_w)N(r5$w3Lt%mQGTNbF#}Hp?jqT#yAhF zd(QW;{=hT(MaUoqIsOZULZL4vT$^d^Mf9-uMJ^2pI!=|ZJVbD;#iDm-F^<3bF7-X0*8W_jZ9 zwsfJ=rnyjtUX>lO!f_$SZVM2r|0e%Dar|GpP;FB%B1BwZX@%FkOg}+XBRDP^t^JmmrZY)Y`m*NYJZ?Z)%0%LL3~0W8S)mRWa2U z?k-{@U8uErCy}97p8!yQD-0Ln+|X3Kwcgd3*=#~Bl|kb3sjN-?hav%1@%-oi%oK%z z&9tZA75}29`wc#6z86*cpSr`M)w5Q!y?^w^gvzBS86q_-h%G%T` zC4EIYkQFEh3@db-_8^XSWoEO9Xr-9nH$IqvXfGgy0MH1re6l+CaJ}-1wuOSCwZ-qxh8O_^Lkx$Zy#y74AvO-f zj}O!59Wv{sU~X_!x8kY<2%kj6khIpMUsx3H>NSI-NQjMM(J^!Lw`}UY}bpW-L zE2d=^97v>GF)ho^Jsb}~&W{Q)Y3~85U@)=2K zR+P%$=3qRCd^pe)jtpZR0GOp16&jIqj(A{!P zUTYnI0+988&E{JF&lq$LSpfP()a1Va00960?AJk(f-n$O2PY< z*n!U5IX4aH#6Yd{$B+Sv?-^MG&~<=XZ=4|m6igp{OGR6l_0Kj#-ARf3$I!Z;{}dEGKU zay~x=o@0@D?h9$;J62%Wr{2U_GBtd3VZ4@W* zM6~u9C$wSw$iG;Mdb~}7jOOVu&}LT|#AkRV637p_d~%3Tn*u?5TQm1QhC9d8o5qoK7(u7V1DSwJtEY9DnaVb5;C>wLZ0~yF7^Q%TIS_J6_T?8 z3m88aWTc{ygk1LFr6FGS$*4*?%x6#Am~&o;tmW*%vAB2lSoIZ;sae^JVCL;qp_S zy`P}z%TVh5zRGNVbGYubyw>%#^!pQ~taIA76tKUJ+5E9l)XfsMCH=_atvJMgMg3laRJjckbwBd)FFl1}!L0=82X<6gZ~@8w zU#0XbyOkh44awB5j!;owC)iDFX>ZbKFP18v+0T0B{z*S!#lu9g2Ja00006UunmM7NgfcnB|sC@Y}bzr+q?WV0q+I z+v_!AdUK|Hwd{?5z36eIMjzvaY6dB}hBkJE*UjJ97sxZ4mNQIYQD}H4-tdP(<+H7< zgAs#h8pF=X8<-cUFq~Y|Say+7gl)ksPli{o88mJ`_CLheu*vqH${P!AkNS1FOPE*K zGF(k(u#{y;%>{}wXe?yTNM=|yU5>Sck%8g=|2NkZ`3^X6xE}m{f7h}+=GHW40pqgE z{T0imo3RM3u+zHmQz<~__r{L>E7wntW9j0&VrAjK;M&1TrmcMEPBVSIJookeY^J{| z7mAp4)h`q^H!^@hg)NZ!zxj=@n_zv7S?~{tRkH&54lD3Dzpw9#Ewz7~oaHP0NFwFx zrGFVJztp7Otf-rRr=G)lT|0xU#df4`UzhTU7q6cR&ztuz{tj!&`R6z0d_1f@NyQV0ZnA={J8Z}c z4A_PR|JLe0gTe~DWM4fY=kip delta 700 zcmez7`_*@X3ge!Qs;7nOZv}E4HsEP~`1}4Yb`8O48=4jOC-^g^-u@ntak_m%>qPx4 ztBMv~e!6JR{^cJtVsobSF};v!n8v=~DPu+@!=}0me?|V^>Emsf#K01eDX_qqA#2wi zKUM|F1}#;GG~qmkDJ&rN(^J-*+g~tnF<;1Gzp$TC!}@ssq2BO@s+SD)S^N&NvJU^* z7UZ1GKg8Rx$#g%bc^qR<`mYj(*fSy**cll9|9^8;k?(*3N882U_p8#@B{sJS9I5{1 zzv73>>^u&YfI8t9F)UrowYnd-{@K0Kb-@gWt%Y--IOhHPlDPl-)h?(uP7}_sY@8;| z!@>Xp2VOIRP2T)L*hR4Z%1q5aKtZ5QvjX`JEATkKukVU2jjst=o9QIeye8_Fe6P!W zm*%J#QO2 zes_wlJ#3h7gNv2v6friy<^vh2K_tb%t_qe4+<_Ruz#SgeYfGpNv{Hy znQM4{Tnsl?DElHRXTGFfhb)L5`>2hd4Y_E1^?FS zKID{U0mdKI;Vst09A-K0{{R3 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 index 3fa06a8fc969f3641e2f6d2a02871c11b4bf20ff..062a7da810b835f0f27d0c9b540ce22f3207e157 100644 GIT binary patch delta 4142 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 delta 4081 zcmYjS2T+rX(oI5Wp@m+R(1{=-AT5+olp;+KL7Iw`(4}|22nZxfZ&CuG2qJ;ohG<=h`Nf=*`E0jekieB0CB^S1TRQdO<)*jDIUj|&!mY{2Dt9HCNao<{ zAZ|46NI3cx)nvg?8!aXCO3C5Z2@P88I6~~ho+9??9KNb!FNSUe^igZMm|j0$MkzMu z?hy~I+fffthW;!TfA;l-HY}`|>SCADim8BKqbKv;0d;;>eW_%nP76P-zBRim=-Q>a zk6<6=V0m@<6oSYXee07ZeX(b;7{s+%|L0Ude9qR9x2W$S7t*raFg?=6N-)^>W__vV zCBIUs`z$sanO;*7=W|o2`rZXeLXqtp$Lu#f83RCb zBJDhRN6DGd?EG8Bi=GV$-I32OhFpjuO*pX{239n(steY(kTeC8jqeRzqaOxpRn9ZzO<&e02YY*`W%|NYYs&-*=&olU70Re+2HNN6e-^NNT5w> z3+OanSEILJq98)0dmc@@9run&1}--8Edt;Od5Nr9k%#>}lC|j7mqwA+`TWGyp0t5H zuYoPl#)|K!$q+qC%>%;319Jz2t$jVTx?Os?Za6GT7?Qa!!t|3BF}R88cQ=-jb6FH3 zuE!{`cRZ0QR*J z2XZkaOYvrcDs#^TdIMS6IZl;YLmyn$;I3z;BB8e(i~sLYQ44QGCH$NnUiHO9fI=e( zsj$s@tn=CJ4R$2K)w#OM>&OZ#T5)(FqD)xm!$%XCKzGjj&*rxtc)Iyi$eju)(0`e^ z{}8dXJotujveJEEBjUIRuqJGwAYPvY3ipSKr;vf&`aA~;)}F&|9-E-S=bz?fnbqvk z6OneJh>ga{5SziE$km43xQ|PyJf~_+rh}#w4#bA8vj$(_ls^^!@oUtkk=6vS89vHY ziJUhiAWu?ji`2B)rK7r|_88MI?6ghjEM}2RAGJ47gZd}%Q8WOqp^`*Uwcki3VSUy9 z7|S7MDMP<FG*Avj3bs{J|F;%Is9@sLqtmm<|OI zbT0^QOa$VyW?6OV8{z`#Kj>sEydCSyNP-O1`7JKOovt2rzB&mBj|W`jB}IjdW;5-j_{zeCvB1dQ0GgUDE4TUBB~e7!u>^{ee7>9m*NJLIDd8!;R~2 zV!!>yQZY-`ac-JFS%YVmHOPr32nRlEV?~Q_xNQGcYHV;bCQX0NJGXx0^j=1eE9!;t`)N`6}7ETC>%|k+Kt2=w;v0kc;R_DgW#`n#XHkLMKGD=4G3q#{+~n_)C; zdcJkE*}I}8frl_PyCvw9g@ETj6*qS{(|s9nl+@*4vu!!f#o?~@sp*jRZawcn(&aR! z&d+r${D!R>qNrBVqH$lgJI!{=-MqiG&V45C@lj?*#8y&7J;YLAEo` zF0b)+WqUsejtXEdC-OVhysNmHYLH%6rLf3L?Gb{|~MGX<;r1a{c!LHyv%G z^HpU8n(6JWyWdjA(r>)iIs(3far%E(1nG6AHhplscRp?0u&IN7ZunqpRW@imx~^aw zg!X3~IRo)OaWIoY@7!0AKtrxWvrx!s#WdK5XgKkzIC_s&U;J+=L$d->4w^R4<;0y^ z{-5_9HGP+_z}?tqv#)1w8aY10nqqC{!Pj#we>qsHq@jS^F_sm<9+h==>6TT?4i%z9 zxuL`~7QB~O=;t2t4lfIfn1$=iU_uhM{H2F#OZ?PMRQjZ~o6$DUZrX2!D2zQS3H8hl zT|B2qZ-wRb$>UQSM33^rkOYsmFh)J_Ii^5)TGi2<@wZR-DX*Ud;o~nF4ADXPgo5-p z`-DuuX29zoT_zrMq3Uh1m0J&;Vrlv@(rA(-?LWhwcBCrN=rtOJtIM!3g!%9`FCfT2 zn$#Xc3NfY(O5$$OhdDv@_dfNm2peH=36R&01#!q0+<0>j_aM>r@)KWm8K$HhO6*%9 z{iB`RtQ8kY!EUl(DAIZ3&uhL#0>$1N71htk+pPw=| zAg|EJ*7K44uz3Of?E7GAR(wFIwca^K1mfxvmqqogJZRn^RGE110T0&*6Ih=~9}ih* z(=B3=-mE)w%o;r4-kCFmLJxI;zpJCw+@@o`BtW<#`q&rieGQ+`c0B$h!z3MCPhtbw znPvh;qovF2yHBS5OY2%GB|IZ-gJe>535eDWmUZC$Z7iR2kjX`gUf$;$8YdUluY+xI zNKq&@jr&rFaYCS?<8Xw7rRkSgNWD*A6v^AemECCScY9UVPLKuqWCt!^OjXe-;rxfw z?K@O7Tq%2`&?jBypfiRIsW1ofBMhEs(Xx1C#afOcyF`7xQVolNQ~V~FHbDpUB6j*? zOVG2&gZ-k>_zYWJtq*t1K*bR37|jsaX2$2I`K}S$O?Sq_u!MJD4H(1@)mvV|LNeRB z>4%e`{N!JxTbBl5?B2c8*j;_k^{Xu5=GD|x)kpMjZs=>*rWdAc(QH7Yq41>vyNIZc zI~v%hAfic+i{KRWJh_|sQ;w7HoyS~g3%CXHx#%VyxFRw3|5%WP5%P%4oHCD9WmFluE_V9EFz$NL-1jg zGgv|(+9aw`hU4`%Z_=J1 zfaNQ;MfVEq-Iw5%bHPccae|@>LD#hEu$F*rzKd(tf^Z`Syz#e{8NS5HB>l3B+4fkm z^|Vedd7Ixg>)MsSsUIJGfR8AKJpYw{dU`x5#VI6rA@I*;wtJXZ3hlN=G1Zr4#4H!n z_5Js!=F0g$9^qbCPwUbpwNplYN57mM|5qO`q1Q6+TEW00F{BsEM@RNF*Dt7;0!S#j zf6;*MsCR>mERr%&-J7856+dE-p*!@sUCFsvPkv?kDc2+^P+@wao{X&TTPRZ$ipRnWm@JV_2 z7dk_Nu8_WAcd2x}|U9DN~ror!yhlfuH zjwI8i5~&^6=S#y|C^j`A*2MOU{Mdvaz&Fe3lB)JI`RY^u0PsKj9|Wihv33_)JmuPN zVp!BxIcFpLcwBXKRkRUSX_uUua=oFPGSwYfP_E8-z;DQGb|VoAQ?IMHF=-TM@QS%o z7F-%ITusIV*9LhH?ZS-2GhLJ)-Y;Vwp_F=fpFhWiGH#v3$$D;y@z-#?*}IM!i=zS- z)fvJ;7sT{+?wW#p8}FatCanyYo=RTvi!5KQ3Z9z4LOJVAGe9z(!#fkHv6~FM;XFfMG>*;7U*glVpbZvROQ>QWP+4EH{pduq>2wh!NN>CEGXU{^$JfOHL zGdD1^NbrAPVd#`iCu831#awOM5sd>(&XbMGz;6dxALZaOwBMw}MIY4Zc0|Jl{A+5J zmt1car;fljs{alOK-G?7@}v)}el^sCS?`HXRJ1g=+&lUy2!V<;l5u7Hz86~gF6p<+ zb!g9D`@x&T!s8lPVIMehQEkdP<{gSi_&S>NJ+}kY88cG03xn++AV{919!O?cW(2DM z(A#8cWvVwQ^4QVBRcDy%HmH7C>||ZwE)i4yC6QMT&C329un>WaD|EIz2j+)sY{WQQ z99xLP7q1@C_}O676f>~N%u_xJG);Q$riY? zWgb4;R`93Qq;DpRQ_g5bdZFHsR`4AlV!>bFhP^ps@aMimt{cH*IV&R2(yYBRHhBs@ zUudV;q9NqKr)@GD79dbECLDkk5T|zeE#w+k3|-WZNa` zWIN_o!(;8uhcpqf4|};2?P4SU=nEI%fI-@C$I+WSl`5JiEM5w`6KhVG)Sp#SJnlC+ zBq`g6w*a#Rx)lI_rfE3W%8{5)2q3}BYlS^?X(p65tJFO`Qx@y?;4ck0&H}6P7iIN+1gX60L8?cNX^AoB*TMP z^UOMEO;L%&>cU_84kNFt9nur=10|bjtNN)#MK+x3d6{ed%1%xRmc+BA-GO-3w8W7Z z&<332+kzt(v{A#$J36