Build & Deploy / deploy (push) Successful in 19s
- Logo generiert: unterer Textbogen ersetzt, Goldringe rekonstruiert - Logo in Karten-Seite + Spielraum-Lobby eingebunden - Favicon-Swap bei aktivem Red Mode - ?tab= URL-Param fuer Tab-Auswahl (Screenshots/Deep-Links)
148 lines
4.8 KiB
Python
148 lines
4.8 KiB
Python
"""Erzeugt das Red-Mode-Logo: unterer Bogen 'NACHT-NAVIGATOR' -> 'RED RALF MODE'."""
|
|
import math
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
SRC = "nacht-navigator.png"
|
|
OUT_FINAL = "app/public/redmode-logo.png"
|
|
OUT_PREVIEW = "logo_preview.png"
|
|
|
|
S = 2
|
|
CS = 209
|
|
CX = CY = 104
|
|
|
|
img = Image.open(SRC).convert("RGB")
|
|
big = img.crop((0, 33, 209, 242)).resize((CS * S, CS * S), Image.LANCZOS)
|
|
cx = cy = CX * S
|
|
px = big.load()
|
|
|
|
def ang_xy(r, deg):
|
|
a = math.radians(deg)
|
|
return cx + r * math.cos(a), cy + r * math.sin(a)
|
|
|
|
def median_patch(x, y, rad=2):
|
|
vals = [[], [], []]
|
|
for dx in range(-rad, rad + 1):
|
|
for dy in range(-rad, rad + 1):
|
|
c = px[int(round(x)) + dx, int(round(y)) + dy]
|
|
for k in range(3):
|
|
vals[k].append(c[k])
|
|
return tuple(sorted(v)[len(v) // 2] for v in vals)
|
|
|
|
# ---------------------------------------------------------------- Bogen löschen
|
|
def radial_profile(deg, r0=104, r1=180, step=2):
|
|
return [median_patch(*ang_xy(r, deg)) for r in range(r0, r1 + 1, step)]
|
|
|
|
profL = radial_profile(18)
|
|
profR = radial_profile(162)
|
|
radii = list(range(104, 181, 2))
|
|
|
|
A0, A1 = 26, 154
|
|
R0, R1 = 104, 180
|
|
|
|
for y in range(cy - R1 - 8, cy + R1 + 9):
|
|
for x in range(cx - R1 - 8, cx + R1 + 9):
|
|
dx, dy = x - cx, y - cy
|
|
r = math.hypot(dx, dy)
|
|
if r < R0 or r > R1:
|
|
continue
|
|
th = math.degrees(math.atan2(dy, dx))
|
|
if th < 0:
|
|
th += 360
|
|
if not (A0 <= th <= A1):
|
|
continue
|
|
t = (th - A0) / (A1 - A0)
|
|
i = min(range(len(radii)), key=lambda k: abs(radii[k] - r))
|
|
cL, cR = profL[i], profR[i]
|
|
px[x, y] = tuple(int(cL[k] + (cR[k] - cL[k]) * t) for k in range(3))
|
|
|
|
# ------------------------------------------------- Gold-Ringe wieder anmalen
|
|
def ring_color(r0, r1):
|
|
"""Helles Gold: pro Winkel das hellste Pixel im Radius-Intervall, dann Median."""
|
|
cols = []
|
|
for deg in list(range(0, 16)) + list(range(164, 181)):
|
|
best = None
|
|
for r in range(r0, r1 + 1):
|
|
x, y = ang_xy(r, deg)
|
|
c = median_patch(x, y, 1)
|
|
lum = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
|
if best is None or lum > best[0]:
|
|
best = (lum, c)
|
|
cols.append(best[1])
|
|
cols.sort(key=lambda c: sum(c))
|
|
return cols[int(len(cols) * 0.75)]
|
|
|
|
gold_inner = ring_color(105, 111)
|
|
gold_outer = ring_color(166, 176)
|
|
print("Gold innen:", gold_inner, "aussen:", gold_outer)
|
|
|
|
dr = ImageDraw.Draw(big)
|
|
for r, col, w in [(108, gold_inner, 7), (172, gold_outer, 9)]:
|
|
bb = (cx - r, cy - r, cx + r, cy + r)
|
|
dr.arc(bb, start=A0 - 3, end=A1 + 3, fill=col, width=w)
|
|
|
|
# ------------------------------------------------------- Bogen-Text zeichnen
|
|
FONT_PATH = "C:/Windows/Fonts/ARIALNB.TTF"
|
|
TEXT = "RED RALF MODE"
|
|
R_BASE = 156
|
|
SPAN_MAX = 100
|
|
SQUEEZE = 0.62
|
|
|
|
def char_w(fnt, ch):
|
|
box = fnt.getbbox(ch, stroke_width=0)
|
|
return box[2] - box[0]
|
|
|
|
size = 72
|
|
while size > 10:
|
|
fnt = ImageFont.truetype(FONT_PATH, size)
|
|
raw = sum(char_w(fnt, ch) for ch in TEXT)
|
|
if raw * SQUEEZE <= R_BASE * math.radians(SPAN_MAX):
|
|
break
|
|
size -= 2
|
|
print("Fontgroesse:", size, "raw width:", raw)
|
|
|
|
fnt = ImageFont.truetype(FONT_PATH, size)
|
|
items = []
|
|
for ch in TEXT:
|
|
if ch == " ":
|
|
items.append((None, int(char_w(fnt, "N") * 0.45)))
|
|
continue
|
|
box = fnt.getbbox(ch, stroke_width=4)
|
|
w, h = box[2] - box[0] + 16, box[3] - box[1] + 16
|
|
ci = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
cd = ImageDraw.Draw(ci)
|
|
cd.text((8 - box[0], 8 - box[1]), ch, font=fnt, fill=(248, 244, 232, 255),
|
|
stroke_width=4, stroke_fill=(22, 14, 6, 255))
|
|
ci = ci.resize((max(4, int(w * SQUEEZE)), h), Image.LANCZOS)
|
|
items.append((ci, int(char_w(fnt, ch) * SQUEEZE) + 2))
|
|
|
|
total_w = sum(w for _, w in items)
|
|
span_deg = math.degrees(total_w / R_BASE)
|
|
print("Bogen-Spanne:", round(span_deg, 1), "deg")
|
|
|
|
layer = Image.new("RGBA", big.size, (0, 0, 0, 0))
|
|
theta = math.radians(90 + span_deg / 2) # linke Bogenkante, laeuft nach rechts abnehmend
|
|
for ci, w in items:
|
|
adv = w / R_BASE # Bogenlaenge/Radius = Winkel in RADIAN
|
|
theta -= adv / 2
|
|
if ci is not None:
|
|
bx, by = ang_xy(R_BASE, math.degrees(theta))
|
|
rot = ci.rotate(90 - math.degrees(theta), expand=True, resample=Image.BICUBIC)
|
|
# Alpha-BBox: Glyph unten (Baseline) an (bx,by) ausrichten
|
|
ab = rot.getbbox()
|
|
ox = bx - (ab[0] + ab[2]) / 2
|
|
oy = by - ab[3]
|
|
layer.alpha_composite(rot, (int(ox), int(oy)))
|
|
theta -= adv / 2
|
|
|
|
out = Image.alpha_composite(big.convert("RGBA"), layer)
|
|
out.save(OUT_PREVIEW)
|
|
|
|
# ------------------------------------------------------- Alpha-Maske + speichern
|
|
final = out.copy()
|
|
mask = Image.new("L", final.size, 0)
|
|
md = ImageDraw.Draw(mask)
|
|
md.ellipse((10, 18, 408, 400), fill=255)
|
|
final.putalpha(mask)
|
|
final.save(OUT_FINAL)
|
|
print("Gespeichert:", OUT_FINAL, final.size)
|