Files
2026-10-16 08:31:15 +02:00

88 lines
4.3 KiB
Python

#!/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], "<u4").tobytes() + atlas.tobytes())
with open(os.path.join(ROOT, "src", "clock_sdf.h"), "w") as f:
f.write("/* Generated by tools/make_clock_sdf.py - the clock's glyph distance atlas\n"
" (assets/ui/clock_sdf.bin: R8, 128 + 8 d, d in px, negative inside). */\n#pragma once\n")
f.write("#define CLOCK_SDF_W %d\n#define CLOCK_SDF_H %d\n#define CLOCK_SDF_PAD %d\n" % (W, H, PAD))
f.write("typedef struct {\n short px; /* font size class: 300 / 110 / 52 */\n char ch;\n"
" short x, y, w, h; /* atlas rect (0 size: no ink) */\n"
" float xoff, yoff; /* rect's top left from the pen position on the baseline */\n"
" float adv;\n} ClockGlyph;\n")
f.write("static const ClockGlyph clock_glyph[%d] = {\n" % len(glyphs))
for i, (px, ch, d, xo, yo, adv) in enumerate(glyphs):
x, y, w, h = place[i]
f.write(" {%d, '%s', %d, %d, %d, %d, %.3ff, %.3ff, %.3ff},\n" % (px, ch, x, y, w, h, xo, yo, adv))
f.write("};\n")
print("atlas %d x %d, %d glyphs, %.0f KB" % (W, H, len(glyphs), W * H / 1024))
if __name__ == "__main__":
main()