@mostajs/qrpanel 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/themes.ts ADDED
@@ -0,0 +1,266 @@
1
+ // @mostajs/qrpanel/themes — built-in theme registry
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+ //
4
+ // Chaque thème = un petit motif SVG répliqué aux 4 coins du composite,
5
+ // laissant le centre libre pour le cartouche blanc + QR. Style ligne
6
+ // minimaliste mono-color via `currentColor` (le composer pilote la
7
+ // couleur via CSS sur le <g> wrapper).
8
+ //
9
+ // Création maison — design original, libre de droits.
10
+ // viewBox local du motif : centré sur l'origine, ~14 unités d'envergure.
11
+
12
+ export type ThemeKey =
13
+ | 'baby'
14
+ | 'animals'
15
+ | 'science'
16
+ | 'physics'
17
+ | 'chemistry'
18
+ | 'math'
19
+ | 'nature'
20
+ | 'tech'
21
+ | 'space'
22
+ | 'music'
23
+ | 'book'
24
+ | 'health'
25
+
26
+ export interface ThemeAsset {
27
+ key: ThemeKey
28
+ label: string
29
+ /** Fragment SVG (sans <svg> wrapper) — un motif centré sur (0,0). */
30
+ motif: string
31
+ }
32
+
33
+ /** Communs à tous les motifs (style ligne fine et propre). */
34
+ const STROKE = 'stroke="currentColor" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round"'
35
+ const FILL = 'fill="currentColor"'
36
+
37
+ export const THEMES: Record<ThemeKey, ThemeAsset> = {
38
+ // ─── 1. baby — sucette + cœur ─────────────────────────────────
39
+ baby: {
40
+ key: 'baby', label: 'Bébé',
41
+ motif: `
42
+ <circle cx="-2" cy="-3" r="4.5" ${STROKE}/>
43
+ <line x1="-2" y1="1.5" x2="-2" y2="6" ${STROKE}/>
44
+ <path d="M 5 -2 c 0.8 -1.2 2.5 -1.2 2.5 0.4 c 0 1.4 -2.5 2.8 -2.5 2.8 c 0 0 -2.5 -1.4 -2.5 -2.8 c 0 -1.6 1.7 -1.6 2.5 -0.4 z" ${FILL} opacity="0.9"/>
45
+ `,
46
+ },
47
+
48
+ // ─── 2. animals — empreinte de patte (4 doigts + pad) ─────────
49
+ animals: {
50
+ key: 'animals', label: 'Animaux',
51
+ motif: `
52
+ <ellipse cx="-3.5" cy="-3" rx="1.3" ry="1.8" ${FILL}/>
53
+ <ellipse cx="-1" cy="-5" rx="1.2" ry="1.7" ${FILL}/>
54
+ <ellipse cx="1.8" cy="-5" rx="1.2" ry="1.7" ${FILL}/>
55
+ <ellipse cx="4.3" cy="-3" rx="1.3" ry="1.8" ${FILL}/>
56
+ <ellipse cx="0.5" cy="2" rx="3.5" ry="3.2" ${FILL}/>
57
+ `,
58
+ },
59
+
60
+ // ─── 3. science — éprouvette + bulles ─────────────────────────
61
+ science: {
62
+ key: 'science', label: 'Science',
63
+ motif: `
64
+ <path d="M -2 -6 L -2 4 a 2.5 2.5 0 0 0 5 0 L 3 -6" ${STROKE}/>
65
+ <line x1="-3" y1="-6" x2="4" y2="-6" ${STROKE}/>
66
+ <circle cx="-0.5" cy="2" r="0.7" ${FILL}/>
67
+ <circle cx="1.4" cy="0" r="0.5" ${FILL}/>
68
+ <circle cx="0.4" cy="-2" r="0.4" ${FILL}/>
69
+ `,
70
+ },
71
+
72
+ // ─── 4. physics — atome (noyau + 2 orbites croisées) ──────────
73
+ physics: {
74
+ key: 'physics', label: 'Physique',
75
+ motif: `
76
+ <ellipse cx="0" cy="0" rx="6" ry="2.4" ${STROKE}/>
77
+ <ellipse cx="0" cy="0" rx="6" ry="2.4" transform="rotate(60)" ${STROKE}/>
78
+ <ellipse cx="0" cy="0" rx="6" ry="2.4" transform="rotate(-60)" ${STROKE}/>
79
+ <circle cx="0" cy="0" r="1.4" ${FILL}/>
80
+ `,
81
+ },
82
+
83
+ // ─── 5. chemistry — bécher avec graduation et liquide ─────────
84
+ chemistry: {
85
+ key: 'chemistry', label: 'Chimie',
86
+ motif: `
87
+ <path d="M -3.5 -5 L -3.5 4 a 1.5 1.5 0 0 0 1.5 1.5 L 2 5.5 a 1.5 1.5 0 0 0 1.5 -1.5 L 3.5 -5" ${STROKE}/>
88
+ <line x1="-4" y1="-5" x2="4" y2="-5" ${STROKE}/>
89
+ <line x1="-2.7" y1="-2.5" x2="-1.4" y2="-2.5" ${STROKE}/>
90
+ <line x1="-2.7" y1="0" x2="-1.4" y2="0" ${STROKE}/>
91
+ <path d="M -3.5 1.5 L 3.5 1.5 L 3.5 4 a 1.5 1.5 0 0 1 -1.5 1.5 L -2 5.5 a 1.5 1.5 0 0 1 -1.5 -1.5 z" ${FILL} opacity="0.35"/>
92
+ `,
93
+ },
94
+
95
+ // ─── 6. math — symbole π et signe = ───────────────────────────
96
+ math: {
97
+ key: 'math', label: 'Mathématique',
98
+ motif: `
99
+ <line x1="-5" y1="-3.5" x2="5" y2="-3.5" ${STROKE}/>
100
+ <line x1="-2.5" y1="-3.5" x2="-2.5" y2="3.5" ${STROKE}/>
101
+ <line x1="2.5" y1="-3.5" x2="2.5" y2="3.5" ${STROKE}/>
102
+ <line x1="-5" y1="5.5" x2="5" y2="5.5" ${STROKE}/>
103
+ <line x1="-5" y1="7.2" x2="5" y2="7.2" ${STROKE}/>
104
+ `,
105
+ },
106
+
107
+ // ─── 7. nature — feuille + tige ───────────────────────────────
108
+ nature: {
109
+ key: 'nature', label: 'Nature',
110
+ motif: `
111
+ <path d="M 0 6 C -6 4 -6 -4 0 -6 C 6 -4 6 4 0 6 Z" ${STROKE}/>
112
+ <line x1="0" y1="-6" x2="0" y2="6" ${STROKE}/>
113
+ <line x1="0" y1="-3" x2="-3" y2="-1" ${STROKE}/>
114
+ <line x1="0" y1="0" x2="-3.5" y2="2" ${STROKE}/>
115
+ <line x1="0" y1="-3" x2="3" y2="-1" ${STROKE}/>
116
+ <line x1="0" y1="0" x2="3.5" y2="2" ${STROKE}/>
117
+ `,
118
+ },
119
+
120
+ // ─── 8. tech — engrenage 8 dents ──────────────────────────────
121
+ tech: {
122
+ key: 'tech', label: 'Tech',
123
+ motif: `
124
+ <path d="
125
+ M -1 -5.6 L 1 -5.6 L 1.4 -4 L 3 -3.4 L 4.2 -4.5 L 5.5 -3.2 L 4.4 -2 L 5 -0.4 L 6.6 0 L 6.6 2
126
+ L 5 2.4 L 4.4 4 L 5.5 5.2 L 4.2 6.5 L 3 5.4 L 1.4 6 L 1 7.6 L -1 7.6 L -1.4 6 L -3 5.4
127
+ L -4.2 6.5 L -5.5 5.2 L -4.4 4 L -5 2.4 L -6.6 2 L -6.6 0 L -5 -0.4 L -4.4 -2 L -5.5 -3.2
128
+ L -4.2 -4.5 L -3 -3.4 L -1.4 -4 Z" transform="translate(0 -1) scale(0.7)" ${STROKE}/>
129
+ <circle cx="0" cy="-1" r="1.5" transform="scale(0.7)" ${STROKE}/>
130
+ `,
131
+ },
132
+
133
+ // ─── 9. space — planète saturne + étoile ──────────────────────
134
+ space: {
135
+ key: 'space', label: 'Espace',
136
+ motif: `
137
+ <circle cx="-1" cy="0" r="3.2" ${STROKE}/>
138
+ <ellipse cx="-1" cy="0" rx="6" ry="1.4" transform="rotate(-20 -1 0)" ${STROKE}/>
139
+ <path d="M 5 -5 L 5.6 -3.5 L 7.2 -3.5 L 6 -2.6 L 6.5 -1.1 L 5 -2 L 3.5 -1.1 L 4 -2.6 L 2.8 -3.5 L 4.4 -3.5 Z" ${FILL}/>
140
+ `,
141
+ },
142
+
143
+ // ─── 10. music — note + portée ────────────────────────────────
144
+ music: {
145
+ key: 'music', label: 'Musique',
146
+ motif: `
147
+ <ellipse cx="-2" cy="4" rx="2.2" ry="1.7" transform="rotate(-22 -2 4)" ${FILL}/>
148
+ <line x1="0" y1="3" x2="0" y2="-6" ${STROKE}/>
149
+ <path d="M 0 -6 C 3 -5 4 -2 1.5 -1" ${STROKE}/>
150
+ <ellipse cx="3.5" cy="2.5" rx="1.7" ry="1.3" transform="rotate(-22 3.5 2.5)" ${FILL} opacity="0.6"/>
151
+ <line x1="5" y1="1.5" x2="5" y2="-4" ${STROKE} opacity="0.6"/>
152
+ `,
153
+ },
154
+
155
+ // ─── 11. book — livre ouvert ──────────────────────────────────
156
+ book: {
157
+ key: 'book', label: 'Livre',
158
+ motif: `
159
+ <path d="M -6 -4 L 0 -3 L 6 -4 L 6 4 L 0 5 L -6 4 Z" ${STROKE}/>
160
+ <line x1="0" y1="-3" x2="0" y2="5" ${STROKE}/>
161
+ <line x1="-4.5" y1="-2" x2="-1.5" y2="-1.5" ${STROKE} opacity="0.6"/>
162
+ <line x1="-4.5" y1="0" x2="-1.5" y2="0.5" ${STROKE} opacity="0.6"/>
163
+ <line x1="1.5" y1="-1.5" x2="4.5" y2="-2" ${STROKE} opacity="0.6"/>
164
+ <line x1="1.5" y1="0.5" x2="4.5" y2="0" ${STROKE} opacity="0.6"/>
165
+ `,
166
+ },
167
+
168
+ // ─── 12. health — croix médicale + battement ──────────────────
169
+ health: {
170
+ key: 'health', label: 'Santé',
171
+ motif: `
172
+ <rect x="-1.8" y="-6" width="3.6" height="12" rx="0.8" ${FILL}/>
173
+ <rect x="-6" y="-1.8" width="12" height="3.6" rx="0.8" ${FILL}/>
174
+ <path d="M -8 7 L -5 7 L -3.5 4 L -1.5 9 L 0 6 L 2 8 L 4 7 L 8 7" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 2)"/>
175
+ `,
176
+ },
177
+ }
178
+
179
+ /** Liste des clés thèmes natifs (ordre fixe pour itération déterministe). */
180
+ export const THEME_KEYS: ThemeKey[] = [
181
+ 'baby', 'animals', 'science', 'physics', 'chemistry', 'math',
182
+ 'nature', 'tech', 'space', 'music', 'book', 'health',
183
+ ]
184
+
185
+ /** Renvoie la liste des clés thèmes natifs. */
186
+ export function listThemes(): ThemeKey[] {
187
+ return [...THEME_KEYS]
188
+ }
189
+
190
+ /** Récupère un thème par clé. Lance si inconnu. */
191
+ export function getTheme(key: ThemeKey): ThemeAsset {
192
+ const theme = THEMES[key]
193
+ if (!theme) throw new Error(`[qrpanel] unknown theme: "${key}". Available: ${THEME_KEYS.join(', ')}`)
194
+ return theme
195
+ }
196
+
197
+ /**
198
+ * Tire un thème au hasard dans le pool fourni (ou tous les thèmes si pool absent).
199
+ * Utilise Math.random() — pas de seed (déterminisme = à la charge du caller s'il
200
+ * en a besoin).
201
+ */
202
+ export function pickRandomTheme(pool?: ThemeKey[]): ThemeKey {
203
+ const candidates = pool && pool.length > 0 ? pool : THEME_KEYS
204
+ return candidates[Math.floor(Math.random() * candidates.length)]!
205
+ }
206
+
207
+ /**
208
+ * Construit le fragment SVG du cadre thématique :
209
+ * 4 instances du motif aux 4 coins, monochrome via CSS color, opacité.
210
+ *
211
+ * Le fragment est destiné à être inséré dans un SVG composite global
212
+ * (viewBox 0 0 width width). Coordonnées dans l'espace [0..width].
213
+ */
214
+ export interface BuildFrameOpts {
215
+ /** Largeur du canvas SVG englobant en unités utilisateur. */
216
+ width: number
217
+ /** Couleur monochrome appliquée via CSS. Default '#1e293b'. */
218
+ color?: string
219
+ /** Opacité 0..1. Default 1. */
220
+ opacity?: number
221
+ /**
222
+ * Proportion du cartouche blanc central / canvas (0..1). Détermine la
223
+ * largeur de la zone-marge où sont placés les motifs.
224
+ * Default 0.62.
225
+ */
226
+ centerWhiteRatio?: number
227
+ /**
228
+ * Position du motif dans la zone-marge (0..1) :
229
+ * 0 = motif collé au bord du canvas
230
+ * 0.5 = motif centré dans la zone-marge (default, optimal)
231
+ * 1 = motif collé contre le cartouche blanc
232
+ */
233
+ framePadding?: number
234
+ /**
235
+ * Échelle du motif (override de l'auto-calc).
236
+ * Auto = (marginZone × 0.65 / 14) où 14 ≈ envergure locale du motif.
237
+ */
238
+ motifScale?: number
239
+ }
240
+
241
+ /** Envergure approximative d'un motif local (en unités SVG). */
242
+ const MOTIF_BASE_SIZE = 14
243
+
244
+ export function buildThemeFrameSvg(theme: ThemeAsset, opts: BuildFrameOpts): string {
245
+ const w = opts.width
246
+ const ratio = opts.centerWhiteRatio ?? 0.62
247
+ const marginZone = (w - w * ratio) / 2 // largeur libre entre cartouche et bord
248
+ const padFactor = opts.framePadding ?? 0.5
249
+ const offset = marginZone * padFactor // distance du bord au centre du motif
250
+ const scale = opts.motifScale ?? (marginZone * 0.65 / MOTIF_BASE_SIZE)
251
+ const color = opts.color ?? '#1e293b'
252
+ const opacity = opts.opacity ?? 1
253
+
254
+ const corners: [number, number][] = [
255
+ [offset, offset], // top-left
256
+ [w - offset, offset], // top-right
257
+ [offset, w - offset], // bottom-left
258
+ [w - offset, w - offset], // bottom-right
259
+ ]
260
+
261
+ const groups = corners.map(([cx, cy]) =>
262
+ `<g transform="translate(${cx} ${cy}) scale(${scale})" style="color:${color};opacity:${opacity}">${theme.motif}</g>`
263
+ ).join('')
264
+
265
+ return groups
266
+ }