Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-data-modules-line-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lglab/react-qr-code': minor
---

Add `lineWidth` option to `dataModulesSettings`. Controls the stroke width (in module units) for connected-shape styles: `vertical-line`, `horizontal-line`, `rounded`, and `circuit-board`. Defaults preserve existing rendering (`1` for line/rounded, `0.5` for `circuit-board`). The `rounded` style fillets exposed outer hub corners so the rounded character is preserved at any `lineWidth`.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ dist-ssr
**/.next/
**/out/

# TypeScript incremental build cache
*.tsbuildinfo

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/src/app/data-modules-settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ const props: Prop[] = [
),
defaultValue: '1',
},
{
name: 'lineWidth',
type: 'number',
description: (
<>
Width of the stroke for connected-shape styles, in module units. Only applies to{' '}
<Bold>vertical-line</Bold>, <Bold>horizontal-line</Bold>, <Bold>rounded</Bold>,
and <Bold>circuit-board</Bold>. Keep between <Bold>0.25</Bold> and <Bold>1</Bold>;
values outside this range may degrade scannability or overflow neighbouring cells.
</>
),
defaultValue: '1 (0.5 for circuit-board)',
},
]

export default function Page() {
Expand Down
29 changes: 29 additions & 0 deletions apps/docs/src/components/demo/data-modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
const size = qrProps.dataModulesSettings?.size ?? 1
const randomSize = qrProps.dataModulesSettings?.randomSize ?? false

const currentStyle = qrProps.dataModulesSettings?.style
const isCircuitBoard = currentStyle === 'circuit-board'
const canSetLineWidth =
currentStyle === 'vertical-line' ||
currentStyle === 'horizontal-line' ||
currentStyle === 'rounded' ||
isCircuitBoard
const defaultLineWidth = isCircuitBoard ? 0.5 : 1
const lineWidth = qrProps.dataModulesSettings?.lineWidth ?? defaultLineWidth

return (
<>
<FormField label='Style'>
Expand All @@ -79,6 +89,25 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
))}
</div>
</FormField>
{canSetLineWidth && (
<FormField label={`Line width (${lineWidth.toFixed(2)})`}>
<Slider
value={[lineWidth]}
onValueChange={([value]) =>
setQrProps((prevProps) => ({
...prevProps,
dataModulesSettings: {
...prevProps.dataModulesSettings,
lineWidth: value,
},
}))
}
min={0.25}
max={1}
step={0.01}
/>
</FormField>
)}
{canBeRandomSize && (
<>
<FormCheckbox
Expand Down
21 changes: 17 additions & 4 deletions apps/docs/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
'use client'

import { ReactQRCode, type ReactQRCodeProps } from '@lglab/react-qr-code'
import { type PropsWithChildren, useState } from 'react'
import {
ReactQRCode,
type ReactQRCodeProps,
type ReactQRCodeRef,
} from '@lglab/react-qr-code'
import { type PropsWithChildren, useRef, useState } from 'react'

import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Button } from '@/components/ui/button'

import { Colors } from './colors'
import { DataModules } from './data-modules'
Expand Down Expand Up @@ -50,6 +55,7 @@ const AccContent = ({ children }: PropsWithChildren) => (
)

export const Demo = () => {
const qrRef = useRef<ReactQRCodeRef>(null)
const [qrProps, setQrProps] = useState<ReactQRCodeProps>({
value: 'https://reactqrcode.com',
size: 400,
Expand Down Expand Up @@ -116,8 +122,15 @@ export const Demo = () => {
</AccordionItem>
</Accordion>
</div>
<div className='flex justify-center [&>svg]:self-start [&>svg]:h-auto sm:col-span-2'>
<ReactQRCode {...qrProps} />
<div className='flex flex-col items-center gap-3 sm:col-span-2 [&>svg]:h-auto'>
<ReactQRCode {...qrProps} ref={qrRef} />
<Button
onClick={() =>
qrRef.current?.download({ name: 'qr-code', format: 'png', size: 1000 })
}
>
Download PNG
</Button>
</div>
</div>
)
Expand Down
105 changes: 50 additions & 55 deletions packages/react-qr-code/src/components/data-modules.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { type ReactNode, useCallback, useMemo } from 'react'

import {
CIRCUIT_BOARD_LINE_WIDTH,
CIRCUIT_BOARD_PAD_RADIUS,
DEFAULT_NUM_STAR_POINTS,
} from '../constants'
import { CIRCUIT_BOARD_PAD_RADIUS, DEFAULT_NUM_STAR_POINTS } from '../constants'
import type { DataModulesProps } from '../types/utils'
import {
bottomRounded,
Expand All @@ -19,6 +15,7 @@ import {
leftRounded,
rect,
rightRounded,
roundedDataModule,
square,
topRounded,
} from '../utils/data-modules'
Expand All @@ -41,7 +38,7 @@ export const DataModules = ({
gradient,
gradientId,
}: DataModulesProps): ReactNode => {
const { color, style, randomSize, size } = useMemo(
const { color, style, randomSize, size, lineWidth } = useMemo(
() => sanitizeDataModulesSettings(settings),
[settings],
)
Expand All @@ -68,41 +65,34 @@ export const DataModules = ({
const scale = scaleFactor()
const size = 1 * scale
const posOffset = (1 - 1 * scale) / 2
const xPos = x + margin + posOffset
const yPos = y + margin + posOffset
const baseX = x + margin
const baseY = y + margin
const xPos = baseX + posOffset
const yPos = baseY + posOffset
const lwOffset = (1 - lineWidth) / 2

if (cell) {
if (style === 'circuit-board') {
const cx = x + margin + 0.5
const cy = y + margin + 0.5
const traceHalf = CIRCUIT_BOARD_LINE_WIDTH / 2
const cx = baseX + 0.5
const cy = baseY + 0.5
const traceHalf = lineWidth / 2
// Traces extend traceHalf past both endpoints so that adjacent
// traces fully cover the cell-center square at every junction
// (preventing white notches at L/T/+ bends under nonzero fill).
const traceLength = 1 + CIRCUIT_BOARD_LINE_WIDTH
const traceLength = 1 + lineWidth
const neighbours = getRenderableDataModuleNeighbours(x, y, modules, numCells)
const { right, bottom, count } = neighbours

if (right) {
ops.push(
rect(cx - traceHalf, cy - traceHalf, traceLength, CIRCUIT_BOARD_LINE_WIDTH),
)
ops.push(rect(cx - traceHalf, cy - traceHalf, traceLength, lineWidth))
}
if (bottom) {
ops.push(
rect(cx - traceHalf, cy - traceHalf, CIRCUIT_BOARD_LINE_WIDTH, traceLength),
)
ops.push(rect(cx - traceHalf, cy - traceHalf, lineWidth, traceLength))
}
if (count === 0) {
const isolatedSize = 0.75
const isolatedOffset = (1 - isolatedSize) / 2
ops.push(
square(
x + margin + isolatedOffset,
y + margin + isolatedOffset,
isolatedSize,
),
)
ops.push(square(baseX + isolatedOffset, baseY + isolatedOffset, isolatedSize))
} else if (circuitBoardShouldDrawPad({ ...neighbours, count })) {
ops.push(circuitBoardPad(cx, cy, CIRCUIT_BOARD_PAD_RADIUS))
}
Expand All @@ -123,32 +113,37 @@ export const DataModules = ({
} else if (style === 'hashtag') {
ops.push(hashtag(xPos, yPos, size))
} else if (style === 'rounded') {
const { left, right, top, bottom, count } = getModuleNeighbours(x, y, modules)
const neighbours = getModuleNeighbours(x, y, modules)
const { left, right, top, bottom, count } = neighbours

if (count === 0) {
ops.push(circle(xPos, yPos, 1))
} else if (count > 2 || (left && right) || (top && bottom)) {
ops.push(square(xPos, yPos, 1))
} else if (count === 2) {
if (left && top) {
ops.push(bottomRightRounded(xPos, yPos))
} else if (top && right) {
ops.push(bottomLeftRounded(xPos, yPos))
} else if (right && bottom) {
ops.push(topLeftRounded(xPos, yPos))
if (lineWidth === 1) {
if (count === 0) {
ops.push(circle(xPos, yPos, 1))
} else if (count > 2 || (left && right) || (top && bottom)) {
ops.push(square(xPos, yPos, 1))
} else if (count === 2) {
if (left && top) {
ops.push(bottomRightRounded(xPos, yPos))
} else if (top && right) {
ops.push(bottomLeftRounded(xPos, yPos))
} else if (right && bottom) {
ops.push(topLeftRounded(xPos, yPos))
} else {
ops.push(topRightRounded(xPos, yPos))
}
} else {
ops.push(topRightRounded(xPos, yPos))
if (top) {
ops.push(bottomRounded(xPos, yPos))
} else if (right) {
ops.push(leftRounded(xPos, yPos))
} else if (bottom) {
ops.push(topRounded(xPos, yPos))
} else {
ops.push(rightRounded(xPos, yPos))
}
}
} else {
if (top) {
ops.push(bottomRounded(xPos, yPos))
} else if (right) {
ops.push(leftRounded(xPos, yPos))
} else if (bottom) {
ops.push(topRounded(xPos, yPos))
} else {
ops.push(rightRounded(xPos, yPos))
}
ops.push(roundedDataModule(baseX, baseY, lineWidth, neighbours))
}
} else if (style === 'leaf') {
const { left, right, top, bottom, count } = getModuleNeighbours(x, y, modules)
Expand All @@ -167,25 +162,25 @@ export const DataModules = ({
const { left, right, top, bottom, count } = getModuleNeighbours(x, y, modules)

if (count === 0 || (left && !(top || bottom)) || (right && !(top || bottom))) {
ops.push(circle(xPos, yPos, 1))
ops.push(circle(baseX + lwOffset, baseY + lwOffset, lineWidth))
} else if (top && bottom) {
ops.push(square(xPos, yPos, 1))
ops.push(rect(baseX + lwOffset, baseY, lineWidth, 1))
} else if (top && !bottom) {
ops.push(bottomRounded(xPos, yPos))
ops.push(bottomRounded(baseX, baseY, lineWidth))
} else if (bottom && !top) {
ops.push(topRounded(xPos, yPos))
ops.push(topRounded(baseX, baseY, lineWidth))
}
} else if (style === 'horizontal-line') {
const { left, right, top, bottom, count } = getModuleNeighbours(x, y, modules)

if (count === 0 || (top && !(left || right)) || (bottom && !(left || right))) {
ops.push(circle(xPos, yPos, 1))
ops.push(circle(baseX + lwOffset, baseY + lwOffset, lineWidth))
} else if (left && right) {
ops.push(square(xPos, yPos, 1))
ops.push(rect(baseX, baseY + lwOffset, 1, lineWidth))
} else if (left && !right) {
ops.push(rightRounded(xPos, yPos))
ops.push(rightRounded(baseX, baseY, lineWidth))
} else if (right && !left) {
ops.push(leftRounded(xPos, yPos))
ops.push(leftRounded(baseX, baseY, lineWidth))
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/react-qr-code/src/test/data-modules-neightbours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export const dataModulesVerticalLineNeighbours = [
bottom: true,
count: 1,
},
'square',
'rect',
],
[
{
Expand All @@ -253,7 +253,7 @@ export const dataModulesVerticalLineNeighbours = [
bottom: true,
count: 2,
},
'square',
'rect',
],
[
{
Expand All @@ -263,7 +263,7 @@ export const dataModulesVerticalLineNeighbours = [
bottom: true,
count: 3,
},
'square',
'rect',
],
[
{
Expand All @@ -273,7 +273,7 @@ export const dataModulesVerticalLineNeighbours = [
bottom: true,
count: 4,
},
'square',
'rect',
],
[
{
Expand Down Expand Up @@ -396,7 +396,7 @@ export const dataModulesHorizontalLineNeighbours = [
bottom: false,
count: 1,
},
'square',
'rect',
],
[
{
Expand Down
9 changes: 9 additions & 0 deletions packages/react-qr-code/src/types/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export interface DataModulesSettings {
* @defaultValue 1
*/
size?: number
/**
* Width of the stroke for connected-shape styles, in module units. Only
* applies to `vertical-line`, `horizontal-line`, `rounded`, and
* `circuit-board`. Keep between 0.25 and 1 — lower values may degrade
* scannability, and values above 1 cause end caps to overflow neighbouring
* cells. Not clamped.
* @defaultValue 1 for `vertical-line` / `horizontal-line` / `rounded`, 0.5 for `circuit-board`
*/
lineWidth?: number
}

export type FinderPatternOuterStyle =
Expand Down
Loading