shield-qr-styler 1.0.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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/index.cjs +1364 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +1334 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var QRCode = require('qrcode');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shield QR Styler - Custom Shaped QR Code Generator
|
|
9
|
+
* ===================================================
|
|
10
|
+
*
|
|
11
|
+
* A framework-agnostic library for generating beautifully shaped
|
|
12
|
+
* QR codes as SVG strings. Works in Node.js, browsers, and edge runtimes.
|
|
13
|
+
*
|
|
14
|
+
* Shape Categories:
|
|
15
|
+
* none, square, rectangle, circle, oval, diamond, heart, hexagon, shield
|
|
16
|
+
* Each category has 2-4 variations (e.g. shield → classic, badge, modern, emblem).
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - 9 shape categories with 22+ variations
|
|
20
|
+
* - 7 module styles: circle, roundedSquare, diamond, dot, square, barH, barV, pond
|
|
21
|
+
* - 7 color presets (or fully custom colours)
|
|
22
|
+
* - 6 gradient presets
|
|
23
|
+
* - Linear & radial gradient support
|
|
24
|
+
* - Glow effects, inner borders
|
|
25
|
+
* - Finder pattern accent colors with solid/pattern modes
|
|
26
|
+
* - Center-clear area for logo overlay
|
|
27
|
+
* - Decorative fill with controllable density, opacity, margins
|
|
28
|
+
* - High error correction (H) for reliable scanning
|
|
29
|
+
* - Extensible: register custom shapes at runtime
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* import { generateShapeQR } from 'shield-qr-styler';
|
|
33
|
+
* const svg = await generateShapeQR('https://example.com', {
|
|
34
|
+
* shapeCategory: 'circle',
|
|
35
|
+
* shapeVariation: 'squircle',
|
|
36
|
+
* preset: 'cyber',
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* @module shield-qr-styler
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// ═════════════════════════════════════════════════
|
|
44
|
+
// Shape Library
|
|
45
|
+
// ═════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
const SHAPE_LIBRARY = {
|
|
48
|
+
|
|
49
|
+
// ────────────────── NONE (bare QR) ──────────────────
|
|
50
|
+
none: {
|
|
51
|
+
label: 'None',
|
|
52
|
+
icon: '⊞',
|
|
53
|
+
description: 'Plain QR code with no shape or border',
|
|
54
|
+
variations: {
|
|
55
|
+
default: {
|
|
56
|
+
label: 'Default',
|
|
57
|
+
description: 'Raw QR code, no framing',
|
|
58
|
+
viewBox: '0 0 300 300',
|
|
59
|
+
width: 300, height: 300,
|
|
60
|
+
path: 'M 0 0 H 300 V 300 H 0 Z',
|
|
61
|
+
qrArea: { x: 8, y: 8, size: 284 },
|
|
62
|
+
bare: true,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// ────────────────── SQUARE ──────────────────
|
|
68
|
+
square: {
|
|
69
|
+
label: 'Square',
|
|
70
|
+
icon: '⬜',
|
|
71
|
+
description: 'Square shapes with corner variations',
|
|
72
|
+
variations: {
|
|
73
|
+
sharp: {
|
|
74
|
+
label: 'Sharp',
|
|
75
|
+
description: 'Clean sharp edges',
|
|
76
|
+
viewBox: '0 0 300 300',
|
|
77
|
+
width: 300, height: 300,
|
|
78
|
+
path: 'M 8 8 H 292 V 292 H 8 Z',
|
|
79
|
+
qrArea: { x: 18, y: 18, size: 264 },
|
|
80
|
+
},
|
|
81
|
+
rounded: {
|
|
82
|
+
label: 'Rounded',
|
|
83
|
+
description: 'Softly rounded corners',
|
|
84
|
+
viewBox: '0 0 300 300',
|
|
85
|
+
width: 300, height: 300,
|
|
86
|
+
path: 'M 30 8 H 270 Q 292 8 292 30 V 270 Q 292 292 270 292 H 30 Q 8 292 8 270 V 30 Q 8 8 30 8 Z',
|
|
87
|
+
qrArea: { x: 18, y: 18, size: 264 },
|
|
88
|
+
},
|
|
89
|
+
pill: {
|
|
90
|
+
label: 'Pill',
|
|
91
|
+
description: 'Very rounded corners',
|
|
92
|
+
viewBox: '0 0 300 300',
|
|
93
|
+
width: 300, height: 300,
|
|
94
|
+
path: 'M 60 8 H 240 Q 292 8 292 60 V 240 Q 292 292 240 292 H 60 Q 8 292 8 240 V 60 Q 8 8 60 8 Z',
|
|
95
|
+
qrArea: { x: 24, y: 24, size: 252 },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ────────────────── RECTANGLE ──────────────────
|
|
101
|
+
rectangle: {
|
|
102
|
+
label: 'Rectangle',
|
|
103
|
+
icon: '▬',
|
|
104
|
+
description: 'Rectangular portrait and landscape shapes',
|
|
105
|
+
variations: {
|
|
106
|
+
portrait: {
|
|
107
|
+
label: 'Portrait',
|
|
108
|
+
description: 'Tall rounded rectangle',
|
|
109
|
+
viewBox: '0 0 260 360',
|
|
110
|
+
width: 260, height: 360,
|
|
111
|
+
path: 'M 26 8 H 234 Q 252 8 252 26 V 334 Q 252 352 234 352 H 26 Q 8 352 8 334 V 26 Q 8 8 26 8 Z',
|
|
112
|
+
qrArea: { x: 14, y: 64, size: 232 },
|
|
113
|
+
},
|
|
114
|
+
landscape: {
|
|
115
|
+
label: 'Landscape',
|
|
116
|
+
description: 'Wide rounded rectangle',
|
|
117
|
+
viewBox: '0 0 360 260',
|
|
118
|
+
width: 360, height: 260,
|
|
119
|
+
path: 'M 26 8 H 334 Q 352 8 352 26 V 234 Q 352 252 334 252 H 26 Q 8 252 8 234 V 26 Q 8 8 26 8 Z',
|
|
120
|
+
qrArea: { x: 64, y: 14, size: 232 },
|
|
121
|
+
},
|
|
122
|
+
ticket: {
|
|
123
|
+
label: 'Ticket',
|
|
124
|
+
description: 'Rounded rectangle with decorative notches',
|
|
125
|
+
viewBox: '0 0 260 360',
|
|
126
|
+
width: 260, height: 360,
|
|
127
|
+
path: [
|
|
128
|
+
'M 26 8 H 234 Q 252 8 252 26 V 148',
|
|
129
|
+
'Q 242 158 242 170 Q 242 182 252 192',
|
|
130
|
+
'V 334 Q 252 352 234 352 H 26 Q 8 352 8 334',
|
|
131
|
+
'V 192 Q 18 182 18 170 Q 18 158 8 148',
|
|
132
|
+
'V 26 Q 8 8 26 8 Z',
|
|
133
|
+
].join(' '),
|
|
134
|
+
qrArea: { x: 22, y: 64, size: 216 },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ────────────────── CIRCLE ──────────────────
|
|
140
|
+
circle: {
|
|
141
|
+
label: 'Circle',
|
|
142
|
+
icon: '⭕',
|
|
143
|
+
description: 'Circular and squircle shapes',
|
|
144
|
+
variations: {
|
|
145
|
+
perfect: {
|
|
146
|
+
label: 'Perfect',
|
|
147
|
+
description: 'True circle',
|
|
148
|
+
viewBox: '0 0 300 300',
|
|
149
|
+
width: 300, height: 300,
|
|
150
|
+
path: 'M 150 8 A 142 142 0 0 1 292 150 A 142 142 0 0 1 150 292 A 142 142 0 0 1 8 150 A 142 142 0 0 1 150 8 Z',
|
|
151
|
+
qrArea: { x: 52, y: 52, size: 196 },
|
|
152
|
+
},
|
|
153
|
+
squircle: {
|
|
154
|
+
label: 'Squircle',
|
|
155
|
+
description: 'Superellipse / iOS icon shape',
|
|
156
|
+
viewBox: '0 0 300 300',
|
|
157
|
+
width: 300, height: 300,
|
|
158
|
+
path: 'M 150 8 C 260 8 292 40 292 150 C 292 260 260 292 150 292 C 40 292 8 260 8 150 C 8 40 40 8 150 8 Z',
|
|
159
|
+
qrArea: { x: 40, y: 40, size: 220 },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// ────────────────── OVAL ──────────────────
|
|
165
|
+
oval: {
|
|
166
|
+
label: 'Oval',
|
|
167
|
+
icon: '⬮',
|
|
168
|
+
description: 'Elliptical shapes',
|
|
169
|
+
variations: {
|
|
170
|
+
vertical: {
|
|
171
|
+
label: 'Vertical',
|
|
172
|
+
description: 'Tall oval',
|
|
173
|
+
viewBox: '0 0 280 360',
|
|
174
|
+
width: 280, height: 360,
|
|
175
|
+
path: 'M 140 8 A 132 172 0 0 1 272 180 A 132 172 0 0 1 140 352 A 132 172 0 0 1 8 180 A 132 172 0 0 1 140 8 Z',
|
|
176
|
+
qrArea: { x: 38, y: 78, size: 204 },
|
|
177
|
+
},
|
|
178
|
+
horizontal: {
|
|
179
|
+
label: 'Horizontal',
|
|
180
|
+
description: 'Wide oval',
|
|
181
|
+
viewBox: '0 0 360 280',
|
|
182
|
+
width: 360, height: 280,
|
|
183
|
+
path: 'M 180 8 A 172 132 0 0 1 352 140 A 172 132 0 0 1 180 272 A 172 132 0 0 1 8 140 A 172 132 0 0 1 180 8 Z',
|
|
184
|
+
qrArea: { x: 78, y: 38, size: 204 },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// ────────────────── DIAMOND ──────────────────
|
|
190
|
+
diamond: {
|
|
191
|
+
label: 'Diamond',
|
|
192
|
+
icon: '◆',
|
|
193
|
+
description: 'Diamond / rhombus shapes',
|
|
194
|
+
variations: {
|
|
195
|
+
classic: {
|
|
196
|
+
label: 'Classic',
|
|
197
|
+
description: 'Sharp diamond',
|
|
198
|
+
viewBox: '0 0 300 340',
|
|
199
|
+
width: 300, height: 340,
|
|
200
|
+
path: 'M 150 8 L 292 170 L 150 332 L 8 170 Z',
|
|
201
|
+
qrArea: { x: 76, y: 96, size: 148 },
|
|
202
|
+
},
|
|
203
|
+
soft: {
|
|
204
|
+
label: 'Soft',
|
|
205
|
+
description: 'Rounded diamond corners',
|
|
206
|
+
viewBox: '0 0 300 340',
|
|
207
|
+
width: 300, height: 340,
|
|
208
|
+
path: 'M 150 18 Q 224 90 282 170 Q 224 250 150 322 Q 76 250 18 170 Q 76 90 150 18 Z',
|
|
209
|
+
qrArea: { x: 78, y: 98, size: 144 },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// ────────────────── HEART ──────────────────
|
|
215
|
+
heart: {
|
|
216
|
+
label: 'Heart',
|
|
217
|
+
icon: '❤️',
|
|
218
|
+
description: 'Heart shapes',
|
|
219
|
+
variations: {
|
|
220
|
+
classic: {
|
|
221
|
+
label: 'Classic',
|
|
222
|
+
description: 'Traditional heart',
|
|
223
|
+
viewBox: '0 0 300 280',
|
|
224
|
+
width: 300, height: 280,
|
|
225
|
+
path: [
|
|
226
|
+
'M 150 268',
|
|
227
|
+
'C 75 218 8 165 8 112',
|
|
228
|
+
'C 8 55 50 18 100 18',
|
|
229
|
+
'C 130 18 150 48 150 48',
|
|
230
|
+
'C 150 48 170 18 200 18',
|
|
231
|
+
'C 250 18 292 55 292 112',
|
|
232
|
+
'C 292 165 225 218 150 268 Z',
|
|
233
|
+
].join(' '),
|
|
234
|
+
qrArea: { x: 62, y: 42, size: 176 },
|
|
235
|
+
},
|
|
236
|
+
rounded: {
|
|
237
|
+
label: 'Rounded',
|
|
238
|
+
description: 'Softer, fuller heart',
|
|
239
|
+
viewBox: '0 0 300 280',
|
|
240
|
+
width: 300, height: 280,
|
|
241
|
+
path: [
|
|
242
|
+
'M 150 258',
|
|
243
|
+
'C 68 204 14 158 14 108',
|
|
244
|
+
'C 14 52 55 18 105 18',
|
|
245
|
+
'C 135 18 150 45 150 45',
|
|
246
|
+
'C 150 45 165 18 195 18',
|
|
247
|
+
'C 245 18 286 52 286 108',
|
|
248
|
+
'C 286 158 232 204 150 258 Z',
|
|
249
|
+
].join(' '),
|
|
250
|
+
qrArea: { x: 58, y: 38, size: 184 },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// ────────────────── HEXAGON ──────────────────
|
|
256
|
+
hexagon: {
|
|
257
|
+
label: 'Hexagon',
|
|
258
|
+
icon: '⬡',
|
|
259
|
+
description: 'Six-sided shapes',
|
|
260
|
+
variations: {
|
|
261
|
+
sharp: {
|
|
262
|
+
label: 'Sharp',
|
|
263
|
+
description: 'Clean hex edges',
|
|
264
|
+
viewBox: '0 0 300 340',
|
|
265
|
+
width: 300, height: 340,
|
|
266
|
+
path: 'M 150 8 L 290 88 L 290 252 L 150 332 L 10 252 L 10 88 Z',
|
|
267
|
+
qrArea: { x: 56, y: 72, size: 188 },
|
|
268
|
+
},
|
|
269
|
+
rounded: {
|
|
270
|
+
label: 'Rounded',
|
|
271
|
+
description: 'Softly rounded hex corners',
|
|
272
|
+
viewBox: '0 0 300 340',
|
|
273
|
+
width: 300, height: 340,
|
|
274
|
+
path: [
|
|
275
|
+
'M 150 14',
|
|
276
|
+
'Q 220 14 284 90',
|
|
277
|
+
'L 284 250',
|
|
278
|
+
'Q 220 326 150 326',
|
|
279
|
+
'Q 80 326 16 250',
|
|
280
|
+
'L 16 90',
|
|
281
|
+
'Q 80 14 150 14 Z',
|
|
282
|
+
].join(' '),
|
|
283
|
+
qrArea: { x: 58, y: 74, size: 184 },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// ────────────────── SHIELD ──────────────────
|
|
289
|
+
shield: {
|
|
290
|
+
label: 'Shield',
|
|
291
|
+
icon: '🛡️',
|
|
292
|
+
description: 'Protective shield shapes',
|
|
293
|
+
variations: {
|
|
294
|
+
classic: {
|
|
295
|
+
label: 'Classic',
|
|
296
|
+
description: 'Heraldic shield with pointed base',
|
|
297
|
+
viewBox: '0 0 300 340',
|
|
298
|
+
width: 300, height: 340,
|
|
299
|
+
path: [
|
|
300
|
+
'M 150 8',
|
|
301
|
+
'C 100 8, 18 18, 8 24',
|
|
302
|
+
'L 8 170',
|
|
303
|
+
'C 8 240, 65 295, 150 332',
|
|
304
|
+
'C 235 295, 292 240, 292 170',
|
|
305
|
+
'L 292 24',
|
|
306
|
+
'C 282 18, 200 8, 150 8',
|
|
307
|
+
'Z',
|
|
308
|
+
].join(' '),
|
|
309
|
+
qrArea: { x: 46, y: 36, size: 208 },
|
|
310
|
+
},
|
|
311
|
+
badge: {
|
|
312
|
+
label: 'Badge',
|
|
313
|
+
description: 'Rounded badge with soft curves',
|
|
314
|
+
viewBox: '0 0 300 330',
|
|
315
|
+
width: 300, height: 330,
|
|
316
|
+
path: [
|
|
317
|
+
'M 150 10',
|
|
318
|
+
'C 90 10, 28 18, 18 24',
|
|
319
|
+
'Q 10 30, 10 40',
|
|
320
|
+
'L 10 185',
|
|
321
|
+
'C 10 245, 60 290, 150 322',
|
|
322
|
+
'C 240 290, 290 245, 290 185',
|
|
323
|
+
'L 290 40',
|
|
324
|
+
'Q 290 30, 282 24',
|
|
325
|
+
'C 272 18, 210 10, 150 10',
|
|
326
|
+
'Z',
|
|
327
|
+
].join(' '),
|
|
328
|
+
qrArea: { x: 44, y: 42, size: 212 },
|
|
329
|
+
},
|
|
330
|
+
modern: {
|
|
331
|
+
label: 'Modern',
|
|
332
|
+
description: 'Clean with rounded bottom',
|
|
333
|
+
viewBox: '0 0 300 310',
|
|
334
|
+
width: 300, height: 310,
|
|
335
|
+
path: [
|
|
336
|
+
'M 150 10',
|
|
337
|
+
'C 100 10, 25 18, 15 24',
|
|
338
|
+
'L 15 205',
|
|
339
|
+
'Q 15 248, 42 268',
|
|
340
|
+
'Q 80 292, 150 300',
|
|
341
|
+
'Q 220 292, 258 268',
|
|
342
|
+
'Q 285 248, 285 205',
|
|
343
|
+
'L 285 24',
|
|
344
|
+
'C 275 18, 200 10, 150 10',
|
|
345
|
+
'Z',
|
|
346
|
+
].join(' '),
|
|
347
|
+
qrArea: { x: 36, y: 30, size: 228 },
|
|
348
|
+
},
|
|
349
|
+
emblem: {
|
|
350
|
+
label: 'Emblem',
|
|
351
|
+
description: 'Angular military chevron',
|
|
352
|
+
viewBox: '0 0 300 350',
|
|
353
|
+
width: 300, height: 350,
|
|
354
|
+
path: [
|
|
355
|
+
'M 150 5',
|
|
356
|
+
'L 292 22',
|
|
357
|
+
'L 292 195',
|
|
358
|
+
'C 292 258, 234 310, 150 342',
|
|
359
|
+
'C 66 310, 8 258, 8 195',
|
|
360
|
+
'L 8 22',
|
|
361
|
+
'Z',
|
|
362
|
+
].join(' '),
|
|
363
|
+
qrArea: { x: 42, y: 34, size: 216 },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// ─────────────────────────────────────────────
|
|
370
|
+
// Backward-compatible alias
|
|
371
|
+
// ─────────────────────────────────────────────
|
|
372
|
+
const SHIELD_PATHS = SHAPE_LIBRARY.shield.variations;
|
|
373
|
+
|
|
374
|
+
// ═════════════════════════════════════════════════
|
|
375
|
+
// Color Presets
|
|
376
|
+
// ═════════════════════════════════════════════════
|
|
377
|
+
|
|
378
|
+
const COLOR_PRESETS = {
|
|
379
|
+
cyber: {
|
|
380
|
+
label: 'Cyber', description: 'Neon cyan on navy', icon: '💠',
|
|
381
|
+
background: '#0a0e27', foreground: '#00d4ff', outline: '#00d4ff',
|
|
382
|
+
finderOuter: '#00ff88', finderInner: '#00d4ff', outlineWidth: 3,
|
|
383
|
+
category: 'dark',
|
|
384
|
+
},
|
|
385
|
+
stealth: {
|
|
386
|
+
label: 'Stealth', description: 'Grey on black, minimal', icon: '🌑',
|
|
387
|
+
background: '#1a1a1a', foreground: '#c8c8c8', outline: '#555555',
|
|
388
|
+
finderOuter: '#ffffff', finderInner: '#999999', outlineWidth: 2,
|
|
389
|
+
category: 'dark',
|
|
390
|
+
},
|
|
391
|
+
royal: {
|
|
392
|
+
label: 'Royal', description: 'Purple & gold, luxurious', icon: '👑',
|
|
393
|
+
background: '#1a0a3e', foreground: '#c9a0ff', outline: '#ffd700',
|
|
394
|
+
finderOuter: '#ffd700', finderInner: '#c9a0ff', outlineWidth: 3,
|
|
395
|
+
category: 'dark',
|
|
396
|
+
},
|
|
397
|
+
military: {
|
|
398
|
+
label: 'Military', description: 'Green on olive, tactical', icon: '🎖️',
|
|
399
|
+
background: '#1a2e1a', foreground: '#4caf50', outline: '#66bb6a',
|
|
400
|
+
finderOuter: '#a5d6a7', finderInner: '#4caf50', outlineWidth: 2.5,
|
|
401
|
+
category: 'dark',
|
|
402
|
+
},
|
|
403
|
+
fire: {
|
|
404
|
+
label: 'Fire', description: 'Red & orange, bold', icon: '🔥',
|
|
405
|
+
background: '#1a0000', foreground: '#ff4444', outline: '#ff6600',
|
|
406
|
+
finderOuter: '#ffaa00', finderInner: '#ff4444', outlineWidth: 3,
|
|
407
|
+
category: 'dark',
|
|
408
|
+
},
|
|
409
|
+
ocean: {
|
|
410
|
+
label: 'Ocean', description: 'Blue tones, professional', icon: '🌊',
|
|
411
|
+
background: '#001a33', foreground: '#0088cc', outline: '#00aaff',
|
|
412
|
+
finderOuter: '#00ddff', finderInner: '#0088cc', outlineWidth: 2.5,
|
|
413
|
+
category: 'dark',
|
|
414
|
+
},
|
|
415
|
+
monochrome: {
|
|
416
|
+
label: 'Monochrome', description: 'Classic black on white', icon: '⬛',
|
|
417
|
+
background: '#ffffff', foreground: '#000000', outline: '#222222',
|
|
418
|
+
finderOuter: '#000000', finderInner: '#000000', outlineWidth: 2.5,
|
|
419
|
+
category: 'light',
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// ═════════════════════════════════════════════════
|
|
424
|
+
// Module Styles
|
|
425
|
+
// ═════════════════════════════════════════════════
|
|
426
|
+
|
|
427
|
+
const MODULE_STYLES = {
|
|
428
|
+
circle: { label: 'Circle', icon: '●', description: 'Circular dots' },
|
|
429
|
+
roundedSquare: { label: 'Rounded', icon: '▢', description: 'Rounded squares' },
|
|
430
|
+
diamond: { label: 'Diamond', icon: '◆', description: 'Diamond shapes' },
|
|
431
|
+
square: { label: 'Square', icon: '■', description: 'Sharp squares' },
|
|
432
|
+
barH: { label: 'H-Bars', icon: '≡', description: 'Horizontal flowing bars' },
|
|
433
|
+
barV: { label: 'V-Bars', icon: '⫿', description: 'Vertical flowing bars' },
|
|
434
|
+
pond: { label: 'Pond', icon: '⬬', description: 'Connected organic blobs' },
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// ═════════════════════════════════════════════════
|
|
438
|
+
// Finder Pattern Styles
|
|
439
|
+
// ═════════════════════════════════════════════════
|
|
440
|
+
|
|
441
|
+
const FINDER_PATTERNS = {
|
|
442
|
+
pattern: { label: 'Pattern', description: 'Individual modules' },
|
|
443
|
+
solid: { label: 'Solid', description: 'Solid concentric shapes' },
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const FINDER_STYLES = {
|
|
447
|
+
rounded: { label: 'Rounded', icon: '▢', description: 'Rounded square' },
|
|
448
|
+
square: { label: 'Square', icon: '■', description: 'Sharp square' },
|
|
449
|
+
circle: { label: 'Circle', icon: '●', description: 'Circle' },
|
|
450
|
+
diamond: { label: 'Diamond', icon: '◆', description: 'Diamond' },
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ═════════════════════════════════════════════════
|
|
454
|
+
// Gradient Presets
|
|
455
|
+
// ═════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
const GRADIENT_PRESETS = {
|
|
458
|
+
none: { label: 'None', value: null },
|
|
459
|
+
neonPulse: {
|
|
460
|
+
label: 'Neon Pulse',
|
|
461
|
+
value: { type: 'linear', colors: ['#00d4ff', '#7b2ff7'], angle: 135, stops: [0, 100] },
|
|
462
|
+
},
|
|
463
|
+
sunset: {
|
|
464
|
+
label: 'Sunset',
|
|
465
|
+
value: { type: 'linear', colors: ['#ff6b6b', '#ffd93d'], angle: 180, stops: [0, 100] },
|
|
466
|
+
},
|
|
467
|
+
aurora: {
|
|
468
|
+
label: 'Aurora',
|
|
469
|
+
value: { type: 'linear', colors: ['#00ff88', '#00d4ff', '#7b2ff7'], angle: 135, stops: [0, 50, 100] },
|
|
470
|
+
},
|
|
471
|
+
golden: {
|
|
472
|
+
label: 'Golden',
|
|
473
|
+
value: { type: 'radial', colors: ['#ffd700', '#ff8c00'], stops: [0, 100] },
|
|
474
|
+
},
|
|
475
|
+
ice: {
|
|
476
|
+
label: 'Ice',
|
|
477
|
+
value: { type: 'linear', colors: ['#e0f7ff', '#00aaff'], angle: 180, stops: [0, 100] },
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// ═════════════════════════════════════════════════
|
|
482
|
+
// Default Options
|
|
483
|
+
// ═════════════════════════════════════════════════
|
|
484
|
+
|
|
485
|
+
const DEFAULT_OPTIONS = {
|
|
486
|
+
// --- Shape & Layout ---
|
|
487
|
+
shapeCategory: 'shield',
|
|
488
|
+
shapeVariation: 'classic',
|
|
489
|
+
shape: null, // DEPRECATED – old single-key format
|
|
490
|
+
moduleStyle: 'circle',
|
|
491
|
+
moduleScale: 0.82,
|
|
492
|
+
finderScale: 1.0,
|
|
493
|
+
finderPattern: 'pattern',
|
|
494
|
+
finderOuterStyle: 'rounded',
|
|
495
|
+
finderInnerStyle: 'rounded',
|
|
496
|
+
|
|
497
|
+
// --- QR Settings ---
|
|
498
|
+
errorCorrection: 'H',
|
|
499
|
+
|
|
500
|
+
// --- Colors ---
|
|
501
|
+
colors: {
|
|
502
|
+
background: '#0a0e27',
|
|
503
|
+
foreground: '#00d4ff',
|
|
504
|
+
outline: '#00d4ff',
|
|
505
|
+
finderOuter: null,
|
|
506
|
+
finderInner: null,
|
|
507
|
+
outlineWidth: 3,
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
// --- Gradient ---
|
|
511
|
+
gradient: null,
|
|
512
|
+
|
|
513
|
+
// --- Effects ---
|
|
514
|
+
glowEffect: false,
|
|
515
|
+
glowColor: null,
|
|
516
|
+
glowIntensity: 8,
|
|
517
|
+
|
|
518
|
+
innerBorder: false,
|
|
519
|
+
innerBorderWidth: 1,
|
|
520
|
+
innerBorderColor: null,
|
|
521
|
+
innerBorderOffset: 8,
|
|
522
|
+
|
|
523
|
+
// --- Center Clear ---
|
|
524
|
+
centerClear: false,
|
|
525
|
+
centerSize: 0.22,
|
|
526
|
+
|
|
527
|
+
// --- Decorative Fill ---
|
|
528
|
+
decorativeFill: true,
|
|
529
|
+
decorativeDensity: 0.35,
|
|
530
|
+
decorativeOpacity: 0.25,
|
|
531
|
+
decorativeSafeMargin: 6,
|
|
532
|
+
decorativeShieldInset: 8,
|
|
533
|
+
decorativeScale: 0.65,
|
|
534
|
+
|
|
535
|
+
// --- Preset ---
|
|
536
|
+
preset: null,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/** Default design config for UI components */
|
|
540
|
+
const DEFAULT_DESIGN = {
|
|
541
|
+
shapeCategory: 'shield',
|
|
542
|
+
shapeVariation: 'classic',
|
|
543
|
+
shape: null,
|
|
544
|
+
preset: 'cyber',
|
|
545
|
+
moduleStyle: 'circle',
|
|
546
|
+
moduleScale: 0.82,
|
|
547
|
+
finderScale: 1.0,
|
|
548
|
+
finderPattern: 'pattern',
|
|
549
|
+
finderOuterStyle: 'rounded',
|
|
550
|
+
finderInnerStyle: 'rounded',
|
|
551
|
+
glowEffect: true,
|
|
552
|
+
innerBorder: false,
|
|
553
|
+
centerClear: false,
|
|
554
|
+
centerSize: 0.22,
|
|
555
|
+
decorativeFill: true,
|
|
556
|
+
decorativeDensity: 0.35,
|
|
557
|
+
decorativeOpacity: 0.25,
|
|
558
|
+
decorativeSafeMargin: 6,
|
|
559
|
+
decorativeShieldInset: 8,
|
|
560
|
+
decorativeScale: 0.65,
|
|
561
|
+
gradient: null,
|
|
562
|
+
customColors: null,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// ═════════════════════════════════════════════════
|
|
566
|
+
// Shape Resolution
|
|
567
|
+
// ═════════════════════════════════════════════════
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Resolve a design config into a concrete shape definition.
|
|
571
|
+
* Handles both the new (shapeCategory + shapeVariation) and old (shape) formats.
|
|
572
|
+
*
|
|
573
|
+
* @param {object} design
|
|
574
|
+
* @returns {{ category: string, variation: string, shape: object }}
|
|
575
|
+
*/
|
|
576
|
+
function resolveShape(design) {
|
|
577
|
+
if (design?.shapeCategory && SHAPE_LIBRARY[design.shapeCategory]) {
|
|
578
|
+
const cat = SHAPE_LIBRARY[design.shapeCategory];
|
|
579
|
+
const varKey = design.shapeVariation || Object.keys(cat.variations)[0];
|
|
580
|
+
const shape = cat.variations[varKey] || Object.values(cat.variations)[0];
|
|
581
|
+
return { category: design.shapeCategory, variation: varKey, shape };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (design?.shape) {
|
|
585
|
+
if (SHIELD_PATHS[design.shape]) {
|
|
586
|
+
return { category: 'shield', variation: design.shape, shape: SHIELD_PATHS[design.shape] };
|
|
587
|
+
}
|
|
588
|
+
if (SHAPE_LIBRARY[design.shape]) {
|
|
589
|
+
const cat = SHAPE_LIBRARY[design.shape];
|
|
590
|
+
const firstKey = Object.keys(cat.variations)[0];
|
|
591
|
+
return { category: design.shape, variation: firstKey, shape: cat.variations[firstKey] };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return { category: 'shield', variation: 'classic', shape: SHIELD_PATHS.classic };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ═════════════════════════════════════════════════
|
|
599
|
+
// Color Resolution
|
|
600
|
+
// ═════════════════════════════════════════════════
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Resolve a design object into full colors.
|
|
604
|
+
* @param {object} design
|
|
605
|
+
* @returns {object} Color object with background, foreground, outline, etc.
|
|
606
|
+
*/
|
|
607
|
+
function resolveColors(design) {
|
|
608
|
+
if (design?.customColors) {
|
|
609
|
+
return { ...design.customColors };
|
|
610
|
+
}
|
|
611
|
+
const preset = COLOR_PRESETS[design?.preset || 'cyber'];
|
|
612
|
+
return preset
|
|
613
|
+
? {
|
|
614
|
+
background: preset.background,
|
|
615
|
+
foreground: preset.foreground,
|
|
616
|
+
outline: preset.outline,
|
|
617
|
+
finderOuter: preset.finderOuter,
|
|
618
|
+
finderInner: preset.finderInner,
|
|
619
|
+
outlineWidth: preset.outlineWidth,
|
|
620
|
+
}
|
|
621
|
+
: { ...COLOR_PRESETS.cyber };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Build a serializable design config (for saving/sharing).
|
|
626
|
+
* @param {object} design
|
|
627
|
+
* @returns {object}
|
|
628
|
+
*/
|
|
629
|
+
function serializeDesign(design) {
|
|
630
|
+
const out = {
|
|
631
|
+
shapeCategory: design.shapeCategory || 'shield',
|
|
632
|
+
shapeVariation: design.shapeVariation || 'classic',
|
|
633
|
+
moduleStyle: design.moduleStyle || 'circle',
|
|
634
|
+
finderScale: design.finderScale ?? 1.0,
|
|
635
|
+
finderPattern: design.finderPattern || 'pattern',
|
|
636
|
+
finderOuterStyle: design.finderOuterStyle || 'rounded',
|
|
637
|
+
finderInnerStyle: design.finderInnerStyle || 'rounded',
|
|
638
|
+
glowEffect: !!design.glowEffect,
|
|
639
|
+
innerBorder: !!design.innerBorder,
|
|
640
|
+
centerClear: !!design.centerClear,
|
|
641
|
+
decorativeFill: design.decorativeFill ?? true,
|
|
642
|
+
decorativeDensity: design.decorativeDensity || 0.35,
|
|
643
|
+
decorativeOpacity: design.decorativeOpacity ?? 0.25,
|
|
644
|
+
decorativeSafeMargin: design.decorativeSafeMargin ?? 6,
|
|
645
|
+
decorativeShieldInset: design.decorativeShieldInset ?? 8,
|
|
646
|
+
decorativeScale: design.decorativeScale ?? 0.65,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (design.preset && !design.customColors) {
|
|
650
|
+
out.preset = design.preset;
|
|
651
|
+
} else if (design.customColors) {
|
|
652
|
+
out.colors = { ...design.customColors };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (design.gradient) {
|
|
656
|
+
out.gradient = { ...design.gradient };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ═════════════════════════════════════════════════
|
|
663
|
+
// Public API - QR Generation
|
|
664
|
+
// ═════════════════════════════════════════════════
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Generate a shaped QR code as an SVG string.
|
|
668
|
+
*
|
|
669
|
+
* @param {string} data - The data to encode (URL, text, etc.)
|
|
670
|
+
* @param {object} [options] - Customization options (see DEFAULT_OPTIONS)
|
|
671
|
+
* @returns {Promise<string>} Complete SVG markup
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* const svg = await generateShapeQR('https://example.com', {
|
|
675
|
+
* shapeCategory: 'circle',
|
|
676
|
+
* shapeVariation: 'squircle',
|
|
677
|
+
* preset: 'cyber',
|
|
678
|
+
* });
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* const svg = await generateShapeQR('https://example.com', {
|
|
682
|
+
* shapeCategory: 'heart',
|
|
683
|
+
* shapeVariation: 'classic',
|
|
684
|
+
* preset: 'fire',
|
|
685
|
+
* moduleStyle: 'diamond',
|
|
686
|
+
* glowEffect: true,
|
|
687
|
+
* });
|
|
688
|
+
*/
|
|
689
|
+
async function generateShapeQR(data, options = {}) {
|
|
690
|
+
const opts = mergeOptions(DEFAULT_OPTIONS, options);
|
|
691
|
+
|
|
692
|
+
// Apply color preset
|
|
693
|
+
if (opts.preset && COLOR_PRESETS[opts.preset]) {
|
|
694
|
+
const p = COLOR_PRESETS[opts.preset];
|
|
695
|
+
opts.colors = {
|
|
696
|
+
...opts.colors,
|
|
697
|
+
background: p.background,
|
|
698
|
+
foreground: p.foreground,
|
|
699
|
+
outline: p.outline,
|
|
700
|
+
finderOuter: p.finderOuter,
|
|
701
|
+
finderInner: p.finderInner,
|
|
702
|
+
outlineWidth: p.outlineWidth,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Resolve shape from the library
|
|
707
|
+
const { shape: shapeDefinition } = resolveShape(opts);
|
|
708
|
+
|
|
709
|
+
// Generate QR matrix
|
|
710
|
+
const qrData = QRCode.create(data, { errorCorrectionLevel: opts.errorCorrection });
|
|
711
|
+
const modules = qrData.modules;
|
|
712
|
+
const moduleCount = modules.size;
|
|
713
|
+
const moduleSize = shapeDefinition.qrArea.size / moduleCount;
|
|
714
|
+
|
|
715
|
+
return buildSVG(modules, moduleCount, moduleSize, shapeDefinition, opts);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Legacy alias */
|
|
719
|
+
const generateShieldQR = generateShapeQR;
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Generate shaped QR as a UTF-8 Buffer (Node.js) or Uint8Array (browser).
|
|
723
|
+
*/
|
|
724
|
+
async function generateShapeQRBuffer(data, options = {}) {
|
|
725
|
+
const svg = await generateShapeQR(data, options);
|
|
726
|
+
if (typeof Buffer !== 'undefined') {
|
|
727
|
+
return Buffer.from(svg, 'utf-8');
|
|
728
|
+
}
|
|
729
|
+
return new TextEncoder().encode(svg);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const generateShieldQRBuffer = generateShapeQRBuffer;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Generate shaped QR as a base64 data URI.
|
|
736
|
+
*/
|
|
737
|
+
async function generateShapeQRDataURI(data, options = {}) {
|
|
738
|
+
const svg = await generateShapeQR(data, options);
|
|
739
|
+
let base64;
|
|
740
|
+
if (typeof Buffer !== 'undefined') {
|
|
741
|
+
base64 = Buffer.from(svg, 'utf-8').toString('base64');
|
|
742
|
+
} else if (typeof btoa !== 'undefined') {
|
|
743
|
+
base64 = btoa(unescape(encodeURIComponent(svg)));
|
|
744
|
+
} else {
|
|
745
|
+
throw new Error('No base64 encoder available');
|
|
746
|
+
}
|
|
747
|
+
return `data:image/svg+xml;base64,${base64}`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const generateShieldQRDataURI = generateShapeQRDataURI;
|
|
751
|
+
|
|
752
|
+
// ═════════════════════════════════════════════════
|
|
753
|
+
// Library Access API
|
|
754
|
+
// ═════════════════════════════════════════════════
|
|
755
|
+
|
|
756
|
+
/** @returns {object} The full SHAPE_LIBRARY */
|
|
757
|
+
function getShapeLibrary() {
|
|
758
|
+
return SHAPE_LIBRARY;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** @returns {string[]} Available shape category keys */
|
|
762
|
+
function getShapeCategories() {
|
|
763
|
+
return Object.keys(SHAPE_LIBRARY);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** @returns {string[]} Variation keys for a given category */
|
|
767
|
+
function getShapeVariations(category) {
|
|
768
|
+
return SHAPE_LIBRARY[category] ? Object.keys(SHAPE_LIBRARY[category].variations) : [];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Legacy: returns shield variation names */
|
|
772
|
+
function getShieldShapes() {
|
|
773
|
+
return Object.keys(SHIELD_PATHS);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** @returns {string[]} Color preset names */
|
|
777
|
+
function getColorPresets() {
|
|
778
|
+
return Object.keys(COLOR_PRESETS);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Get colors for a named preset */
|
|
782
|
+
function getPresetColors(name) {
|
|
783
|
+
return COLOR_PRESETS[name] ? { ...COLOR_PRESETS[name] } : null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Legacy: get a shield variation by name */
|
|
787
|
+
function getShieldPath(name) {
|
|
788
|
+
return SHIELD_PATHS[name] ? { ...SHIELD_PATHS[name] } : null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Register a custom shape category or add variations to an existing one.
|
|
793
|
+
*
|
|
794
|
+
* @param {string} categoryKey
|
|
795
|
+
* @param {object} categoryDef - { label, icon, description, variations: { ... } }
|
|
796
|
+
* @param {boolean} [merge=true] - If true, merges variations into existing category
|
|
797
|
+
*
|
|
798
|
+
* @example
|
|
799
|
+
* registerShape('star', {
|
|
800
|
+
* label: 'Star',
|
|
801
|
+
* icon: '⭐',
|
|
802
|
+
* description: 'Star shapes',
|
|
803
|
+
* variations: {
|
|
804
|
+
* fivePoint: {
|
|
805
|
+
* label: '5 Point',
|
|
806
|
+
* viewBox: '0 0 300 300',
|
|
807
|
+
* width: 300, height: 300,
|
|
808
|
+
* path: '...',
|
|
809
|
+
* qrArea: { x: 60, y: 60, size: 180 },
|
|
810
|
+
* }
|
|
811
|
+
* }
|
|
812
|
+
* });
|
|
813
|
+
*/
|
|
814
|
+
function registerShape(categoryKey, categoryDef, merge = true) {
|
|
815
|
+
if (merge && SHAPE_LIBRARY[categoryKey]) {
|
|
816
|
+
SHAPE_LIBRARY[categoryKey] = {
|
|
817
|
+
...SHAPE_LIBRARY[categoryKey],
|
|
818
|
+
...categoryDef,
|
|
819
|
+
variations: {
|
|
820
|
+
...SHAPE_LIBRARY[categoryKey].variations,
|
|
821
|
+
...(categoryDef.variations || {}),
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
} else {
|
|
825
|
+
SHAPE_LIBRARY[categoryKey] = categoryDef;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ═════════════════════════════════════════════════
|
|
830
|
+
// SVG Builder (internal)
|
|
831
|
+
// ═════════════════════════════════════════════════
|
|
832
|
+
|
|
833
|
+
function buildSVG(modules, moduleCount, moduleSize, shapeDefinition, opts) {
|
|
834
|
+
const { width, height, viewBox, path, qrArea, bare } = shapeDefinition;
|
|
835
|
+
const colors = opts.colors;
|
|
836
|
+
const finderOuter = colors.finderOuter || colors.foreground;
|
|
837
|
+
const finderInner = colors.finderInner || colors.foreground;
|
|
838
|
+
|
|
839
|
+
const quietModules = 1.5;
|
|
840
|
+
const quietPx = quietModules * moduleSize;
|
|
841
|
+
const dataOriginX = qrArea.x + quietPx;
|
|
842
|
+
const dataOriginY = qrArea.y + quietPx;
|
|
843
|
+
const dataSize = qrArea.size - quietPx * 2;
|
|
844
|
+
const cellSize = dataSize / moduleCount;
|
|
845
|
+
|
|
846
|
+
const svg = [];
|
|
847
|
+
|
|
848
|
+
svg.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="${width}" height="${height}" role="img" aria-label="QR Code">`);
|
|
849
|
+
|
|
850
|
+
svg.push(' <defs>');
|
|
851
|
+
svg.push(` <clipPath id="shapeClip"><path d="${path}"/></clipPath>`);
|
|
852
|
+
|
|
853
|
+
if (opts.gradient) {
|
|
854
|
+
svg.push(buildGradientDef(opts.gradient, ' '));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (!bare && opts.glowEffect) {
|
|
858
|
+
const gc = opts.glowColor || colors.outline;
|
|
859
|
+
svg.push(' <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">');
|
|
860
|
+
svg.push(` <feGaussianBlur stdDeviation="${opts.glowIntensity}" result="blur"/>`);
|
|
861
|
+
svg.push(` <feFlood flood-color="${gc}" flood-opacity="0.5" result="color"/>`);
|
|
862
|
+
svg.push(' <feComposite in="color" in2="blur" operator="in" result="shadow"/>');
|
|
863
|
+
svg.push(' <feMerge><feMergeNode in="shadow"/><feMergeNode in="SourceGraphic"/></feMerge>');
|
|
864
|
+
svg.push(' </filter>');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
svg.push(' </defs>');
|
|
868
|
+
|
|
869
|
+
if (bare) {
|
|
870
|
+
svg.push(` <rect x="0" y="0" width="${width}" height="${height}" fill="${colors.background}"/>`);
|
|
871
|
+
} else {
|
|
872
|
+
const filterAttr = opts.glowEffect ? ' filter="url(#glow)"' : '';
|
|
873
|
+
svg.push(` <path d="${path}" fill="${colors.background}"${filterAttr}/>`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const qzRx = fmt(cellSize * 1.5);
|
|
877
|
+
svg.push(` <rect x="${fmt(qrArea.x)}" y="${fmt(qrArea.y)}" width="${fmt(qrArea.size)}" height="${fmt(qrArea.size)}" rx="${qzRx}" ry="${qzRx}" fill="${colors.background}" clip-path="url(#shapeClip)"/>`);
|
|
878
|
+
|
|
879
|
+
svg.push(bare ? ' <g>' : ' <g clip-path="url(#shapeClip)">');
|
|
880
|
+
|
|
881
|
+
const finderPositions = getFinderPatternPositions(moduleCount);
|
|
882
|
+
const fillColor = opts.gradient ? 'url(#qrGradient)' : colors.foreground;
|
|
883
|
+
|
|
884
|
+
const qrCenterX = dataOriginX + dataSize / 2;
|
|
885
|
+
const qrCenterY = dataOriginY + dataSize / 2;
|
|
886
|
+
const clearRadius = opts.centerClear ? (dataSize * opts.centerSize) : 0;
|
|
887
|
+
|
|
888
|
+
const isBarStyle = opts.moduleStyle === 'barH' || opts.moduleStyle === 'barV';
|
|
889
|
+
const isPondStyle = opts.moduleStyle === 'pond';
|
|
890
|
+
|
|
891
|
+
const isCleared = (row, col) => {
|
|
892
|
+
if (!opts.centerClear) return false;
|
|
893
|
+
const x = dataOriginX + col * cellSize;
|
|
894
|
+
const y = dataOriginY + row * cellSize;
|
|
895
|
+
const dx = (x + cellSize / 2) - qrCenterX;
|
|
896
|
+
const dy = (y + cellSize / 2) - qrCenterY;
|
|
897
|
+
return Math.sqrt(dx * dx + dy * dy) < clearRadius;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// ── Finder patterns ──
|
|
901
|
+
const fScale = opts.finderScale ?? 1.0;
|
|
902
|
+
const fOuterStyle = opts.finderOuterStyle || 'rounded';
|
|
903
|
+
const fInnerStyle = opts.finderInnerStyle || 'rounded';
|
|
904
|
+
|
|
905
|
+
if (opts.finderPattern === 'solid') {
|
|
906
|
+
for (const pos of finderPositions) {
|
|
907
|
+
const fpX = dataOriginX + pos.col * cellSize;
|
|
908
|
+
const fpY = dataOriginY + pos.row * cellSize;
|
|
909
|
+
const cxF = fpX + 3.5 * cellSize;
|
|
910
|
+
const cyF = fpY + 3.5 * cellSize;
|
|
911
|
+
|
|
912
|
+
const outerSz = 7 * cellSize * fScale;
|
|
913
|
+
const spaceSz = 5 * cellSize * fScale;
|
|
914
|
+
const innerSz = 3 * cellSize * fScale;
|
|
915
|
+
|
|
916
|
+
const outerRR = outerSz * 0.15;
|
|
917
|
+
const gap1 = (outerSz - spaceSz) / 2;
|
|
918
|
+
const spaceRR = Math.max(outerRR - gap1, 0);
|
|
919
|
+
const gap2 = (spaceSz - innerSz) / 2;
|
|
920
|
+
const innerRR = Math.max(spaceRR - gap2, 0);
|
|
921
|
+
|
|
922
|
+
svg.push(' ' + renderSolidFinderShape(cxF - outerSz / 2, cyF - outerSz / 2, outerSz, fOuterStyle, finderOuter, outerRR));
|
|
923
|
+
svg.push(' ' + renderSolidFinderShape(cxF - spaceSz / 2, cyF - spaceSz / 2, spaceSz, fOuterStyle, colors.background, spaceRR));
|
|
924
|
+
svg.push(' ' + renderSolidFinderShape(cxF - innerSz / 2, cyF - innerSz / 2, innerSz, fInnerStyle, finderInner, innerRR));
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
928
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
929
|
+
if (!modules.get(row, col)) continue;
|
|
930
|
+
const finderType = classifyFinderModule(row, col, finderPositions);
|
|
931
|
+
if (!finderType || finderType === 'space') continue;
|
|
932
|
+
const x = dataOriginX + col * cellSize;
|
|
933
|
+
const y = dataOriginY + row * cellSize;
|
|
934
|
+
let modFill = fillColor;
|
|
935
|
+
if (finderType === 'outer') modFill = finderOuter;
|
|
936
|
+
else if (finderType === 'inner') modFill = finderInner;
|
|
937
|
+
svg.push(' ' + renderModule(x, y, cellSize, finderType === 'outer' ? fOuterStyle : fInnerStyle, fScale, modFill));
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ── Data modules ──
|
|
943
|
+
if (isPondStyle) {
|
|
944
|
+
const inset = cellSize * (1 - opts.moduleScale) / 2;
|
|
945
|
+
const rr = Math.max(inset * 0.85, 0.3);
|
|
946
|
+
const grid = [];
|
|
947
|
+
for (let r = 0; r < moduleCount; r++) {
|
|
948
|
+
grid[r] = [];
|
|
949
|
+
for (let c = 0; c < moduleCount; c++) {
|
|
950
|
+
grid[r][c] = modules.get(r, c) && !classifyFinderModule(r, c, finderPositions) && !isCleared(r, c);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const gd = (r, c) => r >= 0 && r < moduleCount && c >= 0 && c < moduleCount && grid[r][c];
|
|
954
|
+
const parts = [];
|
|
955
|
+
for (let r = 0; r < moduleCount; r++) {
|
|
956
|
+
for (let c = 0; c < moduleCount; c++) {
|
|
957
|
+
if (!grid[r][c]) continue;
|
|
958
|
+
const x0 = dataOriginX + c * cellSize;
|
|
959
|
+
const y0 = dataOriginY + r * cellSize;
|
|
960
|
+
const tD = gd(r - 1, c), rD = gd(r, c + 1), bD = gd(r + 1, c), lD = gd(r, c - 1);
|
|
961
|
+
const t = tD ? y0 : y0 + inset;
|
|
962
|
+
const ri = rD ? x0 + cellSize : x0 + cellSize - inset;
|
|
963
|
+
const b = bD ? y0 + cellSize : y0 + cellSize - inset;
|
|
964
|
+
const l = lD ? x0 : x0 + inset;
|
|
965
|
+
const tlR = !tD && !lD, trR = !tD && !rD, brR = !bD && !rD, blR = !bD && !lD;
|
|
966
|
+
let d = `M ${fmt(l + (tlR ? rr : 0))} ${fmt(t)}`;
|
|
967
|
+
d += ` H ${fmt(ri - (trR ? rr : 0))}`;
|
|
968
|
+
if (trR) d += ` Q ${fmt(ri)} ${fmt(t)} ${fmt(ri)} ${fmt(t + rr)}`;
|
|
969
|
+
d += ` V ${fmt(b - (brR ? rr : 0))}`;
|
|
970
|
+
if (brR) d += ` Q ${fmt(ri)} ${fmt(b)} ${fmt(ri - rr)} ${fmt(b)}`;
|
|
971
|
+
d += ` H ${fmt(l + (blR ? rr : 0))}`;
|
|
972
|
+
if (blR) d += ` Q ${fmt(l)} ${fmt(b)} ${fmt(l)} ${fmt(b - rr)}`;
|
|
973
|
+
d += ` V ${fmt(t + (tlR ? rr : 0))}`;
|
|
974
|
+
if (tlR) d += ` Q ${fmt(l)} ${fmt(t)} ${fmt(l + rr)} ${fmt(t)}`;
|
|
975
|
+
d += ' Z';
|
|
976
|
+
parts.push(d);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (parts.length) {
|
|
980
|
+
svg.push(` <path d="${parts.join(' ')}" fill="${fillColor}"/>`);
|
|
981
|
+
}
|
|
982
|
+
} else if (isBarStyle) {
|
|
983
|
+
const s = cellSize * opts.moduleScale;
|
|
984
|
+
const halfGap = (cellSize - s) / 2;
|
|
985
|
+
const rr = fmt(s * 0.45);
|
|
986
|
+
if (opts.moduleStyle === 'barH') {
|
|
987
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
988
|
+
let runStart = -1;
|
|
989
|
+
for (let col = 0; col <= moduleCount; col++) {
|
|
990
|
+
const isDark = col < moduleCount && modules.get(row, col);
|
|
991
|
+
const finderType = col < moduleCount ? classifyFinderModule(row, col, finderPositions) : null;
|
|
992
|
+
const shouldConnect = isDark && !finderType && !isCleared(row, col);
|
|
993
|
+
if (shouldConnect) { if (runStart === -1) runStart = col; }
|
|
994
|
+
else if (runStart !== -1) {
|
|
995
|
+
const runLen = col - runStart;
|
|
996
|
+
svg.push(` <rect x="${fmt(dataOriginX + runStart * cellSize + halfGap)}" y="${fmt(dataOriginY + row * cellSize + halfGap)}" width="${fmt(runLen * cellSize - 2 * halfGap)}" height="${fmt(s)}" rx="${rr}" ry="${rr}" fill="${fillColor}"/>`);
|
|
997
|
+
runStart = -1;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
1003
|
+
let runStart = -1;
|
|
1004
|
+
for (let row = 0; row <= moduleCount; row++) {
|
|
1005
|
+
const isDark = row < moduleCount && modules.get(row, col);
|
|
1006
|
+
const finderType = row < moduleCount ? classifyFinderModule(row, col, finderPositions) : null;
|
|
1007
|
+
const shouldConnect = isDark && !finderType && !isCleared(row, col);
|
|
1008
|
+
if (shouldConnect) { if (runStart === -1) runStart = row; }
|
|
1009
|
+
else if (runStart !== -1) {
|
|
1010
|
+
const runLen = row - runStart;
|
|
1011
|
+
svg.push(` <rect x="${fmt(dataOriginX + col * cellSize + halfGap)}" y="${fmt(dataOriginY + runStart * cellSize + halfGap)}" width="${fmt(s)}" height="${fmt(runLen * cellSize - 2 * halfGap)}" rx="${rr}" ry="${rr}" fill="${fillColor}"/>`);
|
|
1012
|
+
runStart = -1;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
for (let row = 0; row < moduleCount; row++) {
|
|
1019
|
+
for (let col = 0; col < moduleCount; col++) {
|
|
1020
|
+
if (!modules.get(row, col)) continue;
|
|
1021
|
+
if (isCleared(row, col)) continue;
|
|
1022
|
+
const finderType = classifyFinderModule(row, col, finderPositions);
|
|
1023
|
+
if (finderType) continue;
|
|
1024
|
+
const x = dataOriginX + col * cellSize;
|
|
1025
|
+
const y = dataOriginY + row * cellSize;
|
|
1026
|
+
svg.push(' ' + renderModule(x, y, cellSize, opts.moduleStyle, opts.moduleScale, fillColor));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
svg.push(' </g>');
|
|
1032
|
+
|
|
1033
|
+
// ── Decorative fill ──
|
|
1034
|
+
if (!bare && opts.decorativeFill) {
|
|
1035
|
+
const safeMargin = opts.decorativeSafeMargin ?? 6;
|
|
1036
|
+
const shieldInset = opts.decorativeShieldInset ?? 8;
|
|
1037
|
+
const density = opts.decorativeDensity || 0.35;
|
|
1038
|
+
const opacity = opts.decorativeOpacity ?? 0.25;
|
|
1039
|
+
const decoFill = opts.gradient ? colors.foreground : fillColor;
|
|
1040
|
+
const decoSize = cellSize;
|
|
1041
|
+
const decoScale = opts.decorativeScale ?? 0.65;
|
|
1042
|
+
|
|
1043
|
+
const exclLeft = qrArea.x - safeMargin;
|
|
1044
|
+
const exclTop = qrArea.y - safeMargin;
|
|
1045
|
+
const exclRight = qrArea.x + qrArea.size + safeMargin;
|
|
1046
|
+
const exclBottom = qrArea.y + qrArea.size + safeMargin;
|
|
1047
|
+
|
|
1048
|
+
const insetSx = ((width - shieldInset * 2) / width).toFixed(4);
|
|
1049
|
+
const insetSy = ((height - shieldInset * 2) / height).toFixed(4);
|
|
1050
|
+
svg.push(` <clipPath id="shapeClipInset"><path d="${path}" transform="translate(${shieldInset},${shieldInset}) scale(${insetSx},${insetSy})"/></clipPath>`);
|
|
1051
|
+
svg.push(` <g clip-path="url(#shapeClipInset)" opacity="${opacity}">`);
|
|
1052
|
+
|
|
1053
|
+
const dCols = Math.ceil(width / decoSize);
|
|
1054
|
+
const dRows = Math.ceil(height / decoSize);
|
|
1055
|
+
const decoGrid = [];
|
|
1056
|
+
for (let dr = 0; dr < dRows; dr++) {
|
|
1057
|
+
decoGrid[dr] = [];
|
|
1058
|
+
for (let dc = 0; dc < dCols; dc++) {
|
|
1059
|
+
const gx = dc * decoSize;
|
|
1060
|
+
const gy = dr * decoSize;
|
|
1061
|
+
const cx = gx + decoSize / 2;
|
|
1062
|
+
const cy = gy + decoSize / 2;
|
|
1063
|
+
if (cx >= exclLeft && cx <= exclRight && cy >= exclTop && cy <= exclBottom) {
|
|
1064
|
+
decoGrid[dr][dc] = false;
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const hash = ((Math.imul(gx * 2654435761 | 0, gy * 2246822519 | 0)) >>> 0) / 4294967296;
|
|
1068
|
+
decoGrid[dr][dc] = hash <= density;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (opts.moduleStyle === 'pond') {
|
|
1073
|
+
const pInset = decoSize * (1 - decoScale) / 2;
|
|
1074
|
+
const pRR = Math.max(pInset * 0.85, 0.3);
|
|
1075
|
+
const dgd = (r, c) => r >= 0 && r < dRows && c >= 0 && c < dCols && decoGrid[r][c];
|
|
1076
|
+
const parts = [];
|
|
1077
|
+
for (let r = 0; r < dRows; r++) {
|
|
1078
|
+
for (let c = 0; c < dCols; c++) {
|
|
1079
|
+
if (!decoGrid[r][c]) continue;
|
|
1080
|
+
const x0 = c * decoSize, y0 = r * decoSize;
|
|
1081
|
+
const tD = dgd(r - 1, c), rD = dgd(r, c + 1), bD = dgd(r + 1, c), lD = dgd(r, c - 1);
|
|
1082
|
+
const t = tD ? y0 : y0 + pInset;
|
|
1083
|
+
const ri = rD ? x0 + decoSize : x0 + decoSize - pInset;
|
|
1084
|
+
const b = bD ? y0 + decoSize : y0 + decoSize - pInset;
|
|
1085
|
+
const l = lD ? x0 : x0 + pInset;
|
|
1086
|
+
const tlR = !tD && !lD, trR = !tD && !rD, brR = !bD && !rD, blR = !bD && !lD;
|
|
1087
|
+
let d = `M ${fmt(l + (tlR ? pRR : 0))} ${fmt(t)}`;
|
|
1088
|
+
d += ` H ${fmt(ri - (trR ? pRR : 0))}`;
|
|
1089
|
+
if (trR) d += ` Q ${fmt(ri)} ${fmt(t)} ${fmt(ri)} ${fmt(t + pRR)}`;
|
|
1090
|
+
d += ` V ${fmt(b - (brR ? pRR : 0))}`;
|
|
1091
|
+
if (brR) d += ` Q ${fmt(ri)} ${fmt(b)} ${fmt(ri - pRR)} ${fmt(b)}`;
|
|
1092
|
+
d += ` H ${fmt(l + (blR ? pRR : 0))}`;
|
|
1093
|
+
if (blR) d += ` Q ${fmt(l)} ${fmt(b)} ${fmt(l)} ${fmt(b - pRR)}`;
|
|
1094
|
+
d += ` V ${fmt(t + (tlR ? pRR : 0))}`;
|
|
1095
|
+
if (tlR) d += ` Q ${fmt(l)} ${fmt(t)} ${fmt(l + pRR)} ${fmt(t)}`;
|
|
1096
|
+
d += ' Z';
|
|
1097
|
+
parts.push(d);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (parts.length) svg.push(` <path d="${parts.join(' ')}" fill="${decoFill}"/>`);
|
|
1101
|
+
} else if (opts.moduleStyle === 'barH') {
|
|
1102
|
+
const s = decoSize * decoScale;
|
|
1103
|
+
const halfGap = (decoSize - s) / 2;
|
|
1104
|
+
const bRR = fmt(s * 0.45);
|
|
1105
|
+
for (let dr = 0; dr < dRows; dr++) {
|
|
1106
|
+
let runStart = -1;
|
|
1107
|
+
for (let dc = 0; dc <= dCols; dc++) {
|
|
1108
|
+
if (dc < dCols && decoGrid[dr][dc]) { if (runStart === -1) runStart = dc; }
|
|
1109
|
+
else if (runStart !== -1) {
|
|
1110
|
+
const len = dc - runStart;
|
|
1111
|
+
svg.push(` <rect x="${fmt(runStart * decoSize + halfGap)}" y="${fmt(dr * decoSize + halfGap)}" width="${fmt(len * decoSize - 2 * halfGap)}" height="${fmt(s)}" rx="${bRR}" ry="${bRR}" fill="${decoFill}"/>`);
|
|
1112
|
+
runStart = -1;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
} else if (opts.moduleStyle === 'barV') {
|
|
1117
|
+
const s = decoSize * decoScale;
|
|
1118
|
+
const halfGap = (decoSize - s) / 2;
|
|
1119
|
+
const bRR = fmt(s * 0.45);
|
|
1120
|
+
for (let dc = 0; dc < dCols; dc++) {
|
|
1121
|
+
let runStart = -1;
|
|
1122
|
+
for (let dr = 0; dr <= dRows; dr++) {
|
|
1123
|
+
if (dr < dRows && decoGrid[dr][dc]) { if (runStart === -1) runStart = dr; }
|
|
1124
|
+
else if (runStart !== -1) {
|
|
1125
|
+
const len = dr - runStart;
|
|
1126
|
+
svg.push(` <rect x="${fmt(dc * decoSize + halfGap)}" y="${fmt(runStart * decoSize + halfGap)}" width="${fmt(s)}" height="${fmt(len * decoSize - 2 * halfGap)}" rx="${bRR}" ry="${bRR}" fill="${decoFill}"/>`);
|
|
1127
|
+
runStart = -1;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} else {
|
|
1132
|
+
for (let dr = 0; dr < dRows; dr++) {
|
|
1133
|
+
for (let dc = 0; dc < dCols; dc++) {
|
|
1134
|
+
if (!decoGrid[dr][dc]) continue;
|
|
1135
|
+
svg.push(' ' + renderModule(dc * decoSize, dr * decoSize, decoSize, opts.moduleStyle, decoScale, decoFill));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
svg.push(' </g>');
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (!bare) {
|
|
1144
|
+
if (opts.innerBorder) {
|
|
1145
|
+
const ibColor = opts.innerBorderColor || colors.outline;
|
|
1146
|
+
const off = opts.innerBorderOffset;
|
|
1147
|
+
const sx = (width - off * 2) / width;
|
|
1148
|
+
const sy = (height - off * 2) / height;
|
|
1149
|
+
svg.push(` <path d="${path}" fill="none" stroke="${ibColor}" stroke-width="${opts.innerBorderWidth}" opacity="0.35" transform="translate(${off},${off}) scale(${sx.toFixed(4)},${sy.toFixed(4)})"/>`);
|
|
1150
|
+
}
|
|
1151
|
+
svg.push(` <path d="${path}" fill="none" stroke="${colors.outline}" stroke-width="${colors.outlineWidth}" stroke-linejoin="round"/>`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
svg.push('</svg>');
|
|
1155
|
+
return svg.join('\n');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ═════════════════════════════════════════════════
|
|
1159
|
+
// Module Renderers (internal)
|
|
1160
|
+
// ═════════════════════════════════════════════════
|
|
1161
|
+
|
|
1162
|
+
function renderSolidFinderShape(x, y, size, style, fill, cornerRadius) {
|
|
1163
|
+
const cx = x + size / 2;
|
|
1164
|
+
const cy = y + size / 2;
|
|
1165
|
+
const r = size / 2;
|
|
1166
|
+
|
|
1167
|
+
switch (style) {
|
|
1168
|
+
case 'circle':
|
|
1169
|
+
case 'dot':
|
|
1170
|
+
return `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="${fill}"/>`;
|
|
1171
|
+
case 'rounded':
|
|
1172
|
+
case 'roundedSquare': {
|
|
1173
|
+
const rr = (cornerRadius !== undefined ? Math.max(cornerRadius, 0) : size * 0.15).toFixed(2);
|
|
1174
|
+
return `<rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(size)}" height="${fmt(size)}" rx="${rr}" ry="${rr}" fill="${fill}"/>`;
|
|
1175
|
+
}
|
|
1176
|
+
case 'diamond': {
|
|
1177
|
+
const pts = [
|
|
1178
|
+
`${fmt(cx)},${fmt(cy - r)}`,
|
|
1179
|
+
`${fmt(cx + r)},${fmt(cy)}`,
|
|
1180
|
+
`${fmt(cx)},${fmt(cy + r)}`,
|
|
1181
|
+
`${fmt(cx - r)},${fmt(cy)}`,
|
|
1182
|
+
].join(' ');
|
|
1183
|
+
return `<polygon points="${pts}" fill="${fill}"/>`;
|
|
1184
|
+
}
|
|
1185
|
+
case 'square':
|
|
1186
|
+
default:
|
|
1187
|
+
return `<rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(size)}" height="${fmt(size)}" fill="${fill}"/>`;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function renderModule(x, y, size, style, scale, fill) {
|
|
1192
|
+
const s = size * scale;
|
|
1193
|
+
const offset = (size - s) / 2;
|
|
1194
|
+
const cx = x + size / 2;
|
|
1195
|
+
const cy = y + size / 2;
|
|
1196
|
+
const r = s / 2;
|
|
1197
|
+
|
|
1198
|
+
switch (style) {
|
|
1199
|
+
case 'circle':
|
|
1200
|
+
case 'dot':
|
|
1201
|
+
return `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" fill="${fill}"/>`;
|
|
1202
|
+
case 'rounded':
|
|
1203
|
+
case 'roundedSquare': {
|
|
1204
|
+
const rr = (s * 0.35).toFixed(2);
|
|
1205
|
+
return `<rect x="${fmt(x + offset)}" y="${fmt(y + offset)}" width="${fmt(s)}" height="${fmt(s)}" rx="${rr}" ry="${rr}" fill="${fill}"/>`;
|
|
1206
|
+
}
|
|
1207
|
+
case 'diamond': {
|
|
1208
|
+
const pts = [
|
|
1209
|
+
`${fmt(cx)},${fmt(cy - r)}`,
|
|
1210
|
+
`${fmt(cx + r)},${fmt(cy)}`,
|
|
1211
|
+
`${fmt(cx)},${fmt(cy + r)}`,
|
|
1212
|
+
`${fmt(cx - r)},${fmt(cy)}`,
|
|
1213
|
+
].join(' ');
|
|
1214
|
+
return `<polygon points="${pts}" fill="${fill}"/>`;
|
|
1215
|
+
}
|
|
1216
|
+
case 'square':
|
|
1217
|
+
default:
|
|
1218
|
+
return `<rect x="${fmt(x + offset)}" y="${fmt(y + offset)}" width="${fmt(s)}" height="${fmt(s)}" fill="${fill}"/>`;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function fmt(n) {
|
|
1223
|
+
return Number(n.toFixed(2));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ═════════════════════════════════════════════════
|
|
1227
|
+
// Finder Pattern Detection (internal)
|
|
1228
|
+
// ═════════════════════════════════════════════════
|
|
1229
|
+
|
|
1230
|
+
function getFinderPatternPositions(moduleCount) {
|
|
1231
|
+
return [
|
|
1232
|
+
{ row: 0, col: 0 },
|
|
1233
|
+
{ row: 0, col: moduleCount - 7 },
|
|
1234
|
+
{ row: moduleCount - 7, col: 0 },
|
|
1235
|
+
];
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function classifyFinderModule(row, col, positions) {
|
|
1239
|
+
for (const pos of positions) {
|
|
1240
|
+
if (row >= pos.row && row < pos.row + 7 && col >= pos.col && col < pos.col + 7) {
|
|
1241
|
+
const lr = row - pos.row;
|
|
1242
|
+
const lc = col - pos.col;
|
|
1243
|
+
if (lr === 0 || lr === 6 || lc === 0 || lc === 6) return 'outer';
|
|
1244
|
+
if (lr >= 2 && lr <= 4 && lc >= 2 && lc <= 4) return 'inner';
|
|
1245
|
+
return 'space';
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ═════════════════════════════════════════════════
|
|
1252
|
+
// Gradient Builder (internal)
|
|
1253
|
+
// ═════════════════════════════════════════════════
|
|
1254
|
+
|
|
1255
|
+
function buildGradientDef(gradient, indent = '') {
|
|
1256
|
+
const { type, colors, angle = 135, stops } = gradient;
|
|
1257
|
+
|
|
1258
|
+
const stopElements = colors.map((color, i) => {
|
|
1259
|
+
const pct = stops ? stops[i] : Math.round((i / (colors.length - 1)) * 100);
|
|
1260
|
+
return `${indent} <stop offset="${pct}%" stop-color="${color}"/>`;
|
|
1261
|
+
}).join('\n');
|
|
1262
|
+
|
|
1263
|
+
if (type === 'radial') {
|
|
1264
|
+
return [
|
|
1265
|
+
`${indent}<radialGradient id="qrGradient" cx="50%" cy="50%" r="60%">`,
|
|
1266
|
+
stopElements,
|
|
1267
|
+
`${indent}</radialGradient>`,
|
|
1268
|
+
].join('\n');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const rad = ((angle - 90) * Math.PI) / 180;
|
|
1272
|
+
const x1 = Math.round(50 + Math.sin(rad + Math.PI) * 50);
|
|
1273
|
+
const y1 = Math.round(50 + Math.cos(rad + Math.PI) * 50);
|
|
1274
|
+
const x2 = Math.round(50 + Math.sin(rad) * 50);
|
|
1275
|
+
const y2 = Math.round(50 + Math.cos(rad) * 50);
|
|
1276
|
+
|
|
1277
|
+
return [
|
|
1278
|
+
`${indent}<linearGradient id="qrGradient" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">`,
|
|
1279
|
+
stopElements,
|
|
1280
|
+
`${indent}</linearGradient>`,
|
|
1281
|
+
].join('\n');
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ═════════════════════════════════════════════════
|
|
1285
|
+
// Utility (internal)
|
|
1286
|
+
// ═════════════════════════════════════════════════
|
|
1287
|
+
|
|
1288
|
+
function mergeOptions(defaults, overrides) {
|
|
1289
|
+
const result = { ...defaults };
|
|
1290
|
+
for (const key of Object.keys(overrides)) {
|
|
1291
|
+
if (overrides[key] === undefined) continue;
|
|
1292
|
+
if (overrides[key] !== null && typeof overrides[key] === 'object' && !Array.isArray(overrides[key])) {
|
|
1293
|
+
result[key] = mergeOptions(defaults[key] || {}, overrides[key]);
|
|
1294
|
+
} else {
|
|
1295
|
+
result[key] = overrides[key];
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ═════════════════════════════════════════════════
|
|
1302
|
+
// Default Export
|
|
1303
|
+
// ═════════════════════════════════════════════════
|
|
1304
|
+
|
|
1305
|
+
var index = {
|
|
1306
|
+
// Generation
|
|
1307
|
+
generateShapeQR,
|
|
1308
|
+
generateShapeQRBuffer,
|
|
1309
|
+
generateShapeQRDataURI,
|
|
1310
|
+
generateShieldQR,
|
|
1311
|
+
generateShieldQRBuffer,
|
|
1312
|
+
generateShieldQRDataURI,
|
|
1313
|
+
// Library access
|
|
1314
|
+
getShapeLibrary,
|
|
1315
|
+
getShapeCategories,
|
|
1316
|
+
getShapeVariations,
|
|
1317
|
+
getShieldShapes,
|
|
1318
|
+
getColorPresets,
|
|
1319
|
+
getPresetColors,
|
|
1320
|
+
getShieldPath,
|
|
1321
|
+
registerShape,
|
|
1322
|
+
resolveShape,
|
|
1323
|
+
resolveColors,
|
|
1324
|
+
serializeDesign,
|
|
1325
|
+
// Constants
|
|
1326
|
+
SHAPE_LIBRARY,
|
|
1327
|
+
SHIELD_PATHS,
|
|
1328
|
+
COLOR_PRESETS,
|
|
1329
|
+
MODULE_STYLES,
|
|
1330
|
+
FINDER_PATTERNS,
|
|
1331
|
+
FINDER_STYLES,
|
|
1332
|
+
GRADIENT_PRESETS,
|
|
1333
|
+
DEFAULT_OPTIONS,
|
|
1334
|
+
DEFAULT_DESIGN,
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
exports.COLOR_PRESETS = COLOR_PRESETS;
|
|
1338
|
+
exports.DEFAULT_DESIGN = DEFAULT_DESIGN;
|
|
1339
|
+
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
1340
|
+
exports.FINDER_PATTERNS = FINDER_PATTERNS;
|
|
1341
|
+
exports.FINDER_STYLES = FINDER_STYLES;
|
|
1342
|
+
exports.GRADIENT_PRESETS = GRADIENT_PRESETS;
|
|
1343
|
+
exports.MODULE_STYLES = MODULE_STYLES;
|
|
1344
|
+
exports.SHAPE_LIBRARY = SHAPE_LIBRARY;
|
|
1345
|
+
exports.SHIELD_PATHS = SHIELD_PATHS;
|
|
1346
|
+
exports.default = index;
|
|
1347
|
+
exports.generateShapeQR = generateShapeQR;
|
|
1348
|
+
exports.generateShapeQRBuffer = generateShapeQRBuffer;
|
|
1349
|
+
exports.generateShapeQRDataURI = generateShapeQRDataURI;
|
|
1350
|
+
exports.generateShieldQR = generateShieldQR;
|
|
1351
|
+
exports.generateShieldQRBuffer = generateShieldQRBuffer;
|
|
1352
|
+
exports.generateShieldQRDataURI = generateShieldQRDataURI;
|
|
1353
|
+
exports.getColorPresets = getColorPresets;
|
|
1354
|
+
exports.getPresetColors = getPresetColors;
|
|
1355
|
+
exports.getShapeCategories = getShapeCategories;
|
|
1356
|
+
exports.getShapeLibrary = getShapeLibrary;
|
|
1357
|
+
exports.getShapeVariations = getShapeVariations;
|
|
1358
|
+
exports.getShieldPath = getShieldPath;
|
|
1359
|
+
exports.getShieldShapes = getShieldShapes;
|
|
1360
|
+
exports.registerShape = registerShape;
|
|
1361
|
+
exports.resolveColors = resolveColors;
|
|
1362
|
+
exports.resolveShape = resolveShape;
|
|
1363
|
+
exports.serializeDesign = serializeDesign;
|
|
1364
|
+
//# sourceMappingURL=index.cjs.map
|