Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added fonts/Inter-Regular.ttf
Binary file not shown.
30 changes: 30 additions & 0 deletions include/fonts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef FONTS_H
#define FONTS_H

#include <LovyanGFX.hpp>
#include "fonts/inter_10.h"
#include "fonts/inter_14.h"
#include "fonts/inter_19.h"

// Smooth VLW font identifiers — values match the old bitmap font numbers
// so existing logic (e.g. compact ? FONT_LARGE : FONT_LARGE) stays readable.
enum FontID : uint8_t {
FONT_SMALL = 1, // Inter 10pt (was bitmap Font 1, 8px GLCD)
FONT_BODY = 2, // Inter 14pt (was bitmap Font 2, 16px)
FONT_LARGE = 4, // Inter 19pt (was bitmap Font 4, 26px)
FONT_7SEG = 7, // Built-in 7-segment (kept for clock displays)
};

inline void setFont(lgfx::LovyanGFX& tft, FontID id) {
switch (id) {
case FONT_SMALL: tft.loadFont(inter_10); break;
case FONT_BODY: tft.loadFont(inter_14); break;
case FONT_LARGE: tft.loadFont(inter_19); break;
case FONT_7SEG:
tft.unloadFont();
tft.setTextFont(7);
break;
}
}

#endif // FONTS_H
414 changes: 414 additions & 0 deletions include/fonts/inter_10.h

Large diffs are not rendered by default.

603 changes: 603 additions & 0 deletions include/fonts/inter_14.h

Large diffs are not rendered by default.

898 changes: 898 additions & 0 deletions include/fonts/inter_19.h

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions scripts/generate_vlw_fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Generate TFT_eSPI-compatible VLW font PROGMEM headers from a TTF file.

VLW format (Bodmer/TFT_eSPI smooth font):
Header: glyph_count(u32) | version(u32=6) | font_size(u32) | padding(u32)
ascent(u32) | descent(u32)
Per glyph metrics (24 bytes each):
unicode(u32) | height(u32) | width(u32) | advance(u32)
top_offset(u32) | left_offset(u32)
Glyph bitmaps: 8-bit alpha, row-major, width*height bytes each.

