Elf universelle Hintergründe von grafisch bis fotografisch (Tinte, Goldstaub, Horizont, Kohle, Leinen, Nebel, Dämmerung, Nachthimmel, Tusche, Papier, Aquarell) plus vier v2-Fassungen zur Abstimmung, alle 2048² JPEG mit 320er Vorschauen (7 MB statt 48 MB). Auswahl, Reihenfolge und Helligkeit stehen in src/config/backgrounds.js; der Picker zeigt Name und aktuelle Wahl, das Logo folgt der Bildhelligkeit, Presets zeigen auf die neuen Bilder, alte Pfade werden beim Laden auf den Nachfolger umgeschrieben. Der Service Worker cacht nur die Vorschauen vor. Werkzeuge zum Neu-Rendern (SVG-Builder, Gemini-Prompts) liegen in dev/tools/backgrounds, die Sichtungs-Renders bleiben lokal (gitignore). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
126 lines
6.6 KiB
Python
126 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Builds the graphic LifeWave backgrounds as SVG and renders them to JPEG.
|
|
|
|
Square 2048 px, so `background-size: cover` crops them evenly in portrait and
|
|
landscape. Palette: ink #292a2b / #1a1b1c, gold #f5c26b / #d59040, paper
|
|
#f7f3ea. Deterministic: same seed, same file.
|
|
|
|
Usage: python3 build_graphic.py [out_dir] (default dev/backgrounds/graphic)
|
|
"""
|
|
import os, random, subprocess, sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, 'dev', 'backgrounds', 'graphic')
|
|
S = 2048
|
|
|
|
GRAIN = '''
|
|
<filter id="grain" x="0" y="0" width="100%" height="100%">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" stitchTiles="stitch"/>
|
|
<feColorMatrix type="saturate" values="0"/>
|
|
<feComponentTransfer><feFuncA type="linear" slope="{alpha}"/></feComponentTransfer>
|
|
</filter>'''
|
|
|
|
def svg(defs, body):
|
|
return f'<svg xmlns="http://www.w3.org/2000/svg" width="{S}" height="{S}" viewBox="0 0 {S} {S}">\n<defs>{defs}</defs>\n{body}\n</svg>\n'
|
|
|
|
def ink():
|
|
defs = '''
|
|
<radialGradient id="g" cx="50%" cy="46%" r="72%">
|
|
<stop offset="0" stop-color="#2f3032"/><stop offset="0.65" stop-color="#232425"/><stop offset="1" stop-color="#161718"/>
|
|
</radialGradient>''' + GRAIN.format(alpha=0.06)
|
|
body = f'<rect width="{S}" height="{S}" fill="url(#g)"/><rect width="{S}" height="{S}" filter="url(#grain)" opacity="0.5"/>'
|
|
return svg(defs, body)
|
|
|
|
def gold_dust():
|
|
rnd = random.Random(2026)
|
|
specks = []
|
|
for _ in range(5200):
|
|
# Denser and brighter towards the bottom; the middle band stays calm.
|
|
y = S * (1 - rnd.random() ** 0.45)
|
|
x = rnd.random() * S
|
|
r = rnd.choice([1.0, 1.3, 1.6, 2.0, 2.6, 3.4, 4.2])
|
|
depth = y / S
|
|
o = min(0.95, (0.08 + 1.1 * depth ** 1.6) * rnd.uniform(0.35, 1.0))
|
|
c = '#f5c26b' if rnd.random() < 0.7 else '#d59040'
|
|
specks.append(f'<circle cx="{x:.0f}" cy="{y:.0f}" r="{r}" fill="{c}" opacity="{o:.2f}"/>')
|
|
defs = '''
|
|
<radialGradient id="g" cx="50%" cy="40%" r="75%">
|
|
<stop offset="0" stop-color="#2c2d2f"/><stop offset="1" stop-color="#151617"/>
|
|
</radialGradient>
|
|
<linearGradient id="warm" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0.5" stop-color="#d59040" stop-opacity="0"/><stop offset="1" stop-color="#d59040" stop-opacity="0.22"/>
|
|
</linearGradient>
|
|
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2"/></filter>''' + GRAIN.format(alpha=0.05)
|
|
body = (f'<rect width="{S}" height="{S}" fill="url(#g)"/>'
|
|
f'<rect width="{S}" height="{S}" fill="url(#warm)"/>'
|
|
f'<g filter="url(#soft)">{"".join(specks[:2200])}</g><g>{"".join(specks[2200:])}</g>'
|
|
f'<rect width="{S}" height="{S}" filter="url(#grain)" opacity="0.4"/>')
|
|
return svg(defs, body)
|
|
|
|
def horizon():
|
|
defs = '''
|
|
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0" stop-color="#0f1013"/><stop offset="0.52" stop-color="#1b1c1f"/>
|
|
<stop offset="0.66" stop-color="#2e2415"/><stop offset="0.74" stop-color="#241c12"/><stop offset="1" stop-color="#12100b"/>
|
|
</linearGradient>
|
|
<filter id="blur" x="-50%" y="-400%" width="200%" height="900%"><feGaussianBlur stdDeviation="120"/></filter>''' + GRAIN.format(alpha=0.05)
|
|
body = (f'<rect width="{S}" height="{S}" fill="url(#sky)"/>'
|
|
f'<ellipse cx="{S/2}" cy="{S*0.665:.0f}" rx="{S*1.1:.0f}" ry="60" fill="#d59040" opacity="0.32" filter="url(#blur)"/>'
|
|
f'<ellipse cx="{S/2}" cy="{S*0.665:.0f}" rx="{S*0.7:.0f}" ry="14" fill="#f5c26b" opacity="0.22" filter="url(#blur)"/>'
|
|
f'<rect width="{S}" height="{S}" filter="url(#grain)" opacity="0.5"/>')
|
|
return svg(defs, body)
|
|
|
|
def paper():
|
|
defs = '''
|
|
<radialGradient id="g" cx="50%" cy="45%" r="75%">
|
|
<stop offset="0" stop-color="#f9f5ec"/><stop offset="0.7" stop-color="#f3ecdd"/><stop offset="1" stop-color="#e8dfcc"/>
|
|
</radialGradient>
|
|
<filter id="fibre" x="0" y="0" width="100%" height="100%">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.02 0.35" numOctaves="3" seed="3" stitchTiles="stitch"/>
|
|
<feColorMatrix type="saturate" values="0"/>
|
|
<feComponentTransfer><feFuncA type="linear" slope="0.10"/></feComponentTransfer>
|
|
</filter>''' + GRAIN.format(alpha=0.05)
|
|
body = (f'<rect width="{S}" height="{S}" fill="url(#g)"/>'
|
|
f'<rect width="{S}" height="{S}" filter="url(#fibre)" opacity="0.6"/>'
|
|
f'<rect width="{S}" height="{S}" filter="url(#grain)" opacity="0.5"/>')
|
|
return svg(defs, body)
|
|
|
|
def charcoal():
|
|
rnd = random.Random(11)
|
|
smudges = ''.join(
|
|
f'<ellipse cx="{rnd.uniform(0.05, 0.95) * S:.0f}" cy="{rnd.choice([rnd.uniform(0.02, 0.22), rnd.uniform(0.78, 0.98)]) * S:.0f}" '
|
|
f'rx="{rnd.uniform(220, 520):.0f}" ry="{rnd.uniform(40, 110):.0f}" fill="#f7f3ea" opacity="{rnd.uniform(0.025, 0.06):.3f}" '
|
|
f'transform="rotate({rnd.uniform(-12, 12):.1f} {S/2} {S/2})" filter="url(#blur)"/>'
|
|
for _ in range(9))
|
|
defs = '''
|
|
<radialGradient id="g" cx="50%" cy="50%" r="75%">
|
|
<stop offset="0" stop-color="#2d2e30"/><stop offset="1" stop-color="#1d1e1f"/>
|
|
</radialGradient>
|
|
<filter id="chalk" x="0" y="0" width="100%" height="100%">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="4" seed="5" stitchTiles="stitch"/>
|
|
<feColorMatrix type="saturate" values="0"/>
|
|
<feComponentTransfer><feFuncA type="table" tableValues="0 0 0.02 0.10 0.16"/></feComponentTransfer>
|
|
</filter>
|
|
<filter id="blur" x="-50%" y="-100%" width="200%" height="300%"><feGaussianBlur stdDeviation="40"/></filter>''' + GRAIN.format(alpha=0.08)
|
|
body = (f'<rect width="{S}" height="{S}" fill="url(#g)"/>'
|
|
f'<rect width="{S}" height="{S}" fill="#f7f3ea" filter="url(#chalk)" opacity="0.5"/>'
|
|
f'{smudges}'
|
|
f'<rect width="{S}" height="{S}" filter="url(#grain)" opacity="0.6"/>')
|
|
return svg(defs, body)
|
|
|
|
BACKGROUNDS = [
|
|
('01-tinte', ink), ('02-goldstaub', gold_dust), ('03-horizont', horizon),
|
|
('04-papier', paper), ('07-kohle', charcoal),
|
|
]
|
|
|
|
if __name__ == '__main__':
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for name, fn in BACKGROUNDS:
|
|
svg_path = os.path.join(OUT, f'{name}.svg')
|
|
png_path = os.path.join(OUT, f'{name}.png')
|
|
jpg_path = os.path.join(OUT, f'{name}.jpg')
|
|
open(svg_path, 'w').write(fn())
|
|
subprocess.run(['rsvg-convert', '-w', str(S), '-h', str(S), svg_path, '-o', png_path], check=True)
|
|
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', png_path, '-q:v', '3', jpg_path], check=True)
|
|
os.remove(png_path)
|
|
print(jpg_path, os.path.getsize(jpg_path) // 1024, 'KB')
|