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.
This commit is contained in:
Qwerasd
2025-07-01 16:00:35 -06:00
parent ac87154362
commit c838d3d7d2
6 changed files with 108 additions and 180 deletions

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,
};
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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"