// @mostajs/qrpanel/themes — built-in theme registry // Author: Dr Hamid MADANI // // Chaque thème = un petit motif SVG répliqué aux 4 coins du composite, // laissant le centre libre pour le cartouche blanc + QR. Style ligne // minimaliste mono-color via `currentColor` (le composer pilote la // couleur via CSS sur le wrapper). // // Création maison — design original, libre de droits. // viewBox local du motif : centré sur l'origine, ~14 unités d'envergure. export type ThemeKey = | 'baby' | 'animals' | 'science' | 'physics' | 'chemistry' | 'math' | 'nature' | 'tech' | 'space' | 'music' | 'book' | 'health' export interface ThemeAsset { key: ThemeKey label: string /** Fragment SVG (sans wrapper) — un motif centré sur (0,0). */ motif: string } /** Communs à tous les motifs (style ligne fine et propre). */ const STROKE = 'stroke="currentColor" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round"' const FILL = 'fill="currentColor"' export const THEMES: Record = { // ─── 1. baby — sucette + cœur ───────────────────────────────── baby: { key: 'baby', label: 'Bébé', motif: ` `, }, // ─── 2. animals — empreinte de patte (4 doigts + pad) ───────── animals: { key: 'animals', label: 'Animaux', motif: ` `, }, // ─── 3. science — éprouvette + bulles ───────────────────────── science: { key: 'science', label: 'Science', motif: ` `, }, // ─── 4. physics — atome (noyau + 2 orbites croisées) ────────── physics: { key: 'physics', label: 'Physique', motif: ` `, }, // ─── 5. chemistry — bécher avec graduation et liquide ───────── chemistry: { key: 'chemistry', label: 'Chimie', motif: ` `, }, // ─── 6. math — symbole π et signe = ─────────────────────────── math: { key: 'math', label: 'Mathématique', motif: ` `, }, // ─── 7. nature — feuille + tige ─────────────────────────────── nature: { key: 'nature', label: 'Nature', motif: ` `, }, // ─── 8. tech — engrenage 8 dents ────────────────────────────── tech: { key: 'tech', label: 'Tech', motif: ` `, }, // ─── 9. space — planète saturne + étoile ────────────────────── space: { key: 'space', label: 'Espace', motif: ` `, }, // ─── 10. music — note + portée ──────────────────────────────── music: { key: 'music', label: 'Musique', motif: ` `, }, // ─── 11. book — livre ouvert ────────────────────────────────── book: { key: 'book', label: 'Livre', motif: ` `, }, // ─── 12. health — croix médicale + battement ────────────────── health: { key: 'health', label: 'Santé', motif: ` `, }, } /** Liste des clés thèmes natifs (ordre fixe pour itération déterministe). */ export const THEME_KEYS: ThemeKey[] = [ 'baby', 'animals', 'science', 'physics', 'chemistry', 'math', 'nature', 'tech', 'space', 'music', 'book', 'health', ] /** Renvoie la liste des clés thèmes natifs. */ export function listThemes(): ThemeKey[] { return [...THEME_KEYS] } /** Récupère un thème par clé. Lance si inconnu. */ export function getTheme(key: ThemeKey): ThemeAsset { const theme = THEMES[key] if (!theme) throw new Error(`[qrpanel] unknown theme: "${key}". Available: ${THEME_KEYS.join(', ')}`) return theme } /** * Tire un thème au hasard dans le pool fourni (ou tous les thèmes si pool absent). * Utilise Math.random() — pas de seed (déterminisme = à la charge du caller s'il * en a besoin). */ export function pickRandomTheme(pool?: ThemeKey[]): ThemeKey { const candidates = pool && pool.length > 0 ? pool : THEME_KEYS return candidates[Math.floor(Math.random() * candidates.length)]! } /** * Construit le fragment SVG du cadre thématique : * 4 instances du motif aux 4 coins, monochrome via CSS color, opacité. * * Le fragment est destiné à être inséré dans un SVG composite global * (viewBox 0 0 width width). Coordonnées dans l'espace [0..width]. */ export interface BuildFrameOpts { /** Largeur du canvas SVG englobant en unités utilisateur. */ width: number /** Couleur monochrome appliquée via CSS. Default '#1e293b'. */ color?: string /** Opacité 0..1. Default 1. */ opacity?: number /** * Proportion du cartouche blanc central / canvas (0..1). Détermine la * largeur de la zone-marge où sont placés les motifs. * Default 0.62. */ centerWhiteRatio?: number /** * Position du motif dans la zone-marge (0..1) : * 0 = motif collé au bord du canvas * 0.5 = motif centré dans la zone-marge (default, optimal) * 1 = motif collé contre le cartouche blanc */ framePadding?: number /** * Échelle du motif (override de l'auto-calc). * Auto = (marginZone × 0.65 / 14) où 14 ≈ envergure locale du motif. */ motifScale?: number } /** Envergure approximative d'un motif local (en unités SVG). */ const MOTIF_BASE_SIZE = 14 export function buildThemeFrameSvg(theme: ThemeAsset, opts: BuildFrameOpts): string { const w = opts.width const ratio = opts.centerWhiteRatio ?? 0.62 const marginZone = (w - w * ratio) / 2 // largeur libre entre cartouche et bord const padFactor = opts.framePadding ?? 0.5 const offset = marginZone * padFactor // distance du bord au centre du motif const scale = opts.motifScale ?? (marginZone * 0.65 / MOTIF_BASE_SIZE) const color = opts.color ?? '#1e293b' const opacity = opts.opacity ?? 1 const corners: [number, number][] = [ [offset, offset], // top-left [w - offset, offset], // top-right [offset, w - offset], // bottom-left [w - offset, w - offset], // bottom-right ] const groups = corners.map(([cx, cy]) => `${theme.motif}` ).join('') return groups }