Auf dem Anmeldebildschirm steht jetzt dieselbe Biene wie in der Leiste und im Tab, nur gross — ein Geraet, ein Zeichen. Die 3/4-Ansicht wird nirgends mehr ausgeliefert; sie bleibt als schraeg() im Werkzeug liegen. Dazu sitzt der SVG-Rahmen jetzt auf dem, was wirklich gefuellt ist. Das Raster ist quadratisch, die Biene nicht — unten blieb ein Viertel Luft, und die kostet ueberall Groesse, wo das Bild in einen Kasten gelegt wird. tools/biene.py schreibt die Zeile in honeybar.js jetzt selbst mit: Drei Kopien, die von Hand gepflegt werden, laufen genau einmal auseinander.
242 lines
9.7 KiB
Python
242 lines
9.7 KiB
Python
#!/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 re
|
||
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 ──────────────────────────────────────────────────────
|
||
#
|
||
# Liegt in der Schublade: HoneyOS zeigt überall dasselbe Wappen von der Seite,
|
||
# also wird sie zurzeit nirgends ausgeliefert. Wer sie doch braucht (ein großes
|
||
# Startbild, ein Aufkleber), ruft `schraeg()` und reicht das Ergebnis an `svg()`
|
||
# oder `png()`. 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.
|
||
|
||
Der Rahmen wird auf das gesetzt, was wirklich gefüllt ist: Das Raster ist
|
||
quadratisch, die Biene ist es nicht. Mit dem vollen Quadrat als viewBox
|
||
bliebe unten ein Viertel Luft — und die kostet überall Größe, wo das Bild in
|
||
einen Kasten gelegt wird: in der Leiste, im Tab, auf dem Anmeldebildschirm.
|
||
|
||
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
|
||
voll = [(x, y) for y, z in enumerate(raster) for x, c in enumerate(z) if c in FARBEN]
|
||
x0 = min(x for x, _ in voll)
|
||
y0 = min(y for _, y in voll)
|
||
breite = max(x for x, _ in voll) - x0 + 1
|
||
hoehe = max(y for _, y in voll) - y0 + 1
|
||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x0} {y0} {breite} {hoehe}" '
|
||
'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")
|
||
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"
|
||
)
|
||
bar = WURZEL / "web/honeybar/honeybar.js"
|
||
alt = bar.read_text()
|
||
neu = re.sub(r" const WAPPEN = '[^\n]*';", f" const WAPPEN = '{bild}';", alt)
|
||
if neu != alt:
|
||
bar.write_text(neu)
|
||
print("web/honeybar/honeybar.js: Wappen ersetzt")
|
||
|
||
print(text(BIENE))
|
||
print(f"\nweb/img/biene.svg ({len(bild)} Zeichen), biene-256.png, biene-32.png, web/js/biene.js")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|