panelui-native 0.79.0 → 0.80.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/lib/module/components/accordion/index.js +13 -1
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +9 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +527 -19
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/qr-code/index.js +84 -15
- package/lib/module/components/qr-code/index.js.map +1 -1
- package/lib/module/components/qr-code/qr-shapes.js +202 -0
- package/lib/module/components/qr-code/qr-shapes.js.map +1 -0
- package/lib/module/components/timeline/index.js +145 -18
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +16 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +242 -5
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/qr-code/index.d.ts +42 -3
- package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -1
- package/lib/typescript/src/components/qr-code/qr-shapes.d.ts +64 -0
- package/lib/typescript/src/components/qr-code/qr-shapes.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +33 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +13 -0
- package/src/components/bottom-sheet/index.tsx +9 -2
- package/src/components/panelside/index.tsx +750 -28
- package/src/components/qr-code/index.tsx +124 -18
- package/src/components/qr-code/qr-shapes.ts +234 -0
- package/src/components/timeline/index.tsx +165 -14
- package/src/index.ts +3 -0
|
@@ -16,17 +16,33 @@
|
|
|
16
16
|
*
|
|
17
17
|
* ## What it draws
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* Three `<Path>`s: the body, the three corner rings, and the three corner
|
|
20
|
+
* centres. Every module is a subpath of one of them, not a `<Rect>` of its
|
|
20
21
|
* own. A version 10 code is 3,481 modules; half of them dark is seventeen
|
|
21
22
|
* hundred native views for a picture that never changes, and the difference
|
|
22
|
-
* between that and
|
|
23
|
-
* one that appears eventually.
|
|
23
|
+
* between that and three nodes is the difference between a code that appears
|
|
24
|
+
* and one that appears eventually.
|
|
25
|
+
*
|
|
26
|
+
* Three rather than one because they are the three things worth colouring and
|
|
27
|
+
* shaping separately. A code is found by its corner eyes before a single
|
|
28
|
+
* module is read, so they are the part a design touches first — and the part
|
|
29
|
+
* it is most dangerous to touch badly.
|
|
24
30
|
*
|
|
25
31
|
* The colours come from the theme, so a code drawn on a card is legible on
|
|
26
32
|
* every one of them — and light-on-dark is drawn the way scanners expect it,
|
|
27
33
|
* with the quiet zone painted rather than left transparent. A code with
|
|
28
34
|
* nothing behind it reads at about half the distance.
|
|
29
35
|
*
|
|
36
|
+
* ## Shaping it
|
|
37
|
+
*
|
|
38
|
+
* `moduleShape`, `eyeFrameShape` and `eyeBallShape` on `QRCode.Canvas` change
|
|
39
|
+
* the geometry; `color`, `eyeFrameColor`, `eyeBallColor` and `backgroundColor`
|
|
40
|
+
* change the ink. A scanner samples the centre of each cell, so every shape
|
|
41
|
+
* here reads exactly as a square does — what changes is how much of the cell
|
|
42
|
+
* is covered. `dot` fills about two thirds of it and `diamond` half, which is
|
|
43
|
+
* read distance spent on a look. Raise `errorCorrection` to pay for it, and
|
|
44
|
+
* check a printed code rather than one on a screen.
|
|
45
|
+
*
|
|
30
46
|
* ## The hole in the middle
|
|
31
47
|
*
|
|
32
48
|
* `QRCode.Logo` clears a square of modules and puts its children there.
|
|
@@ -52,8 +68,19 @@ import { Text } from '../../primitives/text';
|
|
|
52
68
|
import { cn } from '../../utils/cn';
|
|
53
69
|
import { Popover, type PopoverContentProps } from '../popover';
|
|
54
70
|
import { encodeQr, type ErrorCorrectionLevel, type QrMatrix } from './qr-encode';
|
|
71
|
+
import {
|
|
72
|
+
eyeBallPath,
|
|
73
|
+
eyeFramePath,
|
|
74
|
+
finderOrigins,
|
|
75
|
+
inFinder,
|
|
76
|
+
modulePath,
|
|
77
|
+
type QRCodeEyeBallShape,
|
|
78
|
+
type QRCodeEyeFrameShape,
|
|
79
|
+
type QRCodeModuleShape,
|
|
80
|
+
} from './qr-shapes';
|
|
55
81
|
|
|
56
82
|
export type { ErrorCorrectionLevel };
|
|
83
|
+
export type { QRCodeModuleShape, QRCodeEyeFrameShape, QRCodeEyeBallShape };
|
|
57
84
|
|
|
58
85
|
const qrCodeVariants = tv({
|
|
59
86
|
slots: {
|
|
@@ -260,6 +287,27 @@ export interface QRCodeCanvasProps extends Omit<ViewProps, 'children'> {
|
|
|
260
287
|
color?: string;
|
|
261
288
|
/** The plate the modules sit on. See the note below. */
|
|
262
289
|
backgroundColor?: string;
|
|
290
|
+
/**
|
|
291
|
+
* How a data module is drawn.
|
|
292
|
+
*
|
|
293
|
+
* `rounded` and `classy` join to their neighbours — a corner is rounded only
|
|
294
|
+
* where both cells touching it are light — so a run reads as one stroke
|
|
295
|
+
* rather than as a string of beads with a light seam through it.
|
|
296
|
+
*
|
|
297
|
+
* `dot` and `diamond` do not tile, and cost read distance for it: `dot`
|
|
298
|
+
* covers about two thirds of its cell and `diamond` exactly half, so the
|
|
299
|
+
* same code is a fainter code at the same size. Raise `errorCorrection` with
|
|
300
|
+
* them, and check a printed one rather than a screen.
|
|
301
|
+
*/
|
|
302
|
+
moduleShape?: QRCodeModuleShape;
|
|
303
|
+
/** How the ring around each of the three corner eyes is drawn. */
|
|
304
|
+
eyeFrameShape?: QRCodeEyeFrameShape;
|
|
305
|
+
/** How the square inside each corner eye is drawn. */
|
|
306
|
+
eyeBallShape?: QRCodeEyeBallShape;
|
|
307
|
+
/** The three corner rings. Defaults to `color`. */
|
|
308
|
+
eyeFrameColor?: string;
|
|
309
|
+
/** The three corner centres. Defaults to `eyeFrameColor`, then `color`. */
|
|
310
|
+
eyeBallColor?: string;
|
|
263
311
|
}
|
|
264
312
|
|
|
265
313
|
/**
|
|
@@ -282,38 +330,88 @@ const PLATE = '#ffffff';
|
|
|
282
330
|
const MODULE = '#111111';
|
|
283
331
|
|
|
284
332
|
const QRCodeCanvas = forwardRef<View, QRCodeCanvasProps>(
|
|
285
|
-
(
|
|
333
|
+
(
|
|
334
|
+
{
|
|
335
|
+
className,
|
|
336
|
+
pixelSize,
|
|
337
|
+
color,
|
|
338
|
+
backgroundColor,
|
|
339
|
+
moduleShape = 'square',
|
|
340
|
+
eyeFrameShape = 'square',
|
|
341
|
+
eyeBallShape = 'square',
|
|
342
|
+
eyeFrameColor,
|
|
343
|
+
eyeBallColor,
|
|
344
|
+
...props
|
|
345
|
+
},
|
|
346
|
+
ref
|
|
347
|
+
) => {
|
|
286
348
|
const { matrix, error, value, size, logoFraction } = useQRCode('QRCode.Canvas');
|
|
287
349
|
|
|
288
350
|
const side = pixelSize ?? CANVAS_SIZE[size];
|
|
289
351
|
const dark = color ?? MODULE;
|
|
290
352
|
const light = backgroundColor ?? PLATE;
|
|
353
|
+
const frameInk = eyeFrameColor ?? dark;
|
|
354
|
+
const ballInk = eyeBallColor ?? frameInk;
|
|
291
355
|
|
|
292
356
|
/*
|
|
293
|
-
*
|
|
357
|
+
* Three paths for the whole code: the body, the three corner rings, and
|
|
358
|
+
* the three corner centres.
|
|
359
|
+
*
|
|
360
|
+
* Three rather than one because they are three different things a reader
|
|
361
|
+
* can colour and shape separately — and still three rather than one node
|
|
362
|
+
* per module, which is what a version 10 code would cost: 3,481 cells,
|
|
363
|
+
* half of them dark, seventeen hundred native views for a picture that
|
|
364
|
+
* never changes.
|
|
294
365
|
*
|
|
295
|
-
* The viewBox is in module units, so
|
|
296
|
-
*
|
|
366
|
+
* The viewBox is in module units, so a shape is written once and scales to
|
|
367
|
+
* whatever `side` is — no arithmetic per module, and no rounding gaps
|
|
297
368
|
* between neighbours at fractional sizes.
|
|
298
369
|
*/
|
|
299
|
-
const
|
|
300
|
-
if (!matrix) return '';
|
|
370
|
+
const paths = useMemo(() => {
|
|
371
|
+
if (!matrix) return { body: '', frames: '', balls: '' };
|
|
301
372
|
|
|
302
373
|
const hole = logoFraction ? holeBounds(matrix.size, logoFraction) : null;
|
|
303
374
|
|
|
375
|
+
/*
|
|
376
|
+
* The body, as its own matrix rather than as a filter inside the loop.
|
|
377
|
+
* The rounded shapes ask their neighbours whether a corner is free, and
|
|
378
|
+
* the answer has to be about what is *drawn* — a module beside the logo
|
|
379
|
+
* hole or beside an eye has nothing there to join to, and joining to it
|
|
380
|
+
* leaves a squared-off corner facing a gap.
|
|
381
|
+
*/
|
|
382
|
+
const body: boolean[][] = [];
|
|
383
|
+
for (let y = 0; y < matrix.size; y++) {
|
|
384
|
+
const row: boolean[] = [];
|
|
385
|
+
for (let x = 0; x < matrix.size; x++) {
|
|
386
|
+
const lit =
|
|
387
|
+
!!matrix.modules[y]![x] &&
|
|
388
|
+
!inFinder(x, y, matrix.size) &&
|
|
389
|
+
// Modules under the logo are not drawn at all — drawing them and
|
|
390
|
+
// covering them leaves a dark edge wherever the logo is smaller
|
|
391
|
+
// than the square it cleared.
|
|
392
|
+
!(hole && x >= hole.from && x < hole.to && y >= hole.from && y < hole.to);
|
|
393
|
+
row.push(lit);
|
|
394
|
+
}
|
|
395
|
+
body.push(row);
|
|
396
|
+
}
|
|
397
|
+
|
|
304
398
|
let d = '';
|
|
305
399
|
for (let y = 0; y < matrix.size; y++) {
|
|
306
400
|
for (let x = 0; x < matrix.size; x++) {
|
|
307
|
-
if (!
|
|
308
|
-
|
|
309
|
-
// covering them leaves a dark edge wherever the logo is smaller
|
|
310
|
-
// than the square it cleared.
|
|
311
|
-
if (hole && x >= hole.from && x < hole.to && y >= hole.from && y < hole.to) continue;
|
|
312
|
-
d += `M${x + QUIET_ZONE} ${y + QUIET_ZONE}h1v1h-1z`;
|
|
401
|
+
if (!body[y]![x]) continue;
|
|
402
|
+
d += modulePath(moduleShape, body, x, y, matrix.size, QUIET_ZONE);
|
|
313
403
|
}
|
|
314
404
|
}
|
|
315
|
-
|
|
316
|
-
|
|
405
|
+
|
|
406
|
+
let frames = '';
|
|
407
|
+
let balls = '';
|
|
408
|
+
for (const [x, y, corner] of finderOrigins(matrix.size)) {
|
|
409
|
+
frames += eyeFramePath(eyeFrameShape, x, y, QUIET_ZONE, corner);
|
|
410
|
+
balls += eyeBallPath(eyeBallShape, x, y, QUIET_ZONE);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { body: d, frames, balls };
|
|
414
|
+
}, [matrix, logoFraction, moduleShape, eyeFrameShape, eyeBallShape]);
|
|
317
415
|
|
|
318
416
|
if (error) {
|
|
319
417
|
// A box the size the code would have been, so the layout does not jump
|
|
@@ -349,7 +447,15 @@ const QRCodeCanvas = forwardRef<View, QRCodeCanvasProps>(
|
|
|
349
447
|
Rounded, because at this point it is an object on the card rather
|
|
350
448
|
than a rectangle of paper. */}
|
|
351
449
|
<Rect x={0} y={0} width={grid} height={grid} rx={2.5} fill={light} />
|
|
352
|
-
<Path d={
|
|
450
|
+
<Path d={paths.body} fill={dark} />
|
|
451
|
+
{/* `evenodd` is what makes the ring a ring: the 5×5 subpath inside
|
|
452
|
+
the 7×7 one is a hole rather than a second filled square. A
|
|
453
|
+
stroke would be centred on its path and put the eye's outer edge
|
|
454
|
+
half a module outside the seven the specification allows, and the
|
|
455
|
+
width of that square is the one measurement a scanner takes
|
|
456
|
+
before it reads anything. */}
|
|
457
|
+
<Path d={paths.frames} fill={frameInk} fillRule="evenodd" />
|
|
458
|
+
<Path d={paths.balls} fill={ballInk} />
|
|
353
459
|
</Svg>
|
|
354
460
|
</View>
|
|
355
461
|
);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The geometry a styled QR code is drawn from.
|
|
3
|
+
*
|
|
4
|
+
* A code is three different things wearing one colour: the body — every module
|
|
5
|
+
* that carries data — and the three corner eyes, each of which is a square
|
|
6
|
+
* ring with a square inside it. Scanners find a code by those three eyes
|
|
7
|
+
* before they read a single module, which is why they are the part worth
|
|
8
|
+
* shaping separately and the part it is most dangerous to shape badly.
|
|
9
|
+
*
|
|
10
|
+
* Everything here works in **module units**: one module is 1×1, and the paths
|
|
11
|
+
* are placed into a viewBox that is the matrix plus its quiet zone. So a shape
|
|
12
|
+
* is written once and scales to whatever the code is drawn at, with no
|
|
13
|
+
* arithmetic per module and no rounding gaps between neighbours at fractional
|
|
14
|
+
* sizes.
|
|
15
|
+
*
|
|
16
|
+
* ## What is safe to change and what is not
|
|
17
|
+
*
|
|
18
|
+
* A scanner locates a module by sampling the centre of its cell, so a shape
|
|
19
|
+
* that stays inside its own cell and keeps that centre dark reads exactly as a
|
|
20
|
+
* square does. All of these do. What actually costs read distance is *area*:
|
|
21
|
+
* `dot` covers about two thirds of its cell and `diamond` exactly half, so a
|
|
22
|
+
* code in either is a fainter code at the same size, and one in `diamond` at a
|
|
23
|
+
* low error-correction level is a code that works on a screen and not on
|
|
24
|
+
* paper.
|
|
25
|
+
*
|
|
26
|
+
* The rounded shapes join to their neighbours rather than rounding every
|
|
27
|
+
* corner. A corner is rounded only where both cells touching it are light —
|
|
28
|
+
* otherwise a run of modules would be a string of beads with a light seam
|
|
29
|
+
* through it, and the seam is what the sampler sees.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** How a data module is drawn. */
|
|
33
|
+
export type QRCodeModuleShape = 'square' | 'rounded' | 'dot' | 'classy' | 'diamond';
|
|
34
|
+
/** How the ring around each of the three corner eyes is drawn. */
|
|
35
|
+
export type QRCodeEyeFrameShape = 'square' | 'rounded' | 'circle' | 'leaf' | 'shield';
|
|
36
|
+
/** How the square inside each corner eye is drawn. */
|
|
37
|
+
export type QRCodeEyeBallShape = 'square' | 'rounded' | 'dot' | 'diamond' | 'leaf';
|
|
38
|
+
|
|
39
|
+
/** A finder pattern is seven modules on a side, in three of the four corners. */
|
|
40
|
+
export const FINDER_SIZE = 7;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How much smaller than its cell a free-standing shape is drawn.
|
|
44
|
+
*
|
|
45
|
+
* Circles and diamonds do not tile, so nothing is gained by having them touch:
|
|
46
|
+
* a hair of space is what makes a field of dots read as dots rather than as a
|
|
47
|
+
* grid that has gone soft. Squares and the rounded shapes keep the full cell,
|
|
48
|
+
* because they *do* tile and any gap there is a light seam through a run.
|
|
49
|
+
*/
|
|
50
|
+
const INSET = 0.05;
|
|
51
|
+
|
|
52
|
+
/** Corner radii, clockwise from the top-left, in module units. */
|
|
53
|
+
type Corners = [number, number, number, number];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A rectangle with a radius per corner.
|
|
57
|
+
*
|
|
58
|
+
* `A` with a zero radius is a straight line by the SVG specification, so a
|
|
59
|
+
* sharp corner needs no branch of its own.
|
|
60
|
+
*/
|
|
61
|
+
function roundedRect(x: number, y: number, w: number, h: number, r: Corners): string {
|
|
62
|
+
const [tl, tr, br, bl] = r;
|
|
63
|
+
return (
|
|
64
|
+
`M${x + tl} ${y}` +
|
|
65
|
+
`H${x + w - tr}A${tr} ${tr} 0 0 1 ${x + w} ${y + tr}` +
|
|
66
|
+
`V${y + h - br}A${br} ${br} 0 0 1 ${x + w - br} ${y + h}` +
|
|
67
|
+
`H${x + bl}A${bl} ${bl} 0 0 1 ${x} ${y + h - bl}` +
|
|
68
|
+
`V${y + tl}A${tl} ${tl} 0 0 1 ${x + tl} ${y}` +
|
|
69
|
+
'Z'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A circle, as two half-arcs — the only way to write one in a path. */
|
|
74
|
+
function circle(cx: number, cy: number, r: number): string {
|
|
75
|
+
return `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0Z`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A square stood on its point, inscribed in the cell. */
|
|
79
|
+
function diamond(cx: number, cy: number, r: number): string {
|
|
80
|
+
return `M${cx} ${cy - r}L${cx + r} ${cy}L${cx} ${cy + r}L${cx - r} ${cy}Z`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Which of a module's four sides have a dark module against them.
|
|
85
|
+
*
|
|
86
|
+
* Only the four orthogonal neighbours, not the diagonals. A corner is rounded
|
|
87
|
+
* when both of its sides are free, and a diagonal neighbour touching that
|
|
88
|
+
* corner is a separate run — rounding away from it is what gives a diagonal
|
|
89
|
+
* stripe its scalloped edge instead of a chain of squares.
|
|
90
|
+
*/
|
|
91
|
+
function neighbours(modules: boolean[][], x: number, y: number, size: number) {
|
|
92
|
+
const at = (nx: number, ny: number) =>
|
|
93
|
+
nx >= 0 && ny >= 0 && nx < size && ny < size && !!modules[ny]![nx];
|
|
94
|
+
return { up: at(x, y - 1), right: at(x + 1, y), down: at(x, y + 1), left: at(x - 1, y) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True where `(x, y)` falls inside one of the three finder patterns. */
|
|
98
|
+
export function inFinder(x: number, y: number, size: number): boolean {
|
|
99
|
+
const far = size - FINDER_SIZE;
|
|
100
|
+
return (
|
|
101
|
+
(x < FINDER_SIZE && y < FINDER_SIZE) ||
|
|
102
|
+
(x >= far && y < FINDER_SIZE) ||
|
|
103
|
+
(x < FINDER_SIZE && y >= far)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Which corner of the code an eye sits in. */
|
|
108
|
+
export type QRCodeEyeCorner = 'tl' | 'tr' | 'bl';
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Where each finder pattern starts, and which corner it is in.
|
|
112
|
+
*
|
|
113
|
+
* The corner is not decoration: an asymmetric eye shape has to be turned to
|
|
114
|
+
* face outwards, or two of the three point into the middle of the code and
|
|
115
|
+
* read as a mistake rather than as a style.
|
|
116
|
+
*/
|
|
117
|
+
export function finderOrigins(size: number): [number, number, QRCodeEyeCorner][] {
|
|
118
|
+
const far = size - FINDER_SIZE;
|
|
119
|
+
return [
|
|
120
|
+
[0, 0, 'tl'],
|
|
121
|
+
[far, 0, 'tr'],
|
|
122
|
+
[0, far, 'bl'],
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One data module, in the requested shape, offset into the quiet zone. */
|
|
127
|
+
export function modulePath(
|
|
128
|
+
shape: QRCodeModuleShape,
|
|
129
|
+
modules: boolean[][],
|
|
130
|
+
x: number,
|
|
131
|
+
y: number,
|
|
132
|
+
size: number,
|
|
133
|
+
offset: number
|
|
134
|
+
): string {
|
|
135
|
+
const px = x + offset;
|
|
136
|
+
const py = y + offset;
|
|
137
|
+
|
|
138
|
+
if (shape === 'square') return `M${px} ${py}h1v1h-1z`;
|
|
139
|
+
if (shape === 'dot') return circle(px + 0.5, py + 0.5, 0.5 - INSET);
|
|
140
|
+
// No inset on the diamond. It already covers half its cell, which is the
|
|
141
|
+
// least of any shape here, and taking another tenth off the radius costs a
|
|
142
|
+
// fifth of what is left.
|
|
143
|
+
if (shape === 'diamond') return diamond(px + 0.5, py + 0.5, 0.5);
|
|
144
|
+
|
|
145
|
+
const { up, right, down, left } = neighbours(modules, x, y, size);
|
|
146
|
+
// Half a module: a free-standing cell becomes a circle and a run becomes a
|
|
147
|
+
// stadium, which is the shape the corner joining exists to produce.
|
|
148
|
+
const r = 0.5;
|
|
149
|
+
const tl = up || left ? 0 : r;
|
|
150
|
+
const tr = up || right ? 0 : r;
|
|
151
|
+
const br = down || right ? 0 : r;
|
|
152
|
+
const bl = down || left ? 0 : r;
|
|
153
|
+
|
|
154
|
+
if (shape === 'classy') {
|
|
155
|
+
// One diagonal rounded, the other left sharp — the leaf. Still joined, so
|
|
156
|
+
// a run of them is one stroke with two rounded ends rather than a row of
|
|
157
|
+
// leaves with seams between.
|
|
158
|
+
return roundedRect(px, py, 1, 1, [tl, 0, br, 0]);
|
|
159
|
+
}
|
|
160
|
+
return roundedRect(px, py, 1, 1, [tl, tr, br, bl]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One eye's ring: a 7×7 outline with a 5×5 hole in it.
|
|
165
|
+
*
|
|
166
|
+
* Two subpaths and `fillRule="evenodd"`, rather than a stroked rectangle. A
|
|
167
|
+
* stroke is centred on its path, so its outer edge lands half a module outside
|
|
168
|
+
* the seven the specification allows — and the eye stops being seven modules
|
|
169
|
+
* wide, which is the one measurement a scanner takes before it reads anything.
|
|
170
|
+
*/
|
|
171
|
+
export function eyeFramePath(
|
|
172
|
+
shape: QRCodeEyeFrameShape,
|
|
173
|
+
x: number,
|
|
174
|
+
y: number,
|
|
175
|
+
offset: number,
|
|
176
|
+
corner: QRCodeEyeCorner = 'tl'
|
|
177
|
+
): string {
|
|
178
|
+
const px = x + offset;
|
|
179
|
+
const py = y + offset;
|
|
180
|
+
const s = FINDER_SIZE;
|
|
181
|
+
const inner = s - 2;
|
|
182
|
+
|
|
183
|
+
if (shape === 'circle') {
|
|
184
|
+
return circle(px + s / 2, py + s / 2, s / 2) + circle(px + s / 2, py + s / 2, inner / 2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* `shield` keeps one corner square and rounds the other three, and the
|
|
189
|
+
* square one is the corner of the code the eye sits in — the corner a
|
|
190
|
+
* reader's eye already finds the edge of the page by. Turned the same way
|
|
191
|
+
* in all three positions, two of them point their flat corner into the
|
|
192
|
+
* middle of the code and the set reads as a mistake.
|
|
193
|
+
*/
|
|
194
|
+
const SHARP: Record<QRCodeEyeCorner, Corners> = {
|
|
195
|
+
tl: [0, 3.5, 3.5, 3.5],
|
|
196
|
+
tr: [3.5, 0, 3.5, 3.5],
|
|
197
|
+
bl: [3.5, 3.5, 3.5, 0],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const outer: Record<Exclude<QRCodeEyeFrameShape, 'circle'>, Corners> = {
|
|
201
|
+
square: [0, 0, 0, 0],
|
|
202
|
+
rounded: [2, 2, 2, 2],
|
|
203
|
+
// Two opposite corners, the same diagonal in all three positions: a leaf
|
|
204
|
+
// is symmetric about that diagonal, so there is no outward to face and
|
|
205
|
+
// turning them would only break the set's rhythm.
|
|
206
|
+
leaf: [3.5, 0, 3.5, 0],
|
|
207
|
+
shield: SHARP[corner],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const o = outer[shape as Exclude<QRCodeEyeFrameShape, 'circle'>];
|
|
211
|
+
// The hole is concentric, so each radius loses the ring's own thickness —
|
|
212
|
+
// one module. Floored at zero: a square corner stays square.
|
|
213
|
+
const i = o.map((value) => Math.max(value - 1, 0)) as Corners;
|
|
214
|
+
|
|
215
|
+
return roundedRect(px, py, s, s, o) + roundedRect(px + 1, py + 1, inner, inner, i);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** One eye's centre: the 3×3 square two modules inside the ring. */
|
|
219
|
+
export function eyeBallPath(
|
|
220
|
+
shape: QRCodeEyeBallShape,
|
|
221
|
+
x: number,
|
|
222
|
+
y: number,
|
|
223
|
+
offset: number
|
|
224
|
+
): string {
|
|
225
|
+
const px = x + offset + 2;
|
|
226
|
+
const py = y + offset + 2;
|
|
227
|
+
const s = 3;
|
|
228
|
+
|
|
229
|
+
if (shape === 'square') return `M${px} ${py}h${s}v${s}h${-s}z`;
|
|
230
|
+
if (shape === 'dot') return circle(px + s / 2, py + s / 2, s / 2);
|
|
231
|
+
if (shape === 'diamond') return diamond(px + s / 2, py + s / 2, s / 2);
|
|
232
|
+
if (shape === 'leaf') return roundedRect(px, py, s, s, [1.5, 0, 1.5, 0]);
|
|
233
|
+
return roundedRect(px, py, s, s, [1, 1, 1, 1]);
|
|
234
|
+
}
|