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/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.md +142 -2
- package/cordis.patch.yml +4 -0
- package/icon.svg +7 -0
- package/lib/client.js +2843 -0
- package/lib/core.js +684 -0
- package/lib/history.js +244 -0
- package/lib/index.js +470 -0
- package/lib/phone.js +355 -0
- package/lib/picker.js +1403 -0
- package/lib/qr.js +263 -0
- package/lib/serve.js +513 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/skills/ui-design/SKILL.md +90 -0
- package/skills/ui-design/references/app.md +30 -0
- package/skills/ui-design/references/email.md +17 -0
- package/skills/ui-design/references/poster.md +21 -0
- package/skills/ui-design/references/slides.md +19 -0
- package/skills/ui-design/references/web.md +21 -0
- package/skills/ui-design/tokens.css +70 -0
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
|
+
}
|