feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed
This commit is contained in:
1 parent
3ff7b9c978
commit
5ca3587325
17 files changed
+3422
-627
No files matched your search
+81
-48
@@ -119,6 +119,24 @@ bool pixel_is(const plot::u8 px[4], plot::Color c) {
|
||||
return near(px[0], c.r) && near(px[1], c.g) && near(px[2], c.b);
|
||||
}
|
||||
|
||||
/// True when `px` holds `c` at some coverage -- the anti-aliased case `pixel_is`
|
||||
/// cannot see. The embedded atlas is greyscale and the renderer draws it at
|
||||
/// `Style::axis_font_px` from a 13 px bake, so at the default size a glyph stroke
|
||||
/// is barely a pixel wide and *no* pixel is ever fully covered: demanding the
|
||||
/// exact colour finds no text at all.
|
||||
///
|
||||
/// A partially covered pixel is `c` scaled toward the frame's dark chrome, so this
|
||||
/// asks for every channel to be at least a third of `c`'s: bright enough to be
|
||||
/// glyph ink, dark enough to exclude a background, and deliberately blind to which
|
||||
/// of two chalk-toned chrome colours a pixel is -- which is why every caller scans
|
||||
/// a region only one element draws in.
|
||||
bool text_ink(const plot::u8 px[4], plot::Color c) {
|
||||
const auto lifted = [](plot::u8 got, plot::f32 want) {
|
||||
return static_cast<plot::f32>(got) >= want * 255.0f * 0.35f;
|
||||
};
|
||||
return lifted(px[0], c.r) && lifted(px[1], c.g) && lifted(px[2], c.b);
|
||||
}
|
||||
|
||||
/// No column held ink.
|
||||
constexpr plot::u32 kNoColumn = 0xFFFFFFFFu;
|
||||
|
||||
@@ -132,6 +150,7 @@ class OnePxPerStringBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 1.0f; }
|
||||
plot::f32 height() const override { return 1.0f; }
|
||||
plot::f32 ascent() const override { return 1.0f; }
|
||||
plot::f32 em_px() const override { return 1.0f; }
|
||||
const plot::render::Glyph* glyph(plot::u32) const override { return nullptr; }
|
||||
plot::Vec2 cell_uv() const override { return plot::Vec2{}; } // no atlas, no cell
|
||||
const plot::u8* atlas_pixels() const override { return nullptr; }
|
||||
@@ -190,6 +209,8 @@ class BakeableBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 8.0f; }
|
||||
plot::f32 height() const override { return static_cast<plot::f32>(size_); }
|
||||
plot::f32 ascent() const override { return static_cast<plot::f32>(size_); }
|
||||
/// Its cell is the whole atlas, so its em is the bake size: one unit is one pixel.
|
||||
plot::f32 em_px() const override { return static_cast<plot::f32>(size_); }
|
||||
const plot::render::Glyph* glyph(plot::u32) const override { return &glyph_; }
|
||||
plot::Vec2 cell_uv() const override { return pixels_.empty() ? plot::Vec2{} : plot::Vec2{1.0f, 1.0f}; }
|
||||
const plot::u8* atlas_pixels() const override { return pixels_.empty() ? nullptr : pixels_.data(); }
|
||||
@@ -217,14 +238,16 @@ struct InkBox {
|
||||
plot::u32 height() const noexcept { return any() ? y1 - y0 + 1u : 0u; }
|
||||
};
|
||||
|
||||
/// Scans the whole framebuffer for fully covered `text`-coloured pixels.
|
||||
/// Scans the whole framebuffer for `text`-coloured ink, anti-aliased fringe and
|
||||
/// all (see `text_ink`). These cases draw nothing but the run, so everything lit
|
||||
/// in the target's colour is the run.
|
||||
InkBox scan_ink_box(plottest::HeadlessContext& ctx, plot::Color text) {
|
||||
InkBox box;
|
||||
for (plot::u32 y = 0; y < ctx.height; ++y) {
|
||||
for (plot::u32 x = 0; x < ctx.width; ++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
if (!pixel_is(px, text)) continue;
|
||||
if (!text_ink(px, text)) continue;
|
||||
if (!box.any()) {
|
||||
box.x0 = x;
|
||||
box.y0 = y;
|
||||
@@ -290,8 +313,8 @@ struct TextInk {
|
||||
plot::u32 widest_row_run = 0;
|
||||
};
|
||||
|
||||
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for fully covered text
|
||||
/// pixels in `text`.
|
||||
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for `text`-coloured ink,
|
||||
/// anti-aliased fringe and all (see `text_ink`).
|
||||
TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1, plot::u32 y,
|
||||
plot::u32 rows, plot::Color text) {
|
||||
TextInk ink;
|
||||
@@ -301,7 +324,7 @@ TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1
|
||||
for (plot::u32 x = x0; x < x1; ++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - (y + r), px));
|
||||
if (!pixel_is(px, text)) continue;
|
||||
if (!text_ink(px, text)) continue;
|
||||
if (first == kNoColumn) first = x;
|
||||
last = x;
|
||||
if (ink.leftmost == kNoColumn) ink.leftmost = x;
|
||||
@@ -661,8 +684,12 @@ PLOT_TEST(renderer_measures_the_labels_it_draws) {
|
||||
const plot::u32 y = static_cast<plot::u32>(labels.y);
|
||||
const plot::u32 rows = static_cast<plot::u32>(labels.h);
|
||||
const plot::u32 strip_x = static_cast<plot::u32>(labels.x);
|
||||
// Exactly the strip, and no overhang: the axes' own border line runs down its
|
||||
// right edge through these same rows, and a scanner that reached past the strip
|
||||
// would read that line as label ink -- it is the same chalk-toned colour family
|
||||
// as the text (see `text_ink`).
|
||||
const TextInk ink =
|
||||
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w) + 16u, y, rows, text);
|
||||
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w), y, rows, text);
|
||||
PLOT_CHECK(ink.leftmost != kNoColumn); // labels were drawn
|
||||
PLOT_CHECK(ink.leftmost >= strip_x); // inside their strip
|
||||
PLOT_CHECK(static_cast<plot::f32>(ink.widest_row_run) >= 0.6f * widest); // full width drawn
|
||||
@@ -1017,7 +1044,7 @@ PLOT_TEST(titled_axis_pairs_get_their_own_rows) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(strip.x) + column,
|
||||
ctx.height - 1u - (static_cast<plot::u32>(strip.y) + row), px));
|
||||
has = pixel_is(px, title);
|
||||
has = text_ink(px, title);
|
||||
}
|
||||
if (has && !previous) ++bands;
|
||||
if (has) ++rows;
|
||||
@@ -1261,11 +1288,13 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
const plot::Vec2 pen{300.0f, 180.0f};
|
||||
const plot::f32 px = 2.0f; // 16 px cells: six glyphs is over a hundred pixels
|
||||
const plot::u32 cell = static_cast<plot::u32>(8.0f * px);
|
||||
const plot::f32 px = 2.0f; // one cell is 16 x 34 screen pixels at this scale
|
||||
const plot::u32 cell = plot::render::EmbeddedMonoFont::cell_height() * 2u;
|
||||
const plot::u32 advance_px =
|
||||
static_cast<plot::u32>(font.glyph(static_cast<plot::u32>('M'))->advance * px);
|
||||
|
||||
const InkBox flat = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {1.0f, 0.0f});
|
||||
PLOT_CHECK(flat.any());
|
||||
@@ -1273,6 +1302,7 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(flat.x0 >= static_cast<plot::u32>(pen.x));
|
||||
PLOT_CHECK(flat.y0 >= static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(flat.height() <= cell);
|
||||
PLOT_CHECK(flat.width() > 4u * advance_px); // six monospaced glyphs, and then some
|
||||
|
||||
const InkBox up = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {0.0f, -1.0f});
|
||||
PLOT_CHECK(up.any());
|
||||
@@ -1281,42 +1311,45 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(up.width() <= cell); // one cell wide, so nothing marched right
|
||||
PLOT_CHECK(up.x0 >= static_cast<plot::u32>(pen.x));
|
||||
PLOT_CHECK(up.x1 < static_cast<plot::u32>(pen.x) + cell);
|
||||
// Ink above the pen: the first cell starts at the pen and every later one is a
|
||||
// cell further up, so the run's rows run from well above the pen down to the
|
||||
// Ink above the pen: the first cell starts at the pen and every later one is an
|
||||
// advance further up, so the run's rows run from well above the pen down to the
|
||||
// pen's own cell -- and the pen's own cell is the lowest row of ink.
|
||||
PLOT_CHECK(up.y0 + 4u * cell < static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(up.y0 + 4u * advance_px < static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(up.y1 < static_cast<plot::u32>(pen.y) + cell);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// The orientation rule, stated rather than hardcoded: a glyph's texel `(col, row)`
|
||||
// lands at the pen plus `col` steps along the cell's +v axis and `row` steps along
|
||||
// its +x axis. That is the mapping kTextVS implements, so with `axis = (0, -1)` the
|
||||
// 'L' -- whose ink is a stem down its left column and a foot along its bottom row --
|
||||
// has to come out as a top bar with the foot running up the right-hand side.
|
||||
// lands at the pen plus `col` steps along the cell's +x axis (`ux` here) and `row`
|
||||
// steps along its +v axis (the perpendicular). That is the mapping kTextVS
|
||||
// implements, so with `axis = (0, -1)` the 'L' -- whose ink is a stem down its left
|
||||
// columns and a foot along its bottom rows -- has to come out as a bar standing up
|
||||
// from the pen with the foot running right.
|
||||
//
|
||||
// The check is two-sided: the basis-derived corner holds ink and the corner a
|
||||
// mirrored perpendicular would put it in does not, so a glyph flipped about its own
|
||||
// diagonal fails even though it is the same size and shape.
|
||||
// The check is two-sided: the texels the basis says hold ink do, and the texels a
|
||||
// mirrored perpendicular would put that ink in hold none -- so a glyph flipped
|
||||
// about its own diagonal fails even though it is the same size and shape. The
|
||||
// coverage is anti-aliased, so "held ink" is a coverage threshold rather than an
|
||||
// exact white: a texel of the stem is fully covered and a texel of the foot is
|
||||
// half, and both are ink.
|
||||
PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
const plot::Vec2 pen{300.0f, 200.0f};
|
||||
const plot::f32 px = 4.0f; // an 8x8 glyph at 32 px: one texel is a 4x4 pixel block
|
||||
const plot::f32 px = 4.0f; // one texel is a 4x4 pixel block
|
||||
const plot::Vec2 ux{0.0f, -1.0f}; // a 90 degree counter-clockwise label
|
||||
const plot::Vec2 uy{-ux.y, ux.x}; // (1, 0): the cell's own +v axis
|
||||
|
||||
// Where the centre of the glyph's texel (col, row) is drawn. Every cell is
|
||||
// `kGlyphPx` texels across, the cell's top-left is the pen because the bitmap
|
||||
// font's bearings are zero, and the rule is the shader's own: the atlas' u runs
|
||||
// along the cell's +x axis (`ux` here, `a_axis` there) and its v along the
|
||||
// perpendicular (`v_axis`), which is what `v_uv = a_uv + corner * u_glyph_uv`
|
||||
// maps corner by corner.
|
||||
// Where the centre of the glyph's texel (col, row) is drawn. The cell's top-left
|
||||
// is the pen (the embedded font bakes its bearings into the cell), and the rule
|
||||
// is the shader's own: the atlas' u runs along the cell's +x axis (`ux` here,
|
||||
// `a_axis` there) and its v along the perpendicular (`v_axis`), which is what
|
||||
// `v_uv = a_uv + corner * u_glyph_uv` maps corner by corner.
|
||||
auto texel_centre = [&](plot::f32 col, plot::f32 row, plot::Vec2 v_axis) {
|
||||
const plot::f32 du = (col + 0.5f) * px;
|
||||
const plot::f32 dv = (row + 0.5f) * px;
|
||||
@@ -1326,28 +1359,28 @@ PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
||||
plot::u8 pxv[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(at.x),
|
||||
ctx.height - 1u - static_cast<plot::u32>(at.y), pxv));
|
||||
return pixel_is(pxv, plot::Color::hex(0xFFFFFF));
|
||||
return pxv[0] >= 96u; // white tint over black: the pixel *is* the coverage
|
||||
};
|
||||
|
||||
PLOT_CHECK(draw_text_run(ctx, atlas, font, "L", pen, px, ux).any());
|
||||
|
||||
// 'L' is inked at texel (0, 0) -- the stem's top -- and at (6, 6), the far end of
|
||||
// its foot. Both corners are where the basis puts them ...
|
||||
PLOT_CHECK(inked(texel_centre(0.0f, 0.0f, uy)));
|
||||
PLOT_CHECK(inked(texel_centre(6.0f, 6.0f, uy)));
|
||||
// ... and the two *off-diagonal* texels of its ink state the rule the diagonal
|
||||
// pair cannot: the stem meets the foot at (0, 6), which is where the cell's
|
||||
// first texel row and last texel column meet, while (6, 0) is blank. A shader
|
||||
// that swapped the cell's two axes would trade those two, and both of these
|
||||
// would fail -- the diagonal probes would not.
|
||||
PLOT_CHECK(inked(texel_centre(0.0f, 6.0f, uy)));
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 0.0f, uy)));
|
||||
// ... and the mirrored basis -- the other perpendicular a shader could build --
|
||||
// would put the stem's top on the opposite side of the pen, where this frame has
|
||||
// no ink at all.
|
||||
// The stem (cell columns 1-2, rows 3-12) and the foot (columns 2-6, rows 11-12)
|
||||
// put ink at these texels ...
|
||||
PLOT_CHECK(inked(texel_centre(2.0f, 4.0f, uy))); // stem, fully covered
|
||||
PLOT_CHECK(inked(texel_centre(2.0f, 11.0f, uy))); // stem meets the foot
|
||||
PLOT_CHECK(inked(texel_centre(5.0f, 11.0f, uy))); // the foot's far end
|
||||
// ... and none at these, which are the cell's blank margin: a shader that
|
||||
// swapped the cell's two axes would put the ink in the wrong half and fail both
|
||||
// sides.
|
||||
PLOT_CHECK(!inked(texel_centre(2.0f, 0.0f, uy))); // above the first row of ink
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 4.0f, uy))); // right of the stem at cap height
|
||||
PLOT_CHECK(!inked(texel_centre(0.0f, 4.0f, uy))); // the cell's empty first column
|
||||
// The mirrored perpendicular -- the other basis a shader could build -- would
|
||||
// draw the whole cell on the opposite side of the pen, where this frame has no
|
||||
// ink at all.
|
||||
const plot::Vec2 mirrored{-uy.x, -uy.y};
|
||||
PLOT_CHECK(!inked(texel_centre(0.0f, 0.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 6.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(2.0f, 4.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(5.0f, 11.0f, mirrored)));
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
@@ -1505,7 +1538,7 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
PLOT_CHECK(plot::gl::available());
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
{
|
||||
RefuseGetIntegerv refused;
|
||||
@@ -1522,8 +1555,8 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
|
||||
// the Status it got.
|
||||
PLOT_CHECK(atlas.sync(font, &st));
|
||||
PLOT_CHECK(!atlas.empty());
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
|
||||
1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
|
||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user