feat(card): Netzwerk- und Marken-Logos
18 Netzwerk-Logos, AirCards-Symbol/-Text und Apple als bereinigte SVGs (Wikimedia Commons, gemeinfrei; abgeleitete Varianten selbst gebaut), Register marks.ts mit Einfärben (currentColor) und Bild-Lader sowie festfarbiges Favicon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a4ed0d595
commit
eb6b4ac59d
24 files changed
+318
-4
No files matched your search
@@ -0,0 +1,131 @@
|
||||
// Logo-Register: SVG-Dateien der Netzwerke und Marken, 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.
|
||||
|
||||
import { TONE_COLOR, type BrandId, type DetailsTone, type NetworkId, NETWORK_IDS } from './config';
|
||||
|
||||
import visaAuto from '../assets/marks/visa-auto.svg?raw';
|
||||
import visaBlue from '../assets/marks/visa-blue.svg?raw';
|
||||
import visaNavy from '../assets/marks/visa-navy.svg?raw';
|
||||
import visaSilver from '../assets/marks/visa-silver.svg?raw';
|
||||
import mcColor from '../assets/marks/mc-color.svg?raw';
|
||||
import mcSilver from '../assets/marks/mc-silver.svg?raw';
|
||||
import mcWordmark from '../assets/marks/mc-wordmark.svg?raw';
|
||||
import mcSilverText from '../assets/marks/mc-silver-text.svg?raw';
|
||||
import mcClassic from '../assets/marks/mc-classic.svg?raw';
|
||||
import upColor from '../assets/marks/up-color.svg?raw';
|
||||
import upSilver from '../assets/marks/up-silver.svg?raw';
|
||||
import upWhiteFrame from '../assets/marks/up-white-frame.svg?raw';
|
||||
import upEnglish from '../assets/marks/up-english.svg?raw';
|
||||
import amexBlue from '../assets/marks/amex-blue.svg?raw';
|
||||
import amexOutline from '../assets/marks/amex-outline.svg?raw';
|
||||
import amexSelect from '../assets/marks/amex-select.svg?raw';
|
||||
import jcb from '../assets/marks/jcb.svg?raw';
|
||||
import discover from '../assets/marks/discover.svg?raw';
|
||||
import aircardsSymbol from '../assets/marks/aircards-symbol.svg?raw';
|
||||
import aircardsText from '../assets/marks/aircards-text.svg?raw';
|
||||
import apple from '../assets/marks/apple.svg?raw';
|
||||
|
||||
export type MarkGroup = 'Visa' | 'Mastercard' | 'UnionPay' | 'American Express' | 'JCB' | 'Discover' | 'Logo';
|
||||
|
||||
export interface MarkDef {
|
||||
id: NetworkId | Exclude<BrandId, 'none'>;
|
||||
/** Gruppenname; zusammen mit dem Label der Text für `aria-label`/`title` („Visa – Blau“). */
|
||||
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 NetworkEntry = Omit<MarkDef, 'id'>;
|
||||
|
||||
const NETWORK_TABLE: Record<NetworkId, NetworkEntry> = {
|
||||
'visa-auto': { group: 'Visa', label: 'Automatisch', svg: visaAuto, tint: true, maxH: 109 },
|
||||
'visa-blue': { group: 'Visa', label: 'Blau', svg: visaBlue, tint: false, maxH: 109 },
|
||||
'visa-navy': { group: 'Visa', label: 'Marineblau', svg: visaNavy, tint: false, maxH: 109 },
|
||||
'visa-silver': { group: 'Visa', label: 'Metallisches Silber', svg: visaSilver, tint: false, maxH: 109 },
|
||||
'mc-color': { group: 'Mastercard', label: 'Farbe', svg: mcColor, tint: false, maxH: 125 },
|
||||
'mc-silver': { group: 'Mastercard', label: 'Metallisches Silber', svg: mcSilver, tint: false, maxH: 125 },
|
||||
'mc-wordmark': { group: 'Mastercard', label: 'Wortmarke', svg: mcWordmark, tint: false, maxH: 109 },
|
||||
'mc-silver-text': { group: 'Mastercard', label: 'Silber + Text', svg: mcSilverText, tint: false, maxH: 109 },
|
||||
'mc-classic': { group: 'Mastercard', label: 'Klassisch', svg: mcClassic, tint: false, maxH: 125 },
|
||||
'up-color': { group: 'UnionPay', label: 'Farbe', svg: upColor, tint: false, maxH: 125 },
|
||||
'up-silver': { group: 'UnionPay', label: 'Metallisches Silber', svg: upSilver, tint: false, maxH: 125 },
|
||||
'up-white-frame': { group: 'UnionPay', label: 'Weißer Rahmen', svg: upWhiteFrame, tint: false, maxH: 125 },
|
||||
'up-english': { group: 'UnionPay', label: 'Englisch', svg: upEnglish, tint: false, maxH: 109 },
|
||||
'amex-blue': { group: 'American Express', label: 'Blau', svg: amexBlue, tint: false, maxH: 125 },
|
||||
'amex-outline': { group: 'American Express', label: 'Umriss', svg: amexOutline, tint: true, maxH: 125 },
|
||||
'amex-select': { group: 'American Express', label: 'Select', svg: amexSelect, tint: false, maxH: 90 },
|
||||
jcb: { group: 'JCB', label: 'JCB', svg: jcb, tint: false, maxH: 125 },
|
||||
discover: { group: 'Discover', label: 'Discover', svg: discover, tint: false, maxH: 109 },
|
||||
};
|
||||
|
||||
const BRAND_TABLE: Record<Exclude<BrandId, 'none'>, NetworkEntry> = {
|
||||
'aircards-symbol': { group: 'Logo', label: 'AirCards · Symbol', svg: aircardsSymbol, tint: true, maxH: 100 },
|
||||
'aircards-text': { group: 'Logo', label: 'AirCards · Text', svg: aircardsText, tint: true, maxH: 100 },
|
||||
apple: { group: 'Logo', label: 'Apple', svg: apple, tint: true, maxH: 100 },
|
||||
};
|
||||
|
||||
export const NETWORK_MARKS: readonly MarkDef[] = NETWORK_IDS.map((id) => ({ id, ...NETWORK_TABLE[id] }));
|
||||
|
||||
export const BRAND_MARKS: readonly MarkDef[] = (
|
||||
Object.keys(BRAND_TABLE) as Exclude<BrandId, 'none'>[]
|
||||
).map((id) => ({ id, ...BRAND_TABLE[id] }));
|
||||
|
||||
export function getNetworkMark(id: NetworkId): MarkDef {
|
||||
return NETWORK_MARKS.find((m) => m.id === id) ?? NETWORK_MARKS[0];
|
||||
}
|
||||
|
||||
export function getBrandMark(id: Exclude<BrandId, 'none'>): MarkDef {
|
||||
return BRAND_MARKS.find((m) => m.id === id) ?? BRAND_MARKS[0];
|
||||
}
|
||||
|
||||
/** 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);
|
||||
if (!hit) return 1;
|
||||
const parts = hit[1].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 : 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('<svg', `<svg width="${w}" height="${h}"`);
|
||||
}
|
||||
|
||||
const imageCache = new Map<string, Promise<HTMLImageElement>>();
|
||||
|
||||
/** 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<HTMLImageElement> {
|
||||
const key = `${def.id}:${tone}`;
|
||||
let pending = imageCache.get(key);
|
||||
if (!pending) {
|
||||
const source = withIntrinsicSize(tintSvg(def.svg, tone), def.maxH);
|
||||
pending = new Promise<HTMLImageElement>((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;
|
||||
}
|
||||
Reference in new issue
Block a user