Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The timer utilizes the Waveshare 1.28-inch round touch display, allowing for con

The firmware includes a compact proportional bitmap font rendered directly at the display's native resolution. It replaces enlargement of MicroPython's 8x8 framebuffer font, so large countdown digits and labels retain smooth shapes instead of scaling into square pixels.

The running track and rest countdown uses the 74-pixel native font, the closest available pre-rendered size to a 10% increase from the previous 64-pixel countdown.
The running track and rest countdown uses the 74-pixel native font, the closest available pre-rendered size to a 10% increase from the previous 64-pixel countdown. Timer digits use equal-width cells, so changing figures do not move the centered countdown or elapsed-time positions.

`font_data.py` and its flash-backed `font_data*.bin` glyph assets are generated from Montserrat SemiBold. The assets contain pre-rasterized native UI sizes, allowing the Pico to use its fast framebuffer blitter without holding the complete font in RAM. To regenerate them, install Pillow and run:

Expand Down
63 changes: 53 additions & 10 deletions font_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,29 @@ def _glyph_index(character):
return code_point - FIRST_CODE_POINT


def measure_text(text, size):
"""Measure proportional text width without loading glyph pixels."""
def _digit_cell_width(widths):
return max(widths[_glyph_index(str(digit))] for digit in range(10))


def _character_advance(character, widths, tabular_digits, digit_cell_width):
if tabular_digits and "0" <= character <= "9":
return digit_cell_width
return widths[_glyph_index(character)]


def measure_text(text, size, tabular_digits=False):
"""Measure text width without loading glyph pixels."""
widths = WIDTHS[_size_index(size)]
return sum(widths[_glyph_index(character)] for character in str(text))
digit_cell_width = _digit_cell_width(widths) if tabular_digits else None
return sum(
_character_advance(
character,
widths,
tabular_digits,
digit_cell_width,
)
for character in str(text)
)


def _glyph_offset(offsets, index):
Expand Down Expand Up @@ -91,13 +110,14 @@ def _blit_glyph(
surface.blit(glyph, x, y, transparent_key, palette)


def draw_text(surface, text, x, y, size, color):
def draw_text(surface, text, x, y, size, color, tabular_digits=False):
"""Draw a pre-rasterized native-resolution font and return its width."""
size_index = _size_index(size)
height = PIXEL_HEIGHTS[size_index]
widths = WIDTHS[size_index]
offsets = OFFSETS[size_index]
cursor = int(x)
digit_cell_width = _digit_cell_width(widths) if tabular_digits else None
use_blitter = framebuf is not None and hasattr(surface, "blit")

palette = None
Expand All @@ -117,6 +137,13 @@ def draw_text(surface, text, x, y, size, color):
for character in str(text):
index = _glyph_index(character)
width = widths[index]
advance = _character_advance(
character,
widths,
tabular_digits,
digit_cell_width,
)
glyph_x = cursor + ((advance - width) // 2)
glyph_size = ((width + 7) // 8) * height
bitmap_file.seek(_glyph_offset(offsets, index))
bitmap = bitmap_file.read(glyph_size)
Expand All @@ -129,21 +156,37 @@ def draw_text(surface, text, x, y, size, color):
bitmap,
width,
height,
cursor,
glyph_x,
int(y),
palette,
transparent_key,
)
else:
_draw_scanlines(surface, bitmap, width, height, cursor, int(y), color)
cursor += width
_draw_scanlines(
surface,
bitmap,
width,
height,
glyph_x,
int(y),
color,
)
cursor += advance

return cursor - int(x)


def draw_centered(surface, text, y, size, color):
def draw_centered(surface, text, y, size, color, tabular_digits=False):
"""Draw text horizontally centered within the framebuffer."""
width = measure_text(text, size)
width = measure_text(text, size, tabular_digits=tabular_digits)
x = (surface.width - width) // 2
draw_text(surface, text, x, y, size, color)
draw_text(
surface,
text,
x,
y,
size,
color,
tabular_digits=tabular_digits,
)
return x, width
11 changes: 11 additions & 0 deletions lcd_1inch28.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,14 @@ def write_text(self, text, x, y, size, color):
def write_centered(self, text, y, size, color):
"""Draw smooth proportional text centered on the display."""
return self._draw_centered(self, text, y, size, color)

def write_time_centered(self, text, y, size, color):
"""Draw a centered time using fixed-width digit cells."""
return self._draw_centered(
self,
text,
y,
size,
color,
tabular_digits=True,
)
31 changes: 31 additions & 0 deletions tests/test_font_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,37 @@ def test_proportional_measurement_and_scaling(self):
self.assertLess(measure_text("Timer", 2), measure_text("Timer", 4))
self.assertGreater(measure_text(" ", 2), 0)

def test_tabular_times_keep_the_same_width_for_every_digit(self):
widths = {
measure_text(value, 7, tabular_digits=True)
for value in ("00:00", "11:11", "24:57", "60:00", "88:88")
}

self.assertEqual(1, len(widths))
self.assertNotEqual(measure_text("11:11", 7), measure_text("88:88", 7))

def test_tabular_centering_keeps_time_origin_fixed(self):
surface = FakeSurface()

narrow = draw_centered(
surface,
"11:11",
20,
7,
1,
tabular_digits=True,
)
wide = draw_centered(
surface,
"88:88",
20,
7,
1,
tabular_digits=True,
)

self.assertEqual(narrow, wide)

def test_draw_uses_single_pixel_scanlines(self):
surface = FakeSurface()
width = draw_text(surface, "8:15", 4, 7, 5, 0xFFFF)
Expand Down
6 changes: 5 additions & 1 deletion tests/test_live_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ def test_maximum_countdown_width_fits_the_live_screen_position(self):
display_radius = 120
y_position = 82
text_height = pixel_height(COUNTDOWN_TEXT_SIZE)
text_width = measure_text("60:00", COUNTDOWN_TEXT_SIZE)
text_width = measure_text(
"60:00",
COUNTDOWN_TEXT_SIZE,
tabular_digits=True,
)

for edge_y in (y_position, y_position + text_height - 1):
distance_from_center = edge_y - display_radius
Expand Down
4 changes: 2 additions & 2 deletions touch_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ def LiveScreen(self, LCD, textsize_rem=None, backColour=None, textColour=None, e
textsize_rem = 5
self.Set_Mode(0)
LCD.fill(backColour)
LCD.write_centered(remaining,82,textsize_rem,textColour)
LCD.write_centered(elapsed,180,3,textColour)
LCD.write_time_centered(remaining,82,textsize_rem,textColour)
LCD.write_time_centered(elapsed,180,3,textColour)
LCD.show()


Expand Down
Loading