feat: Logo deutlich größer und auf jedem Untergrund sichtbar

Das hochgeladene logo.png ist die helle Fassung und war auf der hellen Kopfzeile und im Fuß bis auf das goldene A unsichtbar. Die Seite misst jetzt die Helligkeit (sharp) und ordnet die Datei selbst zu; fehlt eine passende Fassung, wird per CSS abgedunkelt bzw. aufgehellt. Über dem Foto bis 176 px hoch mit weichem Schatten, nach dem Scrollen 80 px. Raster-Logos laufen über die Bildoptimierung (Original 5378 px, 313 KB).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-06 07:15:09 +02:00
1 parent 33f5096bf1
commit 1ca09cbaef
8 files changed
+104 -27

No files matched your search

+62 -8
View File
@@ -12,14 +12,16 @@
*/
import { readdir, stat } from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
import { site, type Media } from "@/content/site";
import { env } from "./env";
import { optimizedImageUrl } from "./image-url";
const IMAGE_EXTS = [".jpg", ".jpeg", ".webp", ".png", ".avif"];
const LOGO_EXTS = [".svg", ".png", ".webp"];
const VIDEO_EXTS = [".mp4", ".webm"];
type Found = { src: string; ext: string };
type Found = { src: string; ext: string; file: string };
/**
* Sucht "<ordner>/<name>.<endung>" ohne Ruecksicht auf Gross- und
@@ -44,7 +46,7 @@ async function find(base: string, exts: readonly string[]): Promise<Found | null
const info = await stat(path.join(dir, match));
if (!info.isFile()) continue;
const rel = folder === "." ? match : `${folder}/${match}`;
return { src: `/media/${encodeURI(rel)}?v=${Math.floor(info.mtimeMs)}`, ext };
return { src: `/media/${encodeURI(rel)}?v=${Math.floor(info.mtimeMs)}`, ext, file: path.join(dir, match) };
} catch {
// Datei zwischenzeitlich entfernt: naechste Endung probieren.
}
@@ -65,14 +67,65 @@ async function image(base: string, alt: string): Promise<Media | null> {
return found ? { kind: "image", src: found.src, alt, width: 1200, height: 1500 } : null;
}
/**
* Logo samt Darstellung: "darken" bzw. "lighten" faerbt ein Logo per CSS um,
* wenn fuer den Untergrund keine passende Fassung hochgeladen wurde (z. B.
* nur ein helles Logo, das auf der hellen Kopfzeile sonst unsichtbar waere).
*/
export type LogoImage = { src: string; tone: "as-is" | "darken" | "lighten" };
type Tone = "light" | "dark";
/** Mittlere Helligkeit (0–255) der sichtbaren Pixel, ab der ein Logo als hell gilt. */
const LIGHT_LOGO_LUMINANCE = 140;
/** Breite der ausgelieferten Rastergrafik: scharf bis ~400 px Anzeigebreite auf 2x-Displays. */
const LOGO_DELIVERY_WIDTH = 828;
const toneCache = new Map<string, Tone>();
/**
* Hell oder dunkel? Gemessen statt am Dateinamen festgemacht: Wird das helle
* Logo als logo.png hochgeladen, landet es trotzdem auf dem dunklen Grund.
*/
async function logoTone(found: Found): Promise<Tone> {
const cached = toneCache.get(found.src);
if (cached) return cached;
const { data } = await sharp(found.file).resize(64, 64, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
let luminance = 0;
let weight = 0;
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3]! / 255;
if (alpha < 0.2) continue;
luminance += (0.2126 * data[i]! + 0.7152 * data[i + 1]! + 0.0722 * data[i + 2]!) * alpha;
weight += alpha;
}
const tone: Tone = weight > 0 && luminance / weight >= LIGHT_LOGO_LUMINANCE ? "light" : "dark";
toneCache.set(found.src, tone);
return tone;
}
function deliver(found: Found): string {
// SVG direkt (laeuft nicht durch die Bildoptimierung), Raster verkleinert:
// das Original-PNG hatte 5378 px Breite.
return found.ext === ".svg" ? found.src : optimizedImageUrl(found.src, LOGO_DELIVERY_WIDTH);
}
async function resolveLogos(candidates: (Found | null)[]): Promise<{ onLight: LogoImage | null; onDark: LogoImage | null }> {
const found = candidates.filter((c): c is Found => c !== null);
const toned = await Promise.all(found.map(async (f) => ({ f, tone: await logoTone(f).catch((): Tone => "dark") })));
const dark = toned.find((t) => t.tone === "dark")?.f;
const light = toned.find((t) => t.tone === "light")?.f;
const onLight: LogoImage | null = dark ? { src: deliver(dark), tone: "as-is" } : light ? { src: deliver(light), tone: "darken" } : null;
const onDark: LogoImage | null = light ? { src: deliver(light), tone: "as-is" } : dark ? { src: deliver(dark), tone: "lighten" } : null;
return { onLight, onDark };
}
export type Reel = { src: string | null; poster: Media | null };
export type IntroVideo = { src: string | null; poster: Media | null };
export type SiteMedia = {
/** Logo fuer helle Flaechen (dunkles Logo). */
logoDark: string | null;
/** Logo auf Foto oder dunklem Grund (helles Logo). */
logoLight: string | null;
/** Logo fuer helle Flaechen (Kopfzeile nach dem Scrollen, Fussbereich). */
logoDark: LogoImage | null;
/** Logo auf Foto oder dunklem Grund. */
logoLight: LogoImage | null;
header: Media;
headerVideo: string | null;
/** Video direkt unter dem Headerbild (intro.mp4, Vorschaubild intro.jpg). */
@@ -104,9 +157,10 @@ export async function loadSiteMedia(): Promise<SiteMedia> {
portraits[p.slug] = portraitList[index] ?? p.portrait;
});
const logos = await resolveLogos([logoDark, logoLight]);
return {
logoDark: logoDark?.src ?? logoLight?.src ?? null,
logoLight: logoLight?.src ?? logoDark?.src ?? null,
logoDark: logos.onLight,
logoLight: logos.onDark,
header: header ?? site.hero.media,
headerVideo: headerVideo?.src ?? null,
intro: { src: introVideo?.src ?? null, poster: introPoster },