diff --git a/app/public/redmode-logo.png b/app/public/redmode-logo.png new file mode 100644 index 0000000..ba88c4c Binary files /dev/null and b/app/public/redmode-logo.png differ diff --git a/app/src/App.tsx b/app/src/App.tsx index d985be2..0a62b16 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -6,8 +6,13 @@ import Cards from './pages/Cards' import Game from './pages/Game' import Contact from './pages/Contact' +const TABS: TabId[] = ['start', 'regeln', 'karten', 'spiel', 'kontakt'] + export default function App() { - const [tab, setTab] = useState('start') + const [tab, setTab] = useState(() => { + const p = new URLSearchParams(window.location.search).get('tab') + return TABS.includes(p as TabId) ? (p as TabId) : 'start' + }) return (
diff --git a/app/src/hooks/useRedMode.ts b/app/src/hooks/useRedMode.ts index 9ff7126..9660bff 100644 --- a/app/src/hooks/useRedMode.ts +++ b/app/src/hooks/useRedMode.ts @@ -1,15 +1,21 @@ import { useEffect } from 'react' -/** Hängt die Design-Klasse red-mode an und passt theme-color an. */ +/** Hängt die Design-Klasse red-mode an , passt theme-color und Favicon an. */ export function useRedMode(aktiv: boolean) { useEffect(() => { document.body.classList.toggle('red-mode', aktiv) const meta = document.querySelector('meta[name="theme-color"]') const vorher = meta?.getAttribute('content') if (meta) meta.setAttribute('content', aktiv ? '#1f060b' : '#170a2e') + + const icon = document.querySelector('link[rel="icon"]') + const iconOriginal = icon?.getAttribute('href') ?? '' + if (icon) icon.href = aktiv ? '/redmode-logo.png' : iconOriginal + return () => { document.body.classList.remove('red-mode') if (meta && vorher) meta.setAttribute('content', vorher) + if (icon && iconOriginal) icon.href = iconOriginal } }, [aktiv]) } diff --git a/app/src/pages/Cards.tsx b/app/src/pages/Cards.tsx index 9cfaa69..6abbec6 100644 --- a/app/src/pages/Cards.tsx +++ b/app/src/pages/Cards.tsx @@ -50,7 +50,11 @@ export default function Cards() { const [filter, setFilter] = useState('alle') const [gezogen, setGezogen] = useState(null) const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation - const [redMode, setRedMode] = useState(() => localStorage.getItem(REDMODE_KEY) === '1') + const [redMode, setRedMode] = useState( + () => + localStorage.getItem(REDMODE_KEY) === '1' || + new URLSearchParams(window.location.search).get('redmode') === '1' + ) useRedMode(redMode) const toggleRedMode = () => { @@ -79,6 +83,13 @@ export default function Cards() { return (
+ {redMode && ( + Red Ralf Mode Logo + )}

20 Statements · 5 Kategorien

diff --git a/app/src/pages/Game.tsx b/app/src/pages/Game.tsx index 930e07d..63095f0 100644 --- a/app/src/pages/Game.tsx +++ b/app/src/pages/Game.tsx @@ -286,6 +286,13 @@ function Lobby({ return (
+ {z.redMode && ( + Red Ralf Mode Logo + )}

Spielraum-Code

{z.code}

diff --git a/make_logo.py b/make_logo.py new file mode 100644 index 0000000..90e1ed2 --- /dev/null +++ b/make_logo.py @@ -0,0 +1,147 @@ +"""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) diff --git a/nacht-navigator.png b/nacht-navigator.png new file mode 100644 index 0000000..f74b62d Binary files /dev/null and b/nacht-navigator.png differ