Hintergründe: 15 kuratierte Bilder statt Testfotos
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>
This commit is contained in:
parent
10251e4f41
commit
522deffa1a
66 changed files with 556 additions and 38 deletions
126
dev/tools/backgrounds/build_graphic.py
Normal file
126
dev/tools/backgrounds/build_graphic.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#!/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')
|
||||
90
dev/tools/backgrounds/generate_photo.py
Normal file
90
dev/tools/backgrounds/generate_photo.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generates the photographic LifeWave backgrounds with Gemini (Nano Banana 2).
|
||||
|
||||
Square, because the app crops with `cover` in both orientations. Every
|
||||
prompt asks for an empty middle band and no focal point: the wave and its
|
||||
dots are the subject, the background only sets the mood.
|
||||
|
||||
Usage: python3 generate_photo.py [name ...] (default: all five)
|
||||
"""
|
||||
import os, sys, time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'lifeline-images'))
|
||||
from gemini import generate # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
OUT = os.path.join(ROOT, 'dev', 'backgrounds', 'photo')
|
||||
|
||||
COMMON = (' Square format, fills the frame edge to edge. No text, no watermark, no people, no animals, '
|
||||
'no buildings, no objects. The centre band of the image is calm and almost uniform, because a '
|
||||
'thin glowing line will be drawn over it later. Photographic, subdued, low contrast, fine grain.')
|
||||
|
||||
PROMPTS = {
|
||||
# Second pass: the graphic ideas rendered as photographed material, so
|
||||
# they carry inner detail without gaining a subject.
|
||||
'01-tinte': (
|
||||
'Macro photograph of the surface of deep charcoal-black handmade paper washed with black ink, so close that '
|
||||
'the paper extends beyond every edge of the frame, no sheet edges, no border, no background. '
|
||||
'Subtle mottled variations of anthracite and graphite, faint cotton fibres visible on close inspection, '
|
||||
'a soft darker vignette towards the edges, raking light from the upper left giving the surface a gentle '
|
||||
'matte relief. Nothing written on it, no objects.' + COMMON),
|
||||
'02-goldstaub': (
|
||||
'Macro photograph of fine gold dust and a few tiny gold leaf flakes scattered on dark anthracite velvet. '
|
||||
'The dust gathers along the bottom edge and thins out quickly upwards, the upper two thirds of the frame '
|
||||
'are almost empty dark velvet with only a handful of distant specks. Warm gold reflections, very shallow '
|
||||
'depth of field so most specks are soft bokeh. Elegant, dark, sparse.' + COMMON),
|
||||
'03-horizont': (
|
||||
'Minimalist photograph of a vast empty plain at the very end of dusk. Black featureless land in the lower '
|
||||
'third, a deep graphite night sky above, and between them only a thin, soft band of warm amber and gold '
|
||||
'afterglow along the horizon at two thirds of the height. No sun, no clouds, no trees, no lights. '
|
||||
'Cinematic, very dark, the glow is the only colour.' + COMMON),
|
||||
'04-papier': (
|
||||
'Macro photograph of the surface of warm cream handmade cotton paper, so close that the paper extends beyond '
|
||||
'every edge of the frame, no sheet edges, no deckle edge, no border, no background. Soft uneven surface with '
|
||||
'fine fibres, lit with raking light from the side so the texture shows as gentle relief, a subtle '
|
||||
'darker vignette in the corners. No writing, no lines, no objects, no folds.' + COMMON),
|
||||
'05-leinen': (
|
||||
'Photograph of heavy dark charcoal-grey linen with a visible slub weave, lit from the side so the threads '
|
||||
'catch a faint warm sheen. The fabric lies flat and even across the whole frame, no folds, no creases, no '
|
||||
'seams, only the weave itself with a very gentle variation of light. Rich, tactile, dark, low contrast.' + COMMON),
|
||||
'06a-aquarell-warm': (
|
||||
'Extreme close-up of a watercolour wash on cold-pressed paper, the paper runs beyond every edge of the '
|
||||
'frame, no border, no tape marks, no stripes, no bands, one continuous sheet. Blooms of warm sepia, golden '
|
||||
'ochre and a little dusty indigo pigment gather in the corners with visible granulation and soft '
|
||||
'cauliflower edges, the middle stays pale warm cream. No shapes, no drips, no splatter.' + COMMON),
|
||||
'06b-aquarell-dunkel': (
|
||||
'Extreme close-up of an ink wash on heavy paper, the paper runs beyond every edge of the frame, no border. '
|
||||
'Deep indigo and graphite ink bleeds in from the top and bottom edges with visible granulation, the '
|
||||
'middle band softens into a muted grey-blue wash, tiny specks of gold pigment caught in the darker '
|
||||
'areas. Moody, dark overall. No shapes, no drips.' + COMMON),
|
||||
'07-kohle': (
|
||||
'Photograph of a dark slate chalkboard surface, matte anthracite with a faint haze of chalk dust and the '
|
||||
'ghost traces of wiped chalk strokes near the top and bottom edges, the centre almost clean. Fine mineral '
|
||||
'texture of the slate visible up close, soft even light.' + COMMON),
|
||||
'08-nebel': (
|
||||
'Photograph of gentle hills at dawn in dense morning fog, almost monochrome grey-blue, dark toned. '
|
||||
'A faint treeline is barely visible at the very bottom, the rest of the frame is soft, empty fog with a '
|
||||
'slight brightening towards the top. No sun, no sky detail.' + COMMON),
|
||||
'09-daemmerung': (
|
||||
'Long-exposure photograph of a calm sea at dusk. The horizon sits in the lower third. Deep blue-grey '
|
||||
'sky above fading into a narrow warm amber band right at the horizon, perfectly smooth water below, '
|
||||
'darker than the sky. No sun disc, no detailed clouds, no boats, no shore.' + COMMON),
|
||||
'10-nachthimmel': (
|
||||
'Photograph of a deep midnight-blue night sky turning to black, with faint thin wisps of high cloud. '
|
||||
'No stars, no moon, no ground, no horizon, no light source. A smooth dark gradient with a hint of '
|
||||
'texture from the clouds.' + COMMON),
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
names = sys.argv[1:] or list(PROMPTS)
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for name in names:
|
||||
out = os.path.join(OUT, f'{name}.jpg')
|
||||
for size in ('2K', '1K'):
|
||||
try:
|
||||
generate(PROMPTS[name], out=out, aspect='1:1', size=size)
|
||||
print(out, size, os.path.getsize(out) // 1024, 'KB')
|
||||
break
|
||||
except RuntimeError as error:
|
||||
print(name, size, 'failed:', str(error)[:200])
|
||||
time.sleep(2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue