@joewinke/jatui 0.9.1 → 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.
Files changed (35) hide show
  1. package/CLAUDE.md +10 -0
  2. package/dist/components/VideoPlayer.svelte +371 -1
  3. package/dist/components/VideoPlayer.svelte.d.ts +44 -1
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.js +10 -0
  6. package/dist/video/README.md +18 -3
  7. package/dist/video/advisors/README.md +20 -0
  8. package/dist/video/advisors/qr.d.ts +95 -0
  9. package/dist/video/advisors/qr.js +546 -0
  10. package/dist/video/advisors/quiet-listening.d.ts +73 -0
  11. package/dist/video/advisors/quiet-listening.js +292 -0
  12. package/dist/video/advisors/resume-point.d.ts +71 -0
  13. package/dist/video/advisors/resume-point.js +302 -0
  14. package/dist/video/advisors/smart-speed.d.ts +82 -0
  15. package/dist/video/advisors/smart-speed.js +314 -0
  16. package/dist/video/advisors/speaker-rate.d.ts +74 -0
  17. package/dist/video/advisors/speaker-rate.js +322 -0
  18. package/dist/video/band/axis.d.ts +81 -0
  19. package/dist/video/band/axis.js +106 -0
  20. package/dist/video/band/drag.d.ts +38 -0
  21. package/dist/video/band/drag.js +45 -0
  22. package/dist/video/band/luma.d.ts +62 -0
  23. package/dist/video/band/luma.js +97 -0
  24. package/dist/video/band/markers.d.ts +73 -0
  25. package/dist/video/band/markers.js +146 -0
  26. package/dist/video/band/memo.d.ts +21 -0
  27. package/dist/video/band/memo.js +15 -0
  28. package/dist/video/band/window.d.ts +35 -0
  29. package/dist/video/band/window.js +61 -0
  30. package/dist/video/band.d.ts +35 -29
  31. package/dist/video/band.js +77 -56
  32. package/dist/video/keys.d.ts +1 -1
  33. package/dist/video/keys.js +3 -0
  34. package/dist/video/settings.js +6 -5
  35. 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 {};