Red Mode: Nacht-Navigator-Logo mit 'RED RALF MODE'-Bogen
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)
This commit is contained in:
Kimi Work
2026-10-10 18:35:58 +02:00
parent 1094cf6948
commit 8cf4986b48
7 changed files with 179 additions and 3 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

+6 -1
View File
@@ -6,8 +6,13 @@ import Cards from './pages/Cards'
import Game from './pages/Game' import Game from './pages/Game'
import Contact from './pages/Contact' import Contact from './pages/Contact'
const TABS: TabId[] = ['start', 'regeln', 'karten', 'spiel', 'kontakt']
export default function App() { export default function App() {
const [tab, setTab] = useState<TabId>('start') const [tab, setTab] = useState<TabId>(() => {
const p = new URLSearchParams(window.location.search).get('tab')
return TABS.includes(p as TabId) ? (p as TabId) : 'start'
})
return ( return (
<div className="mx-auto min-h-dvh max-w-md pb-24"> <div className="mx-auto min-h-dvh max-w-md pb-24">
+7 -1
View File
@@ -1,15 +1,21 @@
import { useEffect } from 'react' import { useEffect } from 'react'
/** Hängt die Design-Klasse red-mode an <body> und passt theme-color an. */ /** Hängt die Design-Klasse red-mode an <body>, passt theme-color und Favicon an. */
export function useRedMode(aktiv: boolean) { export function useRedMode(aktiv: boolean) {
useEffect(() => { useEffect(() => {
document.body.classList.toggle('red-mode', aktiv) document.body.classList.toggle('red-mode', aktiv)
const meta = document.querySelector('meta[name="theme-color"]') const meta = document.querySelector('meta[name="theme-color"]')
const vorher = meta?.getAttribute('content') const vorher = meta?.getAttribute('content')
if (meta) meta.setAttribute('content', aktiv ? '#1f060b' : '#170a2e') if (meta) meta.setAttribute('content', aktiv ? '#1f060b' : '#170a2e')
const icon = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
const iconOriginal = icon?.getAttribute('href') ?? ''
if (icon) icon.href = aktiv ? '/redmode-logo.png' : iconOriginal
return () => { return () => {
document.body.classList.remove('red-mode') document.body.classList.remove('red-mode')
if (meta && vorher) meta.setAttribute('content', vorher) if (meta && vorher) meta.setAttribute('content', vorher)
if (icon && iconOriginal) icon.href = iconOriginal
} }
}, [aktiv]) }, [aktiv])
} }
+12 -1
View File
@@ -50,7 +50,11 @@ export default function Cards() {
const [filter, setFilter] = useState<Filter>('alle') const [filter, setFilter] = useState<Filter>('alle')
const [gezogen, setGezogen] = useState<Spielkarte | null>(null) const [gezogen, setGezogen] = useState<Spielkarte | null>(null)
const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation 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) useRedMode(redMode)
const toggleRedMode = () => { const toggleRedMode = () => {
@@ -79,6 +83,13 @@ export default function Cards() {
return ( return (
<div className="px-5 pt-8"> <div className="px-5 pt-8">
{redMode && (
<img
src="/redmode-logo.png"
alt="Red Ralf Mode Logo"
className="mx-auto mb-4 w-32 rounded-full shadow-[0_0_36px_rgba(239,68,68,0.4)] ring-2 ring-red-500/40"
/>
)}
<p className="mb-1 text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300"> <p className="mb-1 text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">
20 Statements · 5 Kategorien 20 Statements · 5 Kategorien
</p> </p>
+7
View File
@@ -286,6 +286,13 @@ function Lobby({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{z.redMode && (
<img
src="/redmode-logo.png"
alt="Red Ralf Mode Logo"
className="mx-auto w-28 rounded-full shadow-[0_0_36px_rgba(239,68,68,0.4)] ring-2 ring-red-500/40"
/>
)}
<div className="glass-card p-5 text-center"> <div className="glass-card p-5 text-center">
<p className="text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">Spielraum-Code</p> <p className="text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">Spielraum-Code</p>
<p className="mt-1 font-display text-5xl font-bold tracking-[0.25em] text-gradient">{z.code}</p> <p className="mt-1 font-display text-5xl font-bold tracking-[0.25em] text-gradient">{z.code}</p>
+147
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB