@joewinke/jatui 0.10.0 → 0.11.0
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/CLAUDE.md +10 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -0
- package/dist/video/README.md +17 -3
- package/dist/video/advisors/README.md +20 -0
- package/dist/video/advisors/qr.d.ts +95 -0
- package/dist/video/advisors/qr.js +546 -0
- package/dist/video/advisors/quiet-listening.d.ts +73 -0
- package/dist/video/advisors/quiet-listening.js +292 -0
- package/dist/video/advisors/resume-point.d.ts +71 -0
- package/dist/video/advisors/resume-point.js +302 -0
- package/dist/video/advisors/smart-speed.d.ts +82 -0
- package/dist/video/advisors/smart-speed.js +314 -0
- package/dist/video/advisors/speaker-rate.d.ts +74 -0
- package/dist/video/advisors/speaker-rate.js +322 -0
- package/package.json +3 -2
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR Code (ISO/IEC 18004) encoder: versions 1–10, byte mode, no deps. Ported
|
|
3
|
+
* verbatim from extension-library `lib/qr.js` (mos-5lzns.27).
|
|
4
|
+
*
|
|
5
|
+
* Why hand-rolled. The extension ran under a strict CSP where every CDN-hosted
|
|
6
|
+
* QR library was unreachable by construction, and there was no build step to
|
|
7
|
+
* vendor one through. The alternative — pasting a minified blob into the repo —
|
|
8
|
+
* ships a black box into a codebase whose whole premise is that you can read
|
|
9
|
+
* it. This is the spec, written out, and PROVEN by the vitest suite that
|
|
10
|
+
* replaced the `--self-test` block (a reader that decodes our own output, plus
|
|
11
|
+
* structural assertions against published tables) rather than trusted.
|
|
12
|
+
*
|
|
13
|
+
* What it is for: a QR of the current deep link — the player's `?t=` substrate
|
|
14
|
+
* (video/deepLink.ts) plus the host's own file/URL part — so the video you are
|
|
15
|
+
* watching moves from the desk to a phone mid-sentence. That URL is 60–120
|
|
16
|
+
* bytes over a tailnet hostname — a version 3–7 symbol at ECC M. Versions above
|
|
17
|
+
* 10 would buy capacity this will never use and cost another row in four tables
|
|
18
|
+
* each, so the ceiling is deliberate and qrMatrix throws at it instead of
|
|
19
|
+
* quietly growing.
|
|
20
|
+
*
|
|
21
|
+
* Guard philosophy, since it cuts both ways here: garbage OPTIONS degrade to
|
|
22
|
+
* defaults (an unknown ecc string is just "M"), because a slightly
|
|
23
|
+
* wrong-looking QR still scans. Text that genuinely cannot be represented
|
|
24
|
+
* THROWS, because the failure mode of the alternative is a symbol that scans
|
|
25
|
+
* perfectly and sends the phone to a truncated URL — silent, plausible, and
|
|
26
|
+
* wrong.
|
|
27
|
+
*
|
|
28
|
+
* qrMatrix(text, { ecc = "M", minVersion = 1 }) -> { size, modules }
|
|
29
|
+
* qrCanvas(text, { scale = 4, margin = 4, dark, light }) -> HTMLCanvasElement
|
|
30
|
+
*
|
|
31
|
+
* The self-test's independent READER (mask/format re-derivation, de-interleave,
|
|
32
|
+
* round-trip decode) lives with the vitest suite in tests/unit/advisors/ —
|
|
33
|
+
* deliberately NOT built on the encoder's own reserved map or chosen mask, and
|
|
34
|
+
* deliberately not shipped in dist.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_VERSION = 10;
|
|
37
|
+
const MODE_BYTE = 0b0100;
|
|
38
|
+
const PAD_BYTES = [0xec, 0x11]; // spec's alternating filler, not zeros
|
|
39
|
+
const ECC_BITS = { L: 1, M: 0, Q: 3, H: 2 }; // NOT 0..3 in level order — spec's own numbering
|
|
40
|
+
// version → [ecCodewordsPerBlock, group1Blocks, group1DataCw, group2Blocks, group2DataCw]
|
|
41
|
+
// Straight from ISO/IEC 18004 table 13–16. The suite cross-checks every row
|
|
42
|
+
// against the independently-known total codeword count, which catches a typo in
|
|
43
|
+
// any of the five numbers.
|
|
44
|
+
const BLOCKS = {
|
|
45
|
+
L: [
|
|
46
|
+
[7, 1, 19, 0, 0],
|
|
47
|
+
[10, 1, 34, 0, 0],
|
|
48
|
+
[15, 1, 55, 0, 0],
|
|
49
|
+
[20, 1, 80, 0, 0],
|
|
50
|
+
[26, 1, 108, 0, 0],
|
|
51
|
+
[18, 2, 68, 0, 0],
|
|
52
|
+
[20, 2, 78, 0, 0],
|
|
53
|
+
[24, 2, 97, 0, 0],
|
|
54
|
+
[30, 2, 116, 0, 0],
|
|
55
|
+
[18, 2, 68, 2, 69],
|
|
56
|
+
],
|
|
57
|
+
M: [
|
|
58
|
+
[10, 1, 16, 0, 0],
|
|
59
|
+
[16, 1, 28, 0, 0],
|
|
60
|
+
[26, 1, 44, 0, 0],
|
|
61
|
+
[18, 2, 32, 0, 0],
|
|
62
|
+
[24, 2, 43, 0, 0],
|
|
63
|
+
[16, 4, 27, 0, 0],
|
|
64
|
+
[18, 4, 31, 0, 0],
|
|
65
|
+
[22, 2, 38, 2, 39],
|
|
66
|
+
[22, 3, 36, 2, 37],
|
|
67
|
+
[26, 4, 43, 1, 44],
|
|
68
|
+
],
|
|
69
|
+
Q: [
|
|
70
|
+
[13, 1, 13, 0, 0],
|
|
71
|
+
[22, 1, 22, 0, 0],
|
|
72
|
+
[18, 2, 17, 0, 0],
|
|
73
|
+
[26, 2, 24, 0, 0],
|
|
74
|
+
[18, 2, 15, 2, 16],
|
|
75
|
+
[24, 4, 19, 0, 0],
|
|
76
|
+
[18, 2, 14, 4, 15],
|
|
77
|
+
[22, 4, 18, 2, 19],
|
|
78
|
+
[20, 4, 16, 4, 17],
|
|
79
|
+
[24, 6, 19, 2, 20],
|
|
80
|
+
],
|
|
81
|
+
H: [
|
|
82
|
+
[17, 1, 9, 0, 0],
|
|
83
|
+
[28, 1, 16, 0, 0],
|
|
84
|
+
[22, 2, 13, 0, 0],
|
|
85
|
+
[16, 4, 9, 0, 0],
|
|
86
|
+
[22, 2, 11, 2, 12],
|
|
87
|
+
[28, 4, 15, 0, 0],
|
|
88
|
+
[26, 4, 13, 1, 14],
|
|
89
|
+
[26, 4, 14, 2, 15],
|
|
90
|
+
[24, 4, 12, 4, 13],
|
|
91
|
+
[28, 6, 15, 2, 16],
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
// total codewords (data + ec) per version — used only to validate BLOCKS
|
|
95
|
+
const TOTAL_CW = [26, 44, 70, 100, 134, 172, 196, 242, 292, 346];
|
|
96
|
+
// alignment-pattern centre coordinates per version (both axes)
|
|
97
|
+
const ALIGN_POS = [
|
|
98
|
+
[],
|
|
99
|
+
[6, 18],
|
|
100
|
+
[6, 22],
|
|
101
|
+
[6, 26],
|
|
102
|
+
[6, 30],
|
|
103
|
+
[6, 34],
|
|
104
|
+
[6, 22, 38],
|
|
105
|
+
[6, 24, 42],
|
|
106
|
+
[6, 26, 46],
|
|
107
|
+
[6, 28, 50],
|
|
108
|
+
];
|
|
109
|
+
// The eight data masks. Index IS the mask number written into the format info,
|
|
110
|
+
// so the order is load-bearing — do not sort or reorder.
|
|
111
|
+
const MASKS = [
|
|
112
|
+
(r, c) => (r + c) % 2 === 0,
|
|
113
|
+
(r) => r % 2 === 0,
|
|
114
|
+
(r, c) => c % 3 === 0,
|
|
115
|
+
(r, c) => (r + c) % 3 === 0,
|
|
116
|
+
(r, c) => (((r / 2) | 0) + ((c / 3) | 0)) % 2 === 0,
|
|
117
|
+
(r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
118
|
+
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
119
|
+
(r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
120
|
+
];
|
|
121
|
+
// ── GF(256) ─────────────────────────────────────────────────────────────────
|
|
122
|
+
// Primitive polynomial x^8+x^4+x^3+x^2+1 (0x11d) — the QR one, not the
|
|
123
|
+
// Data Matrix one (0x12d). Exp table is doubled so a+b needs no modulo.
|
|
124
|
+
const GF_EXP = new Uint8Array(512);
|
|
125
|
+
const GF_LOG = new Uint8Array(256);
|
|
126
|
+
{
|
|
127
|
+
let x = 1;
|
|
128
|
+
for (let i = 0; i < 255; i++) {
|
|
129
|
+
GF_EXP[i] = x;
|
|
130
|
+
GF_LOG[x] = i;
|
|
131
|
+
x = (x << 1) ^ (x & 0x80 ? 0x11d : 0);
|
|
132
|
+
}
|
|
133
|
+
for (let i = 255; i < 512; i++)
|
|
134
|
+
GF_EXP[i] = GF_EXP[i - 255];
|
|
135
|
+
}
|
|
136
|
+
const gfMul = (a, b) => a === 0 || b === 0 ? 0 : GF_EXP[GF_LOG[a] + GF_LOG[b]];
|
|
137
|
+
// generator = ∏ (x − α^i), coefficients high degree first
|
|
138
|
+
function rsGenerator(n) {
|
|
139
|
+
let g = [1];
|
|
140
|
+
for (let i = 0; i < n; i++) {
|
|
141
|
+
const next = new Array(g.length + 1).fill(0);
|
|
142
|
+
for (let j = 0; j < g.length; j++) {
|
|
143
|
+
next[j] ^= g[j];
|
|
144
|
+
next[j + 1] ^= gfMul(g[j], GF_EXP[i]);
|
|
145
|
+
}
|
|
146
|
+
g = next;
|
|
147
|
+
}
|
|
148
|
+
return g;
|
|
149
|
+
}
|
|
150
|
+
// polynomial long division, keeping only the remainder — the EC codewords
|
|
151
|
+
function rsEncode(data, ecLen) {
|
|
152
|
+
const gen = rsGenerator(ecLen);
|
|
153
|
+
const rem = new Uint8Array(ecLen);
|
|
154
|
+
for (const b of data) {
|
|
155
|
+
const factor = b ^ rem[0];
|
|
156
|
+
rem.copyWithin(0, 1);
|
|
157
|
+
rem[ecLen - 1] = 0;
|
|
158
|
+
for (let i = 0; i < ecLen; i++)
|
|
159
|
+
rem[i] ^= gfMul(gen[i + 1], factor);
|
|
160
|
+
}
|
|
161
|
+
return rem;
|
|
162
|
+
}
|
|
163
|
+
// S_i = block(α^i). All zero ⟺ the block is a valid RS codeword. This is the
|
|
164
|
+
// one check that does not depend on our own encoder being right in the same way
|
|
165
|
+
// twice: it evaluates the finished bytes against the field, full stop.
|
|
166
|
+
function rsSyndromes(block, ecLen) {
|
|
167
|
+
const out = new Uint8Array(ecLen);
|
|
168
|
+
for (let i = 0; i < ecLen; i++) {
|
|
169
|
+
let s = 0;
|
|
170
|
+
for (const b of block)
|
|
171
|
+
s = gfMul(s, GF_EXP[i]) ^ b;
|
|
172
|
+
out[i] = s;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
// ── bit-level spec pieces ───────────────────────────────────────────────────
|
|
177
|
+
// BCH(15,5) over the format's 5 bits, then XOR 0x5412 so an all-zero format
|
|
178
|
+
// (level M, mask 0) is not an all-light region a scanner could miss.
|
|
179
|
+
function formatBits(ecc, mask) {
|
|
180
|
+
const data = (ECC_BITS[ecc] << 3) | mask;
|
|
181
|
+
let rem = data << 10;
|
|
182
|
+
for (let i = 14; i >= 10; i--)
|
|
183
|
+
if ((rem >>> i) & 1)
|
|
184
|
+
rem ^= 0x537 << (i - 10);
|
|
185
|
+
return ((data << 10) | rem) ^ 0x5412;
|
|
186
|
+
}
|
|
187
|
+
// BCH(18,6). No XOR mask here — version info only exists for v≥7, where the
|
|
188
|
+
// symbol is big enough that the pattern is unambiguous on its own.
|
|
189
|
+
function versionBits(version) {
|
|
190
|
+
let rem = version << 12;
|
|
191
|
+
for (let i = 17; i >= 12; i--)
|
|
192
|
+
if ((rem >>> i) & 1)
|
|
193
|
+
rem ^= 0x1f25 << (i - 12);
|
|
194
|
+
return (version << 12) | rem;
|
|
195
|
+
}
|
|
196
|
+
const charCountBits = (version) => (version < 10 ? 8 : 16);
|
|
197
|
+
const dataCodewords = (version, ecc) => {
|
|
198
|
+
const [, b1, d1, b2, d2] = BLOCKS[ecc][version - 1];
|
|
199
|
+
return b1 * d1 + b2 * d2;
|
|
200
|
+
};
|
|
201
|
+
const maxBytes = (version, ecc) => Math.max(0, ((dataCodewords(version, ecc) * 8 - 4 - charCountBits(version)) / 8) | 0);
|
|
202
|
+
// ── data encoding ───────────────────────────────────────────────────────────
|
|
203
|
+
function toBytes(text) {
|
|
204
|
+
// null/undefined encode as the empty symbol rather than "null" — a QR of the
|
|
205
|
+
// literal word "undefined" is worse than a QR of nothing.
|
|
206
|
+
const s = text == null ? "" : typeof text === "string" ? text : String(text);
|
|
207
|
+
return new TextEncoder().encode(s);
|
|
208
|
+
}
|
|
209
|
+
function encodeData(bytes, version, ecc) {
|
|
210
|
+
const capacityBits = dataCodewords(version, ecc) * 8;
|
|
211
|
+
const bits = [];
|
|
212
|
+
const push = (val, len) => {
|
|
213
|
+
for (let i = len - 1; i >= 0; i--)
|
|
214
|
+
bits.push((val >>> i) & 1);
|
|
215
|
+
};
|
|
216
|
+
push(MODE_BYTE, 4);
|
|
217
|
+
push(bytes.length, charCountBits(version));
|
|
218
|
+
for (const b of bytes)
|
|
219
|
+
push(b, 8);
|
|
220
|
+
// Terminator, then pad to a byte boundary. In byte mode these are the same
|
|
221
|
+
// four zeros: the header is 12 or 20 bits, so the stream always sits at 4 mod
|
|
222
|
+
// 8 here and the terminator never truncates (proven over all 3572
|
|
223
|
+
// version/level/length combinations — deleting this line changes no output
|
|
224
|
+
// byte). It stays because the invariant is an accident of byte mode, and the
|
|
225
|
+
// suite asserts it rather than trusting it.
|
|
226
|
+
for (let i = 0; i < 4 && bits.length < capacityBits; i++)
|
|
227
|
+
bits.push(0);
|
|
228
|
+
while (bits.length % 8 !== 0)
|
|
229
|
+
bits.push(0);
|
|
230
|
+
const out = new Uint8Array(capacityBits / 8);
|
|
231
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
232
|
+
let b = 0;
|
|
233
|
+
for (let j = 0; j < 8; j++)
|
|
234
|
+
b = (b << 1) | bits[i + j];
|
|
235
|
+
out[i / 8] = b;
|
|
236
|
+
}
|
|
237
|
+
for (let i = bits.length / 8, k = 0; i < out.length; i++, k++)
|
|
238
|
+
out[i] = PAD_BYTES[k % 2];
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
// Split into blocks, EC each, then interleave — a burst of damage that wipes a
|
|
242
|
+
// contiguous strip of the symbol is thereby spread across every block instead
|
|
243
|
+
// of destroying one block outright.
|
|
244
|
+
function interleave(data, version, ecc) {
|
|
245
|
+
const [ecLen, b1, d1, b2, d2] = BLOCKS[ecc][version - 1];
|
|
246
|
+
const blocks = [];
|
|
247
|
+
let off = 0;
|
|
248
|
+
for (let i = 0; i < b1 + b2; i++) {
|
|
249
|
+
const n = i < b1 ? d1 : d2;
|
|
250
|
+
const chunk = data.subarray(off, off + n);
|
|
251
|
+
off += n;
|
|
252
|
+
blocks.push({ data: chunk, ec: rsEncode(chunk, ecLen) });
|
|
253
|
+
}
|
|
254
|
+
const out = [];
|
|
255
|
+
for (let i = 0; i < Math.max(d1, d2); i++)
|
|
256
|
+
for (const b of blocks)
|
|
257
|
+
if (i < b.data.length)
|
|
258
|
+
out.push(b.data[i]);
|
|
259
|
+
for (let i = 0; i < ecLen; i++)
|
|
260
|
+
for (const b of blocks)
|
|
261
|
+
out.push(b.ec[i]);
|
|
262
|
+
return Uint8Array.from(out);
|
|
263
|
+
}
|
|
264
|
+
function drawFunctionPatterns(version) {
|
|
265
|
+
const size = 4 * version + 17;
|
|
266
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
267
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
268
|
+
const set = (r, c, v) => {
|
|
269
|
+
if (r < 0 || r >= size || c < 0 || c >= size)
|
|
270
|
+
return;
|
|
271
|
+
modules[r][c] = v;
|
|
272
|
+
fn[r][c] = true;
|
|
273
|
+
};
|
|
274
|
+
// finder + its separator ring as one 9×9 stamp per corner. Chebyshev distance
|
|
275
|
+
// from the centre gives the concentric rings for free: dark at 0,1,3 — light
|
|
276
|
+
// at 2 (the gap) and 4+ (the separator).
|
|
277
|
+
for (const [fr, fc] of [
|
|
278
|
+
[0, 0],
|
|
279
|
+
[0, size - 7],
|
|
280
|
+
[size - 7, 0],
|
|
281
|
+
])
|
|
282
|
+
for (let dr = -1; dr <= 7; dr++)
|
|
283
|
+
for (let dc = -1; dc <= 7; dc++) {
|
|
284
|
+
const d = Math.max(Math.abs(dr - 3), Math.abs(dc - 3));
|
|
285
|
+
set(fr + dr, fc + dc, d !== 2 && d <= 3);
|
|
286
|
+
}
|
|
287
|
+
// timing: only the span between the finders, which already own both ends
|
|
288
|
+
for (let i = 8; i < size - 8; i++) {
|
|
289
|
+
set(6, i, i % 2 === 0);
|
|
290
|
+
set(i, 6, i % 2 === 0);
|
|
291
|
+
}
|
|
292
|
+
// alignment: every centre pair except the three that would sit on a finder
|
|
293
|
+
const pos = ALIGN_POS[version - 1];
|
|
294
|
+
const last = size - 7;
|
|
295
|
+
for (const r of pos)
|
|
296
|
+
for (const c of pos) {
|
|
297
|
+
if ((r === 6 && c === 6) ||
|
|
298
|
+
(r === 6 && c === last) ||
|
|
299
|
+
(r === last && c === 6))
|
|
300
|
+
continue;
|
|
301
|
+
for (let dr = -2; dr <= 2; dr++)
|
|
302
|
+
for (let dc = -2; dc <= 2; dc++)
|
|
303
|
+
set(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
|
|
304
|
+
}
|
|
305
|
+
// format-info strips: RESERVED now, valued per-mask later. The two timing
|
|
306
|
+
// modules that fall inside these strips keep the value set above — the format
|
|
307
|
+
// placement below jogs around them, which is what the 14−i index is for.
|
|
308
|
+
for (let i = 0; i <= 8; i++) {
|
|
309
|
+
fn[8][i] = true;
|
|
310
|
+
fn[i][8] = true;
|
|
311
|
+
}
|
|
312
|
+
for (let i = 0; i < 8; i++) {
|
|
313
|
+
fn[8][size - 1 - i] = true;
|
|
314
|
+
fn[size - 1 - i][8] = true;
|
|
315
|
+
}
|
|
316
|
+
// NB the dark module at (4·version+9, 8) is deliberately NOT written here —
|
|
317
|
+
// it sits inside the format region reserved just above, and drawFormatBits
|
|
318
|
+
// owns its value. Writing it in both places made it un-mutatable: breaking
|
|
319
|
+
// one copy left the other silently correcting it.
|
|
320
|
+
if (version >= 7) {
|
|
321
|
+
// the two copies are transposes of each other, so one loop writes both
|
|
322
|
+
const vb = versionBits(version);
|
|
323
|
+
for (let i = 0; i < 18; i++) {
|
|
324
|
+
const on = ((vb >>> i) & 1) === 1;
|
|
325
|
+
const r = (i / 3) | 0;
|
|
326
|
+
const c = size - 11 + (i % 3);
|
|
327
|
+
set(r, c, on);
|
|
328
|
+
set(c, r, on);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { size, modules, fn };
|
|
332
|
+
}
|
|
333
|
+
// Zigzag up-and-down through two-module-wide columns from the bottom right,
|
|
334
|
+
// skipping the vertical timing column entirely (right===6 → 5).
|
|
335
|
+
function placeData(modules, fn, size, cw) {
|
|
336
|
+
let i = 0;
|
|
337
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
338
|
+
if (right === 6)
|
|
339
|
+
right = 5;
|
|
340
|
+
for (let vert = 0; vert < size; vert++)
|
|
341
|
+
for (let j = 0; j < 2; j++) {
|
|
342
|
+
const c = right - j;
|
|
343
|
+
const upward = ((right + 1) & 2) === 0;
|
|
344
|
+
const r = upward ? size - 1 - vert : vert;
|
|
345
|
+
if (!fn[r][c] && i < cw.length * 8) {
|
|
346
|
+
modules[r][c] = ((cw[i >>> 3] >>> (7 - (i & 7))) & 1) === 1;
|
|
347
|
+
i++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return i;
|
|
352
|
+
}
|
|
353
|
+
function drawFormatBits(m, size, ecc, mask) {
|
|
354
|
+
const bits = formatBits(ecc, mask);
|
|
355
|
+
const bit = (i) => ((bits >>> i) & 1) === 1;
|
|
356
|
+
for (let i = 0; i <= 5; i++)
|
|
357
|
+
m[i][8] = bit(i);
|
|
358
|
+
m[7][8] = bit(6);
|
|
359
|
+
m[8][8] = bit(7);
|
|
360
|
+
m[8][7] = bit(8);
|
|
361
|
+
for (let i = 9; i < 15; i++)
|
|
362
|
+
m[8][14 - i] = bit(i);
|
|
363
|
+
for (let i = 0; i < 8; i++)
|
|
364
|
+
m[8][size - 1 - i] = bit(i);
|
|
365
|
+
for (let i = 8; i < 15; i++)
|
|
366
|
+
m[size - 15 + i][8] = bit(i);
|
|
367
|
+
m[size - 8][8] = true;
|
|
368
|
+
}
|
|
369
|
+
// The four penalty rules. Lower is better; this is the whole basis for choosing
|
|
370
|
+
// a mask, and the numbers (3/1, 3, 40, 10) are the spec's, not tuned.
|
|
371
|
+
const P3A = [1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]; // 1:1:3:1:1 finder-alike + light run
|
|
372
|
+
const P3B = [0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1]; // …and its mirror
|
|
373
|
+
function penalty(m, size) {
|
|
374
|
+
let score = 0;
|
|
375
|
+
const line = new Array(size);
|
|
376
|
+
const scan = () => {
|
|
377
|
+
let run = 1;
|
|
378
|
+
for (let i = 1; i < size; i++) {
|
|
379
|
+
if (line[i] === line[i - 1]) {
|
|
380
|
+
run++;
|
|
381
|
+
if (run === 5)
|
|
382
|
+
score += 3;
|
|
383
|
+
else if (run > 5)
|
|
384
|
+
score += 1;
|
|
385
|
+
}
|
|
386
|
+
else
|
|
387
|
+
run = 1;
|
|
388
|
+
}
|
|
389
|
+
for (let i = 0; i + 11 <= size; i++) {
|
|
390
|
+
let a = true;
|
|
391
|
+
let b = true;
|
|
392
|
+
for (let k = 0; k < 11 && (a || b); k++) {
|
|
393
|
+
if (line[i + k] !== P3A[k])
|
|
394
|
+
a = false;
|
|
395
|
+
if (line[i + k] !== P3B[k])
|
|
396
|
+
b = false;
|
|
397
|
+
}
|
|
398
|
+
if (a)
|
|
399
|
+
score += 40;
|
|
400
|
+
if (b)
|
|
401
|
+
score += 40;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
for (let r = 0; r < size; r++) {
|
|
405
|
+
for (let i = 0; i < size; i++)
|
|
406
|
+
line[i] = m[r][i] ? 1 : 0;
|
|
407
|
+
scan();
|
|
408
|
+
}
|
|
409
|
+
for (let c = 0; c < size; c++) {
|
|
410
|
+
for (let i = 0; i < size; i++)
|
|
411
|
+
line[i] = m[i][c] ? 1 : 0;
|
|
412
|
+
scan();
|
|
413
|
+
}
|
|
414
|
+
let dark = 0;
|
|
415
|
+
for (let r = 0; r + 1 < size; r++)
|
|
416
|
+
for (let c = 0; c + 1 < size; c++) {
|
|
417
|
+
const v = m[r][c];
|
|
418
|
+
if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1])
|
|
419
|
+
score += 3;
|
|
420
|
+
}
|
|
421
|
+
for (let r = 0; r < size; r++)
|
|
422
|
+
for (let c = 0; c < size; c++)
|
|
423
|
+
if (m[r][c])
|
|
424
|
+
dark++;
|
|
425
|
+
score += 10 * Math.floor(Math.abs((dark * 100) / (size * size) - 50) / 5);
|
|
426
|
+
return score;
|
|
427
|
+
}
|
|
428
|
+
// forcedMask exists for the test suite (exercise all eight placements); the
|
|
429
|
+
// public API never takes one, because "which mask" is not a user-meaningful
|
|
430
|
+
// choice — it is whichever one scans best.
|
|
431
|
+
function buildMatrix(bytes, version, ecc, forcedMask) {
|
|
432
|
+
const { size, modules, fn } = drawFunctionPatterns(version);
|
|
433
|
+
const cw = interleave(encodeData(bytes, version, ecc), version, ecc);
|
|
434
|
+
placeData(modules, fn, size, cw);
|
|
435
|
+
let best = null;
|
|
436
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
437
|
+
if (forcedMask != null && mask !== forcedMask)
|
|
438
|
+
continue;
|
|
439
|
+
const m = modules.map((row) => row.slice());
|
|
440
|
+
const fmask = MASKS[mask];
|
|
441
|
+
for (let r = 0; r < size; r++)
|
|
442
|
+
for (let c = 0; c < size; c++)
|
|
443
|
+
if (!fn[r][c] && fmask(r, c))
|
|
444
|
+
m[r][c] = !m[r][c];
|
|
445
|
+
drawFormatBits(m, size, ecc, mask);
|
|
446
|
+
const score = penalty(m, size);
|
|
447
|
+
if (!best || score < best.score)
|
|
448
|
+
best = { score, modules: m, mask };
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
size,
|
|
452
|
+
modules: best.modules,
|
|
453
|
+
mask: best.mask,
|
|
454
|
+
version,
|
|
455
|
+
ecc,
|
|
456
|
+
codewords: cw,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function normalizeOpts(opts) {
|
|
460
|
+
const o = opts && typeof opts === "object" ? opts : {};
|
|
461
|
+
const raw = typeof o.ecc === "string" ? o.ecc.toUpperCase() : "";
|
|
462
|
+
const ecc = BLOCKS[raw]
|
|
463
|
+
? raw
|
|
464
|
+
: "M";
|
|
465
|
+
let minVersion = Math.trunc(Number(o.minVersion));
|
|
466
|
+
if (!Number.isFinite(minVersion) || minVersion < 1)
|
|
467
|
+
minVersion = 1;
|
|
468
|
+
if (minVersion > MAX_VERSION)
|
|
469
|
+
minVersion = MAX_VERSION;
|
|
470
|
+
return { ecc, minVersion };
|
|
471
|
+
}
|
|
472
|
+
export function qrMatrix(text, opts) {
|
|
473
|
+
const { ecc, minVersion } = normalizeOpts(opts);
|
|
474
|
+
const bytes = toBytes(text);
|
|
475
|
+
let version = 0;
|
|
476
|
+
for (let v = minVersion; v <= MAX_VERSION; v++)
|
|
477
|
+
if (bytes.length <= maxBytes(v, ecc)) {
|
|
478
|
+
version = v;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (!version)
|
|
482
|
+
throw new Error(`qr: ${bytes.length} bytes will not fit — version ${MAX_VERSION} at ECC ${ecc} holds ` +
|
|
483
|
+
`${maxBytes(MAX_VERSION, ecc)}. Shorten the URL or drop to ECC "L".`);
|
|
484
|
+
const { size, modules } = buildMatrix(bytes, version, ecc, null);
|
|
485
|
+
return { size, modules };
|
|
486
|
+
}
|
|
487
|
+
export function qrCanvas(text, opts) {
|
|
488
|
+
if (typeof document === "undefined")
|
|
489
|
+
throw new Error("qr: qrCanvas needs a document — under node, call qrMatrix and render it yourself.");
|
|
490
|
+
const o = opts && typeof opts === "object" ? opts : {};
|
|
491
|
+
// null/undefined mean "unspecified", NOT zero — Number(null) is 0, and a
|
|
492
|
+
// margin of 0 is a symbol with no quiet zone, which scans far worse than one
|
|
493
|
+
// with the default. An explicit 0 still gets through.
|
|
494
|
+
const num = (v, def, lo, hi) => {
|
|
495
|
+
if (v == null)
|
|
496
|
+
return def;
|
|
497
|
+
const n = Math.round(Number(v));
|
|
498
|
+
return Number.isFinite(n) && n >= lo && n <= hi ? n : def;
|
|
499
|
+
};
|
|
500
|
+
const scale = num(o.scale, 4, 1, 64);
|
|
501
|
+
const margin = num(o.margin, 4, 0, 32); // 4 modules is the spec's minimum quiet zone
|
|
502
|
+
const dark = typeof o.dark === "string" && o.dark ? o.dark : "#0c0e12";
|
|
503
|
+
const light = typeof o.light === "string" && o.light ? o.light : "#e2e5ea";
|
|
504
|
+
const { size, modules } = qrMatrix(text, o);
|
|
505
|
+
const px = (size + margin * 2) * scale;
|
|
506
|
+
const canvas = document.createElement("canvas");
|
|
507
|
+
canvas.width = px;
|
|
508
|
+
canvas.height = px;
|
|
509
|
+
// A QR is a hard-edged bitmap: any smoothing on upscale eats the 1-module
|
|
510
|
+
// gaps a decoder measures. This is the one property that matters if CSS ever
|
|
511
|
+
// sizes the element away from its intrinsic pixels.
|
|
512
|
+
canvas.style.imageRendering = "pixelated";
|
|
513
|
+
canvas.setAttribute("role", "img");
|
|
514
|
+
canvas.setAttribute("aria-label", `QR code for ${text == null ? "" : String(text)}`);
|
|
515
|
+
const ctx = canvas.getContext("2d");
|
|
516
|
+
if (!ctx)
|
|
517
|
+
return canvas; // context can be refused under memory pressure; a blank canvas beats a throw
|
|
518
|
+
ctx.fillStyle = light;
|
|
519
|
+
ctx.fillRect(0, 0, px, px);
|
|
520
|
+
ctx.fillStyle = dark;
|
|
521
|
+
// merge horizontal runs into one rect each: same picture, ~5× fewer draw calls,
|
|
522
|
+
// and no hairline seams between adjacent modules on fractional device pixels
|
|
523
|
+
for (let r = 0; r < size; r++) {
|
|
524
|
+
let c = 0;
|
|
525
|
+
while (c < size) {
|
|
526
|
+
if (!modules[r][c]) {
|
|
527
|
+
c++;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
let end = c;
|
|
531
|
+
while (end < size && modules[r][end])
|
|
532
|
+
end++;
|
|
533
|
+
ctx.fillRect((margin + c) * scale, (margin + r) * scale, (end - c) * scale, scale);
|
|
534
|
+
c = end;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Nothing here animates — a QR that moves is a QR that does not scan — so
|
|
538
|
+
// prefers-reduced-motion has nothing to collapse. Callers that fade this
|
|
539
|
+
// element in should honour it at their own call site.
|
|
540
|
+
return canvas;
|
|
541
|
+
}
|
|
542
|
+
// The remaining internals are exported for the vitest suite only (the
|
|
543
|
+
// round-trip reader, the published-table cross-checks and the mask/penalty
|
|
544
|
+
// decomposition live in tests/unit/advisors/qr.test.ts). They are internal
|
|
545
|
+
// surface: not re-exported from the package barrel, and not a stability promise.
|
|
546
|
+
export { MAX_VERSION, MODE_BYTE, PAD_BYTES, ECC_BITS, BLOCKS, TOTAL_CW, ALIGN_POS, MASKS, P3A, P3B, GF_EXP, GF_LOG, gfMul, rsGenerator, rsEncode, rsSyndromes, formatBits, versionBits, charCountBits, dataCodewords, maxBytes, toBytes, encodeData, interleave, drawFunctionPatterns, placeData, drawFormatBits, penalty, buildMatrix, };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Night ears": loudness-compensated quiet listening. Ported verbatim from
|
|
3
|
+
* extension-library `lib/quiet-listening.js` (mos-5lzns.27). The graph builder
|
|
4
|
+
* lands here as mos-5lzns.31's contract promised (audio.ts is that task's
|
|
5
|
+
* wiring target); `amountFor` is the piece every caller can already use.
|
|
6
|
+
*
|
|
7
|
+
* This module owns TONE, never LEVEL. The host's loudness matching already
|
|
8
|
+
* rides the gain toward a target and a limiter catches the peaks; nothing in
|
|
9
|
+
* here adds makeup gain, and the fact that it doesn't is the whole reason the
|
|
10
|
+
* two can run at once without fighting. If this file ever grows a gain node
|
|
11
|
+
* that isn't pinned at unity, the rider downstream will chase it and you'll get
|
|
12
|
+
* the pumping that loudness matching was written to stop.
|
|
13
|
+
*
|
|
14
|
+
* The problem it does solve: your ears are not flat, and they get less flat the
|
|
15
|
+
* quieter things are. Equal-loudness contours (Fletcher–Munson, and the ISO 226
|
|
16
|
+
* revision that corrected them) fall off at both ends as level drops — at 2am
|
|
17
|
+
* at a volume that won't wake anyone, the bass has gone and the air on top has
|
|
18
|
+
* gone with it, and what's left is a midrange smear where dialogue used to be.
|
|
19
|
+
* The usual response is to keep nudging the volume up for the quiet line and
|
|
20
|
+
* back down for the next loud one. This tilts the spectrum back instead, and
|
|
21
|
+
* tightens the dynamics just enough that a whispered line rides above the room
|
|
22
|
+
* noise of a fridge and a fan.
|
|
23
|
+
*
|
|
24
|
+
* Rejected: a real ISO 226 inverse curve per SPL. It needs the absolute
|
|
25
|
+
* playback SPL to mean anything, and a browser cannot know the amp, the
|
|
26
|
+
* speakers, or the room — a curve derived from a number we can't measure is
|
|
27
|
+
* theatre. Two shelves and a presence bump are the honest version of the same
|
|
28
|
+
* idea: right shape, calibration left as one dial the listener can hear.
|
|
29
|
+
*
|
|
30
|
+
* createQuietChain(ctx) -> { input, output, setAmount, dispose }
|
|
31
|
+
* amountFor(volume, nightOn, opts) -> 0..1
|
|
32
|
+
*
|
|
33
|
+
* Module rules (src/lib/video/README.md): reads no settings, touches no
|
|
34
|
+
* <video> — the graph builder is the audio graph's one sanctioned exception
|
|
35
|
+
* and it takes an AudioContext(-shaped) object, never the element.
|
|
36
|
+
*/
|
|
37
|
+
/** Minimal duck-typed surface of an AudioContext this module touches. */
|
|
38
|
+
export interface QuietAudioContext {
|
|
39
|
+
createGain?: unknown;
|
|
40
|
+
createBiquadFilter?: unknown;
|
|
41
|
+
createDynamicsCompressor?: unknown;
|
|
42
|
+
currentTime?: unknown;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
interface AudioParamLike {
|
|
46
|
+
value: number;
|
|
47
|
+
setTargetAtTime?(value: number, startTime: number, timeConstant: number): void;
|
|
48
|
+
}
|
|
49
|
+
interface AudioNodeLike {
|
|
50
|
+
gain?: AudioParamLike;
|
|
51
|
+
frequency?: AudioParamLike;
|
|
52
|
+
Q?: AudioParamLike;
|
|
53
|
+
threshold?: AudioParamLike;
|
|
54
|
+
ratio?: AudioParamLike;
|
|
55
|
+
knee?: AudioParamLike;
|
|
56
|
+
attack?: AudioParamLike;
|
|
57
|
+
release?: AudioParamLike;
|
|
58
|
+
type?: string;
|
|
59
|
+
connect?(node: unknown): void;
|
|
60
|
+
disconnect?(): void;
|
|
61
|
+
}
|
|
62
|
+
export interface QuietChain {
|
|
63
|
+
input: AudioNodeLike | null;
|
|
64
|
+
output: AudioNodeLike | null;
|
|
65
|
+
setAmount(a: unknown): void;
|
|
66
|
+
dispose(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createQuietChain(ctx: QuietAudioContext | null | undefined): QuietChain;
|
|
69
|
+
export interface AmountOpts {
|
|
70
|
+
knee?: number;
|
|
71
|
+
}
|
|
72
|
+
export declare function amountFor(volume: unknown, nightOn: unknown, opts?: AmountOpts | null): number;
|
|
73
|
+
export {};
|