Red Mode: Nacht-Navigator-Logo mit 'RED RALF MODE'-Bogen
Build & Deploy / deploy (push) Successful in 19s
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:
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
+6
-1
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 |
Reference in New Issue
Block a user