@hyperframes/engine 0.6.118 → 0.6.120

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 (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,738 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- sampleRgb48le,
4
- mix16,
5
- clamp16,
6
- smoothstep,
7
- hash,
8
- vnoise,
9
- fbm,
10
- crossfade,
11
- flashThroughWhite,
12
- hdrToLinear,
13
- linearToHdr,
14
- convertTransfer,
15
- TRANSITIONS,
16
- type TransitionFn,
17
- } from "./shaderTransitions.js";
18
-
19
- // ── sampleRgb48le ─────────────────────────────────────────────────────────────
20
-
21
- describe("sampleRgb48le", () => {
22
- it("samples center pixel of a uniform 1x1 buffer", () => {
23
- const buf = Buffer.alloc(6);
24
- buf.writeUInt16LE(10000, 0); // R
25
- buf.writeUInt16LE(20000, 2); // G
26
- buf.writeUInt16LE(30000, 4); // B
27
- const [r, g, b] = sampleRgb48le(buf, 0.5, 0.5, 1, 1);
28
- expect(r).toBe(10000);
29
- expect(g).toBe(20000);
30
- expect(b).toBe(30000);
31
- });
32
-
33
- it("clamps out-of-bounds UV below 0 to first pixel", () => {
34
- const buf = Buffer.alloc(6);
35
- buf.writeUInt16LE(5000, 0);
36
- buf.writeUInt16LE(6000, 2);
37
- buf.writeUInt16LE(7000, 4);
38
- const [r, g, b] = sampleRgb48le(buf, -0.5, -0.5, 1, 1);
39
- expect(r).toBe(5000);
40
- expect(g).toBe(6000);
41
- expect(b).toBe(7000);
42
- });
43
-
44
- it("clamps out-of-bounds UV above 1 to last pixel", () => {
45
- const buf = Buffer.alloc(6);
46
- buf.writeUInt16LE(5000, 0);
47
- buf.writeUInt16LE(6000, 2);
48
- buf.writeUInt16LE(7000, 4);
49
- const [r, g, b] = sampleRgb48le(buf, 1.5, 1.5, 1, 1);
50
- expect(r).toBe(5000);
51
- expect(g).toBe(6000);
52
- expect(b).toBe(7000);
53
- });
54
-
55
- it("bilinearly interpolates between two horizontally adjacent pixels", () => {
56
- // 2x1 buffer: pixel 0 = (0,0,0), pixel 1 = (65534,65534,65534)
57
- const buf = Buffer.alloc(12);
58
- buf.writeUInt16LE(0, 0);
59
- buf.writeUInt16LE(0, 2);
60
- buf.writeUInt16LE(0, 4);
61
- buf.writeUInt16LE(65534, 6);
62
- buf.writeUInt16LE(65534, 8);
63
- buf.writeUInt16LE(65534, 10);
64
- // u=0.5, w=2 → x = 0.5*(2-1) = 0.5 → equal blend of pixels 0 and 1
65
- const [r, g, b] = sampleRgb48le(buf, 0.5, 0, 2, 1);
66
- expect(r).toBe(32767);
67
- expect(g).toBe(32767);
68
- expect(b).toBe(32767);
69
- });
70
-
71
- it("samples from exact pixel 0 at u=0", () => {
72
- const buf = Buffer.alloc(12);
73
- buf.writeUInt16LE(1000, 0);
74
- buf.writeUInt16LE(2000, 2);
75
- buf.writeUInt16LE(3000, 4);
76
- buf.writeUInt16LE(60000, 6);
77
- buf.writeUInt16LE(60000, 8);
78
- buf.writeUInt16LE(60000, 10);
79
- const [r, g, b] = sampleRgb48le(buf, 0, 0, 2, 1);
80
- expect(r).toBe(1000);
81
- expect(g).toBe(2000);
82
- expect(b).toBe(3000);
83
- });
84
-
85
- // ── Wider coverage for the perf-migration follow-up ────────────────────────
86
- // These pin down sub-pixel sampling semantics so a future Uint16Array
87
- // implementation can swap in and verify byte-equivalent output.
88
-
89
- it("bilinearly interpolates between two vertically adjacent pixels", () => {
90
- // 1x2 buffer: row 0 = (0,0,0), row 1 = (40000,40000,40000)
91
- const buf = Buffer.alloc(12);
92
- buf.writeUInt16LE(0, 0);
93
- buf.writeUInt16LE(0, 2);
94
- buf.writeUInt16LE(0, 4);
95
- buf.writeUInt16LE(40000, 6);
96
- buf.writeUInt16LE(40000, 8);
97
- buf.writeUInt16LE(40000, 10);
98
- const [r, g, b] = sampleRgb48le(buf, 0, 0.5, 1, 2);
99
- expect(r).toBe(20000);
100
- expect(g).toBe(20000);
101
- expect(b).toBe(20000);
102
- });
103
-
104
- it("bilinearly interpolates the centroid of a 2x2 block", () => {
105
- // Layout (R channel only, others mirror):
106
- // (1000) (5000)
107
- // (3000) (7000)
108
- // Centroid (u=v=0.5) → average of all four = 4000
109
- const buf = Buffer.alloc(24);
110
- const corners = [1000, 5000, 3000, 7000];
111
- for (let i = 0; i < 4; i++) {
112
- const off = i * 6;
113
- buf.writeUInt16LE(corners[i] ?? 0, off);
114
- buf.writeUInt16LE(corners[i] ?? 0, off + 2);
115
- buf.writeUInt16LE(corners[i] ?? 0, off + 4);
116
- }
117
- const [r, g, b] = sampleRgb48le(buf, 0.5, 0.5, 2, 2);
118
- expect(r).toBe(4000);
119
- expect(g).toBe(4000);
120
- expect(b).toBe(4000);
121
- });
122
-
123
- it("does not bleed channels — R, G, B sampled independently", () => {
124
- // 2x1 buffer with distinct per-channel gradients.
125
- // pixel 0: R=1000 G=20000 B=50000
126
- // pixel 1: R=9000 G=30000 B=60000
127
- const buf = Buffer.alloc(12);
128
- buf.writeUInt16LE(1000, 0);
129
- buf.writeUInt16LE(20000, 2);
130
- buf.writeUInt16LE(50000, 4);
131
- buf.writeUInt16LE(9000, 6);
132
- buf.writeUInt16LE(30000, 8);
133
- buf.writeUInt16LE(60000, 10);
134
- const [r, g, b] = sampleRgb48le(buf, 0.5, 0, 2, 1);
135
- expect(r).toBe(5000);
136
- expect(g).toBe(25000);
137
- expect(b).toBe(55000);
138
- });
139
-
140
- it("samples last pixel exactly when u=v=1 (no overflow past the edge)", () => {
141
- // 2x2 buffer where (1,1) corner has a unique value the first three pixels
142
- // do not. If sampleRgb48le tried to read off-edge, the result would mix in
143
- // out-of-bounds garbage.
144
- const buf = Buffer.alloc(24);
145
- const fill = [10, 20, 30, 65000];
146
- for (let i = 0; i < 4; i++) {
147
- const off = i * 6;
148
- buf.writeUInt16LE(fill[i] ?? 0, off);
149
- buf.writeUInt16LE(fill[i] ?? 0, off + 2);
150
- buf.writeUInt16LE(fill[i] ?? 0, off + 4);
151
- }
152
- const [r, g, b] = sampleRgb48le(buf, 1, 1, 2, 2);
153
- expect(r).toBe(65000);
154
- expect(g).toBe(65000);
155
- expect(b).toBe(65000);
156
- });
157
-
158
- it("respects asymmetric off-center UV weights", () => {
159
- // 2x1 buffer, R-only differentiation: pixel 0 = 0, pixel 1 = 10000
160
- // u=0.25 → x = 0.25 * (2 - 1) = 0.25
161
- // weight on pixel 0 = 0.75, weight on pixel 1 = 0.25
162
- // expected R = round(0 * 0.75 + 10000 * 0.25) = 2500
163
- const buf = Buffer.alloc(12);
164
- buf.writeUInt16LE(0, 0);
165
- buf.writeUInt16LE(0, 2);
166
- buf.writeUInt16LE(0, 4);
167
- buf.writeUInt16LE(10000, 6);
168
- buf.writeUInt16LE(10000, 8);
169
- buf.writeUInt16LE(10000, 10);
170
- const [r, g, b] = sampleRgb48le(buf, 0.25, 0, 2, 1);
171
- expect(r).toBe(2500);
172
- expect(g).toBe(2500);
173
- expect(b).toBe(2500);
174
- });
175
-
176
- it("preserves max 16-bit values without clipping or rollover", () => {
177
- // Verify the 65535 ceiling round-trips through bilinear weights without
178
- // losing precision. A naïve 32-bit accumulator would still be fine here,
179
- // but a future packed-Uint16 implementation must be checked for overflow
180
- // in intermediate sums.
181
- const buf = Buffer.alloc(24);
182
- for (let i = 0; i < 4; i++) {
183
- const off = i * 6;
184
- buf.writeUInt16LE(65535, off);
185
- buf.writeUInt16LE(65535, off + 2);
186
- buf.writeUInt16LE(65535, off + 4);
187
- }
188
- const [r, g, b] = sampleRgb48le(buf, 0.5, 0.5, 2, 2);
189
- expect(r).toBe(65535);
190
- expect(g).toBe(65535);
191
- expect(b).toBe(65535);
192
- });
193
-
194
- it("works on a large 256x256 canvas with sub-pixel UV", () => {
195
- // Sanity check that buffer offset math scales — exercises the (y * w + x) * 6
196
- // indexing on a non-trivial stride.
197
- const w = 256;
198
- const h = 256;
199
- const buf = Buffer.alloc(w * h * 6);
200
- // Diagonal gradient in R: pixel (x, y).R = x
201
- for (let y = 0; y < h; y++) {
202
- for (let x = 0; x < w; x++) {
203
- const off = (y * w + x) * 6;
204
- buf.writeUInt16LE(x, off);
205
- }
206
- }
207
- // u = 0.5 → sx = 0.5 * (w - 1) = 127.5 → R should be ≈ 127.5 → rounds to 128
208
- const [r] = sampleRgb48le(buf, 0.5, 0.5, w, h);
209
- expect(r).toBe(128);
210
- });
211
-
212
- it("handles 1x1 source with arbitrary UV (no x1=x0 division by zero)", () => {
213
- // x0+1 gets clamped back to w-1=0, so x0 == x1. The weights still sum to 1
214
- // and the result must equal the single pixel value.
215
- const buf = Buffer.alloc(6);
216
- buf.writeUInt16LE(12345, 0);
217
- buf.writeUInt16LE(23456, 2);
218
- buf.writeUInt16LE(34567, 4);
219
- for (const [u, v] of [
220
- [0, 0],
221
- [0.5, 0.5],
222
- [1, 1],
223
- [0.7, 0.3],
224
- ] as const) {
225
- const [r, g, b] = sampleRgb48le(buf, u, v, 1, 1);
226
- expect(r).toBe(12345);
227
- expect(g).toBe(23456);
228
- expect(b).toBe(34567);
229
- }
230
- });
231
- });
232
-
233
- // ── mix16 ─────────────────────────────────────────────────────────────────────
234
-
235
- describe("mix16", () => {
236
- it("returns a at t=0", () => {
237
- expect(mix16(1000, 60000, 0)).toBe(1000);
238
- });
239
-
240
- it("returns b at t=1", () => {
241
- expect(mix16(1000, 60000, 1)).toBe(60000);
242
- });
243
-
244
- it("returns midpoint at t=0.5", () => {
245
- expect(mix16(0, 60000, 0.5)).toBe(30000);
246
- });
247
-
248
- it("returns rounded result for non-integer midpoints", () => {
249
- // 0 * 0.5 + 1 * 0.5 = 0.5 → rounds to 1
250
- expect(mix16(0, 1, 0.5)).toBe(1);
251
- });
252
- });
253
-
254
- // ── clamp16 ───────────────────────────────────────────────────────────────────
255
-
256
- describe("clamp16", () => {
257
- it("clamps negative to 0", () => {
258
- expect(clamp16(-100)).toBe(0);
259
- });
260
-
261
- it("clamps overflow to 65535", () => {
262
- expect(clamp16(70000)).toBe(65535);
263
- });
264
-
265
- it("passes normal values through", () => {
266
- expect(clamp16(32768)).toBe(32768);
267
- });
268
-
269
- it("passes boundary values through", () => {
270
- expect(clamp16(0)).toBe(0);
271
- expect(clamp16(65535)).toBe(65535);
272
- });
273
- });
274
-
275
- // ── smoothstep ────────────────────────────────────────────────────────────────
276
-
277
- describe("smoothstep", () => {
278
- it("returns 0 when x <= edge0", () => {
279
- expect(smoothstep(0.2, 0.8, 0.1)).toBe(0);
280
- expect(smoothstep(0.2, 0.8, 0.2)).toBe(0);
281
- });
282
-
283
- it("returns 1 when x >= edge1", () => {
284
- expect(smoothstep(0.2, 0.8, 0.9)).toBe(1);
285
- expect(smoothstep(0.2, 0.8, 0.8)).toBe(1);
286
- });
287
-
288
- it("returns ~0.5 at midpoint between edge0 and edge1", () => {
289
- // t = (0.5 - 0.2) / (0.8 - 0.2) = 0.5; hermite(0.5) = 0.5*0.5*(3-2*0.5) = 0.5
290
- expect(smoothstep(0.2, 0.8, 0.5)).toBeCloseTo(0.5, 10);
291
- });
292
-
293
- it("is monotonically increasing", () => {
294
- const vals = [0.3, 0.4, 0.5, 0.6, 0.7].map((x) => smoothstep(0.2, 0.8, x));
295
- for (let i = 1; i < vals.length; i++) {
296
- expect(vals[i]).toBeGreaterThan(vals[i - 1] ?? 0);
297
- }
298
- });
299
- });
300
-
301
- // ── hash ──────────────────────────────────────────────────────────────────────
302
-
303
- describe("hash", () => {
304
- it("returns a value in [0, 1)", () => {
305
- const h = hash(1.5, 2.7);
306
- expect(h).toBeGreaterThanOrEqual(0);
307
- expect(h).toBeLessThan(1);
308
- });
309
-
310
- it("is deterministic for the same inputs", () => {
311
- expect(hash(3.14, 2.71)).toBe(hash(3.14, 2.71));
312
- });
313
-
314
- it("returns different values for different inputs", () => {
315
- expect(hash(0, 0)).not.toBe(hash(1, 0));
316
- expect(hash(0, 0)).not.toBe(hash(0, 1));
317
- });
318
-
319
- it("returns values in [0,1) for integer grid points", () => {
320
- for (let i = 0; i < 5; i++) {
321
- for (let j = 0; j < 5; j++) {
322
- const h = hash(i, j);
323
- expect(h).toBeGreaterThanOrEqual(0);
324
- expect(h).toBeLessThan(1);
325
- }
326
- }
327
- });
328
- });
329
-
330
- // ── vnoise ────────────────────────────────────────────────────────────────────
331
-
332
- describe("vnoise", () => {
333
- it("returns a value in [0, 1]", () => {
334
- const v = vnoise(1.5, 2.3);
335
- expect(v).toBeGreaterThanOrEqual(0);
336
- expect(v).toBeLessThanOrEqual(1);
337
- });
338
-
339
- it("is deterministic for the same inputs", () => {
340
- expect(vnoise(3.14, 2.71)).toBe(vnoise(3.14, 2.71));
341
- });
342
-
343
- it("returns [0,1] range over a grid", () => {
344
- for (let i = 0; i < 4; i++) {
345
- for (let j = 0; j < 4; j++) {
346
- const v = vnoise(i * 0.7, j * 0.7);
347
- expect(v).toBeGreaterThanOrEqual(0);
348
- expect(v).toBeLessThanOrEqual(1);
349
- }
350
- }
351
- });
352
-
353
- it("produces variation across the domain (not constant)", () => {
354
- const values = new Set([
355
- vnoise(0, 0),
356
- vnoise(1, 0),
357
- vnoise(0, 1),
358
- vnoise(1, 1),
359
- vnoise(0.5, 0.5),
360
- ]);
361
- // At least 2 distinct values among 5 samples
362
- expect(values.size).toBeGreaterThan(1);
363
- });
364
- });
365
-
366
- // ── fbm ───────────────────────────────────────────────────────────────────────
367
-
368
- describe("fbm", () => {
369
- it("is deterministic for the same inputs", () => {
370
- expect(fbm(1.5, 2.3)).toBe(fbm(1.5, 2.3));
371
- });
372
-
373
- it("returns consistent known values", () => {
374
- const v0 = fbm(0, 0);
375
- const v1 = fbm(1, 0);
376
- const v2 = fbm(0, 1);
377
- // All should be finite numbers (not NaN/Infinity)
378
- expect(Number.isFinite(v0)).toBe(true);
379
- expect(Number.isFinite(v1)).toBe(true);
380
- expect(Number.isFinite(v2)).toBe(true);
381
- // Should produce different values for different inputs
382
- expect(v0).not.toBe(v1);
383
- expect(v0).not.toBe(v2);
384
- });
385
-
386
- it("produces values in a reasonable range", () => {
387
- // fbm sums 5 octaves of vnoise (0–1) with amplitudes 0.5,0.25,0.125,0.0625,0.03125
388
- // max possible ≈ 0.96875; values should be positive
389
- const v = fbm(2.5, 3.7);
390
- expect(v).toBeGreaterThan(0);
391
- expect(v).toBeLessThan(1.1);
392
- });
393
- });
394
-
395
- // ── transition helpers ────────────────────────────────────────────────────────
396
-
397
- function makeBuffer(w: number, h: number, r: number, g: number, b: number): Buffer {
398
- const buf = Buffer.alloc(w * h * 6);
399
- for (let i = 0; i < w * h; i++) {
400
- buf.writeUInt16LE(r, i * 6);
401
- buf.writeUInt16LE(g, i * 6 + 2);
402
- buf.writeUInt16LE(b, i * 6 + 4);
403
- }
404
- return buf;
405
- }
406
-
407
- function runTransition(
408
- fn: TransitionFn,
409
- w: number,
410
- h: number,
411
- fR: number,
412
- fG: number,
413
- fB: number,
414
- tR: number,
415
- tG: number,
416
- tB: number,
417
- progress: number,
418
- ): Buffer {
419
- const from = makeBuffer(w, h, fR, fG, fB);
420
- const to = makeBuffer(w, h, tR, tG, tB);
421
- const out = Buffer.alloc(w * h * 6);
422
- fn(from, to, out, w, h, progress);
423
- return out;
424
- }
425
-
426
- // ── crossfade ─────────────────────────────────────────────────────────────────
427
-
428
- describe("crossfade", () => {
429
- it("at progress=0 output equals from", () => {
430
- const out = runTransition(crossfade, 2, 2, 10000, 20000, 30000, 50000, 55000, 60000, 0);
431
- for (let i = 0; i < 4; i++) {
432
- expect(out.readUInt16LE(i * 6)).toBe(10000);
433
- expect(out.readUInt16LE(i * 6 + 2)).toBe(20000);
434
- expect(out.readUInt16LE(i * 6 + 4)).toBe(30000);
435
- }
436
- });
437
-
438
- it("at progress=1 output equals to", () => {
439
- const out = runTransition(crossfade, 2, 2, 10000, 20000, 30000, 50000, 55000, 60000, 1);
440
- for (let i = 0; i < 4; i++) {
441
- expect(out.readUInt16LE(i * 6)).toBe(50000);
442
- expect(out.readUInt16LE(i * 6 + 2)).toBe(55000);
443
- expect(out.readUInt16LE(i * 6 + 4)).toBe(60000);
444
- }
445
- });
446
-
447
- it("at progress=0.5 output is midpoint of from and to", () => {
448
- const out = runTransition(crossfade, 1, 1, 0, 0, 0, 60000, 60000, 60000, 0.5);
449
- expect(out.readUInt16LE(0)).toBe(30000);
450
- expect(out.readUInt16LE(2)).toBe(30000);
451
- expect(out.readUInt16LE(4)).toBe(30000);
452
- });
453
-
454
- it("is registered in TRANSITIONS", () => {
455
- expect(TRANSITIONS["crossfade"]).toBe(crossfade);
456
- });
457
- });
458
-
459
- // ── flashThroughWhite ─────────────────────────────────────────────────────────
460
-
461
- describe("flashThroughWhite", () => {
462
- it("at progress=0 output approximates from", () => {
463
- // toWhite = smoothstep(0,0.45,0) = 0, fromWhite = 1-smoothstep(0.5,1,0) = 1,
464
- // blend = smoothstep(0.35,0.65,0) = 0 → output = fromC = from (untouched)
465
- const out = runTransition(flashThroughWhite, 1, 1, 10000, 20000, 30000, 50000, 55000, 60000, 0);
466
- expect(out.readUInt16LE(0)).toBe(10000);
467
- expect(out.readUInt16LE(2)).toBe(20000);
468
- expect(out.readUInt16LE(4)).toBe(30000);
469
- });
470
-
471
- it("at progress≈0.45 all channels are near white (>50000)", () => {
472
- // toWhite = smoothstep(0,0.45,0.45) = 1 → fromC = white
473
- // fromWhite = 1-smoothstep(0.5,1,0.45) = 1 → toC = white
474
- // both inputs to blend are white → output is white
475
- const out = runTransition(
476
- flashThroughWhite,
477
- 1,
478
- 1,
479
- 10000,
480
- 20000,
481
- 30000,
482
- 50000,
483
- 55000,
484
- 60000,
485
- 0.45,
486
- );
487
- expect(out.readUInt16LE(0)).toBeGreaterThan(50000);
488
- expect(out.readUInt16LE(2)).toBeGreaterThan(50000);
489
- expect(out.readUInt16LE(4)).toBeGreaterThan(50000);
490
- });
491
-
492
- it("at progress=1 output approximates to", () => {
493
- // toWhite = smoothstep(0,0.45,1) = 1, fromWhite = 1-smoothstep(0.5,1,1) = 0,
494
- // blend = smoothstep(0.35,0.65,1) = 1 → output = toC = to (untouched)
495
- const out = runTransition(flashThroughWhite, 1, 1, 10000, 20000, 30000, 50000, 55000, 60000, 1);
496
- expect(out.readUInt16LE(0)).toBe(50000);
497
- expect(out.readUInt16LE(2)).toBe(55000);
498
- expect(out.readUInt16LE(4)).toBe(60000);
499
- });
500
-
501
- it("is registered in TRANSITIONS", () => {
502
- expect(TRANSITIONS["flash-through-white"]).toBe(flashThroughWhite);
503
- });
504
- });
505
-
506
- // ── all transitions smoke test ────────────────────────────────────────────────
507
-
508
- const ALL_SHADERS = [
509
- "crossfade",
510
- "flash-through-white",
511
- "chromatic-split",
512
- "sdf-iris",
513
- "whip-pan",
514
- "cinematic-zoom",
515
- "gravitational-lens",
516
- "glitch",
517
- "ripple-waves",
518
- "swirl-vortex",
519
- "thermal-distortion",
520
- "domain-warp",
521
- "ridged-burn",
522
- "cross-warp-morph",
523
- "light-leak",
524
- ];
525
-
526
- // Pixel offset selector for the "at progress=0, center pixel ≈ from" test.
527
- // Most transitions use the center pixel (4*8+4). Two shaders require a
528
- // different test pixel because their design does not produce `from` at the
529
- // center when p=0:
530
- // sdf-iris: the iris reveal shows `to` inside and `from` outside.
531
- // At any p>0 the center is inside → shows `to`. We use
532
- // a corner pixel (row 0, col 0) that stays outside the
533
- // iris until p is large.
534
- // gravitational-lens: at p=0 the horizon mask is 0 at center (dist=0),
535
- // producing black. A corner pixel (dist≈0.7) has
536
- // horizon > 0 and shows a lensed version of `from`.
537
- const P0_PIXEL: Record<string, number> = {
538
- "sdf-iris": 0 * 6, // top-left corner: always outside iris at p=0
539
- "gravitational-lens": (0 * 8 + 0) * 6, // top-left corner: non-zero dist
540
- };
541
-
542
- describe("all transitions smoke test", () => {
543
- for (const name of ALL_SHADERS) {
544
- describe(name, () => {
545
- it("exists in registry", () => {
546
- expect(TRANSITIONS[name]).toBeDefined();
547
- });
548
- it("at progress=0, center pixel ≈ from", () => {
549
- const from = makeBuffer(8, 8, 40000, 30000, 20000);
550
- const to = makeBuffer(8, 8, 10000, 10000, 10000);
551
- const out = Buffer.alloc(8 * 8 * 6);
552
- const fn = TRANSITIONS[name];
553
- expect(fn).toBeDefined();
554
- fn?.(from, to, out, 8, 8, 0);
555
- const o = P0_PIXEL[name] ?? (4 * 8 + 4) * 6;
556
- // At progress=0 the result should be the `from` pixel (R=40000).
557
- // The midpoint between from (40000) and to (10000) is 25000, so a
558
- // tighter threshold catches transitions that are halfway-blended
559
- // when they should be fully on the `from` side.
560
- expect(out.readUInt16LE(o)).toBeGreaterThan(35000);
561
- });
562
- it("at progress=1, center pixel ≈ to", () => {
563
- const from = makeBuffer(8, 8, 40000, 30000, 20000);
564
- const to = makeBuffer(8, 8, 10000, 10000, 10000);
565
- const out = Buffer.alloc(8 * 8 * 6);
566
- const fn = TRANSITIONS[name];
567
- expect(fn).toBeDefined();
568
- fn?.(from, to, out, 8, 8, 1);
569
- const o = (4 * 8 + 4) * 6;
570
- // At progress=1 the result should be the `to` pixel (R=10000).
571
- // Tighter than the previous halfway midpoint (25000) so that any
572
- // transition that is still half-blended will fail.
573
- expect(out.readUInt16LE(o)).toBeLessThan(15000);
574
- });
575
- });
576
- }
577
- });
578
-
579
- // ── all transitions: midpoint regressions (p=0.5) ───────────────────────────
580
- //
581
- // Endpoint smoke tests above lock down p=0 (≈from) and p=1 (≈to). They miss
582
- // regressions where a shader becomes a no-op, prematurely completes, returns
583
- // garbage, or accidentally introduces non-determinism — specifically at the
584
- // midpoint where the transition is most visible to viewers. Four invariants
585
- // every shader must satisfy at p=0.5:
586
- //
587
- // 1. Output ≠ from catches "shader is a no-op, returns input as-is"
588
- // 2. Output ≠ to catches "shader prematurely completes at midpoint"
589
- // 3. Output is non-zero catches "shader didn't write anything to the buf"
590
- // 4. Output is deterministic — catches accidental Math.random / Date.now /
591
- // uninitialized-state regressions that would surface as flaky CI.
592
- //
593
- // Two distinct uniform colors give buffer-equality checks distinct byte
594
- // patterns to compare against. Even shaders that warp UVs (which would be
595
- // no-ops on uniform input alone) produce mix16(from, to, 0.5) = (25000, 20000,
596
- // 15000), distinct from both inputs at every pixel.
597
- describe("all transitions: midpoint regressions (p=0.5)", () => {
598
- for (const name of ALL_SHADERS) {
599
- describe(name, () => {
600
- const w = 8;
601
- const h = 8;
602
- const from = makeBuffer(w, h, 40000, 30000, 20000);
603
- const to = makeBuffer(w, h, 10000, 10000, 10000);
604
- const zeros = Buffer.alloc(w * h * 6);
605
-
606
- it("output ≠ from (not a no-op at midpoint)", () => {
607
- const fn = TRANSITIONS[name];
608
- expect(fn).toBeDefined();
609
- const out = Buffer.alloc(w * h * 6);
610
- fn?.(from, to, out, w, h, 0.5);
611
- expect(out.equals(from)).toBe(false);
612
- });
613
-
614
- it("output ≠ to (not premature completion at midpoint)", () => {
615
- const fn = TRANSITIONS[name];
616
- expect(fn).toBeDefined();
617
- const out = Buffer.alloc(w * h * 6);
618
- fn?.(from, to, out, w, h, 0.5);
619
- expect(out.equals(to)).toBe(false);
620
- });
621
-
622
- it("output is non-zero (shader actually wrote pixels)", () => {
623
- const fn = TRANSITIONS[name];
624
- expect(fn).toBeDefined();
625
- const out = Buffer.alloc(w * h * 6);
626
- fn?.(from, to, out, w, h, 0.5);
627
- expect(out.equals(zeros)).toBe(false);
628
- });
629
-
630
- it("output is deterministic across repeated calls", () => {
631
- const fn = TRANSITIONS[name];
632
- expect(fn).toBeDefined();
633
- const out1 = Buffer.alloc(w * h * 6);
634
- const out2 = Buffer.alloc(w * h * 6);
635
- fn?.(from, to, out1, w, h, 0.5);
636
- fn?.(from, to, out2, w, h, 0.5);
637
- expect(out2.equals(out1)).toBe(true);
638
- });
639
- });
640
- }
641
- });
642
-
643
- // ── hdrToLinear / linearToHdr roundtrip ────────────────────────────────────
644
-
645
- describe("hdrToLinear / linearToHdr", () => {
646
- for (const transfer of ["pq", "hlg"] as const) {
647
- describe(transfer, () => {
648
- it("roundtrip preserves mid-to-high values", () => {
649
- // PQ concentrates precision in the dark range — linearizing then
650
- // re-quantizing at 16-bit loses bits for values below ~16000.
651
- // HLG squares small inputs, similar effect. Test the mid-to-high
652
- // range where roundtrip error is bounded.
653
- const values = [16384, 32768, 50000, 65535];
654
- const buf = Buffer.alloc(values.length * 2);
655
- for (let i = 0; i < values.length; i++) {
656
- buf.writeUInt16LE(values[i] ?? 0, i * 2);
657
- }
658
- const original = Buffer.from(buf);
659
- hdrToLinear(buf, transfer);
660
- linearToHdr(buf, transfer);
661
- for (let i = 0; i < values.length; i++) {
662
- const got = buf.readUInt16LE(i * 2);
663
- const want = original.readUInt16LE(i * 2);
664
- expect(Math.abs(got - want)).toBeLessThanOrEqual(30);
665
- }
666
- });
667
-
668
- it("zero maps to zero", () => {
669
- const buf = Buffer.alloc(2);
670
- buf.writeUInt16LE(0, 0);
671
- hdrToLinear(buf, transfer);
672
- expect(buf.readUInt16LE(0)).toBe(0);
673
- });
674
-
675
- it("65535 maps to 65535", () => {
676
- const buf = Buffer.alloc(2);
677
- buf.writeUInt16LE(65535, 0);
678
- hdrToLinear(buf, transfer);
679
- linearToHdr(buf, transfer);
680
- expect(buf.readUInt16LE(0)).toBe(65535);
681
- });
682
-
683
- it("hdrToLinear produces monotonically increasing output", () => {
684
- const steps = [0, 1000, 5000, 10000, 20000, 40000, 65535];
685
- const buf = Buffer.alloc(steps.length * 2);
686
- for (let i = 0; i < steps.length; i++) {
687
- buf.writeUInt16LE(steps[i] ?? 0, i * 2);
688
- }
689
- hdrToLinear(buf, transfer);
690
- let prev = 0;
691
- for (let i = 0; i < steps.length; i++) {
692
- const val = buf.readUInt16LE(i * 2);
693
- expect(val).toBeGreaterThanOrEqual(prev);
694
- prev = val;
695
- }
696
- });
697
- });
698
- }
699
- });
700
-
701
- // ── convertTransfer (HLG↔PQ) ─────────────────────────────────────────────
702
-
703
- describe("convertTransfer", () => {
704
- it("no-op when from === to", () => {
705
- const buf = Buffer.alloc(6);
706
- buf.writeUInt16LE(32768, 0);
707
- buf.writeUInt16LE(16384, 2);
708
- buf.writeUInt16LE(8192, 4);
709
- const original = Buffer.from(buf);
710
- convertTransfer(buf, "pq", "pq");
711
- expect(buf.equals(original)).toBe(true);
712
- });
713
-
714
- it("hlg→pq→hlg roundtrip preserves mid-high values", () => {
715
- const values = [16384, 32768, 50000, 65535];
716
- const buf = Buffer.alloc(values.length * 2);
717
- for (let i = 0; i < values.length; i++) {
718
- buf.writeUInt16LE(values[i] ?? 0, i * 2);
719
- }
720
- const original = Buffer.from(buf);
721
- convertTransfer(buf, "hlg", "pq");
722
- expect(buf.equals(original)).toBe(false);
723
- convertTransfer(buf, "pq", "hlg");
724
- for (let i = 0; i < values.length; i++) {
725
- const got = buf.readUInt16LE(i * 2);
726
- const want = original.readUInt16LE(i * 2);
727
- expect(Math.abs(got - want)).toBeLessThanOrEqual(30);
728
- }
729
- });
730
-
731
- it("hlg→pq produces different values", () => {
732
- const buf = Buffer.alloc(2);
733
- buf.writeUInt16LE(32768, 0);
734
- const before = buf.readUInt16LE(0);
735
- convertTransfer(buf, "hlg", "pq");
736
- expect(buf.readUInt16LE(0)).not.toBe(before);
737
- });
738
- });