#!/usr/bin/env python3
"""
Erzeugt saemtliche Logodateien aus einer einzigen Geometrie.
Die Form entsteht nicht von Hand, sondern aus Punkten und Radien: ein
Kurbelarm ist die Huellkurve zweier Kreise (dick am Tretlager, duenn am
Pedal), die Bohrungen sind gegenlaeufig gewickelte Kreise im selben Pfad.
Wer die Marke aendert, aendert die Zahlen hier oben und laesst das Skript
laufen — dann stimmen App-Symbol, Schriftzug und PNG wieder zueinander.
python3 design/logo/build.py
Braucht Inkscape (Vereinigung der Teilformen) und rsvg-convert (PNG).
Der Schriftzug liegt als fertiger Pfad in `word.d`; er stammt aus Inter
ExtraBold, in Kurven gewandelt, und wird hier nur noch skaliert.
"""
import json, math, os, re, subprocess
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
STATIC = os.path.join(ROOT, "frontend", "static")
YELLOW = "#eab308" # yellow-500, die Themenfarbe der App
INK = "#18181b" # zinc-900
PAPER = "#fafaf9"
# Schriftzug: Inter ExtraBold, 128 px, Laufweite -3, in Pfade gewandelt.
WORD = open(os.path.join(HERE, "word.d")).read().strip()
WORD_BOX = dict(x=17.0455, y=46.9091, w=517.182, h=94.4091)
def hull(p1, r1, p2, r2):
"""Aussenkontur zweier Kreise — ein kegeliger Kurbelarm."""
(x1, y1), (x2, y2) = p1, p2
dx, dy = x2 - x1, y2 - y1
th = math.atan2(dy, dx)
be = math.acos(max(-1, min(1, (r1 - r2) / math.hypot(dx, dy))))
at = lambda c, r, a: (c[0] + r * math.cos(a), c[1] + r * math.sin(a))
A, B = at(p1, r1, th + be), at(p2, r2, th + be)
C, D = at(p2, r2, th - be), at(p1, r1, th - be)
return (f'M{A[0]:.3f} {A[1]:.3f}L{B[0]:.3f} {B[1]:.3f}'
f'A{r2} {r2} 0 0 0 {C[0]:.3f} {C[1]:.3f}'
f'L{D[0]:.3f} {D[1]:.3f}A{r1} {r1} 0 0 0 {A[0]:.3f} {A[1]:.3f}Z')
def bore(c, r):
"""Bohrung: gegenlaeufig gewickelt, damit sie im selben Pfad ein Loch ist."""
x, y = c
return (f'M{x - r:.3f} {y:.3f}A{r} {r} 0 1 1 {x + r:.3f} {y:.3f}'
f'A{r} {r} 0 1 1 {x - r:.3f} {y:.3f}Z')
def rrect(x, y, w, h, r):
return (f'M{x + r} {y}H{x + w - r}A{r} {r} 0 0 1 {x + w} {y + r}V{y + h - r}'
f'A{r} {r} 0 0 1 {x + w - r} {y + h}H{x + r}A{r} {r} 0 0 1 {x} {y + h - r}'
f'V{y + r}A{r} {r} 0 0 1 {x + r} {y}Z')
def union(name, parts):
"""Teilformen zu einem Umriss verschmelzen — sonst zeigen sich an den
Ueberlappungen Nahtkanten, und die Bohrungen wuerden dort zufaellig
wieder zuwachsen."""
body = "".join(f'' for i, d in enumerate(parts))
src = os.path.join(HERE, f".{name}-parts.svg")
dst = os.path.join(HERE, f".{name}-union.svg")
open(src, "w").write(
f'')
subprocess.run(["inkscape", src, "--actions",
f"select-all;path-union;export-plain-svg;"
f"export-filename:{dst};export-do"], capture_output=True)
ds = re.findall(r'\sd="([^"]+)"', open(dst).read(), re.S)
assert len(ds) == 1, f"{name}: {len(ds)} Pfade statt einem"
os.remove(src); os.remove(dst)
return " ".join(ds[0].split())
# --- Die drei Varianten -----------------------------------------------------
# Tretlager J, Pedalaugen UP/LO. Die Arme sind am Lager dick (8.5) und am
# Pedal duenn (5.5) — so herum ist eine Kurbel gebaut.
J, UP, LO = (18.5, 32), (45, 13), (45, 51)
VARIANTS = {
"kurbel-k": dict(
name="Kurbel-K",
parts=[rrect(10, 10.5, 9.5, 43, 4.75), hull(J, 8.5, UP, 5.5), hull(J, 8.5, LO, 5.5)],
bores=[(J, 4.4), (UP, 2.7), (LO, 2.7)],
),
"kurbelarm": dict(
name="Kurbelarm",
parts=[hull((23, 14), 9.5, (41, 50), 6.2)],
bores=[((23, 14), 4.5), ((41, 50), 3.0)],
),
"garnitur": dict(
name="Kurbelgarnitur",
parts=[hull((32, 32), 10, (16.5, 47.5), 6.2), hull((32, 32), 10, (47.5, 16.5), 6.2)],
bores=[((32, 32), 4.8), ((16.5, 47.5), 3.0), ((47.5, 16.5), 3.0)],
),
}
def ink_box(path):
"""Tintenrahmen der fertigen Form — fuers optische Zentrieren."""
tmp = os.path.join(HERE, ".measure.svg")
open(tmp, "w").write(f'')
out = subprocess.run(["inkscape", tmp, "--query-all"], capture_output=True, text=True).stdout
os.remove(tmp)
x, y, w, h = (float(v) for v in out.splitlines()[0].split(",")[1:5])
return x, y, x + w, y + h
def svg(w, h, body, view=None):
return (f'\n')
def build(key, spec, outdir):
path = union(key, spec["parts"]) + "".join(bore(c, r) for c, r in spec["bores"])
x0, y0, x1, y1 = ink_box(path)
mid = lambda s: (32 - (x0 + x1) / 2 * s, 32 - (y0 + y1) / 2 * s)
def centred(scale=1.0):
dx, dy = mid(scale)
return f'translate({dx:.4f} {dy:.4f}) scale({scale})'
def word_at(colour, cap, x):
s = cap / WORD_BOX["h"]
return (f''
f''), WORD_BOX["w"] * s
files = {}
files["mark.svg"] = svg(64, 64,
f'')
# Die Kachel: Marke auf 84 %, damit sie nicht an die Rundung stoesst.
files["icon.svg"] = svg(64, 64,
f''
f'')
files["icon-dark.svg"] = svg(64, 64,
f''
f'')
# Randlos fuer Android: 62 %, alles ausserhalb kann beschnitten werden.
files["icon-maskable.svg"] = svg(64, 64,
f''
f'')
for fn, mc, tc in (("logo.svg", YELLOW, INK),
("logo-dark.svg", YELLOW, PAPER),
("logo-mono.svg", "currentColor", "currentColor")):
g, ww = word_at(tc, 38, x1 + 15)
files[fn] = svg(round(x1 + 15 + ww - x0, 2), 64,
f'{g}',
view=f'{x0:g} 0 {x1 + 15 + ww - x0:.2f} 64')
os.makedirs(outdir, exist_ok=True)
for fn, content in files.items():
open(os.path.join(outdir, fn), "w").write(content)
for px in (192, 512):
subprocess.run(["rsvg-convert", "-w", str(px), "-h", str(px),
os.path.join(outdir, "icon.svg"),
"-o", os.path.join(outdir, f"icon-{px}.png")], check=True)
return path, (x0, y0, x1, y1)
if __name__ == "__main__":
geometry = {}
for key, spec in VARIANTS.items():
path, box = build(key, spec, os.path.join(HERE, key))
geometry[key] = dict(path=path, box=box)
print(f"{key:12} {spec['name']}")
# Variante 01 ist die gesetzte: sie wandert unter ihrem Markennamen nach
# static/ und wird von Manifest, Favicon und den Komponenten benutzt.
live = "kurbel-k"
for src, dst in (("mark.svg", "kurbeler-mark.svg"),
("icon.svg", "kurbeler-icon.svg"),
("icon-maskable.svg", "kurbeler-icon-maskable.svg"),
("logo.svg", "kurbeler-logo.svg"),
("logo-dark.svg", "kurbeler-logo-dark.svg"),
("icon-192.png", "kurbeler-icon-192.png"),
("icon-512.png", "kurbeler-icon-512.png")):
s = os.path.join(HERE, live, src)
open(os.path.join(STATIC, dst), "wb").write(open(s, "rb").read())
print(f"\nnach frontend/static/ kopiert: {live}")
json.dump(geometry, open(os.path.join(HERE, "geometry.json"), "w"), indent=1)