Usage:
python scripts/generate_vlw_fonts.py
"""

import struct
import sys
from pathlib import Path

try:
import freetype
except ImportError:
print("Install freetype-py: pip install freetype-py")
sys.exit(1)

# Character set: printable ASCII + degree symbol
CHARSET = list(range(0x20, 0x7F)) + [0xB0]

# Font sizes to generate: (name, pixel_size)
# Tuned to match TFT_eSPI bitmap font metrics while fitting flash budget
FONTS = [
("inter_10", 10),
("inter_14", 14),
("inter_19", 19),
]

TTF_PATH = Path(__file__).parent.parent / "fonts" / "Inter-Regular.ttf"
OUT_DIR = Path(__file__).parent.parent / "include" / "fonts"


def generate_vlw(ttf_path: str, pixel_size: int) -> bytes:
"""Generate VLW binary data for the given font and size."""
face = freetype.Face(str(ttf_path))
face.set_pixel_sizes(0, pixel_size)

glyphs = []
for codepoint in CHARSET:
face.load_char(chr(codepoint), freetype.FT_LOAD_RENDER)
g = face.glyph
bmp = g.bitmap

width = bmp.width
height = bmp.rows
advance = g.advance.x >> 6 # 26.6 fixed point to pixels
top_offset = g.bitmap_top
left_offset = g.bitmap_left

# Extract 8-bit alpha bitmap
if width > 0 and height > 0:
alpha = bytes(bmp.buffer)
# Handle pitch != width (padding per row)
if bmp.pitch != width:
alpha = b""
for row in range(height):
start = row * bmp.pitch
alpha += bytes(bmp.buffer[start:start + width])
else:
alpha = b""

glyphs.append({
"unicode": codepoint,
"height": height,
"width": width,
"advance": advance,
"top_offset": top_offset,
"left_offset": left_offset,
"bitmap": alpha,
})

# Compute font metrics
ascent = face.size.ascender >> 6
descent = -(face.size.descender >> 6) # TFT_eSPI expects positive descent

# Build VLW binary
buf = bytearray()

# Header (6 x uint32 big-endian)
glyph_count = len(glyphs)
buf += struct.pack(">I", glyph_count)
buf += struct.pack(">I", 6) # version
buf += struct.pack(">I", pixel_size)
buf += struct.pack(">I", 0) # padding
buf += struct.pack(">I", ascent)
buf += struct.pack(">I", descent)

# Glyph metrics table (28 bytes each — 7 x uint32)
# TFT_eSPI reads: unicode, height, width, xAdvance, dY, dX, padding(ignored)
for g in glyphs:
buf += struct.pack(">I", g["unicode"])
buf += struct.pack(">I", g["height"])
buf += struct.pack(">I", g["width"])
buf += struct.pack(">I", g["advance"])
buf += struct.pack(">i", g["top_offset"])
buf += struct.pack(">i", g["left_offset"])
buf += struct.pack(">I", 0) # padding — TFT_eSPI reads and discards

# Glyph bitmaps (sequential, same order as metrics)
for g in glyphs:
buf += g["bitmap"]

return bytes(buf)


def vlw_to_header(name: str, vlw_data: bytes) -> str:
"""Convert VLW binary to a C PROGMEM header."""
lines = [
f"// Auto-generated VLW font: {name}",
f"// Size: {len(vlw_data)} bytes ({len(vlw_data) / 1024:.1f} KB)",
"#pragma once",
"#include <pgmspace.h>",
"",
f"const uint8_t {name}[] PROGMEM = {{",
]

# Emit bytes, 16 per line
for i in range(0, len(vlw_data), 16):
chunk = vlw_data[i:i + 16]
hex_vals = ", ".join(f"0x{b:02X}" for b in chunk)
lines.append(f" {hex_vals},")

lines.append("};")
lines.append("")
return "\n".join(lines)


def main():
if not TTF_PATH.exists():
print(f"TTF not found: {TTF_PATH}")
sys.exit(1)

OUT_DIR.mkdir(parents=True, exist_ok=True)

for name, size in FONTS:
print(f"Generating {name} ({size}px)...", end=" ")
vlw_data = generate_vlw(str(TTF_PATH), size)
header = vlw_to_header(name, vlw_data)

out_path = OUT_DIR / f"{name}.h"
out_path.write_text(header, encoding="utf-8")
print(f"{len(vlw_data)} bytes -> {out_path}")

print("Done.")


if __name__ == "__main__":
main()
11 changes: 6 additions & 5 deletions src/clock_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "settings.h"
#include "config.h"
#include "layout.h"
#include "fonts.h"
#include <time.h>

// Font 7 digit dimensions (same as pong clock)
Expand Down Expand Up @@ -53,7 +54,7 @@ void drawClock() {
int cy = LY_CLK_TIME_Y - CLK_DIGIT_H / 2;
tft.fillRect(cx, cy, CLK_COLON_W, CLK_DIGIT_H, bg);
if (colonOn) {
tft.setTextFont(7);
setFont(tft, FONT_7SEG);
tft.setTextSize(1);
tft.setTextColor(timeClr, bg);
tft.drawChar(':', cx, cy, 7);
Expand Down Expand Up @@ -81,7 +82,7 @@ void drawClock() {
digits[4] = '0' + (now.tm_min % 10);

// Draw only changed digits
tft.setTextFont(7);
setFont(tft, FONT_7SEG);
tft.setTextSize(1);
tft.setTextColor(timeClr, bg);

Expand Down Expand Up @@ -109,7 +110,7 @@ void drawClock() {
const char* ampm = now.tm_hour < 12 ? "AM" : "PM";
if (strcmp(ampm, prevAmPm) != 0) {
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
setFont(tft, FONT_LARGE);
tft.setTextColor(dateClr, bg);
int ampmW = tft.textWidth("PM");
tft.fillRect(LY_W / 2 - ampmW / 2 - 2, LY_CLK_AMPM_Y - 12, ampmW + 4, 24, bg);
Expand All @@ -118,7 +119,7 @@ void drawClock() {
}
} else if (prevAmPm[0] != '\0') {
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
setFont(tft, FONT_LARGE);
int ampmW = tft.textWidth("PM");
tft.fillRect(LY_W / 2 - ampmW / 2 - 2, LY_CLK_AMPM_Y - 12, ampmW + 4, 24, bg);
prevAmPm[0] = '\0';
Expand All @@ -140,7 +141,7 @@ void drawClock() {

if (strcmp(dateBuf, prevDateBuf) != 0) {
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
setFont(tft, FONT_LARGE);
tft.setTextColor(dateClr, bg);
// Clear previous date, draw new
int dateW = tft.textWidth(prevDateBuf[0] ? prevDateBuf : dateBuf);
Expand Down
7 changes: 4 additions & 3 deletions src/clock_pong.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "layout.h"
#include "settings.h"
#include "display_ui.h"
#include "fonts.h"
#include <time.h>

// ========== Layout constants (from layout profile) ==========
Expand Down Expand Up @@ -569,7 +570,7 @@ static void drawTime() {
bool colon = showColon();

// Draw digits - only redraw changed ones
tft.setTextFont(7);
setFont(tft, FONT_7SEG);
tft.setTextSize(1);
tft.setTextColor(dispSettings.clockTimeColor, TFT_BLACK);

Expand Down Expand Up @@ -607,7 +608,7 @@ static void drawTime() {

// AM/PM for 12h mode
if (!netSettings.use24h) {
tft.setTextFont(2);
setFont(tft, FONT_BODY);
tft.setTextColor(dispSettings.clockDateColor, TFT_BLACK);
int ampmX = digitX(4) + DIGIT_W + 2;
tft.setCursor(ampmX, layout.timeY + DIGIT_H - 16);
Expand Down Expand Up @@ -727,7 +728,7 @@ void tickPongClock() {
default: snprintf(dateStr, sizeof(dateStr), "%s %02d.%02d.%04d", days[now.tm_wday], day, mon, year); break;
}
if (strcmp(dateStr, prevDateStr) != 0) {
tft.setTextFont(2);
setFont(tft, FONT_BODY);
tft.setTextSize(1);
tft.setTextDatum(TC_DATUM);
tft.setTextColor(dispSettings.clockDateColor, TFT_BLACK);
Expand Down
3 changes: 2 additions & 1 deletion src/display_anim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "config.h"
#include "settings.h"
#include "icons.h"
#include "fonts.h"

// Use user-configured bg color instead of hardcoded CLR_BG
#undef CLR_BG
Expand Down Expand Up @@ -51,7 +52,7 @@ void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color) {
unsigned long ms = millis();
int phase = (ms / 400) % 4;

tft.setTextFont(2);
setFont(tft, FONT_BODY);
tft.setTextDatum(TL_DATUM);

for (int i = 0; i < 3; i++) {
Expand Down
Loading