feat: qr code

This commit is contained in:
skytek_xinliang
2026-06-23 11:06:11 +08:00
parent 6cc393cd8a
commit e58e9d40d7
5 changed files with 660 additions and 222 deletions
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed } from 'vue'
import { createQrCodeMatrix } from '@/utils/qrcode'
const props = withDefaults(defineProps<{
value: string
size?: number
}>(), {
size: 180,
})
const quietZone = 4
const qrCode = computed(() => createQrCodeMatrix(props.value))
const viewBoxSize = computed(() => qrCode.value.size + quietZone * 2)
const darkModules = computed(() => {
return qrCode.value.modules.flatMap((row, y) => {
return row
.map((dark, x) => ({ dark, x: x + quietZone, y: y + quietZone }))
.filter(module => module.dark)
})
})
</script>
<template>
<svg
aria-label="報到 QR Code"
class="qr-code"
:height="size"
role="img"
shape-rendering="crispEdges"
:viewBox="`0 0 ${viewBoxSize} ${viewBoxSize}`"
:width="size"
xmlns="http://www.w3.org/2000/svg"
>
<rect fill="#ffffff" height="100%" width="100%" />
<rect
v-for="module in darkModules"
:key="`${module.x}-${module.y}`"
fill="#000000"
height="1"
width="1"
:x="module.x"
:y="module.y"
/>
</svg>
</template>
<style scoped>
.qr-code {
display: block;
}
</style>
+19 -180
View File
@@ -1,10 +1,13 @@
<template> <template>
<Layout> <Layout>
<template #main> <template #main>
<div class="mb-6 text-center"> <v-card class="mb-6" variant="flat">
<h1 class="text-h5 font-weight-black text-primary">公益普測與活動預約</h1> <v-card-title class="text-primary">公益普測與活動預約</v-card-title>
<p class="text-caption text-grey">報名免費體能普測取得個人運動處方</p>
</div> <v-card-subtitle class="text-grey-darken-3">
報名免費體能普測取得個人運動處方
</v-card-subtitle>
</v-card>
<!-- 活動分類選擇 --> <!-- 活動分類選擇 -->
<v-tabs v-model="filterTab" class="mb-4 bg-white rounded-lg elevation-1" color="primary" grow> <v-tabs v-model="filterTab" class="mb-4 bg-white rounded-lg elevation-1" color="primary" grow>
@@ -169,156 +172,10 @@
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p> <p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
<!-- 模擬 QR Code SVG更高級精緻 -->
<div <div
class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center" class="qrcode-wrapper mx-auto mb-4 elevation-3 rounded-lg pa-3 bg-white border d-flex justify-center align-center"
> >
<svg class="qrcode-svg" height="180" viewBox="0 0 100 100" width="180"> <QRCode :size="220" :value="selectedCheckinPayload" />
<!-- 四個角定位框 -->
<path d="M 0,0 H 25 V 8 H 8 V 25 H 0 Z" fill="#1D7063" />
<path d="M 75,0 H 100 V 25 H 92 V 8 H 75 Z" fill="#1D7063" />
<path d="M 0,75 V 100 H 25 V 92 H 8 V 75 Z" fill="#1D7063" />
<path d="M 92,75 V 92 H 75 V 100 H 100 V 75 Z" fill="#1D7063" />
<!-- 內嵌二維碼方塊 -->
<rect
fill="#1D7063"
height="20"
width="20"
x="12"
y="12"
/>
<rect
fill="white"
height="12"
width="12"
x="16"
y="16"
/>
<rect
fill="#E66926"
height="6"
width="6"
x="19"
y="19"
/>
<rect
fill="#1D7063"
height="20"
width="20"
x="68"
y="12"
/>
<rect
fill="white"
height="12"
width="12"
x="72"
y="16"
/>
<rect
fill="#E66926"
height="6"
width="6"
x="75"
y="75"
/>
<rect
fill="#1D7063"
height="20"
width="20"
x="12"
y="68"
/>
<rect
fill="white"
height="12"
width="12"
x="16"
y="72"
/>
<rect
fill="#1D7063"
height="6"
width="6"
x="19"
y="75"
/>
<!-- 中間隨機小方塊 -->
<rect
fill="#1D7063"
height="8"
width="8"
x="40"
y="20"
/>
<rect
fill="#E66926"
height="14"
width="6"
x="52"
y="12"
/>
<rect
fill="#1D7063"
height="6"
width="16"
x="42"
y="36"
/>
<rect
fill="#47C2A6"
height="10"
width="28"
x="36"
y="48"
/>
<rect
fill="#1D7063"
height="12"
width="14"
x="68"
y="42"
/>
<rect
fill="#E66926"
height="16"
width="16"
x="72"
y="60"
/>
<rect
fill="#1D7063"
height="12"
width="18"
x="44"
y="68"
/>
<rect
fill="#47C2A6"
height="6"
width="24"
x="40"
y="82"
/>
</svg>
<!-- 紅色掃描雷射線 -->
<div class="laser-line" />
</div> </div>
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4"> <div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
@@ -359,6 +216,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Layout from '@/components/layout/DefaultLayout.vue' import Layout from '@/components/layout/DefaultLayout.vue'
import QRCode from '@/components/QRCode.vue'
import { type Appointment, useAppStore } from '@/stores/app' import { type Appointment, useAppStore } from '@/stores/app'
const store = useAppStore() const store = useAppStore()
@@ -374,6 +232,14 @@ const filteredAppointments = computed(() => {
return store.appointments return store.appointments
}) })
const selectedCheckinPayload = computed(() => {
if (!selectedApt.value) {
return 'https://hp.local/c'
}
return `https://hp.local/c/${selectedApt.value.id}`
})
function getStatusColor (status: string) { function getStatusColor (status: string) {
switch (status) { switch (status) {
case '未報名': { case '未報名': {
@@ -422,36 +288,9 @@ function simulateOnSiteCheckin () {
.qrcode-wrapper { .qrcode-wrapper {
position: relative; position: relative;
width: 204px; width: 244px;
height: 204px; height: 244px;
overflow: hidden; overflow: hidden;
} }
.qrcode-svg {
display: block;
}
/* 掃描雷射線效果 */
.laser-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #ff3b30;
box-shadow: 0 0 8px #ff3b30;
animation: scan 2.5s infinite linear;
}
@keyframes scan {
0% {
top: 0%;
}
50% {
top: 100%;
}
100% {
top: 0%;
}
}
</style> </style>
+11 -12
View File
@@ -180,23 +180,24 @@
<template v-else> <template v-else>
<div class="text-center mb-6"> <div class="text-center mb-6">
<BrandAvatar class="elevation-3 mb-4" :icon-size="42" :size="72" /> <BrandAvatar class="elevation-2" :icon-size="42" :size="72" />
<p class="text-body-medium text-grey-darken-1 mb-0">
不登入也可以先完成線上健康自評<br />登入後可預約普測與查看量測紀錄
</p>
</div> </div>
<v-card class="rounded-xl elevation-2 mb-4" color="surface"> <v-card class="rounded-xl elevation-2 mb-12 py-4" color="surface">
<v-card-text> <v-card-text>
<div class="d-flex align-center mb-4"> <div class="d-flex align-center mb-4">
<v-avatar class="mr-3 text-white" color="accent" size="36"> <v-avatar class="mr-4 text-white" color="accent" size="36">
<v-icon icon="mdi-clipboard-text-outline" /> <v-icon icon="mdi-clipboard-text-outline" />
</v-avatar> </v-avatar>
<div class="text-left"> <div class="text-left">
<div class="text-title-medium font-weight-bold text-primary">先做線上健康自評</div> <div class="text-title-medium font-weight-bold text-primary">
<div class="text-title-small text-grey"> 3 分鐘完成取得初步健康風險分級</div> 無須登入先做線上健康自評
</div>
<div class="text-title-small text-grey mt-2">
3 分鐘完成取得初步健康風險分級
</div>
</div> </div>
</div> </div>
@@ -231,9 +232,7 @@
<div> <div>
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div> <div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
<div class="text-title-small text-grey"> <div class="text-title-small text-grey">普測預約現場報到量測紀錄與運動處方</div>
普測預約現場報到量測紀錄與運動處方需要登入狀態
</div>
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
+33 -29
View File
@@ -71,28 +71,7 @@ function createDefaultSelfAssessment (): SelfAssessment {
} }
function createDefaultAppointments (): Appointment[] { function createDefaultAppointments (): Appointment[] {
return [ const measurementItems = [
{
id: 'apt-01',
title: '信義區銀髮活力公益普測',
date: '2026-06-20',
time: '09:00 - 11:30',
location: '信義區運動中心 3 樓多功能教室',
status: '已報名',
slots: 12,
organizer: '臺北市政府體育局',
items: ['血壓', '體脂', '握力', '30秒椅子坐立', '原地抬膝踏步', '抓背測驗', '開眼單足立'],
},
{
id: 'apt-02',
title: '中山區樂齡健康普測與運動處方體驗站',
date: '2026-06-25',
time: '14:00 - 16:30',
location: '中山區民生社區活動中心 4 樓',
status: '已報到',
slots: 5,
organizer: '中山區健康服務中心',
items: [
'血壓', '血壓',
'體脂', '體脂',
'握力', '握力',
@@ -104,18 +83,43 @@ function createDefaultAppointments (): Appointment[] {
'坐姿體前彎', '坐姿體前彎',
'2.44米繞物', '2.44米繞物',
'開眼單足立', '開眼單足立',
], ]
const location = '信義區運動中心 3 樓多功能教室'
const organizer = '臺北市政府體育局'
return [
{
id: 'apt-01',
title: '銀髮活力公益普測 A 時段',
date: '2026-07-04',
time: '09:00 - 10:00',
location,
status: '已報名',
slots: 12,
organizer,
items: measurementItems,
},
{
id: 'apt-02',
title: '銀髮活力公益普測 B 時段',
date: '2026-07-04',
time: '10:30 - 11:30',
location,
status: '已報到',
slots: 5,
organizer,
items: measurementItems,
}, },
{ {
id: 'apt-03', id: 'apt-03',
title: '大安區樂齡體適能檢測活動', title: '銀髮活力公益普測 C 時段',
date: '2026-07-02', date: '2026-07-04',
time: '09:00 - 12:00', time: '13:30 - 14:30',
location: '大安運動中心 2 樓籃球場', location,
status: '未報名', status: '未報名',
slots: 45, slots: 45,
organizer: '大安區公所', organizer,
items: ['血壓', '體脂', '握力', '5次坐站', '原地抬膝踏步', '坐姿體前彎', '開眼單足立'], items: measurementItems,
}, },
] ]
} }
+541
View File
@@ -0,0 +1,541 @@
type Cell = boolean | null
interface BlockGroup {
count: number
dataCodewords: number
}
interface VersionSpec {
version: number
size: number
dataCodewords: number
eccCodewords: number
blockGroups: BlockGroup[]
capacity: number
alignment: number[]
}
export interface QrCodeMatrix {
size: number
modules: boolean[][]
}
const VERSION_SPECS: VersionSpec[] = [
{ version: 1, size: 21, dataCodewords: 16, eccCodewords: 10, blockGroups: [{ count: 1, dataCodewords: 16 }], capacity: 14, alignment: [] },
{ version: 2, size: 25, dataCodewords: 28, eccCodewords: 16, blockGroups: [{ count: 1, dataCodewords: 28 }], capacity: 26, alignment: [6, 18] },
{ version: 3, size: 29, dataCodewords: 44, eccCodewords: 26, blockGroups: [{ count: 1, dataCodewords: 44 }], capacity: 42, alignment: [6, 22] },
{ version: 4, size: 33, dataCodewords: 64, eccCodewords: 18, blockGroups: [{ count: 2, dataCodewords: 32 }], capacity: 62, alignment: [6, 26] },
{ version: 5, size: 37, dataCodewords: 86, eccCodewords: 24, blockGroups: [{ count: 2, dataCodewords: 43 }], capacity: 84, alignment: [6, 30] },
{ version: 6, size: 41, dataCodewords: 108, eccCodewords: 16, blockGroups: [{ count: 4, dataCodewords: 27 }], capacity: 106, alignment: [6, 34] },
{ version: 7, size: 45, dataCodewords: 124, eccCodewords: 18, blockGroups: [{ count: 4, dataCodewords: 31 }], capacity: 122, alignment: [6, 22, 38] },
{ version: 8, size: 49, dataCodewords: 154, eccCodewords: 22, blockGroups: [{ count: 2, dataCodewords: 38 }, { count: 2, dataCodewords: 39 }], capacity: 152, alignment: [6, 24, 42] },
{ version: 9, size: 53, dataCodewords: 182, eccCodewords: 22, blockGroups: [{ count: 3, dataCodewords: 36 }, { count: 2, dataCodewords: 37 }], capacity: 180, alignment: [6, 26, 46] },
{ version: 10, size: 57, dataCodewords: 216, eccCodewords: 26, blockGroups: [{ count: 4, dataCodewords: 43 }, { count: 1, dataCodewords: 44 }], capacity: 213, alignment: [6, 28, 50] },
]
const FORMAT_XOR = 0x54_12
const FORMAT_POLY = 0x5_37
const VERSION_POLY = 0x1f_25
const gfExp = Array.from({ length: 512 }, () => 0)
const gfLog = Array.from({ length: 256 }, () => 0)
let x = 1
for (let i = 0; i < 255; i += 1) {
gfExp[i] = x
gfLog[x] = i
x <<= 1
if (x & 0x1_00) {
x ^= 0x1_1d
}
}
for (let i = 255; i < 512; i += 1) {
gfExp[i] = gfExp[i - 255]
}
export function createQrCodeMatrix (value: string): QrCodeMatrix {
const data = [...new TextEncoder().encode(value)]
const spec = VERSION_SPECS.find(item => data.length <= item.capacity)
if (!spec) {
throw new Error('QR Code 內容過長,請將 payload 控制在 213 bytes 以內')
}
const dataCodewords = createDataCodewords(data, spec)
const codewords = createFinalCodewords(dataCodewords, spec)
const dataBits = codewords.flatMap(codeword => byteToBits(codeword))
const base = createBaseMatrix(spec)
let bestModules = base.modules
let bestScore = Number.POSITIVE_INFINITY
for (let mask = 0; mask < 8; mask += 1) {
const modules = cloneMatrix(base.modules)
placeDataBits(modules, base.reserved, dataBits, mask)
drawFormatBits(modules, spec.size, mask)
if (spec.version >= 7) {
drawVersionBits(modules, spec)
}
const score = getPenaltyScore(modules)
if (score < bestScore) {
bestScore = score
bestModules = modules
}
}
return {
size: spec.size,
modules: bestModules.map(row => row.map(Boolean)),
}
}
export function createQrCodeSvg (value: string, options: { scale?: number, quietZone?: number } = {}) {
const { size, modules } = createQrCodeMatrix(value)
const quietZone = options.quietZone ?? 4
const scale = options.scale ?? 6
const viewSize = size + quietZone * 2
const rects: string[] = []
for (const [y, row] of modules.entries()) {
for (const [x, dark] of row.entries()) {
if (dark) {
rects.push(`<rect x="${x + quietZone}" y="${y + quietZone}" width="1" height="1"/>`)
}
}
}
return [
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewSize} ${viewSize}" width="${viewSize * scale}" height="${viewSize * scale}" shape-rendering="crispEdges">`,
'<rect width="100%" height="100%" fill="#fff"/>',
'<g fill="#000">',
...rects,
'</g>',
'</svg>',
].join('')
}
function createDataCodewords (data: number[], spec: VersionSpec) {
const bits: number[] = []
appendBits(bits, 0b0100, 4)
appendBits(bits, data.length, spec.version < 10 ? 8 : 16)
for (const byte of data) {
appendBits(bits, byte, 8)
}
const capacityBits = spec.dataCodewords * 8
appendBits(bits, 0, Math.min(4, capacityBits - bits.length))
while (bits.length % 8 !== 0) {
bits.push(0)
}
const codewords: number[] = []
for (let i = 0; i < bits.length; i += 8) {
codewords.push(bitsToByte(bits.slice(i, i + 8)))
}
for (let pad = 0xec; codewords.length < spec.dataCodewords; pad ^= 0xec ^ 0x11) {
codewords.push(pad)
}
return codewords
}
function createFinalCodewords (dataCodewords: number[], spec: VersionSpec) {
const blocks: { data: number[], ecc: number[] }[] = []
let offset = 0
for (const group of spec.blockGroups) {
for (let i = 0; i < group.count; i += 1) {
const data = dataCodewords.slice(offset, offset + group.dataCodewords)
offset += group.dataCodewords
blocks.push({ data, ecc: createErrorCorrectionCodewords(data, spec.eccCodewords) })
}
}
const result: number[] = []
const maxDataLength = Math.max(...blocks.map(block => block.data.length))
for (let i = 0; i < maxDataLength; i += 1) {
for (const block of blocks) {
if (i < block.data.length) {
result.push(block.data[i])
}
}
}
for (let i = 0; i < spec.eccCodewords; i += 1) {
for (const block of blocks) {
result.push(block.ecc[i])
}
}
return result
}
function createErrorCorrectionCodewords (data: number[], degree: number) {
const generator = createGeneratorPolynomial(degree)
const result = Array.from({ length: degree }, () => 0)
for (const byte of data) {
const factor = byte ^ result.shift()!
result.push(0)
for (const [index, coefficient] of generator.entries()) {
result[index] ^= gfMultiply(coefficient, factor)
}
}
return result
}
function createGeneratorPolynomial (degree: number) {
let result = [1]
for (let i = 0; i < degree; i += 1) {
const next = Array.from({ length: result.length + 1 }, () => 0)
for (const [index, coefficient] of result.entries()) {
next[index] ^= gfMultiply(coefficient, 1)
next[index + 1] ^= gfMultiply(coefficient, gfExp[i])
}
result = next
}
return result.slice(1)
}
function createBaseMatrix (spec: VersionSpec) {
const modules = Array.from({ length: spec.size }, (): Cell[] => {
return Array.from({ length: spec.size }, (): Cell => null)
})
const reserved = Array.from({ length: spec.size }, (): boolean[] => {
return Array.from({ length: spec.size }, () => false)
})
drawFinderPattern(modules, reserved, 3, 3)
drawFinderPattern(modules, reserved, spec.size - 4, 3)
drawFinderPattern(modules, reserved, 3, spec.size - 4)
drawTimingPatterns(modules, reserved)
drawAlignmentPatterns(modules, reserved, spec.alignment)
drawDarkModule(modules, reserved, spec.size)
reserveFormatAreas(reserved, spec.size)
if (spec.version >= 7) {
reserveVersionAreas(reserved, spec.size)
}
return { modules, reserved }
}
function drawFinderPattern (modules: Cell[][], reserved: boolean[][], centerX: number, centerY: number) {
for (let y = -4; y <= 4; y += 1) {
for (let x = -4; x <= 4; x += 1) {
const xx = centerX + x
const yy = centerY + y
if (!isInBounds(modules.length, xx, yy)) {
continue
}
const distance = Math.max(Math.abs(x), Math.abs(y))
modules[yy][xx] = distance !== 2 && distance !== 4
reserved[yy][xx] = true
}
}
}
function drawTimingPatterns (modules: Cell[][], reserved: boolean[][]) {
for (let i = 8; i < modules.length - 8; i += 1) {
const dark = i % 2 === 0
modules[6][i] = dark
modules[i][6] = dark
reserved[6][i] = true
reserved[i][6] = true
}
}
function drawAlignmentPatterns (modules: Cell[][], reserved: boolean[][], positions: number[]) {
for (const y of positions) {
for (const x of positions) {
if (reserved[y][x]) {
continue
}
for (let dy = -2; dy <= 2; dy += 1) {
for (let dx = -2; dx <= 2; dx += 1) {
const distance = Math.max(Math.abs(dx), Math.abs(dy))
modules[y + dy][x + dx] = distance !== 1
reserved[y + dy][x + dx] = true
}
}
}
}
}
function drawDarkModule (modules: Cell[][], reserved: boolean[][], size: number) {
modules[size - 8][8] = true
reserved[size - 8][8] = true
}
function reserveFormatAreas (reserved: boolean[][], size: number) {
for (let i = 0; i <= 5; i += 1) {
reserved[8][i] = true
reserved[i][8] = true
}
reserved[8][7] = true
reserved[8][8] = true
reserved[7][8] = true
for (let i = 0; i < 8; i += 1) {
reserved[8][size - 1 - i] = true
}
for (let i = 8; i < 15; i += 1) {
reserved[size - 15 + i][8] = true
}
}
function reserveVersionAreas (reserved: boolean[][], size: number) {
for (let i = 0; i < 6; i += 1) {
for (let j = 0; j < 3; j += 1) {
reserved[size - 11 + j][i] = true
reserved[i][size - 11 + j] = true
}
}
}
function placeDataBits (modules: Cell[][], reserved: boolean[][], bits: number[], mask: number) {
const size = modules.length
let bitIndex = 0
let direction = -1
let y = size - 1
for (let right = size - 1; right >= 1; right -= 2) {
if (right === 6) {
right -= 1
}
while (y >= 0 && y < size) {
for (let column = 0; column < 2; column += 1) {
const x = right - column
if (!reserved[y][x]) {
const bit = bitIndex < bits.length ? bits[bitIndex] === 1 : false
modules[y][x] = bit !== getMaskBit(mask, x, y)
bitIndex += 1
}
}
y += direction
}
y -= direction
direction = -direction
}
}
function drawFormatBits (modules: Cell[][], size: number, mask: number) {
const bits = getFormatBits(mask)
for (let i = 0; i <= 5; i += 1) {
setModule(modules, 8, i, getBit(bits, i))
}
setModule(modules, 8, 7, getBit(bits, 6))
setModule(modules, 8, 8, getBit(bits, 7))
setModule(modules, 7, 8, getBit(bits, 8))
for (let i = 9; i < 15; i += 1) {
setModule(modules, 14 - i, 8, getBit(bits, i))
}
for (let i = 0; i < 8; i += 1) {
setModule(modules, size - 1 - i, 8, getBit(bits, i))
}
for (let i = 8; i < 15; i += 1) {
setModule(modules, 8, size - 15 + i, getBit(bits, i))
}
}
function drawVersionBits (modules: Cell[][], spec: VersionSpec) {
const bits = getVersionBits(spec.version)
const size = spec.size
for (let i = 0; i < 18; i += 1) {
const bit = getBit(bits, i)
const x = i % 3
const y = Math.floor(i / 3)
setModule(modules, size - 11 + x, y, bit)
setModule(modules, y, size - 11 + x, bit)
}
}
function getFormatBits (mask: number) {
let data = mask
let bits = data << 10
for (let i = 4; i >= 0; i -= 1) {
if (((bits >>> (i + 10)) & 1) === 1) {
bits ^= FORMAT_POLY << i
}
}
data = (data << 10) | (bits & 0x03_ff)
return data ^ FORMAT_XOR
}
function getVersionBits (version: number) {
let bits = version << 12
for (let i = 5; i >= 0; i -= 1) {
if (((bits >>> (i + 12)) & 1) === 1) {
bits ^= VERSION_POLY << i
}
}
return (version << 12) | (bits & 0x0f_ff)
}
function getPenaltyScore (modules: Cell[][]) {
const size = modules.length
let score = 0
for (let y = 0; y < size; y += 1) {
score += getRunPenalty(modules[y].map(Boolean))
}
for (let x = 0; x < size; x += 1) {
score += getRunPenalty(modules.map(row => Boolean(row[x])))
}
for (let y = 0; y < size - 1; y += 1) {
for (let x = 0; x < size - 1; x += 1) {
const dark = modules[y][x]
if (modules[y][x + 1] === dark && modules[y + 1][x] === dark && modules[y + 1][x + 1] === dark) {
score += 3
}
}
}
score += getFinderLikePenalty(modules)
const darkCount = modules.flat().filter(Boolean).length
const percent = (darkCount * 100) / (size * size)
score += Math.floor(Math.abs(percent - 50) / 5) * 10
return score
}
function getRunPenalty (line: boolean[]) {
let score = 0
let runColor = line[0]
let runLength = 1
for (let i = 1; i < line.length; i += 1) {
if (line[i] === runColor) {
runLength += 1
} else {
if (runLength >= 5) {
score += runLength - 2
}
runColor = line[i]
runLength = 1
}
}
return runLength >= 5 ? score + runLength - 2 : score
}
function getFinderLikePenalty (modules: Cell[][]) {
const pattern = '10111010000'
const reversePattern = '00001011101'
const size = modules.length
let score = 0
const scan = (line: boolean[]) => {
const text = line.map(bit => (bit ? '1' : '0')).join('')
for (let i = 0; i <= text.length - pattern.length; i += 1) {
const chunk = text.slice(i, i + pattern.length)
if (chunk === pattern || chunk === reversePattern) {
score += 40
}
}
}
for (const row of modules) {
scan(row.map(Boolean))
}
for (let x = 0; x < size; x += 1) {
scan(modules.map(row => Boolean(row[x])))
}
return score
}
function getMaskBit (mask: number, x: number, y: number) {
switch (mask) {
case 0: {
return (x + y) % 2 === 0
}
case 1: {
return y % 2 === 0
}
case 2: {
return x % 3 === 0
}
case 3: {
return (x + y) % 3 === 0
}
case 4: {
return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0
}
case 5: {
return ((x * y) % 2) + ((x * y) % 3) === 0
}
case 6: {
return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0
}
case 7: {
return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0
}
default: {
return false
}
}
}
function appendBits (target: number[], value: number, length: number) {
for (let i = length - 1; i >= 0; i -= 1) {
target.push((value >>> i) & 1)
}
}
function byteToBits (value: number) {
const bits: number[] = []
appendBits(bits, value, 8)
return bits
}
function bitsToByte (bits: number[]) {
return bits.reduce((value, bit) => (value << 1) | bit, 0)
}
function gfMultiply (a: number, b: number) {
if (a === 0 || b === 0) {
return 0
}
return gfExp[gfLog[a] + gfLog[b]]
}
function getBit (value: number, index: number) {
return ((value >>> index) & 1) === 1
}
function cloneMatrix (modules: Cell[][]) {
return modules.map(row => [...row])
}
function setModule (modules: Cell[][], x: number, y: number, value: boolean) {
modules[y][x] = value
}
function isInBounds (size: number, x: number, y: number) {
return x >= 0 && y >= 0 && x < size && y < size
}