// Logo-Register: SVG-Dateien der AirCard-IOS-Marken (Logo oben links), Einfärben und Laden als Bild für den Canvas. // Reine Daten plus ein kleiner Browser-Lader; die Größe der Logos bestimmt allein die viewBox. // Die Logos unten rechts kommen von der Community (logos/logo-asset.ts), hier steht keine fremde Marke. import { msg } from '../i18n/i18n'; import { TONE_COLOR, type BrandId, type DetailsTone } from './config'; import aircardsSymbol from '../assets/marks/aircards-symbol.svg?raw'; import aircardsText from '../assets/marks/aircards-text.svg?raw'; export type MarkGroup = 'Logo'; export interface MarkDef { id: Exclude; /** * Gruppenname; zusammen mit dem Label der Text für `aria-label`/`title` („Logo – AirCard-IOS · Symbol“). * Beide sind deutsch markiert (`msg`) und werden erst beim Anzeigen mit `t()` übersetzt. */ group: MarkGroup; label: string; svg: string; /** true: enthält `currentColor` und folgt der Details-Farbe. */ tint: boolean; /** Höchsthöhe auf der Karte in px (Breite folgt aus dem Seitenverhältnis der viewBox). */ maxH: number; } type MarkEntry = Omit; const BRAND_TABLE: Record, MarkEntry> = { 'aircards-symbol': { group: msg('Logo'), label: msg('AirCard-IOS · Symbol'), svg: aircardsSymbol, tint: true, maxH: 100 }, 'aircards-text': { group: msg('Logo'), label: msg('AirCard-IOS · Text'), svg: aircardsText, tint: true, maxH: 100 }, }; export const BRAND_MARKS: readonly MarkDef[] = ( Object.keys(BRAND_TABLE) as Exclude[] ).map((id) => ({ id, ...BRAND_TABLE[id] })); export function getBrandMark(id: Exclude): MarkDef { return BRAND_MARKS.find((m) => m.id === id) ?? BRAND_MARKS[0]; } /** Breite durch Höhe aus dem Wert einer viewBox („x y Breite Höhe“, mit Leerraum oder Kommas); null, wenn er nicht brauchbar ist. */ export function viewBoxAspect(value: string): number | null { const parts = value.trim().split(/[\s,]+/).map(Number); const [w, h] = [parts[2], parts[3]]; return parts.length === 4 && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 ? w / h : null; } /** Breite durch Höhe aus der viewBox; 1, wenn keine brauchbare viewBox da ist. */ export function svgAspect(svg: string): number { const hit = /viewBox\s*=\s*"([^"]+)"/.exec(svg); return (hit && viewBoxAspect(hit[1])) ?? 1; } /** Ersetzt jedes `currentColor` durch die Details-Farbe. */ export function tintSvg(svg: string, tone: DetailsTone): string { return svg.replaceAll('currentColor', TONE_COLOR[tone]); } /** Gibt dem Wurzelelement eine eigene Größe (viermal maxH hoch), damit alle Browser scharf auf den Canvas zeichnen. */ function withIntrinsicSize(svg: string, maxH: number): string { const h = maxH * 4; const w = Math.round(h * svgAspect(svg) * 100) / 100; return svg.replace('>(); /** Lädt ein Logo (eingefärbt) als Bild für `drawImage`. Je (id, tone) wird nur einmal geladen. */ export function loadMarkImage(def: MarkDef, tone: DetailsTone): Promise { const key = `${def.id}:${tone}`; let pending = imageCache.get(key); if (!pending) { const source = withIntrinsicSize(tintSvg(def.svg, tone), def.maxH); pending = new Promise((resolve, reject) => { const url = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml' })); const img = new Image(); img.onload = () => { URL.revokeObjectURL(url); resolve(img); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error(`Logo konnte nicht geladen werden: ${def.id}`)); }; img.src = url; }); pending.catch(() => imageCache.delete(key)); imageCache.set(key, pending); } return pending; }