Files
HoneyOS/tools/biene.py
T
marc ff413864a9 Die Anmeldung bekommt die Biene in Schraegsicht
Die 3/4-Ansicht ist unter 64 px unbrauchbar und deshalb genau hier richtig:
Auf dem Anmeldebildschirm ist Platz, und man wartet ohnehin auf den Anbieter.
Sie schwebt sechs Sekunden lang zwei Pixel weit — eher ein Atmen als eine
Bewegung, und bei prefers-reduced-motion steht sie still.

Gerechnet wird sie in tools/biene.py aus demselben Raster wie die anderen
Fassungen.
2026-09-02 10:27:03 +02:00

227 lines
8.9 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.
#!/usr/bin/env python3
"""Erzeugt das HoneyOS-Wappen aus einem Textraster.
Die Biene steht **hier** und nirgends sonst: als 16 × 16 Zeichen, ein Zeichen
je Pixel. Daraus entstehen alle Fassungen, die HoneyOS braucht — SVG für die
beiden Leisten, PNG für Dokumentation und Vorschau, und eine Fassung aus
Schriftzeichen fürs System-Fenster und den Startlauf.
python3 tools/biene.py
Kein Pillow, keine Abhängigkeit: Ein PNG ist ein Kopfteil, ein zlib-Strom aus
Bildzeilen (jede mit einem Filterbyte davor) und eine Prüfsumme — das kann die
Standardbibliothek. Der Vorteil gegenüber einem Bildprogramm ist nicht die
Bequemlichkeit, sondern dass die **Quelle Text bleibt**: Ein Pixel ändern heißt
einen Buchstaben ändern, danach stimmen wieder alle Fassungen überein.
"""
import json
import pathlib
import struct
import zlib
WURZEL = pathlib.Path(__file__).resolve().parent.parent
# Die Farben nach dem Vorbild: Die Streifen sind **braun**. Schwarz sind nur
# Augen, Beine und Fühler — das ist der Unterschied zwischen warm und hart.
FARBEN = {
"Y": (242, 179, 61), # Honiggelb, der Körper
"L": (255, 208, 96), # Oberkante, eine Spur heller
"y": (210, 149, 42), # Unterseite im Schatten
"S": (92, 58, 28), # Streifen
"s": (70, 43, 20), # Streifen an der Unterkante
"E": (24, 20, 16), # Augenfeld und Beine
"B": (99, 199, 192), # der Lichtpunkt im Auge
"D": (59, 36, 18), # Fühler
"W": (245, 242, 231), # Flügel
"w": (214, 209, 190), # Flügel, untere Lage
}
# Von der Seite: Stachel hinten, Kopf in Laufrichtung. Diese Ansicht ist es
# geworden, weil ihre Streifen **senkrecht** laufen — deshalb bleibt sie auch
# als Text lesbar, wo die Vorderansicht in Streifen zerfällt.
BIENE = [
"....WW...WW.....",
"..WWWWW.WWWWW...",
"..WWWWWWWWWWW.D.",
"...WWWWWWWWW.DD.",
"...LLLLLLLLLLL..",
"..YYSSYYSSYYYYY.",
".YYYSSYYSSYYEBY.",
"SYYYSSYYSSYYEEY.",
".YYYSSYYSSYYYYY.",
"..yssyyssyyyyyy.",
"...EE..EE..EE...",
"...EE..EE..EE...",
"................",
"................",
"................",
"................",
]
# ── Die 3/4-Ansicht ──────────────────────────────────────────────────────
#
# Nur für den Anmeldebildschirm: groß, einmal am Tag, mit Zeit zum Hinsehen.
# Unter 64 px zerfällt sie — zu viele Kanten auf zu wenig Pixeln.
#
# Ein Würfel hat in Pixelkunst die Steigung 2:1: zwei Pixel nach rechts, einer
# nach unten. Von Hand wird das krumm, also werden die drei sichtbaren Flächen
# gerechnet — Deckel als Raute, darunter zwei Wangen. Wichtig ist, **zeilenweise**
# zu rechnen: Wer die Raute Punkt für Punkt setzt, lässt Löcher, weil zwei
# Schritte nach rechts nur einen nach unten kosten.
def wuerfel(breite, wand):
hoehe = breite // 2
mitte = breite / 2
deckel = {
(x, y)
for y in range(hoehe)
for x in range(breite)
if abs(x + 0.5 - mitte) / 2 + abs(y + 0.5 - hoehe / 2) <= hoehe / 2
}
unterkante = {}
for (x, y) in deckel:
unterkante[x] = max(unterkante.get(x, -1), y)
flaechen = {q: "T" for q in deckel}
for x, yb in unterkante.items():
for t in range(1, wand + 1):
flaechen[(x, yb + t)] = "V" if x < mitte else "R" # vorn / rechts
return flaechen, unterkante, mitte
def schraeg(breite=22, wand=10, dx=5, dy=9, n=32, band=5):
flaechen, unterkante, mitte = wuerfel(breite, wand)
g = [["." for _ in range(n)] for _ in range(n)]
def setz(x, y, c):
if 0 <= x + dx < n and 0 <= y + dy < n:
g[y + dy][x + dx] = c
# Die Streifen laufen parallel zur linken Deckelkante (u = x - 2y) und
# setzen sich auf der rechten Wange als senkrechte Bänder fort: dieselbe
# Rechnung, ausgewertet an der Kante, an der die Wange beginnt. Deshalb
# passen sie über den Knick hinweg zusammen.
streifig = lambda x, y: ((x - 2 * y) // band) % 2 == 0
for (x, y), art in sorted(flaechen.items()):
if art == "T":
setz(x, y, "S" if streifig(x, y) else "L")
elif art == "R":
setz(x, y, "s" if streifig(x, unterkante[x]) else "y")
else:
setz(x, y, "Y")
# Augen: Rechtecke in Flächenkoordinaten — auf dem Schirm werden daraus
# Parallelogramme, genau wie beim Vorbild.
for ax in (2, 7):
for x in range(ax, ax + 3):
if x in unterkante:
for t in (3, 4, 5):
setz(x, unterkante[x] + t, "E")
if ax + 1 in unterkante:
setz(ax + 1, unterkante[ax + 1] + 4, "B")
for k in range(3): # zwei Flügel in derselben Schräge
for b in range(6):
setz(3 + 2 * k + b, -2 + k, "W")
setz(10 + 2 * k + b, -4 + k, "w")
for k in range(2): # kurze Fühler am Deckelrand
setz(int(mitte) - 5 - k, -3 - k, "D")
setz(int(mitte) + 6 + k, -5 - k, "D")
return ["".join(z) for z in g]
def svg(raster):
"""Waagerechte Läufe gleicher Farbe werden ein Rechteck — dieselbe Idee wie
bei den Pixelsymbolen in web/js/glyphs.js, nur mit Farbe statt currentColor.
Warum SVG und nicht PNG für die Leisten: Die HoneyBar läuft in *fremden*
Apps. Ein <img> holt dort eine Datei von HoneyOS, und ob das erlaubt ist,
entscheidet die CSP der fremden App. Markup kann niemand blockieren."""
teile = []
for y, zeile in enumerate(raster):
x = 0
while x < len(zeile):
z = zeile[x]
if z not in FARBEN:
x += 1
continue
lauf = 1
while x + lauf < len(zeile) and zeile[x + lauf] == z:
lauf += 1
r, g, b = FARBEN[z]
teile.append(f'<rect x="{x}" y="{y}" width="{lauf}" height="1" fill="#{r:02x}{g:02x}{b:02x}"/>')
x += lauf
n = len(raster)
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {n} {n}" '
'shape-rendering="crispEdges" aria-hidden="true">' + "".join(teile) + "</svg>")
def png(raster, faktor, ziel):
"""Vergrößert wird um ganze Faktoren, damit jedes Pixel ein Quadrat bleibt."""
n = len(raster)
roh = bytearray()
for zeile in raster:
pixel = bytearray()
for zeichen in zeile:
r, g, b = FARBEN.get(zeichen, (0, 0, 0))
pixel += bytes((r, g, b, 255 if zeichen in FARBEN else 0)) * faktor
for _ in range(faktor):
roh += b"\x00" + pixel # Filterbyte 0: keine Vorhersage
def block(art, daten):
return (struct.pack(">I", len(daten)) + art + daten
+ struct.pack(">I", zlib.crc32(art + daten) & 0xFFFFFFFF))
kopf = struct.pack(">IIBBBBB", n * faktor, n * faktor, 8, 6, 0, 0, 0)
ziel.write_bytes(b"\x89PNG\r\n\x1a\n" + block(b"IHDR", kopf)
+ block(b"IDAT", zlib.compress(bytes(roh), 9)) + block(b"IEND", b""))
# Ein Zeichen ist etwa halb so breit wie hoch — **ein Pixel sind zwei Zeichen**,
# sonst steht die Biene hochkant. Blockzeichen (█ ▓ ▒) wären naheliegend,
# kacheln aber nicht: Zwischen den Zellen bleiben Fugen, und der Körper wird zur
# Waffel. Gewöhnliche Schriftzeichen behaupten gar nicht erst, eine Fläche zu
# sein — deshalb sehen sie besser aus.
ZEICHEN = {"Y": "#", "L": "#", "y": "#", "S": ":", "s": ":", "E": ":", "D": ":", "W": "-", "B": "o"}
def text(raster):
zeilen = ["".join(ZEICHEN.get(c, " ") * 2 for c in z).rstrip() for z in raster]
while zeilen and not zeilen[0]:
zeilen.pop(0)
while zeilen and not zeilen[-1]:
zeilen.pop()
return "\n".join(zeilen)
def main():
bild = svg(BIENE)
zeichen = text(BIENE)
(WURZEL / "web/img/biene.svg").write_text(bild + "\n")
# Für die Anmeldung: einmal groß, in Schrägsicht.
drei = schraeg()
(WURZEL / "web/img/biene-3d.svg").write_text(svg(drei) + "\n")
png(drei, 8, WURZEL / "web/img/biene-3d-256.png")
png(BIENE, 16, WURZEL / "web/img/biene-256.png")
png(BIENE, 2, WURZEL / "web/img/biene-32.png")
# Für die Oberfläche als Modul: Das Wappen im Markup (die Leiste holt kein
# Bild nach) und die Zeichenfassung fürs System-Fenster.
(WURZEL / "web/js/biene.js").write_text(
"/* Erzeugt von tools/biene.py — nicht von Hand ändern.\n"
" *\n"
" * Das Wappen als Markup statt als Bilddatei: So kommt es ohne zweite\n"
" * Anfrage aus und lässt sich mit dem Text daneben ausrichten.\n"
" */\n"
# Einfache Anführungszeichen: So steht das Markup wörtlich da und ist
# mit web/img/biene.svg vergleichbar — ein Test hält beides gleich.
f"export const BIENE_SVG = '{bild}';\n\n"
f"export const BIENE_ASCII = {json.dumps(zeichen)};\n"
)
print(text(BIENE))
print(f"\nweb/img/biene.svg ({len(bild)} Zeichen), biene-3d.svg, drei PNG, web/js/biene.js")
if __name__ == "__main__":
main()