feat: qr code
This commit is contained in:
@@ -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>
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<div class="mb-6 text-center">
|
||||
<h1 class="text-h5 font-weight-black text-primary">公益普測與活動預約</h1>
|
||||
<p class="text-caption text-grey">報名免費體能普測,取得個人運動處方</p>
|
||||
</div>
|
||||
<v-card class="mb-6" variant="flat">
|
||||
<v-card-title class="text-primary">公益普測與活動預約</v-card-title>
|
||||
|
||||
<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>
|
||||
@@ -169,156 +172,10 @@
|
||||
|
||||
<p class="text-caption text-grey mb-4">請在現場合對處出示此條碼進行報到</p>
|
||||
|
||||
<!-- 模擬 QR Code SVG,更高級精緻 -->
|
||||
<div
|
||||
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">
|
||||
<!-- 四個角定位框 -->
|
||||
<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" />
|
||||
<QRCode :size="220" :value="selectedCheckinPayload" />
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey-darken-2 font-weight-bold mb-4">
|
||||
@@ -359,6 +216,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Layout from '@/components/layout/DefaultLayout.vue'
|
||||
import QRCode from '@/components/QRCode.vue'
|
||||
import { type Appointment, useAppStore } from '@/stores/app'
|
||||
|
||||
const store = useAppStore()
|
||||
@@ -374,6 +232,14 @@ const filteredAppointments = computed(() => {
|
||||
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) {
|
||||
switch (status) {
|
||||
case '未報名': {
|
||||
@@ -422,36 +288,9 @@ function simulateOnSiteCheckin () {
|
||||
|
||||
.qrcode-wrapper {
|
||||
position: relative;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
width: 244px;
|
||||
height: 244px;
|
||||
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>
|
||||
|
||||
@@ -180,23 +180,24 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="text-center mb-6">
|
||||
<BrandAvatar class="elevation-3 mb-4" :icon-size="42" :size="72" />
|
||||
|
||||
<p class="text-body-medium text-grey-darken-1 mb-0">
|
||||
不登入也可以先完成線上健康自評,<br />登入後可預約普測與查看量測紀錄。
|
||||
</p>
|
||||
<BrandAvatar class="elevation-2" :icon-size="42" :size="72" />
|
||||
</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>
|
||||
<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-avatar>
|
||||
|
||||
<div class="text-left">
|
||||
<div class="text-title-medium font-weight-bold text-primary">先做線上健康自評</div>
|
||||
<div class="text-title-small text-grey">約 3 分鐘完成,取得初步健康風險分級。</div>
|
||||
<div class="text-title-medium font-weight-bold text-primary">
|
||||
無須登入,先做線上健康自評
|
||||
</div>
|
||||
|
||||
<div class="text-title-small text-grey mt-2">
|
||||
約 3 分鐘完成,取得初步健康風險分級。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,9 +232,7 @@
|
||||
<div>
|
||||
<div class="font-weight-bold text-grey-darken-4">登入後可使用完整功能</div>
|
||||
|
||||
<div class="text-title-small text-grey">
|
||||
普測預約、現場報到、量測紀錄與運動處方需要登入狀態。
|
||||
</div>
|
||||
<div class="text-title-small text-grey">普測預約、現場報到、量測紀錄與運動處方。</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -71,51 +71,55 @@ function createDefaultSelfAssessment (): SelfAssessment {
|
||||
}
|
||||
|
||||
function createDefaultAppointments (): Appointment[] {
|
||||
const measurementItems = [
|
||||
'血壓',
|
||||
'體脂',
|
||||
'握力',
|
||||
'5次坐站',
|
||||
'30秒椅子坐立',
|
||||
'原地抬膝踏步',
|
||||
'手臂屈舉',
|
||||
'抓背測驗',
|
||||
'坐姿體前彎',
|
||||
'2.44米繞物',
|
||||
'開眼單足立',
|
||||
]
|
||||
const location = '信義區運動中心 3 樓多功能教室'
|
||||
const organizer = '臺北市政府體育局'
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'apt-01',
|
||||
title: '信義區銀髮活力公益普測',
|
||||
date: '2026-06-20',
|
||||
time: '09:00 - 11:30',
|
||||
location: '信義區運動中心 3 樓多功能教室',
|
||||
title: '銀髮活力公益普測 A 時段',
|
||||
date: '2026-07-04',
|
||||
time: '09:00 - 10:00',
|
||||
location,
|
||||
status: '已報名',
|
||||
slots: 12,
|
||||
organizer: '臺北市政府體育局',
|
||||
items: ['血壓', '體脂', '握力', '30秒椅子坐立', '原地抬膝踏步', '抓背測驗', '開眼單足立'],
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
{
|
||||
id: 'apt-02',
|
||||
title: '中山區樂齡健康普測與運動處方體驗站',
|
||||
date: '2026-06-25',
|
||||
time: '14:00 - 16:30',
|
||||
location: '中山區民生社區活動中心 4 樓',
|
||||
title: '銀髮活力公益普測 B 時段',
|
||||
date: '2026-07-04',
|
||||
time: '10:30 - 11:30',
|
||||
location,
|
||||
status: '已報到',
|
||||
slots: 5,
|
||||
organizer: '中山區健康服務中心',
|
||||
items: [
|
||||
'血壓',
|
||||
'體脂',
|
||||
'握力',
|
||||
'5次坐站',
|
||||
'30秒椅子坐立',
|
||||
'原地抬膝踏步',
|
||||
'手臂屈舉',
|
||||
'抓背測驗',
|
||||
'坐姿體前彎',
|
||||
'2.44米繞物',
|
||||
'開眼單足立',
|
||||
],
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
{
|
||||
id: 'apt-03',
|
||||
title: '大安區樂齡體適能檢測活動',
|
||||
date: '2026-07-02',
|
||||
time: '09:00 - 12:00',
|
||||
location: '大安運動中心 2 樓籃球場',
|
||||
title: '銀髮活力公益普測 C 時段',
|
||||
date: '2026-07-04',
|
||||
time: '13:30 - 14:30',
|
||||
location,
|
||||
status: '未報名',
|
||||
slots: 45,
|
||||
organizer: '大安區公所',
|
||||
items: ['血壓', '體脂', '握力', '5次坐站', '原地抬膝踏步', '坐姿體前彎', '開眼單足立'],
|
||||
organizer,
|
||||
items: measurementItems,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user