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>
This commit is contained in:
2026-09-09 00:59:03 +12:00
co-authored by Cursor
parent 5094c7bcee
commit 3d444d1cb5
127 changed files with 10318 additions and 62632 deletions
+251
View File
@@ -0,0 +1,251 @@
"""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