font/sprite: rework undercurl, fix out of bounds underlines
Use z2d to draw the undercurl instead of the manual raster code we had before- the code was cool but unnecessarily complicated. Plus z2d lets us have rounded caps on the undercurl which is neat. Also make sure we won't draw off the canvas with our underlines-- the canvas has padding but it's not infinite.pull/9654/head
parent
410d79b151
commit
81a6c24186
|
|
@ -30,6 +30,7 @@ metrics: font.Metrics,
|
||||||
|
|
||||||
pub const DrawFnError =
|
pub const DrawFnError =
|
||||||
Allocator.Error ||
|
Allocator.Error ||
|
||||||
|
z2d.Path.Error ||
|
||||||
z2d.painter.FillError ||
|
z2d.painter.FillError ||
|
||||||
z2d.painter.StrokeError ||
|
z2d.painter.StrokeError ||
|
||||||
error{
|
error{
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,19 @@ pub fn underline(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
// We can go beyond the height of the cell a bit, but
|
||||||
|
// we want to be sure never to exceed the height of the
|
||||||
|
// canvas, which extends a quarter cell below the cell
|
||||||
|
// height.
|
||||||
|
const y = @min(
|
||||||
|
metrics.underline_position,
|
||||||
|
height +| canvas.padding_y -| metrics.underline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(metrics.underline_position),
|
.y = @intCast(y),
|
||||||
.width = @intCast(width),
|
.width = @intCast(width),
|
||||||
.height = @intCast(metrics.underline_thickness),
|
.height = @intCast(metrics.underline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
|
|
@ -38,20 +46,28 @@ pub fn underline_double(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
// We can go beyond the height of the cell a bit, but
|
||||||
|
// we want to be sure never to exceed the height of the
|
||||||
|
// canvas, which extends a quarter cell below the cell
|
||||||
|
// height.
|
||||||
|
const y = @min(
|
||||||
|
metrics.underline_position,
|
||||||
|
height +| canvas.padding_y -| 2 * metrics.underline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
// We place one underline above the underline position, and one below
|
// We place one underline above the underline position, and one below
|
||||||
// by one thickness, creating a "negative" underline where the single
|
// by one thickness, creating a "negative" underline where the single
|
||||||
// underline would be placed.
|
// underline would be placed.
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(metrics.underline_position -| metrics.underline_thickness),
|
.y = @intCast(y -| metrics.underline_thickness),
|
||||||
.width = @intCast(width),
|
.width = @intCast(width),
|
||||||
.height = @intCast(metrics.underline_thickness),
|
.height = @intCast(metrics.underline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(metrics.underline_position +| metrics.underline_thickness),
|
.y = @intCast(y +| metrics.underline_thickness),
|
||||||
.width = @intCast(width),
|
.width = @intCast(width),
|
||||||
.height = @intCast(metrics.underline_thickness),
|
.height = @intCast(metrics.underline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
|
|
@ -65,12 +81,32 @@ pub fn underline_dotted(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
|
||||||
// TODO: Rework this now that we can go out of bounds, just
|
// We can go beyond the height of the cell a bit, but
|
||||||
// make sure that adjacent versions of this glyph align.
|
// we want to be sure never to exceed the height of the
|
||||||
const dot_width = @max(metrics.underline_thickness, 3);
|
// canvas, which extends a quarter cell below the cell
|
||||||
const dot_count = @max((width / dot_width) / 2, 1);
|
// height.
|
||||||
|
const y = @min(
|
||||||
|
metrics.underline_position,
|
||||||
|
height +| canvas.padding_y -| metrics.underline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dot_width = @max(
|
||||||
|
// Dots should be at least as thick as the underline.
|
||||||
|
metrics.underline_thickness,
|
||||||
|
// At least as thick as a quarter of the cell, since
|
||||||
|
// less than that starts to look a little bit silly.
|
||||||
|
metrics.cell_width / 4,
|
||||||
|
// And failing all else, be at least 1 pixel wide.
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const dot_count = @max(
|
||||||
|
// We should try to have enough dots that the
|
||||||
|
// space between them is the same as their size.
|
||||||
|
(width / dot_width) / 2,
|
||||||
|
// And we must have at least one dot per cell.
|
||||||
|
1,
|
||||||
|
);
|
||||||
const gap_width = std.math.divCeil(
|
const gap_width = std.math.divCeil(
|
||||||
u32,
|
u32,
|
||||||
width -| (dot_count * dot_width),
|
width -| (dot_count * dot_width),
|
||||||
|
|
@ -78,13 +114,11 @@ pub fn underline_dotted(
|
||||||
) catch return error.MathError;
|
) catch return error.MathError;
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < dot_count) : (i += 1) {
|
while (i < dot_count) : (i += 1) {
|
||||||
// Ensure we never go out of bounds for the rect
|
const x = i * (dot_width + gap_width);
|
||||||
const x = @min(i * (dot_width + gap_width), width - 1);
|
|
||||||
const rect_width = @min(width - x, dot_width);
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = @intCast(x),
|
.x = @intCast(x),
|
||||||
.y = @intCast(metrics.underline_position),
|
.y = @intCast(y),
|
||||||
.width = @intCast(rect_width),
|
.width = @intCast(dot_width),
|
||||||
.height = @intCast(metrics.underline_thickness),
|
.height = @intCast(metrics.underline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
}
|
}
|
||||||
|
|
@ -98,19 +132,25 @@ pub fn underline_dashed(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
// We can go beyond the height of the cell a bit, but
|
||||||
|
// we want to be sure never to exceed the height of the
|
||||||
|
// canvas, which extends a quarter cell below the cell
|
||||||
|
// height.
|
||||||
|
const y = @min(
|
||||||
|
metrics.underline_position,
|
||||||
|
height +| canvas.padding_y -| metrics.underline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
const dash_width = width / 3 + 1;
|
const dash_width = width / 3 + 1;
|
||||||
const dash_count = (width / dash_width) + 1;
|
const dash_count = (width / dash_width) + 1;
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < dash_count) : (i += 2) {
|
while (i < dash_count) : (i += 2) {
|
||||||
// Ensure we never go out of bounds for the rect
|
const x = i * dash_width;
|
||||||
const x = @min(i * dash_width, width - 1);
|
|
||||||
const rect_width = @min(width - x, dash_width);
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = @intCast(x),
|
.x = @intCast(x),
|
||||||
.y = @intCast(metrics.underline_position),
|
.y = @intCast(y),
|
||||||
.width = @intCast(rect_width),
|
.width = @intCast(dash_width),
|
||||||
.height = @intCast(metrics.underline_thickness),
|
.height = @intCast(metrics.underline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
}
|
}
|
||||||
|
|
@ -124,105 +164,66 @@ pub fn underline_curly(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
|
||||||
// TODO: Rework this using z2d, this is pretty cool code and all but
|
var ctx = canvas.getContext();
|
||||||
// it doesn't need to be highly optimized and z2d path drawing
|
defer ctx.deinit();
|
||||||
// code would be clearer and nicer to have.
|
|
||||||
|
|
||||||
const float_width: f64 = @floatFromInt(width);
|
const float_width: f64 = @floatFromInt(width);
|
||||||
|
const float_height: f64 = @floatFromInt(height);
|
||||||
|
const float_pos: f64 = @floatFromInt(metrics.underline_position);
|
||||||
|
|
||||||
// Because of we way we draw the undercurl, we end up making it around 1px
|
// 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.
|
// 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
|
// We use a minimum thickness of 0.414 because this empirically produces
|
||||||
// the nicest undercurls at 1px underline thickness; thinner tends to look
|
// the nicest undercurls at 1px underline thickness; thinner tends to look
|
||||||
// too thin compared to straight underlines and has artefacting.
|
// too thin compared to straight underlines and has artefacting.
|
||||||
const float_thick: f64 = @max(
|
ctx.line_width = @floatFromInt(metrics.underline_thickness);
|
||||||
0.414,
|
|
||||||
@as(f64, @floatFromInt(metrics.underline_thickness -| 1)),
|
// Rounded caps, adjacent underlines will have these overlap and so not be
|
||||||
|
// visible, but it makes the ends look cleaner.
|
||||||
|
ctx.line_cap_mode = .round;
|
||||||
|
|
||||||
|
// Empirically this looks good.
|
||||||
|
const amplitude = float_width / std.math.pi;
|
||||||
|
|
||||||
|
// Make sure we don't exceed the drawable area. This can still be outside
|
||||||
|
// of the cell by some amount (one quarter of the height), but we don't
|
||||||
|
// want underlines to disappear for fonts with bad metadata or when users
|
||||||
|
// set their underline position way too low.
|
||||||
|
const padding: f64 = @floatFromInt(canvas.padding_y);
|
||||||
|
const top: f64 = @min(
|
||||||
|
float_pos,
|
||||||
|
// The lowest we can draw this and not get clipped.
|
||||||
|
float_height + padding - amplitude - ctx.line_width,
|
||||||
);
|
);
|
||||||
|
const bottom = top + amplitude;
|
||||||
|
|
||||||
// Calculate the wave period for a single character
|
// Curvature multiplier.
|
||||||
// `2 * pi...` = 1 peak per character
|
// To my eye, 0.4 creates a nice smooth wiggle.
|
||||||
// `4 * pi...` = 2 peaks per character
|
const r = 0.4;
|
||||||
const wave_period = 2 * std.math.pi / float_width;
|
|
||||||
|
|
||||||
// The full amplitude of the wave can be from the bottom to the
|
const center = 0.5 * float_width;
|
||||||
// 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.
|
// We create a single cycle of a wave that peaks at the center of the cell.
|
||||||
const y_off: u32 = @intFromFloat(half_amplitude * 0.5);
|
try ctx.moveTo(0, bottom);
|
||||||
|
try ctx.curveTo(
|
||||||
// This is used in calculating the offset curve estimate below.
|
center * r,
|
||||||
const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(
|
bottom,
|
||||||
1.0,
|
center - center * r,
|
||||||
half_amplitude * wave_period,
|
top,
|
||||||
|
center,
|
||||||
|
top,
|
||||||
);
|
);
|
||||||
|
try ctx.curveTo(
|
||||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
center + center * r,
|
||||||
var x: u32 = 0;
|
top,
|
||||||
while (x < width) : (x += 1) {
|
float_width - center * r,
|
||||||
// We sample the wave function at the *middle* of each
|
bottom,
|
||||||
// pixel column, to ensure that it renders symmetrically.
|
float_width,
|
||||||
const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period;
|
bottom,
|
||||||
// Use the slope at this location to add thickness to
|
);
|
||||||
// the line on this column, counteracting the thinning
|
try ctx.stroke();
|
||||||
// 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(
|
pub fn strikethrough(
|
||||||
|
|
@ -253,9 +254,18 @@ pub fn overline(
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
_ = height;
|
||||||
|
|
||||||
|
// We can go beyond the top of the cell a bit, but we
|
||||||
|
// want to be sure never to exceed the height of the
|
||||||
|
// canvas, which extends a quarter cell above the top
|
||||||
|
// of the cell.
|
||||||
|
const y = @max(
|
||||||
|
metrics.overline_position,
|
||||||
|
-@as(i32, @intCast(canvas.padding_y)),
|
||||||
|
);
|
||||||
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(metrics.overline_position),
|
.y = y,
|
||||||
.width = @intCast(width),
|
.width = @intCast(width),
|
||||||
.height = @intCast(metrics.overline_thickness),
|
.height = @intCast(metrics.overline_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
|
|
@ -335,11 +345,19 @@ pub fn cursor_underline(
|
||||||
metrics: font.Metrics,
|
metrics: font.Metrics,
|
||||||
) !void {
|
) !void {
|
||||||
_ = cp;
|
_ = cp;
|
||||||
_ = height;
|
|
||||||
|
// We can go beyond the height of the cell a bit, but
|
||||||
|
// we want to be sure never to exceed the height of the
|
||||||
|
// canvas, which extends a quarter cell below the cell
|
||||||
|
// height.
|
||||||
|
const y = @min(
|
||||||
|
metrics.underline_position,
|
||||||
|
height +| canvas.padding_y -| metrics.underline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(metrics.underline_position),
|
.y = @intCast(y),
|
||||||
.width = @intCast(width),
|
.width = @intCast(width),
|
||||||
.height = @intCast(metrics.cursor_thickness),
|
.height = @intCast(metrics.cursor_thickness),
|
||||||
}, .on);
|
}, .on);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue