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:
1 parent
33f5096bf1
commit
1ca09cbaef
8 files changed
+104
-27
No files matched your search
+62
-8
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user