Filmkorn auf allen Fotos

- Hero: feine, dezente Körnung (Variante A)
- Team-Porträts, Galerie und Menü-Fotos: Film-Look ISO 800 mit leicht
  farbigem Korn, etwas angehobenem Schwarz und leicht entsättigt (Variante B)
- Unbearbeitete Originale in design/original/, scripts/grain.py wendet den
  Effekt reproduzierbar an; Vorschaubild und README aktualisiert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBHsCnWNXAB2Le3gAEsivF
This commit is contained in:
Claude committed 2026-09-29 15:09:01 +00:00
1 parent 9e22ce616d
commit 734c2775b5
47 files changed
+75

No files matched your search

+5
View File
@@ -57,6 +57,11 @@ Preis-Thumbnails). Neue Porträts im selben Format ablegen und in `src/data/salo
laufen beim Scrollen seitlich durch, auf dem Handy wird gewischt. Neues Bild: als WebP (900 px breit) in
`src/assets/gallery/` ablegen und in `moments` in `Gallery.tsx` eintragen (Titel + Seitenverhältnis).
**Körnung:** Alle Fotos haben einen Filmkorn-Look – Hero fein (Variante A), alle übrigen Fotos wie Film
ISO 800 (Variante B). Die unbearbeiteten Originale liegen in `design/original/`. Neue oder geänderte Fotos
dort ablegen (gleicher Pfad wie in `src/assets/`) und `python3 scripts/grain.py` ausführen – das Skript
schreibt die gekörnten Versionen nach `src/assets/`.
**Direktbuchung:** Jede Leistung trägt ihre Setmore-ID (`id`). Wird bei Setmore eine Leistung
neu angelegt, bekommt sie eine neue ID. Sie steht im Buchungslink der Leistung auf der
Setmore-Seite hinter `products=`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+70
View File
@@ -0,0 +1,70 @@
"""Filmkorn auf die Website-Fotos rechnen.
Liest die unbearbeiteten Bilder aus design/original/ und schreibt die gekörnten Versionen
nach src/assets/. Hero: Variante A (fein), alle anderen Fotos: Variante B (Film, ISO 800).
pip install numpy pillow
python3 scripts/grain.py
"""
from pathlib import Path
import numpy as np
from PIL import Image, ImageFilter
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / 'design' / 'original'
DST = ROOT / 'src' / 'assets'
VARIANTS = {
# fein & dezent
'A': dict(amount=0.016, size=1.2),
# Film, ISO 800: sichtbares, leicht farbiges Korn, etwas angehobenes Schwarz, leicht entsättigt
'B': dict(amount=0.028, size=1.5, color=0.15, lift=0.03, contrast=1.05, sat=0.92),
}
HERO = {'photos/hero.webp', 'photos/hero-1000.webp', 'photos/hero-mobile.webp'}
LUMA = np.array([0.2126, 0.7152, 0.0722], np.float32)
def noise(rng, h, w, size, color):
"""Gauß-Rauschen, auf Korngröße weichgezeichnet und wieder auf Standardabweichung 1 normiert."""
def one():
n = rng.normal(0, 1, (h, w)).astype(np.float32)
if size > 1:
im = Image.fromarray((n * 40 + 128).clip(0, 255).astype(np.uint8))
n = (np.asarray(im.filter(ImageFilter.GaussianBlur(size * 0.5)), np.float32) - 128) / 40
n /= n.std() + 1e-6
return n
mono = one()
if not color:
return np.stack([mono] * 3, -1)
return np.stack([mono + color * one() for _ in range(3)], -1)
def grain(img, rng, amount, size, color=0.0, lift=0.0, contrast=1.0, sat=1.0):
a = np.asarray(img.convert('RGB'), np.float32) / 255
h, w, _ = a.shape
a = (a - 0.5) * contrast + 0.5
lum = a @ LUMA
a = lum[..., None] + (a - lum[..., None]) * sat
a = lift + a * (1 - lift)
# Korn vor allem in den Mitteltönen, wie bei Film
lum = np.clip(a @ LUMA, 0, 1)
weight = 4 * lum * (1 - lum) + 0.35
a = a + noise(rng, h, w, size, color) * amount * weight[..., None]
return Image.fromarray((np.clip(a, 0, 1) * 255).astype(np.uint8))
def main():
for i, src in enumerate(sorted(SRC.rglob('*.webp'))):
rel = src.relative_to(SRC).as_posix()
variant = 'A' if rel in HERO else 'B'
out = grain(Image.open(src), np.random.default_rng(i + 1), **VARIANTS[variant])
dst = DST / rel
out.save(dst, 'WEBP', quality=82, method=6)
print(f'{variant} {rel:28} {src.stat().st_size // 1024:4} KB -> {dst.stat().st_size // 1024:4} KB')
if __name__ == '__main__':
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 166 KiB