dsh-design-studio 0.0.0-stage → 0.3.2

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/qr.js ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * 二维码(给「手机上看」用):字节模式、纠错等级 M、版本 1–10(最长 213 个字节,链接绰绰有余),
3
+ * 8 种掩码按标准的扣分规则挑最好的。只依赖 Node / 浏览器都有的东西,不引第三方库。
4
+ *
5
+ * qrMatrix('http://192.168.1.8:51234/?k=…') → [[true, false, …], …](true = 黑块,不含四周留白)
6
+ */
7
+
8
+ // 纠错等级 M:每版 [每块纠错码字数, 第一组块数, 第一组每块数据码字, 第二组块数, 第二组每块数据码字]
9
+ const BLOCKS_M = [
10
+ null,
11
+ [10, 1, 16, 0, 0],
12
+ [16, 1, 28, 0, 0],
13
+ [26, 1, 44, 0, 0],
14
+ [18, 2, 32, 0, 0],
15
+ [24, 2, 43, 0, 0],
16
+ [16, 4, 27, 0, 0],
17
+ [18, 4, 31, 0, 0],
18
+ [22, 2, 38, 2, 39],
19
+ [22, 3, 36, 2, 37],
20
+ [26, 4, 43, 1, 44],
21
+ ]
22
+ const ALIGN = [null, [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]]
23
+ const MAX_VERSION = 10
24
+
25
+ /** GF(256) 乘法,本原多项式 0x11D */
26
+ function gfMul(x, y) {
27
+ let z = 0
28
+ for (let i = 7; i >= 0; i--) {
29
+ z = (z << 1) ^ ((z >>> 7) * 0x11d)
30
+ z ^= ((y >>> i) & 1) * x
31
+ }
32
+ return z & 0xff
33
+ }
34
+
35
+ function rsDivisor(degree) {
36
+ const out = new Array(degree).fill(0)
37
+ out[degree - 1] = 1
38
+ let root = 1
39
+ for (let i = 0; i < degree; i++) {
40
+ for (let j = 0; j < degree; j++) {
41
+ out[j] = gfMul(out[j], root)
42
+ if (j + 1 < degree) out[j] ^= out[j + 1]
43
+ }
44
+ root = gfMul(root, 2)
45
+ }
46
+ return out
47
+ }
48
+
49
+ function rsRemainder(data, divisor) {
50
+ const out = divisor.map(() => 0)
51
+ for (const b of data) {
52
+ const factor = b ^ out.shift()
53
+ out.push(0)
54
+ divisor.forEach((c, i) => { out[i] ^= gfMul(c, factor) })
55
+ }
56
+ return out
57
+ }
58
+
59
+ function dataCapacity(v) {
60
+ const [, n1, d1, n2, d2] = BLOCKS_M[v]
61
+ return n1 * d1 + n2 * d2
62
+ }
63
+
64
+ /** 数据码字:模式 + 长度 + 内容 + 结束符 + 补齐 */
65
+ function encodeData(bytes, v) {
66
+ const bits = []
67
+ const put = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1) }
68
+ put(0b0100, 4)
69
+ put(bytes.length, v < 10 ? 8 : 16)
70
+ for (const b of bytes) put(b, 8)
71
+ const cap = dataCapacity(v) * 8
72
+ put(0, Math.min(4, cap - bits.length))
73
+ while (bits.length % 8) bits.push(0)
74
+ const out = []
75
+ for (let i = 0; i < bits.length; i += 8) out.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0))
76
+ for (let pad = 0xec; out.length < cap / 8; pad ^= 0xec ^ 0x11) out.push(pad)
77
+ return out
78
+ }
79
+
80
+ /** 分块、加纠错码、交错 */
81
+ function interleave(data, v) {
82
+ const [ec, n1, d1, n2, d2] = BLOCKS_M[v]
83
+ const div = rsDivisor(ec)
84
+ const blocks = []
85
+ let k = 0
86
+ for (let i = 0; i < n1 + n2; i++) {
87
+ const len = i < n1 ? d1 : d2
88
+ const d = data.slice(k, k + len)
89
+ k += len
90
+ blocks.push({ d, e: rsRemainder(d, div) })
91
+ }
92
+ const out = []
93
+ for (let i = 0; i < Math.max(d1, d2); i++) for (const b of blocks) if (i < b.d.length) out.push(b.d[i])
94
+ for (let i = 0; i < ec; i++) for (const b of blocks) out.push(b.e[i])
95
+ return out
96
+ }
97
+
98
+ /** 格式信息:纠错等级 M(00)+ 掩码,BCH(15,5) */
99
+ export function formatBits(mask) {
100
+ const data = (0 << 3) | mask
101
+ let rem = data
102
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537)
103
+ return ((data << 10) | rem) ^ 0x5412
104
+ }
105
+
106
+ function versionBits(v) {
107
+ let rem = v
108
+ for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25)
109
+ return (v << 12) | rem
110
+ }
111
+
112
+ const MASKS = [
113
+ (x, y) => (x + y) % 2 === 0,
114
+ (x, y) => y % 2 === 0,
115
+ (x) => x % 3 === 0,
116
+ (x, y) => (x + y) % 3 === 0,
117
+ (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
118
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
119
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
120
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
121
+ ]
122
+
123
+ /** 一版的底子:定位、对齐、时序图案,格式 / 版本信息的位置先占上;m[y][x] */
124
+ function base(v) {
125
+ const size = v * 4 + 17
126
+ const m = Array.from({ length: size }, () => new Array(size).fill(false))
127
+ const fn = Array.from({ length: size }, () => new Array(size).fill(false))
128
+ const set = (x, y, dark) => { m[y][x] = dark; fn[y][x] = true }
129
+ for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0) }
130
+ for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
131
+ for (let dy = -4; dy <= 4; dy++) {
132
+ for (let dx = -4; dx <= 4; dx++) {
133
+ const x = cx + dx
134
+ const y = cy + dy
135
+ if (x < 0 || y < 0 || x >= size || y >= size) continue
136
+ const d = Math.max(Math.abs(dx), Math.abs(dy))
137
+ set(x, y, d !== 2 && d !== 4)
138
+ }
139
+ }
140
+ }
141
+ const pos = ALIGN[v]
142
+ for (let i = 0; i < pos.length; i++) {
143
+ for (let j = 0; j < pos.length; j++) {
144
+ const last = pos.length - 1
145
+ if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) continue
146
+ for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(pos[i] + dx, pos[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1)
147
+ }
148
+ }
149
+ drawFormat(m, fn, 0)
150
+ if (v >= 7) {
151
+ const bits = versionBits(v)
152
+ for (let i = 0; i < 18; i++) {
153
+ const dark = ((bits >>> i) & 1) === 1
154
+ const a = size - 11 + (i % 3)
155
+ const b = Math.floor(i / 3)
156
+ set(a, b, dark)
157
+ set(b, a, dark)
158
+ }
159
+ }
160
+ return { size, m, fn }
161
+ }
162
+
163
+ function drawFormat(m, fn, mask) {
164
+ const size = m.length
165
+ const bits = formatBits(mask)
166
+ const set = (x, y, dark) => { m[y][x] = dark; fn[y][x] = true }
167
+ const bit = (i) => ((bits >>> i) & 1) === 1
168
+ for (let i = 0; i <= 5; i++) set(8, i, bit(i))
169
+ set(8, 7, bit(6))
170
+ set(8, 8, bit(7))
171
+ set(7, 8, bit(8))
172
+ for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i))
173
+ for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i))
174
+ for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i))
175
+ set(8, size - 8, true) // 固定的黑块
176
+ }
177
+
178
+ /** 码字按之字形从右下角往上填 */
179
+ function placeData(m, fn, codewords) {
180
+ const size = m.length
181
+ let i = 0
182
+ for (let right = size - 1; right >= 1; right -= 2) {
183
+ if (right === 6) right = 5
184
+ for (let vert = 0; vert < size; vert++) {
185
+ for (let j = 0; j < 2; j++) {
186
+ const x = right - j
187
+ const up = ((right + 1) & 2) === 0
188
+ const y = up ? size - 1 - vert : vert
189
+ if (fn[y][x] || i >= codewords.length * 8) continue
190
+ m[y][x] = ((codewords[i >>> 3] >>> (7 - (i & 7))) & 1) === 1
191
+ i++
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ function applyMask(m, fn, mask) {
198
+ const f = MASKS[mask]
199
+ for (let y = 0; y < m.length; y++) for (let x = 0; x < m.length; x++) if (!fn[y][x] && f(x, y)) m[y][x] = !m[y][x]
200
+ }
201
+
202
+ /** 标准扣分:连续同色、2×2 色块、像定位图案的序列、黑白比例 */
203
+ function penalty(m) {
204
+ const size = m.length
205
+ let score = 0
206
+ const lines = []
207
+ for (let y = 0; y < size; y++) lines.push(m[y])
208
+ for (let x = 0; x < size; x++) lines.push(m.map((row) => row[x]))
209
+ const LIKE = [true, false, true, true, true, false, true]
210
+ for (const line of lines) {
211
+ let run = 1
212
+ for (let i = 1; i <= size; i++) {
213
+ if (i < size && line[i] === line[i - 1]) run++
214
+ else { if (run >= 5) score += 3 + (run - 5); run = 1 }
215
+ }
216
+ for (let i = 0; i + 7 <= size; i++) {
217
+ if (!LIKE.every((v, k) => line[i + k] === v)) continue
218
+ const lightBefore = i >= 4 && [1, 2, 3, 4].every((k) => !line[i - k])
219
+ const lightAfter = i + 11 <= size && [7, 8, 9, 10].every((k) => !line[i + k])
220
+ if (lightBefore || lightAfter) score += 40
221
+ }
222
+ }
223
+ for (let y = 0; y + 1 < size; y++) {
224
+ for (let x = 0; x + 1 < size; x++) {
225
+ const c = m[y][x]
226
+ if (m[y][x + 1] === c && m[y + 1][x] === c && m[y + 1][x + 1] === c) score += 3
227
+ }
228
+ }
229
+ let dark = 0
230
+ for (const row of m) for (const c of row) if (c) dark++
231
+ score += Math.floor(Math.abs((dark * 100) / (size * size) - 50) / 5) * 10
232
+ return score
233
+ }
234
+
235
+ /**
236
+ * 文字 → 二维码矩阵(m[行][列],true 是黑块)。太长(超过版本 10 的容量)抛错。
237
+ * @param mask 指定掩码(测试用);不给就挑扣分最少的
238
+ */
239
+ export function qrMatrix(text, mask) {
240
+ const bytes = [...new TextEncoder().encode(String(text))]
241
+ let v = 1
242
+ while (v <= MAX_VERSION && 4 + (v < 10 ? 8 : 16) + bytes.length * 8 > dataCapacity(v) * 8) v++
243
+ if (v > MAX_VERSION) throw new Error('内容太长,放不进二维码')
244
+ const codewords = interleave(encodeData(bytes, v), v)
245
+ let best = null
246
+ for (let k = 0; k < 8; k++) {
247
+ if (mask !== undefined && k !== mask) continue
248
+ const { m, fn } = base(v)
249
+ placeData(m, fn, codewords)
250
+ applyMask(m, fn, k)
251
+ drawFormat(m, fn, k)
252
+ const s = mask !== undefined ? 0 : penalty(m)
253
+ if (!best || s < best.s) best = { m, s }
254
+ }
255
+ return best.m
256
+ }
257
+
258
+ /** 矩阵 → SVG 路径(每个黑块一个 1×1 的方块,viewBox 留 4 格白边) */
259
+ export function qrPath(m) {
260
+ let d = ''
261
+ for (let y = 0; y < m.length; y++) for (let x = 0; x < m.length; x++) if (m[y][x]) d += `M${x + 4} ${y + 4}h1v1h-1z`
262
+ return d
263
+ }