Skip to content

Commit 31d170c

Browse files
committed
feat: qr code gradient
1 parent c734c17 commit 31d170c

11 files changed

Lines changed: 113 additions & 28 deletions

File tree

apps/playground/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ function App() {
3232
size: 500,
3333
marginSize: 3,
3434
bgColor: '#f1f1f1',
35+
gradient: {
36+
type: 'linear',
37+
rotation: 0,
38+
stops: [
39+
{ offset: '0%', color: '#ff0000' },
40+
{ offset: '100%', color: '#0000ff' },
41+
],
42+
},
3543
dataModulesSettings: {
3644
color: '#560bad',
3745
style: dataModulesStyle,

packages/react-qr-code/src/components/data-modules.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ReactNode, useCallback, useMemo } from 'react'
22

3-
import { DEFAULT_NUM_STAR_POINTS } from '../constants'
3+
import { DEFAULT_NUM_STAR_POINTS, GRADIENT_ID } from '../constants'
44
import type { DataModulesProps } from '../types/utils'
55
import {
66
bottomRounded,
@@ -29,6 +29,7 @@ export const DataModules = ({
2929
modules,
3030
margin,
3131
settings,
32+
gradient,
3233
}: DataModulesProps): ReactNode => {
3334
const { color, style, randomSize } = useMemo(
3435
() => sanitizeDataModulesSettings(settings),
@@ -148,7 +149,7 @@ export const DataModules = ({
148149
})
149150
return (
150151
<path
151-
fill={color}
152+
fill={gradient ? `url(#${GRADIENT_ID})` : color}
152153
d={ops.join('')}
153154
shapeRendering={style === 'square' ? 'crispEdges' : 'geometricPrecision'}
154155
/>

packages/react-qr-code/src/components/finder-patterns-inner.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
FINDER_PATTERN_INNER_SIZE,
77
FINDER_PATTERN_OUTER_ROTATIONS,
88
FINDER_PATTERN_SIZE,
9+
GRADIENT_ID,
910
} from '../constants'
1011
import type { FinderPatternsInnerProps } from '../types/utils'
1112
import {
@@ -19,11 +20,13 @@ export const FinderPatternsInner = ({
1920
modules,
2021
margin,
2122
settings,
23+
gradient,
2224
}: FinderPatternsInnerProps): ReactNode => {
2325
const { color, style } = useMemo(
2426
() => sanitizeFinderPatternInnerSettings(settings),
2527
[settings],
2628
)
29+
const fill = gradient ? `url(#${GRADIENT_ID})` : color
2730

2831
const coordinates = useMemo(
2932
() => [
@@ -52,7 +55,7 @@ export const FinderPatternsInner = ({
5255
y={y}
5356
width={FINDER_PATTERN_INNER_SIZE}
5457
height={FINDER_PATTERN_INNER_SIZE}
55-
fill={color}
58+
fill={fill}
5659
rx={FINDER_PATTERN_INNER_RADIUSES[style]}
5760
/>
5861
)
@@ -72,7 +75,7 @@ export const FinderPatternsInner = ({
7275
y={y + posDiff / 2}
7376
width={size}
7477
height={size}
75-
fill={color}
78+
fill={fill}
7679
style={{
7780
transform: `rotate(${45}deg)`,
7881
transformOrigin: 'center',
@@ -112,7 +115,7 @@ export const FinderPatternsInner = ({
112115
return (
113116
<path
114117
key={key(x, y)}
115-
fill={color}
118+
fill={fill}
116119
d={path}
117120
style={{
118121
transform: `rotate(${rotation}deg)`,
@@ -127,7 +130,7 @@ export const FinderPatternsInner = ({
127130
if (style === 'heart') {
128131
return coordinates.map(({ x, y }) => {
129132
return (
130-
<path key={key(x, y)} fill={color} d={heart(x, y, FINDER_PATTERN_INNER_SIZE)} />
133+
<path key={key(x, y)} fill={fill} d={heart(x, y, FINDER_PATTERN_INNER_SIZE)} />
131134
)
132135
})
133136
}
@@ -137,7 +140,7 @@ export const FinderPatternsInner = ({
137140
const cx = x + FINDER_PATTERN_INNER_SIZE / 2
138141
const cy = y + FINDER_PATTERN_INNER_SIZE / 2
139142
const path = star(cx, cy, FINDER_PATTERN_INNER_SIZE * 1.2, DEFAULT_NUM_STAR_POINTS)
140-
return <path key={key(x, y)} fill={color} d={path} />
143+
return <path key={key(x, y)} fill={fill} d={path} />
141144
})
142145
}
143146

packages/react-qr-code/src/components/finder-patterns-outer.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
FINDER_PATTERN_OUTER_RADIUSES,
55
FINDER_PATTERN_OUTER_ROTATIONS,
66
FINDER_PATTERN_SIZE,
7+
GRADIENT_ID,
78
} from '../constants'
89
import type { FinderPatternsOuterProps } from '../types/utils'
910
import {
@@ -17,11 +18,13 @@ export const FinderPatternsOuter = ({
1718
modules,
1819
margin,
1920
settings,
21+
gradient,
2022
}: FinderPatternsOuterProps): ReactNode => {
2123
const { style, color } = useMemo(
2224
() => sanitizeFinderPatternOuterSettings(settings),
2325
[settings],
2426
)
27+
const fill = gradient ? `url(#${GRADIENT_ID})` : color
2528

2629
const ops: Array<string> = []
2730

@@ -34,9 +37,6 @@ export const FinderPatternsOuter = ({
3437
[margin, modules.length],
3538
)
3639

37-
console.log('modules.length', modules.length)
38-
console.log('coordinates', coordinates)
39-
4040
if (['rounded-sm', 'rounded', 'rounded-lg', 'circle', 'square'].includes(style)) {
4141
for (const coordinate of coordinates) {
4242
const { x, y } = coordinate
@@ -74,7 +74,7 @@ export const FinderPatternsOuter = ({
7474
)
7575
}
7676
}
77-
return <path fill={color} d={ops.join('')} />
77+
return <path fill={fill} d={ops.join('')} />
7878
}
7979

8080
if (
@@ -106,7 +106,7 @@ export const FinderPatternsOuter = ({
106106
return (
107107
<path
108108
key={`finder-patterns-outer-${style}-${x}-${y}`}
109-
fill={color}
109+
fill={fill}
110110
d={path}
111111
style={{
112112
transform: `rotate(${rotation}deg)`,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GRADIENT_ID } from '../constants'
2+
import type { GradientSettings } from '../types/lib'
3+
import { calculateGradientVectors } from '../utils/svg'
4+
5+
interface GradientProps {
6+
gradient?: GradientSettings
7+
}
8+
9+
export const Gradient = ({ gradient }: GradientProps) => {
10+
if (!gradient) {
11+
return null
12+
}
13+
14+
const vectors = calculateGradientVectors(gradient?.rotation || 0)
15+
16+
return (
17+
<defs>
18+
{gradient.type === 'linear' ? (
19+
<linearGradient id={GRADIENT_ID} gradientUnits='userSpaceOnUse' {...vectors}>
20+
{gradient.stops?.map((stop, index) => (
21+
<stop key={index} offset={stop.offset} stopColor={stop.color} />
22+
))}
23+
</linearGradient>
24+
) : (
25+
<radialGradient
26+
id={GRADIENT_ID}
27+
gradientUnits='userSpaceOnUse'
28+
cx='50%'
29+
cy='50%'
30+
r='50%'
31+
>
32+
{gradient.stops?.map((stop, index) => (
33+
<stop key={index} offset={stop.offset} stopColor={stop.color} />
34+
))}
35+
</radialGradient>
36+
)}
37+
</defs>
38+
)
39+
}

packages/react-qr-code/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export const DEFAULT_FINDER_PATTERN_OUTER_STYLE: FinderPatternOuterStyle = 'squa
3333
export const DEFAULT_FINDER_PATTERN_INNER_STYLE: FinderPatternInnerStyle = 'square'
3434
export const DEFAULT_DATA_MODULES_STYLE: DataModulesStyle = 'square'
3535

36+
export const GRADIENT_ID = 'react-qr-code-gradient'
37+
3638
// This is *very* rough estimate of max amount of QRCode allowed to be covered.
3739
// It is "wrong" in a lot of ways (area is a terrible way to estimate, it
3840
// really should be number of modules covered), but if for some reason we don't

packages/react-qr-code/src/react-qr-code.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useImperativeHandle, useRef } from 'react'
33
import { DataModules } from './components/data-modules'
44
import { FinderPatternsInner } from './components/finder-patterns-inner'
55
import { FinderPatternsOuter } from './components/finder-patterns-outer'
6+
import { Gradient } from './components/gradient'
67
import {
78
DEFAULT_BGCOLOR,
89
DEFAULT_LEVEL,
@@ -21,6 +22,7 @@ const ReactQRCode = (props: ReactQRCodeProps) => {
2122
size = DEFAULT_SIZE,
2223
level = DEFAULT_LEVEL,
2324
bgColor = DEFAULT_BGCOLOR,
25+
gradient,
2426
minVersion = DEFAULT_MINVERSION,
2527
boostLevel,
2628
marginSize,
@@ -92,6 +94,12 @@ const ReactQRCode = (props: ReactQRCodeProps) => {
9294
)
9395
}
9496

97+
const svgElementsProps = {
98+
modules,
99+
margin,
100+
gradient,
101+
}
102+
95103
return (
96104
<svg
97105
height={size}
@@ -102,18 +110,11 @@ const ReactQRCode = (props: ReactQRCodeProps) => {
102110
aria-label={svgProps?.['aria-label'] || 'QR Code'}
103111
{...svgProps}
104112
>
113+
<Gradient gradient={gradient} />
105114
<path fill={bgColor} d={`M0,0 h${numCells}v${numCells}H0z`} />
106-
<FinderPatternsOuter
107-
modules={modules}
108-
margin={margin}
109-
settings={finderPatternOuterSettings}
110-
/>
111-
<FinderPatternsInner
112-
modules={modules}
113-
margin={margin}
114-
settings={finderPatternInnerSettings}
115-
/>
116-
<DataModules modules={modules} margin={margin} settings={dataModulesSettings} />
115+
<FinderPatternsOuter settings={finderPatternOuterSettings} {...svgElementsProps} />
116+
<FinderPatternsInner settings={finderPatternInnerSettings} {...svgElementsProps} />
117+
<DataModules settings={dataModulesSettings} {...svgElementsProps} />
117118
{image}
118119
</svg>
119120
)

packages/react-qr-code/src/types/lib.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { Ref } from 'react'
2-
31
import type qrcodegen from '../lib/qrcodegen'
42

53
/**
@@ -17,6 +15,17 @@ export type ERROR_LEVEL_MAPPED_TYPE = {
1715
/**
1816
* ReactQRCode props.
1917
*/
18+
export type GradientSettingsType = 'linear' | 'radial'
19+
export interface GradientSettingsStop {
20+
offset: string
21+
color: string
22+
}
23+
export interface GradientSettings {
24+
type: GradientSettingsType
25+
stops: GradientSettingsStop[]
26+
rotation?: number
27+
}
28+
2029
export type DataModulesStyle =
2130
| 'square'
2231
| 'square-sm'
@@ -93,7 +102,7 @@ export interface ReactQRCodeRef {
93102
}
94103

95104
export interface ReactQRCodeProps {
96-
ref?: Ref<ReactQRCodeRef>
105+
ref?: React.Ref<ReactQRCodeRef>
97106
/**
98107
* The value to encode into the QR Code. An array of strings can be passed in
99108
* to represent multiple segments to further optimize the QR Code.
@@ -139,6 +148,10 @@ export interface ReactQRCodeProps {
139148
* @defaultValue #FFFFFF
140149
*/
141150
bgColor?: string
151+
/**
152+
* The gradient settings applied to the whole qr code.
153+
*/
154+
gradient?: GradientSettings
142155
/**
143156
* The settings for the data modules.
144157
*/

packages/react-qr-code/src/types/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Excavation,
88
FinderPatternInnerSettings,
99
FinderPatternOuterSettings,
10+
GradientSettings,
1011
ImageSettings,
1112
Modules,
1213
} from './lib'
@@ -20,6 +21,7 @@ export interface FilterFnProps {
2021
export interface GeneratePathFnProps {
2122
modules: Modules
2223
margin: number
24+
gradient?: GradientSettings
2325
}
2426

2527
export interface FinderPatternsOuterProps extends GeneratePathFnProps {

packages/react-qr-code/src/utils/download.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const downloadRaster = ({
7979
a.click()
8080
document.body.removeChild(a)
8181
}
82-
82+
// eslint-disable-next-line no-console
8383
logoImg.onerror = (err) => console.error('Error loading logo:', err)
8484
} else {
8585
const imageType = fileFormat === 'png' ? 'image/png' : 'image/jpeg'
@@ -91,6 +91,6 @@ export const downloadRaster = ({
9191
document.body.removeChild(a)
9292
}
9393
}
94-
94+
// eslint-disable-next-line no-console
9595
qrImg.onerror = (err) => console.error('Error loading QR code:', err)
9696
}

0 commit comments

Comments
 (0)