#include #include namespace plot::render { namespace { // Glyph data and its provenance // ----------------------------- // The 95 printable ASCII glyphs (slots 0..94, U+0020..U+007E) are the // `font8x8_basic` table of the font8x8 project by Daniel Hepper, released into // the public domain. It is itself based on Marcel Sondaar's public-domain 8x8 // VGA font (via the IBM public-domain VGA fonts). Fetched on 2026-09-27 from // https://raw.githubusercontent.com/dhepper/font8x8/master/font8x8_basic.h // The glyphs the plot layer's SI labels need come from the same project: // U+00B0 DEGREE SIGN and U+00B5 MICRO SIGN from font8x8_ext_latin.h (which // covers U+00A0..U+00FF), and U+03A9 GREEK CAPITAL LETTER OMEGA from // font8x8_greek.h (which covers U+0390..U+03C9). // The missing-glyph box in the last slot is authored here: a hollow 8x8 square. // Nothing else in this table is hand-drawn, and none of it is a library: it is // public-domain data, so `plot_core` still links nothing third-party. // // Row 0 is the top row and bit 0 is the leftmost pixel, one byte per row. constexpr u8 kGlyphBits[BitmapFont::kGlyphCount][BitmapFont::kGlyphPx] = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 0] U+0020 space {0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // [ 1] U+0021 '!' {0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 2] U+0022 '"' {0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // [ 3] U+0023 '#' {0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // [ 4] U+0024 '$' {0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // [ 5] U+0025 '%' {0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // [ 6] U+0026 '&' {0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 7] U+0027 ''' {0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // [ 8] U+0028 '(' {0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // [ 9] U+0029 ')' {0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // [10] U+002A '*' {0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // [11] U+002B '+' {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [12] U+002C ',' {0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // [13] U+002D '-' {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [14] U+002E '.' {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // [15] U+002F '/' {0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // [16] U+0030 '0' {0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // [17] U+0031 '1' {0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // [18] U+0032 '2' {0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // [19] U+0033 '3' {0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // [20] U+0034 '4' {0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // [21] U+0035 '5' {0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // [22] U+0036 '6' {0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // [23] U+0037 '7' {0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // [24] U+0038 '8' {0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // [25] U+0039 '9' {0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [26] U+003A ':' {0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [27] U+003B ';' {0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // [28] U+003C '<' {0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // [29] U+003D '=' {0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // [30] U+003E '>' {0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // [31] U+003F '?' {0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // [32] U+0040 '@' {0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // [33] U+0041 'A' {0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // [34] U+0042 'B' {0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // [35] U+0043 'C' {0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // [36] U+0044 'D' {0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // [37] U+0045 'E' {0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // [38] U+0046 'F' {0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // [39] U+0047 'G' {0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // [40] U+0048 'H' {0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [41] U+0049 'I' {0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // [42] U+004A 'J' {0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // [43] U+004B 'K' {0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // [44] U+004C 'L' {0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // [45] U+004D 'M' {0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // [46] U+004E 'N' {0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // [47] U+004F 'O' {0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // [48] U+0050 'P' {0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // [49] U+0051 'Q' {0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // [50] U+0052 'R' {0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // [51] U+0053 'S' {0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [52] U+0054 'T' {0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // [53] U+0055 'U' {0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [54] U+0056 'V' {0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // [55] U+0057 'W' {0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // [56] U+0058 'X' {0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // [57] U+0059 'Y' {0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // [58] U+005A 'Z' {0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // [59] U+005B '[' {0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // [60] U+005C '\' {0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // [61] U+005D ']' {0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // [62] U+005E '^' {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // [63] U+005F '_' {0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // [64] U+0060 '`' {0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // [65] U+0061 'a' {0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // [66] U+0062 'b' {0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // [67] U+0063 'c' {0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00}, // [68] U+0064 'd' {0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00}, // [69] U+0065 'e' {0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00}, // [70] U+0066 'f' {0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [71] U+0067 'g' {0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // [72] U+0068 'h' {0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [73] U+0069 'i' {0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // [74] U+006A 'j' {0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // [75] U+006B 'k' {0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [76] U+006C 'l' {0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // [77] U+006D 'm' {0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // [78] U+006E 'n' {0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // [79] U+006F 'o' {0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // [80] U+0070 'p' {0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // [81] U+0071 'q' {0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // [82] U+0072 'r' {0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // [83] U+0073 's' {0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // [84] U+0074 't' {0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // [85] U+0075 'u' {0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [86] U+0076 'v' {0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // [87] U+0077 'w' {0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // [88] U+0078 'x' {0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [89] U+0079 'y' {0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // [90] U+007A 'z' {0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // [91] U+007B '{' {0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // [92] U+007C '|' {0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // [93] U+007D '}' {0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [94] U+007E '~' {0x1C, 0x36, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00}, // [95] U+00B0 degree {0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x03}, // [96] U+00B5 micro {0x3E, 0x63, 0x63, 0x63, 0x36, 0x36, 0x77, 0x00}, // [97] U+03A9 ohm {0x7E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00}, // [98] missing-glyph box, authored here }; /// Repacks the glyph-major table into the atlas image the text pipeline /// uploads: one 8x8 cell per slot, 255 for ink, straight alpha, no dilation. constexpr std::array pack_atlas() noexcept { std::array image{}; for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) { const u32 cell_x = (slot % BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx; const u32 cell_y = (slot / BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx; for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) { const u8 bits = kGlyphBits[slot][y]; for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) { image[(cell_y + y) * BitmapFont::kAtlasWidth + cell_x + x] = static_cast(((bits >> x) & 1u) != 0u ? 0xFFu : 0x00u); } } } return image; } constexpr std::array kAtlasImage = pack_atlas(); /// Span of set bits in a glyph's rows: 0 for a blank glyph such as space. constexpr u32 ink_width(const u8* rows) noexcept { u32 lo = BitmapFont::kGlyphPx; u32 hi = 0; bool any = false; for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) { for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) { if (((rows[y] >> x) & 1u) != 0u) { if (x < lo) { lo = x; } if (x > hi) { hi = x; } any = true; } } } return any ? hi - lo + 1u : 0u; } } // namespace // The decoder is public because the text batch stages glyphs from the same // codepoints `measure` sums: one implementation keeps "what a string is worth" // and "what a string draws" the same thing. u32 BitmapFont::next_codepoint(const char*& p) noexcept { const auto b0 = static_cast(static_cast(p[0])); if (b0 < 0x80u) { ++p; return b0; } u32 codepoint = 0; u32 extra = 0; if ((b0 & 0xE0u) == 0xC0u) { codepoint = b0 & 0x1Fu; extra = 1u; } else if ((b0 & 0xF0u) == 0xE0u) { codepoint = b0 & 0x0Fu; extra = 2u; } else if ((b0 & 0xF8u) == 0xF0u) { codepoint = b0 & 0x07u; extra = 3u; } else { ++p; return kInvalidCodepoint; } for (u32 i = 1; i <= extra; ++i) { const auto b = static_cast(static_cast(p[i])); if ((b & 0xC0u) != 0x80u) { // truncated: stop before the terminator ++p; return kInvalidCodepoint; } codepoint = (codepoint << 6) | (b & 0x3Fu); } p += extra + 1u; constexpr u32 kMinForLength[4] = {0u, 0x80u, 0x800u, 0x10000u}; if (codepoint < kMinForLength[extra] || codepoint > 0x10FFFFu || (codepoint >= 0xD800u && codepoint <= 0xDFFFu)) { return kInvalidCodepoint; // overlong, out of range or a surrogate } return codepoint; } BitmapFont::BitmapFont() noexcept { for (u32 slot = 0; slot < kGlyphCount; ++slot) { const u32 ink = ink_width(kGlyphBits[slot]); const u32 with_spacing = ink + 1u; advances_[slot] = static_cast(with_spacing < 2u ? 2u : with_spacing); } } f32 BitmapFont::measure(const char* utf8) const { if (utf8 == nullptr) { return 0.0f; } f32 width = 0.0f; for (const char* p = utf8; *p != '\0';) { width += advances_[glyph_index(next_codepoint(p))]; } return width; } f32 BitmapFont::height() const { return static_cast(kGlyphPx); } f32 BitmapFont::ascent() const { return static_cast(kGlyphPx - 1u); } f32 BitmapFont::advance(char c) const noexcept { return advances_[glyph_index(static_cast(static_cast(c)))]; } f32 BitmapFont::advance_codepoint(u32 codepoint) const noexcept { return advances_[glyph_index(codepoint)]; } u32 BitmapFont::glyph_index(u32 codepoint) noexcept { if (codepoint >= kFirstChar && codepoint < kFirstChar + kAsciiGlyphs) { return codepoint - kFirstChar; } switch (codepoint) { case 0x00B0u: return kDegreeSlot; case 0x00B5u: return kMicroSlot; case 0x03A9u: return kOhmSlot; default: return kFallbackSlot; } } u32 BitmapFont::glyph_rows(char) const noexcept { return kGlyphPx; } const u8* BitmapFont::glyph_bitmap(char c) const noexcept { return glyph_bitmap_of(glyph_index(static_cast(static_cast(c)))); } const u8* BitmapFont::glyph_bitmap_of(u32 slot) noexcept { return kGlyphBits[slot < kGlyphCount ? slot : kFallbackSlot]; } const u8* BitmapFont::atlas_image() noexcept { return kAtlasImage.data(); } Vec2 BitmapFont::atlas_origin(u32 slot) noexcept { const u32 s = slot < kGlyphCount ? slot : kFallbackSlot; return Vec2{static_cast((s % kAtlasCols) * kGlyphPx), static_cast((s / kAtlasCols) * kGlyphPx)}; } } // namespace plot::render