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
No files matched your search
@@ -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
|
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).
|
`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
|
**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
|
neu angelegt, bekommt sie eine neue ID. Sie steht im Buchungslink der Leistung auf der
|
||||||
Setmore-Seite hinter `products=`.
|
Setmore-Seite hinter `products=`.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 65 KiB |
@@ -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()
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 166 KiB |