#!/usr/bin/env python3 """Glyph signed-distance atlas for the clock mode's glass text (URW Gothic Demi, like the UI). python3 tools/make_clock_sdf.py FONT.otf (FONT: URW Gothic Demi; not shipped) Three sizes: 300 px (digits, ':'), 110 px (digits, ':', ' ', A P M) and 52 px (the date: letters, digits, space). Each glyph is rendered at 2x, thresholded at half coverage, and the exact Euclidean distance transform gives the signed distance in target pixels (negative inside), clamped to +-16 and stored as R8: 128 + 8 d (1/8 px steps). 16 px of padding around every glyph. Writes assets/ui/clock_sdf.bin ('CSD1', u32 width, u32 height, R8 rows top-down) and src/clock_sdf.h (per glyph: size class, character, atlas rect, offset of the rect from the pen position on the baseline, advance).""" import os, sys import numpy as np from PIL import Image, ImageDraw, ImageFont from scipy import ndimage as nd HERE = os.path.dirname(os.path.abspath(__file__)); ROOT = os.path.dirname(HERE) SIZES = [(300, "0123456789:"), (110, "0123456789: APM"), (52, " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")] PAD, CLAMP, W = 16, 16.0, 2048 def glyph_sdf(font2, ch, px): """signed distance (target px) of one glyph with PAD around it; the rect's offset from the pen position on the baseline (target px); the advance""" asc, desc = font2.getmetrics() l, t, r, b = font2.getbbox(ch, anchor="ls") adv = font2.getlength(ch) / 2.0 if r <= l or b <= t: # space: no ink return None, 0, 0, adv l0, t0 = l - l % 2, t - t % 2 # even 2x origin: whole-pixel offsets at 1x w2, h2 = (r - l0) + 4 * PAD, (b - t0) + 4 * PAD w2 += w2 % 2; h2 += h2 % 2 im = Image.new("L", (w2, h2), 0) ImageDraw.Draw(im).text((2 * PAD - l0, 2 * PAD - t0), ch, font=font2, fill=255, anchor="ls") m = np.asarray(im) >= 128 d = (nd.distance_transform_edt(~m) - nd.distance_transform_edt(m)) / 2.0 # 2x -> target px d = d.reshape(h2 // 2, 2, w2 // 2, 2).mean((1, 3)) return d, (l0 // 2 - PAD), (t0 // 2 - PAD), adv def main(): glyphs = [] for px, chars in SIZES: f2 = ImageFont.truetype(sys.argv[1], 2 * px) for ch in chars: d, xo, yo, adv = glyph_sdf(f2, ch, px) glyphs.append((px, ch, d, xo, yo, adv)) # shelf packing, tallest first order = sorted(range(len(glyphs)), key=lambda i: -(glyphs[i][2].shape[0] if glyphs[i][2] is not None else 0)) x = y = shelf = 0; place = {} for i in order: d = glyphs[i][2] if d is None: place[i] = (0, 0, 0, 0); continue h, w = d.shape if x + w > W: x, y, shelf = 0, y + shelf, 0 place[i] = (x, y, w, h); x += w; shelf = max(shelf, h) H = y + shelf; H += (-H) % 4 atlas = np.full((H, W), 255, np.uint8) for i, (x, y, w, h) in place.items(): d = glyphs[i][2] if d is not None: atlas[y:y + h, x:x + w] = np.clip(np.round(128 + 8 * np.clip(d, -CLAMP, CLAMP - 0.125)), 0, 255) os.makedirs(os.path.join(ROOT, "assets", "ui"), exist_ok=True) with open(os.path.join(ROOT, "assets", "ui", "clock_sdf.bin"), "wb") as f: f.write(b"CSD1" + np.array([W, H], "