Files
portal/src/leds/font.py
T
jimmyandCursor 3d444d1cb5 Ship the portal FastAPI controller with presets, sequences, and live audio.
Move the app under src/, add ring-spanning patterns with shared fonts and
beat-driven motion, and wire global audio settings into the web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 00:59:03 +12:00

252 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Angular rune and hieroglyph bitmaps for 9-row portal panels."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Sequence
if TYPE_CHECKING:
from leds.pattern import Param
Glyph = tuple[str, ...]
RGB = tuple[int, int, int]
LIT = frozenset("#Xx1")
# Elder Futhark, carved from straight strokes on 5×7.
FUTHARK: tuple[Glyph, ...] = (
("#...#", "#..#.", "###..", "#..#.", "#...#", "#....", "#...."), # fehu
("#...#", "#...#", "#...#", "#...#", "##..#", "#.#.#", "#..##"), # uruz
("#....", "#.##.", "##..#", "#.##.", "#....", "#....", "#...."), # thurisaz
("#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#"), # ansuz
("####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"), # raidho
("#...#", ".#.#.", "..#..", ".#.#.", "#...#", ".....", "....."), # kenaz
("#...#", ".#.#.", "..#..", ".#.#.", "#...#", ".#.#.", "#...#"), # gebo
("####.", "#...#", "#...#", "####.", "#....", "#....", "#...."), # wunjo
("#...#", "#.#.#", "#.#.#", "#####", "#.#.#", "#.#.#", "#...#"), # hagalaz
("#...#", ".#..#", "..#.#", "...##", "..#.#", ".#..#", "#...#"), # naudiz
("..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."), # isa
("....#", "...#.", "#.#.#", ".#.#.", "#.#.#", ".#...", "#...."), # jera
("....#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...."), # eihwaz
(".####", "#....", "#....", "#....", "#....", "#....", ".####"), # pertho
("#...#", ".#.#.", "..#..", "..#..", "..#..", "..#..", "..#.."), # algiz
("...##", "..#..", ".#...", "#....", ".#...", "..#..", "...##"), # sowilo
("#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."), # tiwaz
("####.", "#...#", "#...#", "####.", "#...#", "#...#", "####."), # berkano
("#...#", "#...#", "#.#.#", "##.##", "#.#.#", "#...#", "#...#"), # ehwaz
("#...#", "##.##", "#.#.#", "#.#.#", "#.#.#", "##.##", "#...#"), # mannaz
("#....", "#....", "#....", "#....", "#..#.", "#...#", ".##.."), # laguz
("..#..", ".#.#.", "#...#", ".#.#.", "..#..", ".....", "....."), # ingwaz
("#...#", "##.##", "#.#.#", "#.#.#", "#.#.#", "##.##", "#...#"), # dagaz
("..#..", ".#.#.", "#...#", "#...#", ".#.#.", "#...#", "#...#"), # othala
)
# Egyptian-style pictograms, 7×7, with one pixel of vertical padding on 9 rows.
HIEROGLYPHS: tuple[Glyph, ...] = (
("..###..", "..#.#..", "..###..", "...#...", ".#####.", "...#...", "...#..."), # ankh
(".......", ".#####.", "#..#..#", "#.#####", "#..#..#", ".#####.", "......."), # eye
("#.....#", ".#...#.", ".#####.", "##.###.", ".#.#...", ".#..#..", "#......"), # falcon
("...#...", "..#.#..", "..#.#..", "...#...", "...#...", "...#...", "..###.."), # reed
(".#...#.", "##.#.##", ".#####.", "#.#.#.#", ".#####.", "..###..", ".#...#."), # owl
(".......", ".#..#..", "#.##.##", ".......", "..#..#.", ".##.##.", "......."), # water
("...#...", ".#.#.#.", "..###..", "#######", "..###..", ".#.#.#.", "...#..."), # sun
(".###...", "#...#..", "...#...", "..#....", ".#...#.", "#.....#", ".#####."), # snake
("...#...", "..#.#..", "..#.#..", ".#...#.", ".#...#.", "#######", "#.....#"), # pyramid
("#.....#", ".##.##.", "#.#####", "#######", "#.#####", ".##.##.", "#.....#"), # scarab
(".......", ".......", "#.....#", ".#...#.", "..###..", ".......", "......."), # basket
("...#...", "..###..", ".#.#.#.", "...#...", "...#...", "...#...", "..#.#.."), # staff
)
# Ogham-style stem with side notches (5×7).
OGHAM: tuple[Glyph, ...] = (
("..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."), # ailm
("#.#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
("#.#.#", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
(".#.#.", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#.#", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "#.#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "#.#.#", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", ".#.#.", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#.#", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#..", "#.#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#..", "#.#.#", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#..", ".#.#.", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#..", "..#.#", "..#..", "..#..", "..#..", "..#.."),
("#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "#####", "..#..", "..#..", "..#..", "..#..", "..#.."),
("..#..", "..#..", "#####", "..#..", "..#..", "..#..", "..#.."),
("#...#", ".#.#.", "..#..", ".#.#.", "#...#", ".....", "....."),
(".....", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "....."),
(".###.", "#...#", "....#", "..##.", "....#", "#...#", ".###."),
("##.##", "#.#.#", "#####", "#.#.#", "##.##", ".....", "....."),
)
# Wedge / nailhead marks (6×7).
CUNEIFORM: tuple[Glyph, ...] = (
("#.....", "##....", "###...", "####..", "###...", "##....", "#....."),
(".....#", "....##", "...###", "..####", "...###", "....##", ".....#"),
("######", ".#....", "..#...", "...#..", "....#.", ".....#", "......"),
("######", "....#.", "...#..", "..#...", ".#....", "#.....", "......"),
("..##..", ".####.", "######", ".####.", "..##..", "...#..", "...#.."),
("#....#", ".#..#.", "..##..", "..##..", ".#..#.", "#....#", "......"),
("###...", "#.#...", "###...", "..#...", "..#...", "..###.", "......"),
("...###", "...#.#", "...###", "...#..", "...#..", ".###..", "......"),
(".####.", "#....#", "#.##.#", "#....#", "#....#", ".####.", "......"),
("#.#.#.", ".###..", "#.#.#.", ".###..", "#.#.#.", "......", "......"),
("##..##", "##..##", "..##..", "..##..", "##..##", "##..##", "......"),
(".#..#.", "#.##.#", ".#..#.", "#.##.#", ".#..#.", "......", "......"),
("#.....", "#.#...", "#.#.#.", "######", "#.#.#.", "#.#...", "#....."),
("#####.", "#...#.", "#...#.", "#####.", "#.#...", "#..#..", "#...#."),
("..#...", ".###..", "#.#.#.", "..#...", ".#.#..", "#...#.", "......"),
("#...#.", ".#.#..", "..#...", ".#.#..", "#...#.", ".#.#..", "..#..."),
)
# Alchemical / planetary marks (7×7).
ALCHEMY: tuple[Glyph, ...] = (
("...#...", "..###..", ".#...#.", "#.....#", ".#...#.", "..###..", "...#..."), # sun
(".......", ".#####.", "#.....#", "#.....#", "#.....#", ".#####.", "...#..."), # moon
("...#...", "..#.#..", ".#...#.", "#..#..#", ".#...#.", "..###..", "...#..."), # mercury
("#.....#", ".#...#.", "..#.#..", "...#...", "..#.#..", ".#...#.", "#.....#"), # antimony
("...#...", "..###..", ".#####.", "...#...", "...#...", "..###..", ".#####."), # sulfur
("#######", "#.....#", "#.###.#", "#.#.#.#", "#.###.#", "#.....#", "#######"), # salt
("..###..", ".#...#.", "#.....#", "#..#..#", ".#...#.", "..#.#..", "...#..."), # venus
("...#...", "..###..", ".#.#.#.", "#..#..#", "...#...", "...#...", "..###.."), # mars
(".......", ".#.#.#.", "#.#.#.#", ".#####.", "#.#.#.#", ".#.#.#.", "......."), # jupiter
("#.....#", ".#.#.#.", "..###..", ".#####.", "..###..", ".#.#.#.", "#.....#"), # saturn
("...#...", ".#####.", "#..#..#", "...#...", ".#####.", "...#...", "...#..."), # earth
("..###..", ".#...#.", "#.#.#.#", "#.....#", "#.#.#.#", ".#...#.", "..###.."), # spirit
(".#...#.", "##.#.##", ".#####.", "..###..", ".#####.", "##.#.##", ".#...#."), # fire
(".......", "#.....#", ".#...#.", "..###..", ".#...#.", "#.....#", "......."), # air
("#######", ".#...#.", "..###..", "...#...", "..###..", ".#...#.", "#######"), # water
("...#...", "..#.#..", ".#...#.", "#.....#", ".#####.", "...#...", "..###.."), # copper
)
# Geometric occult sigils (7×7).
SIGILS: tuple[Glyph, ...] = (
("...#...", "..#.#..", ".#...#.", "#.....#", ".#...#.", "..#.#..", "...#..."),
("#######", "#.....#", "#.#.#.#", "#.....#", "#.#.#.#", "#.....#", "#######"),
("#.....#", ".#.#.#.", "..#.#..", ".#####.", "..#.#..", ".#.#.#.", "#.....#"),
("..###..", ".#...#.", "#..#..#", "#.#.#.#", "#..#..#", ".#...#.", "..###.."),
("#.#.#.#", ".#.#.#.", "#.#.#.#", ".#.#.#.", "#.#.#.#", ".#.#.#.", "#.#.#.#"),
("...#...", ".#####.", "#.....#", ".#####.", "#.....#", ".#####.", "...#..."),
(".#####.", "#.....#", "#.#.#.#", "#.#.#.#", "#.#.#.#", "#.....#", ".#####."),
("#.....#", "##...##", "#.#.#.#", "#..#..#", "#.#.#.#", "##...##", "#.....#"),
("..#.#..", ".#.#.#.", "#.#.#.#", ".#####.", "#.#.#.#", ".#.#.#.", "..#.#.."),
("#####.#", "#...#.#", "#.###.#", "#.#...#", "#.#####", ".......", "......."),
(".#.###.", "#.#...#", ".#.##.#", "...#.#.", "###.#..", ".......", "......."),
("#..#..#", ".##.##.", "#..#..#", ".##.##.", "#..#..#", ".##.##.", "#..#..#"),
("...#...", "..###..", ".##.##.", "##...##", ".##.##.", "..###..", "...#..."),
("##...##", "#.#.#.#", ".##.##.", "..###..", ".##.##.", "#.#.#.#", "##...##"),
("#.#.#.#", ".......", "#.#.#.#", ".......", "#.#.#.#", ".......", "#.#.#.#"),
("#######", ".......", "#######", ".......", "#######", ".......", "#######"),
)
def _bitmap_rows_to_glyph(rows: tuple[int, ...], *, width: int = 5) -> Glyph:
"""Convert 5×7 bit rows (bit width-1 = left) into ``#``.`` strings."""
out: list[str] = []
for row in rows:
cells: list[str] = []
for col in range(width - 1, -1, -1):
cells.append("#" if (int(row) >> col) & 1 else ".")
out.append("".join(cells))
return tuple(out)
# Latin capitals from the shared 5×7 LED text font.
def _load_latin() -> tuple[Glyph, ...]:
from leds.text import _FONT
return tuple(_bitmap_rows_to_glyph(_FONT[ch]) for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
LATIN: tuple[Glyph, ...] = _load_latin()
FONT_NAMES: tuple[str, ...] = (
"hieroglyphs",
"runes",
"ogham",
"cuneiform",
"alchemy",
"sigils",
"latin",
"mixed",
"ancient",
)
FONT_SETS: dict[str, tuple[Glyph, ...]] = {
"hieroglyphs": HIEROGLYPHS,
"runes": FUTHARK,
"ogham": OGHAM,
"cuneiform": CUNEIFORM,
"alchemy": ALCHEMY,
"sigils": SIGILS,
"latin": LATIN,
"mixed": FUTHARK + HIEROGLYPHS,
"ancient": FUTHARK + HIEROGLYPHS + OGHAM + CUNEIFORM + ALCHEMY + SIGILS,
}
def font_param(default: str = "hieroglyphs") -> Param:
# Lazy import avoids circular import with leds.pattern.
from leds.pattern import Param
choice = default if default in FONT_NAMES else "hieroglyphs"
return Param(choice, kind="choice", choices=FONT_NAMES, label="Font")
def resolve_glyphs(params: dict[str, Any] | None, *, default: str = "hieroglyphs") -> tuple[Glyph, ...]:
"""Return the glyph set selected on the preset (shared Font control)."""
extras = (params or {}).get("extras")
extras = extras if isinstance(extras, dict) else {}
name = str(extras.get("font") or default).strip().lower()
if name not in FONT_SETS:
name = default if default in FONT_SETS else "hieroglyphs"
return FONT_SETS[name]
def glyph_width(glyph: Glyph) -> int:
return max((len(row) for row in glyph), default=0)
def sequence_width(glyphs: Sequence[Glyph], gap: int = 1) -> int:
if not glyphs:
return 0
return sum(glyph_width(g) for g in glyphs) + gap * (len(glyphs) - 1)
def vcenter(panel_h: int, glyph_h: int) -> int:
return max(0, (panel_h - glyph_h) // 2)
def blit_glyph(surface, origin_x: int, origin_y: int, glyph: Glyph, color: RGB) -> None:
from leds.pattern import set_pixel, size
w, h = size(surface)
for gy, row in enumerate(glyph):
y = origin_y + gy
if y < 0 or y >= h:
continue
for gx, ch in enumerate(row):
if ch not in LIT:
continue
x = origin_x + gx
if 0 <= x < w:
set_pixel(surface, x, y, color)
def blit_sequence(
surface,
origin_x: int,
origin_y: int,
glyphs: Sequence[Glyph],
color: RGB,
*,
gap: int = 1,
wrap: int | None = None,
) -> None:
cursor = origin_x
for glyph in glyphs:
blit_glyph(surface, cursor, origin_y, glyph, color)
if wrap is not None:
blit_glyph(surface, cursor + wrap, origin_y, glyph, color)
cursor += glyph_width(glyph) + gap