@kitlangton/knobs 0.4.0-next.15 → 0.4.0-next.17
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/README.md +46 -14
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-bejz4qcg.js → chunk-3vag4n3q.js} +47 -10
- package/dist/dev/{chunk-49hkdggk.js → chunk-5cv3g7ca.js} +395 -104
- package/dist/dev/{chunk-902v1jgd.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +549 -121
- package/dist/review-react.js +1341 -880
- package/dist/review-solid.js +1341 -880
- package/dist/review.js +1711 -1248
- package/dist/themes.js +8 -0
- package/dist/types/core/schema.d.ts +11 -2
- package/dist/types/core/store.d.ts +29 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/panel/active.d.ts +53 -0
- package/dist/types/panel/description-ids.d.ts +6 -0
- package/dist/types/panel/folds.d.ts +7 -4
- package/dist/types/panel/gpu/color.d.ts +2 -0
- package/dist/types/panel/gpu/renderer.d.ts +8 -2
- package/dist/types/panel/index.d.ts +1 -0
- package/dist/types/panel/page-hints.d.ts +3 -2
- package/dist/types/panel/page.d.ts +2 -15
- package/dist/types/panel/position.d.ts +23 -0
- package/dist/types/panel/summary.d.ts +22 -0
- package/dist/types/tokens/schema.d.ts +9 -0
- package/dist/types/ui/compare-css.d.ts +1 -1
- package/dist/types/ui/compare-frames.d.ts +64 -2
- package/dist/types/ui/compare-model.d.ts +23 -0
- package/dist/types/ui/compare.d.ts +6 -4
- package/dist/types/ui/player-css.d.ts +1 -1
- package/dist/types/ui/player.d.ts +6 -0
- package/dist/types/ui/react.d.ts +1 -1
- package/dist/types/ui/solid.d.ts +1 -1
- package/dist/types/ui/tooltip-model.d.ts +35 -0
- package/dist/types/ui/tooltip.d.ts +1 -1
- package/dist/types/ui/walkthrough-model.d.ts +4 -2
- package/dist/ui-react.js +2015 -1507
- package/dist/ui-solid.js +2014 -1506
- package/dist/ui.js +2013 -1505
- package/package.json +1 -1
package/dist/ui-react.js
CHANGED
|
@@ -648,7 +648,6 @@ var compareCss = `
|
|
|
648
648
|
:where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}
|
|
649
649
|
.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
|
|
650
650
|
.knobs-cmp-note:empty{display:none}
|
|
651
|
-
.knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
|
|
652
651
|
.knobs-cmp-timeline-host{flex:none;min-height:0}
|
|
653
652
|
/* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
|
|
654
653
|
.knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
|
|
@@ -668,6 +667,9 @@ var playerCss = `
|
|
|
668
667
|
.knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
|
|
669
668
|
opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
|
|
670
669
|
.knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
|
|
670
|
+
/* Mid-scrub, the cached frame under the hand, over the video until it lands. */
|
|
671
|
+
.knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
|
|
672
|
+
.knobs-player-preview[hidden]{display:none}
|
|
671
673
|
.knobs-player-line{flex:none;min-height:0}
|
|
672
674
|
/* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
|
|
673
675
|
about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
|
|
@@ -1449,6 +1451,14 @@ var init_schema = __esm(() => {
|
|
|
1449
1451
|
doc: "Panel and card radius, concentric with controls.",
|
|
1450
1452
|
deprecated: ["--k-panel-radius", "--ku-panel-radius"]
|
|
1451
1453
|
},
|
|
1454
|
+
{
|
|
1455
|
+
name: "radius-frame",
|
|
1456
|
+
tier: "semantic",
|
|
1457
|
+
type: "length",
|
|
1458
|
+
group: "Shape",
|
|
1459
|
+
source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
|
|
1460
|
+
doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
|
|
1461
|
+
},
|
|
1452
1462
|
{
|
|
1453
1463
|
name: "round",
|
|
1454
1464
|
tier: "semantic",
|
|
@@ -2567,7 +2577,16 @@ var init_glide = __esm(() => {
|
|
|
2567
2577
|
"border-subtle",
|
|
2568
2578
|
"bg-page"
|
|
2569
2579
|
];
|
|
2570
|
-
LENGTH_TOKENS = [
|
|
2580
|
+
LENGTH_TOKENS = [
|
|
2581
|
+
"pad",
|
|
2582
|
+
"inset",
|
|
2583
|
+
"row",
|
|
2584
|
+
"gap",
|
|
2585
|
+
"radius",
|
|
2586
|
+
"radius-panel",
|
|
2587
|
+
"radius-frame",
|
|
2588
|
+
"font-size"
|
|
2589
|
+
];
|
|
2571
2590
|
NUMBER_TOKENS = ["weight-label", "blur", "glow", "glass", "glass-tone"];
|
|
2572
2591
|
ANIMATED = [...COLOR_TOKENS, ...LENGTH_TOKENS, ...NUMBER_TOKENS, "width"];
|
|
2573
2592
|
TOKEN_TRANSITIONS = ANIMATED.map((t) => `--kt-${t} var(--k-token-t)`).join(",");
|
|
@@ -4605,12 +4624,24 @@ function createRegistry(doc) {
|
|
|
4605
4624
|
return;
|
|
4606
4625
|
if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
|
|
4607
4626
|
return;
|
|
4608
|
-
if (isTyping(e.target) || !wide())
|
|
4627
|
+
if (isTyping(e.target) || !wide(e.target))
|
|
4609
4628
|
return;
|
|
4610
4629
|
if (registry.start())
|
|
4611
4630
|
e.preventDefault();
|
|
4612
4631
|
};
|
|
4613
|
-
const
|
|
4632
|
+
const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
|
|
4633
|
+
const wide = (target) => {
|
|
4634
|
+
if (!listening)
|
|
4635
|
+
return false;
|
|
4636
|
+
if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
|
|
4637
|
+
return true;
|
|
4638
|
+
if (sources.length < 2)
|
|
4639
|
+
return false;
|
|
4640
|
+
if (target === undefined)
|
|
4641
|
+
return true;
|
|
4642
|
+
const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
|
|
4643
|
+
return Boolean(piece?.classList.contains("knobs-ui"));
|
|
4644
|
+
};
|
|
4614
4645
|
const relisten = () => {
|
|
4615
4646
|
const next = sources.some((s) => s.listen);
|
|
4616
4647
|
if (next === listening)
|
|
@@ -9757,6 +9788,31 @@ function seeked(s, current, fps) {
|
|
|
9757
9788
|
return { state: { seeking: true, pending: null }, apply: s.pending, settled: false };
|
|
9758
9789
|
return { state: SEEK_IDLE, apply: null, settled: true };
|
|
9759
9790
|
}
|
|
9791
|
+
function releaseSeek(current, t, fps, duration) {
|
|
9792
|
+
const at = seekTime(t, fps, duration);
|
|
9793
|
+
return sameFrame(current, at, fps) ? null : at;
|
|
9794
|
+
}
|
|
9795
|
+
function stallGuard(timers, onStall, ms = SEEK_STALL_MS) {
|
|
9796
|
+
let id = 0;
|
|
9797
|
+
const clear = () => {
|
|
9798
|
+
if (id)
|
|
9799
|
+
timers.clearTimeout(id);
|
|
9800
|
+
id = 0;
|
|
9801
|
+
};
|
|
9802
|
+
return {
|
|
9803
|
+
arm() {
|
|
9804
|
+
clear();
|
|
9805
|
+
id = timers.setTimeout(() => {
|
|
9806
|
+
id = 0;
|
|
9807
|
+
onStall();
|
|
9808
|
+
}, ms);
|
|
9809
|
+
},
|
|
9810
|
+
clear,
|
|
9811
|
+
get armed() {
|
|
9812
|
+
return id !== 0;
|
|
9813
|
+
}
|
|
9814
|
+
};
|
|
9815
|
+
}
|
|
9760
9816
|
function letterbox(media, box) {
|
|
9761
9817
|
if (media.w <= 0 || media.h <= 0 || box.w <= 0 || box.h <= 0)
|
|
9762
9818
|
return box;
|
|
@@ -9877,7 +9933,7 @@ function stripPeaks(diffs, o = {}) {
|
|
|
9877
9933
|
});
|
|
9878
9934
|
return out;
|
|
9879
9935
|
}
|
|
9880
|
-
var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, fuseCorners = (r, k) => {
|
|
9936
|
+
var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, SEEK_STALL_MS = 2500, FUSE, fuseCorners = (r, k) => {
|
|
9881
9937
|
const inner = r * Math.min(1, Math.max(0, k));
|
|
9882
9938
|
return { before: { left: r, right: inner }, after: { left: inner, right: r } };
|
|
9883
9939
|
}, labelFold = (fold, mark, blur = 3) => {
|
|
@@ -10124,6 +10180,25 @@ var init_compare_mp4 = __esm(() => {
|
|
|
10124
10180
|
});
|
|
10125
10181
|
|
|
10126
10182
|
// src/ui/compare-frames.ts
|
|
10183
|
+
function scrubCaching(option, win) {
|
|
10184
|
+
if (option === false)
|
|
10185
|
+
return false;
|
|
10186
|
+
if (option === true)
|
|
10187
|
+
return true;
|
|
10188
|
+
const net = win?.navigator?.connection;
|
|
10189
|
+
if (net?.saveData)
|
|
10190
|
+
return false;
|
|
10191
|
+
return !win?.matchMedia?.("(prefers-reduced-data: reduce)").matches;
|
|
10192
|
+
}
|
|
10193
|
+
function proxyStride(bytes, count, total, budget) {
|
|
10194
|
+
if (count <= 0 || total <= 0)
|
|
10195
|
+
return 1;
|
|
10196
|
+
const each = bytes / count;
|
|
10197
|
+
let stride = 1;
|
|
10198
|
+
while (total / stride * each > budget && stride < total)
|
|
10199
|
+
stride *= 2;
|
|
10200
|
+
return stride;
|
|
10201
|
+
}
|
|
10127
10202
|
function gridPoints(total, count = GRID) {
|
|
10128
10203
|
if (total <= 0)
|
|
10129
10204
|
return [];
|
|
@@ -10154,6 +10229,24 @@ function nearestKey(keys, frame, within = Infinity) {
|
|
|
10154
10229
|
}
|
|
10155
10230
|
return best !== null && bestD <= within ? best : null;
|
|
10156
10231
|
}
|
|
10232
|
+
function claim(core) {
|
|
10233
|
+
if (!round2) {
|
|
10234
|
+
const r = new Set;
|
|
10235
|
+
round2 = r;
|
|
10236
|
+
queueMicrotask(() => {
|
|
10237
|
+
for (const c of actives)
|
|
10238
|
+
if (!r.has(c)) {
|
|
10239
|
+
c.active = false;
|
|
10240
|
+
c.dropFull();
|
|
10241
|
+
}
|
|
10242
|
+
actives = r;
|
|
10243
|
+
round2 = null;
|
|
10244
|
+
});
|
|
10245
|
+
}
|
|
10246
|
+
round2.add(core);
|
|
10247
|
+
actives.add(core);
|
|
10248
|
+
core.active = true;
|
|
10249
|
+
}
|
|
10157
10250
|
function pump(win) {
|
|
10158
10251
|
if (running)
|
|
10159
10252
|
return;
|
|
@@ -10161,12 +10254,14 @@ function pump(win) {
|
|
|
10161
10254
|
(async () => {
|
|
10162
10255
|
try {
|
|
10163
10256
|
for (;; ) {
|
|
10164
|
-
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active));
|
|
10257
|
+
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active) || a.ran - b.ran);
|
|
10165
10258
|
let unit = null;
|
|
10166
10259
|
for (const c of order) {
|
|
10167
10260
|
unit = c.work();
|
|
10168
|
-
if (unit)
|
|
10261
|
+
if (unit) {
|
|
10262
|
+
c.ran = ++turns;
|
|
10169
10263
|
break;
|
|
10264
|
+
}
|
|
10170
10265
|
}
|
|
10171
10266
|
if (!unit)
|
|
10172
10267
|
break;
|
|
@@ -10196,12 +10291,99 @@ async function toBitmap(doc, source, w, h, cap) {
|
|
|
10196
10291
|
return createImageBitmap(c);
|
|
10197
10292
|
}
|
|
10198
10293
|
}
|
|
10294
|
+
function holdEncoders() {
|
|
10295
|
+
encoderUsers++;
|
|
10296
|
+
}
|
|
10297
|
+
function dropEncoders() {
|
|
10298
|
+
encoderUsers = Math.max(0, encoderUsers - 1);
|
|
10299
|
+
if (encoderUsers > 0)
|
|
10300
|
+
return;
|
|
10301
|
+
endEncoders("released");
|
|
10302
|
+
encoders = undefined;
|
|
10303
|
+
}
|
|
10304
|
+
function encodersOf(host) {
|
|
10305
|
+
if (encoders !== undefined)
|
|
10306
|
+
return encoders;
|
|
10307
|
+
encoders = null;
|
|
10308
|
+
const W = host.Worker;
|
|
10309
|
+
if (!W || !host.OffscreenCanvas)
|
|
10310
|
+
return null;
|
|
10311
|
+
const workers = [];
|
|
10312
|
+
let url = null;
|
|
10313
|
+
try {
|
|
10314
|
+
url = host.URL.createObjectURL(new host.Blob([ENCODER], { type: "text/javascript" }));
|
|
10315
|
+
for (let i = 0;i < ENCODERS; i++)
|
|
10316
|
+
workers.push(new W(url));
|
|
10317
|
+
} catch {
|
|
10318
|
+
for (const w of workers)
|
|
10319
|
+
w.terminate();
|
|
10320
|
+
return null;
|
|
10321
|
+
} finally {
|
|
10322
|
+
if (url)
|
|
10323
|
+
host.URL.revokeObjectURL(url);
|
|
10324
|
+
}
|
|
10325
|
+
const pool = { workers, waiting: new Map, next: 0 };
|
|
10326
|
+
for (const w of workers) {
|
|
10327
|
+
w.onmessage = (e) => {
|
|
10328
|
+
const job = pool.waiting.get(e.data.id);
|
|
10329
|
+
pool.waiting.delete(e.data.id);
|
|
10330
|
+
if (e.data.blob)
|
|
10331
|
+
job?.done(e.data.blob);
|
|
10332
|
+
else
|
|
10333
|
+
job?.fail(new Error(e.data.error));
|
|
10334
|
+
};
|
|
10335
|
+
w.onerror = () => {
|
|
10336
|
+
if (encoders !== pool)
|
|
10337
|
+
return;
|
|
10338
|
+
endEncoders("encoder");
|
|
10339
|
+
encoders = null;
|
|
10340
|
+
};
|
|
10341
|
+
}
|
|
10342
|
+
encoders = pool;
|
|
10343
|
+
return pool;
|
|
10344
|
+
}
|
|
10345
|
+
function encodeOn(pool, bitmap, quality) {
|
|
10346
|
+
const id = pool.next++;
|
|
10347
|
+
const worker = pool.workers[id % pool.workers.length];
|
|
10348
|
+
return new Promise((done, fail) => {
|
|
10349
|
+
pool.waiting.set(id, { done, fail });
|
|
10350
|
+
try {
|
|
10351
|
+
worker.postMessage({ id, bitmap, quality }, [bitmap]);
|
|
10352
|
+
} catch (err) {
|
|
10353
|
+
pool.waiting.delete(id);
|
|
10354
|
+
bitmap.close();
|
|
10355
|
+
fail(err);
|
|
10356
|
+
}
|
|
10357
|
+
});
|
|
10358
|
+
}
|
|
10359
|
+
async function toProxy(doc, source, w, h) {
|
|
10360
|
+
const bm = await toBitmap(doc, source, w, h, PROXY.size);
|
|
10361
|
+
const pool = encodersOf(doc.defaultView);
|
|
10362
|
+
if (pool)
|
|
10363
|
+
return encodeOn(pool, bm, PROXY.quality);
|
|
10364
|
+
try {
|
|
10365
|
+
const Off = doc.defaultView?.OffscreenCanvas;
|
|
10366
|
+
if (Off) {
|
|
10367
|
+
const c = new Off(bm.width, bm.height);
|
|
10368
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
10369
|
+
return await c.convertToBlob({ type: "image/jpeg", quality: PROXY.quality });
|
|
10370
|
+
}
|
|
10371
|
+
const c = doc.createElement("canvas");
|
|
10372
|
+
c.width = bm.width;
|
|
10373
|
+
c.height = bm.height;
|
|
10374
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
10375
|
+
return await new Promise((done, fail) => c.toBlob((b) => b ? done(b) : fail(new Error("toBlob")), "image/jpeg", PROXY.quality));
|
|
10376
|
+
} finally {
|
|
10377
|
+
bm.close();
|
|
10378
|
+
}
|
|
10379
|
+
}
|
|
10199
10380
|
function acquireFrames(doc, src, fps) {
|
|
10200
10381
|
const key = `${fps}|${src}`;
|
|
10201
10382
|
let core = cores.get(key);
|
|
10202
10383
|
if (!core) {
|
|
10203
10384
|
core = makeCore(doc, key, src, fps);
|
|
10204
10385
|
cores.set(key, core);
|
|
10386
|
+
holdEncoders();
|
|
10205
10387
|
}
|
|
10206
10388
|
core.refs++;
|
|
10207
10389
|
return core.cache;
|
|
@@ -10222,6 +10404,71 @@ function makeCore(doc, key, src, fps) {
|
|
|
10222
10404
|
let generation = 0;
|
|
10223
10405
|
let tick = 0;
|
|
10224
10406
|
let notifyRaf = 0;
|
|
10407
|
+
const proxies = new Map;
|
|
10408
|
+
let proxyBytes = 0;
|
|
10409
|
+
let stride = 1;
|
|
10410
|
+
const decoded = new Map;
|
|
10411
|
+
const decoding = new Set;
|
|
10412
|
+
let aim = null;
|
|
10413
|
+
let lastAsked = null;
|
|
10414
|
+
const storeProxy = (frame, blob) => {
|
|
10415
|
+
if (disposed || frame % stride || proxies.has(frame))
|
|
10416
|
+
return;
|
|
10417
|
+
proxies.set(frame, blob);
|
|
10418
|
+
proxyBytes += blob.size;
|
|
10419
|
+
const next = proxyStride(proxyBytes, proxies.size, total, PROXY.budget);
|
|
10420
|
+
if (next <= stride)
|
|
10421
|
+
return;
|
|
10422
|
+
stride = next;
|
|
10423
|
+
for (const [f, b] of proxies)
|
|
10424
|
+
if (f % stride) {
|
|
10425
|
+
proxies.delete(f);
|
|
10426
|
+
proxyBytes -= b.size;
|
|
10427
|
+
}
|
|
10428
|
+
};
|
|
10429
|
+
const proxyNear = (frame) => {
|
|
10430
|
+
const at = Math.round(frame / stride) * stride;
|
|
10431
|
+
let best = null;
|
|
10432
|
+
for (const f of [at, at - stride, at + stride])
|
|
10433
|
+
if (proxies.has(f) && (best === null || Math.abs(f - frame) < Math.abs(best - frame)))
|
|
10434
|
+
best = f;
|
|
10435
|
+
return best;
|
|
10436
|
+
};
|
|
10437
|
+
const proxyTodo = () => {
|
|
10438
|
+
const out = [];
|
|
10439
|
+
for (let f = 0;f < total; f += stride)
|
|
10440
|
+
if (!proxies.has(f))
|
|
10441
|
+
out.push(f);
|
|
10442
|
+
return out;
|
|
10443
|
+
};
|
|
10444
|
+
const decodeProxy = (frame) => {
|
|
10445
|
+
if (frame === null || decoded.has(frame) || decoding.has(frame) || disposed)
|
|
10446
|
+
return;
|
|
10447
|
+
const blob = proxies.get(frame);
|
|
10448
|
+
if (!blob)
|
|
10449
|
+
return;
|
|
10450
|
+
decoding.add(frame);
|
|
10451
|
+
createImageBitmap(blob).then((bitmap) => {
|
|
10452
|
+
decoding.delete(frame);
|
|
10453
|
+
if (disposed)
|
|
10454
|
+
return bitmap.close();
|
|
10455
|
+
decoded.set(frame, { bitmap, used: ++tick });
|
|
10456
|
+
if (decoded.size > PROXY.decoded) {
|
|
10457
|
+
const [oldest] = [...decoded].sort((a, b) => a[1].used - b[1].used);
|
|
10458
|
+
if (oldest && oldest[0] !== frame) {
|
|
10459
|
+
oldest[1].bitmap.close();
|
|
10460
|
+
decoded.delete(oldest[0]);
|
|
10461
|
+
}
|
|
10462
|
+
}
|
|
10463
|
+
for (const f of listeners)
|
|
10464
|
+
f();
|
|
10465
|
+
}, () => decoding.delete(frame));
|
|
10466
|
+
};
|
|
10467
|
+
const dropDecoded = () => {
|
|
10468
|
+
for (const d of decoded.values())
|
|
10469
|
+
d.bitmap.close();
|
|
10470
|
+
decoded.clear();
|
|
10471
|
+
};
|
|
10225
10472
|
const notify = () => {
|
|
10226
10473
|
if (notifyRaf)
|
|
10227
10474
|
return;
|
|
@@ -10352,18 +10599,50 @@ function makeCore(doc, key, src, fps) {
|
|
|
10352
10599
|
return;
|
|
10353
10600
|
const forFocus = todo[0].kind === "focus";
|
|
10354
10601
|
const gen = generation;
|
|
10355
|
-
|
|
10602
|
+
await decodeRun(t, data, run, forFocus ? () => gen !== generation : () => false, (vf, f) => {
|
|
10603
|
+
const want = plan.get(f);
|
|
10604
|
+
if (!want)
|
|
10605
|
+
return null;
|
|
10606
|
+
plan.delete(f);
|
|
10607
|
+
return toBitmap(doc, vf, vf.displayWidth, vf.displayHeight, want.full ? FULL : PREVIEW).then((bm) => store(f, bm, want.full, want.kind));
|
|
10608
|
+
});
|
|
10609
|
+
};
|
|
10610
|
+
const proxyUnit = async () => {
|
|
10611
|
+
const todo = proxyTodo();
|
|
10612
|
+
if (!todo.length || !track || !buf)
|
|
10613
|
+
return;
|
|
10614
|
+
const runs = decodeRuns(track.samples, new Set(todo), (pts) => frameOf(pts, fps));
|
|
10615
|
+
const at = aim ?? focusAt ?? 0;
|
|
10616
|
+
let run = runs[0];
|
|
10617
|
+
let best = Number.POSITIVE_INFINITY;
|
|
10618
|
+
for (const r of runs) {
|
|
10619
|
+
const d = Math.min(...r.frames.map((f) => Math.abs(f - at)));
|
|
10620
|
+
if (d < best) {
|
|
10621
|
+
best = d;
|
|
10622
|
+
run = r;
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
if (!run)
|
|
10626
|
+
return;
|
|
10627
|
+
const gen = generation;
|
|
10628
|
+
let made = 0;
|
|
10629
|
+
await decodeRun(track, buf, run, () => gen !== generation || made >= PROXY.turn, (vf, f) => {
|
|
10630
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
10631
|
+
return null;
|
|
10632
|
+
made++;
|
|
10633
|
+
return toProxy(doc, vf, vf.displayWidth, vf.displayHeight).then((b) => storeProxy(f, b));
|
|
10634
|
+
});
|
|
10635
|
+
};
|
|
10636
|
+
const decodeRun = async (t, data, run, stale, take) => {
|
|
10356
10637
|
let pending = 0;
|
|
10357
10638
|
let failed = false;
|
|
10358
10639
|
const decoder = new VideoDecoder({
|
|
10359
10640
|
output: (vf) => {
|
|
10360
|
-
const
|
|
10361
|
-
|
|
10362
|
-
if (!want || stale() || disposed)
|
|
10641
|
+
const work = stale() || disposed ? null : take(vf, frameOf(vf.timestamp / 1e6, fps));
|
|
10642
|
+
if (!work)
|
|
10363
10643
|
return vf.close();
|
|
10364
|
-
plan.delete(f);
|
|
10365
10644
|
pending++;
|
|
10366
|
-
|
|
10645
|
+
work.catch(() => {}).finally(() => {
|
|
10367
10646
|
vf.close();
|
|
10368
10647
|
pending--;
|
|
10369
10648
|
});
|
|
@@ -10477,6 +10756,25 @@ function makeCore(doc, key, src, fps) {
|
|
|
10477
10756
|
return points.some((p) => p.frame + p.tolerance >= f && !covered(p));
|
|
10478
10757
|
}, 6000, false);
|
|
10479
10758
|
};
|
|
10759
|
+
const videoProxyUnit = async () => {
|
|
10760
|
+
const v = sampler;
|
|
10761
|
+
const todo = proxyTodo();
|
|
10762
|
+
if (!v || !todo.length || !v.videoWidth)
|
|
10763
|
+
return;
|
|
10764
|
+
const at = aim ?? focusAt ?? 0;
|
|
10765
|
+
const first = todo.find((f) => f >= at) ?? todo[0];
|
|
10766
|
+
const grab = async (f) => {
|
|
10767
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
10768
|
+
return;
|
|
10769
|
+
storeProxy(f, await toProxy(doc, v, v.videoWidth, v.videoHeight));
|
|
10770
|
+
};
|
|
10771
|
+
await seekTo(v, (first + 0.5) / fps);
|
|
10772
|
+
await grab(frameOf(v.currentTime, fps));
|
|
10773
|
+
await playThrough(v, 1, async (f) => {
|
|
10774
|
+
await grab(f);
|
|
10775
|
+
return proxyTodo().length > 0;
|
|
10776
|
+
}, 4000, true);
|
|
10777
|
+
};
|
|
10480
10778
|
const cache = {
|
|
10481
10779
|
src,
|
|
10482
10780
|
get backend() {
|
|
@@ -10485,29 +10783,56 @@ function makeCore(doc, key, src, fps) {
|
|
|
10485
10783
|
get size() {
|
|
10486
10784
|
return entries.size;
|
|
10487
10785
|
},
|
|
10488
|
-
|
|
10786
|
+
get proxies() {
|
|
10787
|
+
return { count: proxies.size, stride, bytes: proxyBytes };
|
|
10788
|
+
},
|
|
10789
|
+
nearest(frame, within = Infinity, decode = false) {
|
|
10489
10790
|
const k = nearestKey(entries.keys(), frame, within);
|
|
10791
|
+
const kd = nearestKey(decoded.keys(), frame, within);
|
|
10792
|
+
const dk = k === null ? Number.POSITIVE_INFINITY : Math.abs(k - frame);
|
|
10793
|
+
const dd = kd === null ? Number.POSITIVE_INFINITY : Math.abs(kd - frame);
|
|
10794
|
+
if (decode) {
|
|
10795
|
+
const p = proxyNear(frame);
|
|
10796
|
+
if (p !== null && Math.abs(p - frame) < Math.min(dk, dd))
|
|
10797
|
+
decodeProxy(p);
|
|
10798
|
+
const ahead = aheadOf(frame, lastAsked, total);
|
|
10799
|
+
if (ahead !== null)
|
|
10800
|
+
decodeProxy(proxyNear(ahead));
|
|
10801
|
+
lastAsked = frame;
|
|
10802
|
+
}
|
|
10803
|
+
if (kd !== null && (dd < dk || dd === dk && !entries.get(k)?.full)) {
|
|
10804
|
+
const d = decoded.get(kd);
|
|
10805
|
+
d.used = ++tick;
|
|
10806
|
+
return { frame: kd, bitmap: d.bitmap, full: false };
|
|
10807
|
+
}
|
|
10490
10808
|
if (k === null)
|
|
10491
10809
|
return null;
|
|
10492
10810
|
const e = entries.get(k);
|
|
10493
10811
|
e.used = ++tick;
|
|
10494
10812
|
return { frame: k, bitmap: e.bitmap, full: e.full };
|
|
10495
10813
|
},
|
|
10814
|
+
covers(frame) {
|
|
10815
|
+
if (entries.has(frame) || decoded.has(frame))
|
|
10816
|
+
return true;
|
|
10817
|
+
const p = proxyNear(frame);
|
|
10818
|
+
return p !== null && Math.abs(p - frame) <= stride / 2;
|
|
10819
|
+
},
|
|
10496
10820
|
focus(frame) {
|
|
10497
|
-
|
|
10498
|
-
active?.dropFull();
|
|
10499
|
-
if (active)
|
|
10500
|
-
active.active = false;
|
|
10501
|
-
active = core;
|
|
10502
|
-
core.active = true;
|
|
10503
|
-
}
|
|
10821
|
+
claim(core);
|
|
10504
10822
|
const moved = focusAt === null || Math.abs(frame - focusAt) > FOCUS_RADIUS;
|
|
10505
10823
|
focusAt = frame;
|
|
10824
|
+
aim = null;
|
|
10506
10825
|
if (moved)
|
|
10507
10826
|
generation++;
|
|
10508
10827
|
start();
|
|
10509
10828
|
pump(win);
|
|
10510
10829
|
},
|
|
10830
|
+
follow(frame) {
|
|
10831
|
+
claim(core);
|
|
10832
|
+
aim = frame;
|
|
10833
|
+
start();
|
|
10834
|
+
pump(win);
|
|
10835
|
+
},
|
|
10511
10836
|
want(points) {
|
|
10512
10837
|
wants = points;
|
|
10513
10838
|
if (core.shown > 0)
|
|
@@ -10545,12 +10870,15 @@ function makeCore(doc, key, src, fps) {
|
|
|
10545
10870
|
refs: 0,
|
|
10546
10871
|
shown: 0,
|
|
10547
10872
|
active: false,
|
|
10873
|
+
ran: 0,
|
|
10548
10874
|
work() {
|
|
10549
10875
|
if (disposed || backend === "pending")
|
|
10550
10876
|
return null;
|
|
10551
|
-
if (
|
|
10552
|
-
return
|
|
10553
|
-
|
|
10877
|
+
if (missing().length)
|
|
10878
|
+
return backend === "webcodecs" ? decodeUnit() : videoUnit();
|
|
10879
|
+
if (proxyTodo().length)
|
|
10880
|
+
return backend === "webcodecs" ? proxyUnit() : videoProxyUnit();
|
|
10881
|
+
return null;
|
|
10554
10882
|
},
|
|
10555
10883
|
dropFull() {
|
|
10556
10884
|
for (const [f, e] of entries)
|
|
@@ -10559,8 +10887,13 @@ function makeCore(doc, key, src, fps) {
|
|
|
10559
10887
|
entries.delete(f);
|
|
10560
10888
|
}
|
|
10561
10889
|
focusAt = null;
|
|
10890
|
+
aim = null;
|
|
10891
|
+
lastAsked = null;
|
|
10892
|
+
dropDecoded();
|
|
10562
10893
|
},
|
|
10563
10894
|
dispose() {
|
|
10895
|
+
if (disposed)
|
|
10896
|
+
return;
|
|
10564
10897
|
disposed = true;
|
|
10565
10898
|
generation++;
|
|
10566
10899
|
if (notifyRaf)
|
|
@@ -10568,6 +10901,8 @@ function makeCore(doc, key, src, fps) {
|
|
|
10568
10901
|
for (const e of entries.values())
|
|
10569
10902
|
e.bitmap.close();
|
|
10570
10903
|
entries.clear();
|
|
10904
|
+
dropDecoded();
|
|
10905
|
+
proxies.clear();
|
|
10571
10906
|
listeners.clear();
|
|
10572
10907
|
if (sampler) {
|
|
10573
10908
|
sampler.removeAttribute("src");
|
|
@@ -10575,24 +10910,45 @@ function makeCore(doc, key, src, fps) {
|
|
|
10575
10910
|
}
|
|
10576
10911
|
buf = null;
|
|
10577
10912
|
track = null;
|
|
10578
|
-
|
|
10579
|
-
|
|
10913
|
+
actives.delete(core);
|
|
10914
|
+
round2?.delete(core);
|
|
10580
10915
|
cores.delete(key);
|
|
10916
|
+
dropEncoders();
|
|
10581
10917
|
}
|
|
10582
10918
|
};
|
|
10583
10919
|
return core;
|
|
10584
10920
|
}
|
|
10585
|
-
var FOCUS_RADIUS = 6, GRID = 48, SEEN = 12, WANTED = 48, PREVIEW = 384, FULL = 1280, cores,
|
|
10921
|
+
var FOCUS_RADIUS = 6, GRID = 48, SEEN = 12, WANTED = 48, PREVIEW = 384, FULL = 1280, PROXY, aheadOf = (frame, last, total) => last === null || last === frame ? null : Math.min(Math.max(0, total - 1), Math.max(0, frame + (frame - last))), cores, actives, round2 = null, running = false, turns = 0, idle = (win) => new Promise((done) => {
|
|
10586
10922
|
const ric = win.requestIdleCallback;
|
|
10587
10923
|
if (ric)
|
|
10588
10924
|
ric(() => done(), { timeout: 60 });
|
|
10589
10925
|
else
|
|
10590
10926
|
win.setTimeout(done, 4);
|
|
10591
|
-
}), sleep = (win, ms) => new Promise((done) => win.setTimeout(done, ms));
|
|
10927
|
+
}), sleep = (win, ms) => new Promise((done) => win.setTimeout(done, ms)), ENCODER = `onmessage=async(e)=>{const{id,bitmap,quality}=e.data;try{const c=new OffscreenCanvas(bitmap.width,bitmap.height);c.getContext("2d").drawImage(bitmap,0,0);bitmap.close();postMessage({id,blob:await c.convertToBlob({type:"image/jpeg",quality})})}catch(err){postMessage({id,error:String(err)})}}`, ENCODERS = 2, encoders, encoderUsers = 0, endEncoders = (why) => {
|
|
10928
|
+
const pool = encoders;
|
|
10929
|
+
if (!pool)
|
|
10930
|
+
return;
|
|
10931
|
+
for (const w of pool.workers) {
|
|
10932
|
+
w.onmessage = null;
|
|
10933
|
+
w.onerror = null;
|
|
10934
|
+
w.terminate();
|
|
10935
|
+
}
|
|
10936
|
+
for (const j of pool.waiting.values())
|
|
10937
|
+
j.fail(new Error(why));
|
|
10938
|
+
pool.waiting.clear();
|
|
10939
|
+
};
|
|
10592
10940
|
var init_compare_frames = __esm(() => {
|
|
10593
10941
|
init_compare_model();
|
|
10594
10942
|
init_compare_mp4();
|
|
10943
|
+
PROXY = {
|
|
10944
|
+
size: 960,
|
|
10945
|
+
quality: 0.8,
|
|
10946
|
+
budget: 48 * 2 ** 20,
|
|
10947
|
+
decoded: 8,
|
|
10948
|
+
turn: 90
|
|
10949
|
+
};
|
|
10595
10950
|
cores = new Map;
|
|
10951
|
+
actives = new Set;
|
|
10596
10952
|
});
|
|
10597
10953
|
|
|
10598
10954
|
// src/ui/compare-gpu.ts
|
|
@@ -10858,1062 +11214,711 @@ var init_compare_gpu = __esm(() => {
|
|
|
10858
11214
|
lostListeners = new Set;
|
|
10859
11215
|
});
|
|
10860
11216
|
|
|
10861
|
-
// src/ui/compare
|
|
10862
|
-
function
|
|
11217
|
+
// src/ui/compare.ts
|
|
11218
|
+
function compare(container, options) {
|
|
10863
11219
|
const doc = container.ownerDocument;
|
|
10864
11220
|
const win = doc.defaultView;
|
|
10865
|
-
const
|
|
10866
|
-
const
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
11221
|
+
const caching = scrubCaching(options.scrubCache, win);
|
|
11222
|
+
const look = {
|
|
11223
|
+
...options.preset ? { preset: options.preset } : {},
|
|
11224
|
+
...options.theme ? { theme: options.theme } : {},
|
|
11225
|
+
...options.appearance ? { appearance: options.appearance } : {}
|
|
11226
|
+
};
|
|
11227
|
+
const offered = compareModes(options.modes, options.mode);
|
|
11228
|
+
const modeList = offered.modes;
|
|
11229
|
+
let mode = offered.mode;
|
|
11230
|
+
let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
|
|
11231
|
+
let loupeOn = options.loupe ?? false;
|
|
11232
|
+
let threshold = options.threshold ?? 4 / 255;
|
|
11233
|
+
let fpsOpt = options.fps;
|
|
11234
|
+
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
11235
|
+
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
11236
|
+
const auto = container.clientHeight < 1;
|
|
11237
|
+
const el = (tag, cls) => {
|
|
11238
|
+
const n = doc.createElement(tag);
|
|
11239
|
+
n.className = cls;
|
|
11240
|
+
return n;
|
|
11241
|
+
};
|
|
11242
|
+
const root = el("div", "knobs-compare");
|
|
11243
|
+
root.tabIndex = 0;
|
|
11244
|
+
root.setAttribute("role", "group");
|
|
11245
|
+
root.setAttribute("aria-roledescription", "compare");
|
|
11246
|
+
root.setAttribute("aria-label", options.label ?? "Compare");
|
|
11247
|
+
root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
|
|
10873
11248
|
applyLook2(root, options);
|
|
10874
|
-
|
|
11249
|
+
root.dataset.mode = mode;
|
|
11250
|
+
if (auto)
|
|
11251
|
+
root.dataset.auto = "";
|
|
11252
|
+
const bar = el("div", "knobs-cmp-bar");
|
|
11253
|
+
const modes = el("div", "knobs-cmp-modes");
|
|
11254
|
+
modes.setAttribute("role", "radiogroup");
|
|
11255
|
+
modes.setAttribute("aria-label", "Mode");
|
|
11256
|
+
modes.hidden = modeList.length < 2;
|
|
11257
|
+
const modeBtns = modeList.map((m, i) => {
|
|
11258
|
+
const b = el("button", "knobs-cmp-mode");
|
|
11259
|
+
b.type = "button";
|
|
11260
|
+
b.tabIndex = -1;
|
|
11261
|
+
b.textContent = MODE_LABEL[m].short;
|
|
11262
|
+
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
11263
|
+
b.setAttribute("role", "radio");
|
|
11264
|
+
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
11265
|
+
b.addEventListener("click", () => {
|
|
11266
|
+
setMode(m);
|
|
11267
|
+
root.focus({ preventScroll: true });
|
|
11268
|
+
});
|
|
11269
|
+
modes.append(b);
|
|
11270
|
+
return b;
|
|
11271
|
+
});
|
|
11272
|
+
const toggleBtn = (name, label, title) => {
|
|
11273
|
+
const b = el("button", "knobs-cmp-toggle");
|
|
11274
|
+
b.type = "button";
|
|
11275
|
+
b.tabIndex = -1;
|
|
11276
|
+
b.innerHTML = icon(name);
|
|
11277
|
+
b.setAttribute("aria-label", label);
|
|
11278
|
+
b.title = title;
|
|
11279
|
+
return b;
|
|
11280
|
+
};
|
|
11281
|
+
const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
|
|
11282
|
+
loupeBtn.addEventListener("click", () => {
|
|
11283
|
+
setLoupe(!loupeOn);
|
|
11284
|
+
root.focus({ preventScroll: true });
|
|
11285
|
+
});
|
|
11286
|
+
const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
|
|
11287
|
+
loopBtn.setAttribute("aria-keyshortcuts", "L");
|
|
11288
|
+
loopBtn.addEventListener("click", () => {
|
|
11289
|
+
api.setLoop(!loopNow());
|
|
11290
|
+
root.focus({ preventScroll: true });
|
|
11291
|
+
});
|
|
11292
|
+
bar.append(modes, el("span", "knobs-cmp-fill"), loopBtn, loupeBtn);
|
|
11293
|
+
const stage = el("div", "knobs-cmp-stage");
|
|
11294
|
+
const canvas = el("canvas", "knobs-cmp-canvas");
|
|
10875
11295
|
canvas.setAttribute("aria-hidden", "true");
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
const
|
|
10879
|
-
const
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
11296
|
+
const split = el("div", "knobs-cmp-split");
|
|
11297
|
+
split.setAttribute("aria-hidden", "true");
|
|
11298
|
+
const zoomPill = el("div", "knobs-cmp-zoom");
|
|
11299
|
+
const zoomPct = el("span", "knobs-cmp-zoom-pct");
|
|
11300
|
+
const zoomReset = el("button", "knobs-cmp-zoom-reset");
|
|
11301
|
+
zoomReset.type = "button";
|
|
11302
|
+
zoomReset.tabIndex = -1;
|
|
11303
|
+
zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
|
|
11304
|
+
zoomReset.setAttribute("aria-label", "Reset zoom");
|
|
11305
|
+
zoomReset.title = "Fit · 0 or double-click";
|
|
11306
|
+
zoomReset.addEventListener("click", () => {
|
|
11307
|
+
api.setZoom(null);
|
|
11308
|
+
root.focus({ preventScroll: true });
|
|
10889
11309
|
});
|
|
10890
|
-
|
|
10891
|
-
const
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
s.seen = true;
|
|
10901
|
-
}
|
|
10902
|
-
} else {
|
|
10903
|
-
const img = doc.createElement("img");
|
|
10904
|
-
img.crossOrigin = "anonymous";
|
|
10905
|
-
img.decoding = "async";
|
|
10906
|
-
img.addEventListener("load", () => {
|
|
10907
|
-
pairDiffs.clear();
|
|
10908
|
-
schedule();
|
|
10909
|
-
});
|
|
10910
|
-
img.src = s.source.src;
|
|
10911
|
-
s.still = img;
|
|
11310
|
+
zoomPill.append(zoomPct, zoomReset);
|
|
11311
|
+
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
11312
|
+
labels[0].dataset.side = "before";
|
|
11313
|
+
labels[1].dataset.side = "after";
|
|
11314
|
+
const setLabel = (index, text) => {
|
|
11315
|
+
const l = labels[index];
|
|
11316
|
+
if (!l.firstElementChild) {
|
|
11317
|
+
const mark = el("span", "knobs-cmp-mark");
|
|
11318
|
+
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
11319
|
+
l.append(mark, el("span", "knobs-cmp-name"));
|
|
10912
11320
|
}
|
|
11321
|
+
const name = l.lastElementChild;
|
|
11322
|
+
if (name.textContent !== text)
|
|
11323
|
+
name.textContent = text;
|
|
10913
11324
|
};
|
|
10914
|
-
const
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
11325
|
+
const seam = el("div", "knobs-cmp-seam");
|
|
11326
|
+
seam.setAttribute("aria-hidden", "true");
|
|
11327
|
+
const handle = el("div", "knobs-cmp-handle");
|
|
11328
|
+
handle.setAttribute("role", "slider");
|
|
11329
|
+
handle.setAttribute("aria-label", "Wipe");
|
|
11330
|
+
handle.setAttribute("aria-valuemin", "0");
|
|
11331
|
+
handle.setAttribute("aria-valuemax", "100");
|
|
11332
|
+
const grip = el("span", "knobs-cmp-grip");
|
|
11333
|
+
grip.setAttribute("aria-hidden", "true");
|
|
11334
|
+
handle.append(grip);
|
|
11335
|
+
const note = el("div", "knobs-cmp-note");
|
|
11336
|
+
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
11337
|
+
root.append(bar, stage);
|
|
11338
|
+
let tl = bound;
|
|
11339
|
+
let ownTimeline = false;
|
|
11340
|
+
if (!tl && options.timeline === true) {
|
|
11341
|
+
const host = el("div", "knobs-cmp-timeline-host");
|
|
11342
|
+
root.append(host);
|
|
11343
|
+
tl = timeline(host, {
|
|
11344
|
+
...look,
|
|
11345
|
+
variant: "line",
|
|
11346
|
+
duration: 1,
|
|
11347
|
+
fps: fps(),
|
|
11348
|
+
arrows: { step: 0.5, shift: 2 },
|
|
11349
|
+
label: "Compare time",
|
|
11350
|
+
...options.hints === false ? { hints: false } : {}
|
|
11351
|
+
});
|
|
11352
|
+
ownTimeline = true;
|
|
11353
|
+
}
|
|
11354
|
+
container.append(root);
|
|
11355
|
+
const sides = [null, null];
|
|
11356
|
+
let duration = 0;
|
|
11357
|
+
let renderer = "dom";
|
|
11358
|
+
let pending = false;
|
|
11359
|
+
let gpu = null;
|
|
11360
|
+
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
|
|
11361
|
+
const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
|
|
11362
|
+
let fromMode = mode;
|
|
11363
|
+
let toMode = mode;
|
|
11364
|
+
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
11365
|
+
let wipeCatching = false;
|
|
11366
|
+
const gripAt = new Spring(0.5, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
|
|
11367
|
+
let loupeHeld = false;
|
|
11368
|
+
let pointer = null;
|
|
11369
|
+
let drag = null;
|
|
11370
|
+
let tap = null;
|
|
11371
|
+
const touches = new Map;
|
|
11372
|
+
let pinch = null;
|
|
11373
|
+
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11374
|
+
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11375
|
+
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11376
|
+
let viewSent = "0.0000,0.0000,1.0000|";
|
|
11377
|
+
let ownLoop = null;
|
|
11378
|
+
let scrubbing = false;
|
|
11379
|
+
let stageW = 1;
|
|
11380
|
+
let stageH = 1;
|
|
11381
|
+
let stageRect = null;
|
|
11382
|
+
let accent = [0.49, 0.81, 0.61];
|
|
11383
|
+
let labelW = [0, 0];
|
|
11384
|
+
let frameRadius = 0;
|
|
11385
|
+
let dirty = true;
|
|
11386
|
+
let disposed = false;
|
|
11387
|
+
let near = options.lazy === false;
|
|
11388
|
+
let onScreen = options.lazy === false;
|
|
11389
|
+
let releaseTimer = 0;
|
|
11390
|
+
let ownTime = 0;
|
|
11391
|
+
let ownPlaying = false;
|
|
11392
|
+
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
11393
|
+
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
11394
|
+
const follower = () => {
|
|
11395
|
+
const m = master();
|
|
11396
|
+
const v = videoOf(sides[0]);
|
|
11397
|
+
return m && v && v !== m ? v : null;
|
|
10927
11398
|
};
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
const
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
11399
|
+
const now = () => tl ? tl.time : ownTime;
|
|
11400
|
+
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
11401
|
+
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
11402
|
+
const sizeOf = (s) => {
|
|
11403
|
+
if (s.size.w > 0)
|
|
11404
|
+
return s.size;
|
|
11405
|
+
const other = sides[s.index === 0 ? 1 : 0];
|
|
11406
|
+
if (other && other.size.w > 0)
|
|
11407
|
+
return other.size;
|
|
11408
|
+
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
10937
11409
|
};
|
|
10938
|
-
const
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
});
|
|
10943
|
-
const measure = () => {
|
|
10944
|
-
const r = root.getBoundingClientRect();
|
|
10945
|
-
w = Math.max(1, r.width);
|
|
10946
|
-
h = Math.max(1, r.height);
|
|
10947
|
-
geo = { x: 0, w };
|
|
10948
|
-
if (align === "track") {
|
|
10949
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
10950
|
-
const t = track?.getBoundingClientRect();
|
|
10951
|
-
if (t && t.width > 0)
|
|
10952
|
-
geo = { x: t.left - r.left, w: t.width };
|
|
10953
|
-
}
|
|
10954
|
-
readColors();
|
|
10955
|
-
schedule();
|
|
11410
|
+
const wantOf = (s) => {
|
|
11411
|
+
const v = s.el;
|
|
11412
|
+
const f = frameOf(now(), fps());
|
|
11413
|
+
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
10956
11414
|
};
|
|
10957
|
-
const
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
s.
|
|
11415
|
+
const emitFrame = (s, frame, source) => root.dispatchEvent(new CustomEvent("knobs-compare-frame", { detail: { side: s.index, frame, source } }));
|
|
11416
|
+
const setCached = (s, on) => {
|
|
11417
|
+
if (renderer === "dom")
|
|
11418
|
+
s.preview.style.display = on ? "" : "none";
|
|
10961
11419
|
};
|
|
10962
|
-
const
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
11420
|
+
const showVideo = (s) => {
|
|
11421
|
+
const v = s.el;
|
|
11422
|
+
if (renderer === "gpu") {
|
|
11423
|
+
if (!gpu)
|
|
11424
|
+
return;
|
|
11425
|
+
if (!gpu.upload(s.index, v, v.videoWidth, v.videoHeight))
|
|
11426
|
+
return miss(s);
|
|
11427
|
+
s.misses = 0;
|
|
11428
|
+
}
|
|
11429
|
+
const f = frameOf(v.currentTime, fps());
|
|
11430
|
+
s.shown = { frame: f, cached: false };
|
|
11431
|
+
setCached(s, false);
|
|
11432
|
+
emitFrame(s, f, "video");
|
|
11433
|
+
requestDraw();
|
|
10967
11434
|
};
|
|
10968
|
-
const
|
|
10969
|
-
|
|
10970
|
-
probe.height = DIFF.h;
|
|
10971
|
-
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
10972
|
-
const differ = (a, b) => {
|
|
10973
|
-
if (!probeCtx)
|
|
11435
|
+
const showCached = (s, c) => {
|
|
11436
|
+
if (s.shown?.cached && s.shown.frame === c.frame)
|
|
10974
11437
|
return;
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
const
|
|
10980
|
-
|
|
10981
|
-
|
|
11438
|
+
if (renderer === "gpu") {
|
|
11439
|
+
if (!gpu || !gpu.upload(2 + s.index, c.bitmap, c.bitmap.width, c.bitmap.height))
|
|
11440
|
+
return;
|
|
11441
|
+
} else if (renderer === "dom" && !pending) {
|
|
11442
|
+
const p = s.preview;
|
|
11443
|
+
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
11444
|
+
p.width = c.bitmap.width;
|
|
11445
|
+
p.height = c.bitmap.height;
|
|
11446
|
+
}
|
|
11447
|
+
p.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
11448
|
+
} else
|
|
10982
11449
|
return;
|
|
10983
|
-
}
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
10988
|
-
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
11450
|
+
s.shown = { frame: c.frame, cached: true };
|
|
11451
|
+
setCached(s, true);
|
|
11452
|
+
emitFrame(s, c.frame, "cache");
|
|
11453
|
+
requestDraw();
|
|
10989
11454
|
};
|
|
10990
|
-
const
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
10997
|
-
};
|
|
11455
|
+
const miss = (s) => {
|
|
11456
|
+
if (s.failed || ++s.misses < 3)
|
|
11457
|
+
return;
|
|
11458
|
+
gpu?.dispose();
|
|
11459
|
+
gpu = null;
|
|
11460
|
+
setRenderer("dom");
|
|
10998
11461
|
};
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
let asked = "";
|
|
11002
|
-
function schedule() {
|
|
11003
|
-
if (!raf && !disposed)
|
|
11004
|
-
raf = win.requestAnimationFrame(draw);
|
|
11005
|
-
}
|
|
11006
|
-
const draw = () => {
|
|
11007
|
-
raf = 0;
|
|
11008
|
-
if (!near)
|
|
11462
|
+
const pushFrame = (s) => {
|
|
11463
|
+
if (!s.loaded || disposed)
|
|
11009
11464
|
return;
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11465
|
+
if (s.kind === "image") {
|
|
11466
|
+
const img = s.el;
|
|
11467
|
+
if (!(img.complete && img.naturalWidth))
|
|
11468
|
+
return;
|
|
11469
|
+
if (renderer === "gpu" && gpu) {
|
|
11470
|
+
if (!gpu.upload(s.index, img, img.naturalWidth, img.naturalHeight))
|
|
11471
|
+
return miss(s);
|
|
11472
|
+
}
|
|
11473
|
+
s.shown = { frame: 0, cached: false };
|
|
11474
|
+
return requestDraw();
|
|
11016
11475
|
}
|
|
11017
|
-
const
|
|
11018
|
-
if (
|
|
11476
|
+
const v = s.el;
|
|
11477
|
+
if (v.readyState < 2)
|
|
11478
|
+
return;
|
|
11479
|
+
if (isPlaying() || !s.frames)
|
|
11480
|
+
return showVideo(s);
|
|
11481
|
+
const want = wantOf(s);
|
|
11482
|
+
const at = frameOf(v.currentTime, fps());
|
|
11483
|
+
if (at === want) {
|
|
11484
|
+
showVideo(s);
|
|
11485
|
+
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
11019
11486
|
return;
|
|
11020
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
11021
|
-
ctx.clearRect(0, 0, w, h);
|
|
11022
|
-
const cols = columns();
|
|
11023
|
-
const key = cols.map((c) => c.frame).join(",");
|
|
11024
|
-
if (key !== asked) {
|
|
11025
|
-
asked = key;
|
|
11026
|
-
ask(cols);
|
|
11027
11487
|
}
|
|
11028
|
-
const
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11488
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
11489
|
+
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
11490
|
+
showCached(s, c);
|
|
11491
|
+
else
|
|
11492
|
+
showVideo(s);
|
|
11493
|
+
};
|
|
11494
|
+
const refreshCached = (s) => {
|
|
11495
|
+
if (!s.loaded || !s.frames || isPlaying())
|
|
11496
|
+
return;
|
|
11497
|
+
const want = wantOf(s);
|
|
11498
|
+
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
11499
|
+
return;
|
|
11500
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
11501
|
+
if (!c)
|
|
11502
|
+
return;
|
|
11503
|
+
const was = s.shown ? Math.abs(s.shown.frame - want) : Number.POSITIVE_INFINITY;
|
|
11504
|
+
if (Math.abs(c.frame - want) < was)
|
|
11505
|
+
showCached(s, c);
|
|
11506
|
+
};
|
|
11507
|
+
const giveUp = (s) => {
|
|
11508
|
+
s.seek = SEEK_IDLE;
|
|
11509
|
+
if (!s.loaded || disposed)
|
|
11510
|
+
return;
|
|
11511
|
+
if (s.el.readyState >= 2)
|
|
11512
|
+
return showVideo(s);
|
|
11513
|
+
s.shown = null;
|
|
11514
|
+
setCached(s, false);
|
|
11515
|
+
requestDraw();
|
|
11516
|
+
};
|
|
11517
|
+
const makeSide = (index, source) => {
|
|
11518
|
+
const kind = mediaKind(source.src, source.kind);
|
|
11519
|
+
const media = el(kind === "video" ? "video" : "img", "knobs-cmp-media");
|
|
11520
|
+
media.dataset.side = index === 0 ? "before" : "after";
|
|
11521
|
+
media.crossOrigin = "anonymous";
|
|
11522
|
+
media.setAttribute("aria-hidden", "true");
|
|
11523
|
+
const preview = el("canvas", "knobs-cmp-media knobs-cmp-preview");
|
|
11524
|
+
preview.dataset.side = media.dataset.side;
|
|
11525
|
+
preview.style.display = "none";
|
|
11526
|
+
preview.setAttribute("aria-hidden", "true");
|
|
11527
|
+
const offs = [];
|
|
11528
|
+
const on = (type, f) => {
|
|
11529
|
+
media.addEventListener(type, f);
|
|
11530
|
+
offs.push(() => media.removeEventListener(type, f));
|
|
11531
|
+
};
|
|
11532
|
+
const side = {
|
|
11533
|
+
index,
|
|
11534
|
+
source,
|
|
11535
|
+
kind,
|
|
11536
|
+
el: media,
|
|
11537
|
+
preview,
|
|
11538
|
+
size: { w: 0, h: 0 },
|
|
11539
|
+
seek: SEEK_IDLE,
|
|
11540
|
+
loaded: false,
|
|
11541
|
+
frames: null,
|
|
11542
|
+
unlisten: null,
|
|
11543
|
+
seen: false,
|
|
11544
|
+
shown: null,
|
|
11545
|
+
frameAt: 0,
|
|
11546
|
+
misses: 0,
|
|
11547
|
+
failed: false,
|
|
11548
|
+
stall: stallGuard(win, () => giveUp(side)),
|
|
11549
|
+
stop: () => {
|
|
11550
|
+
side.stall.clear();
|
|
11551
|
+
for (const f of offs)
|
|
11552
|
+
f();
|
|
11044
11553
|
}
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
return d;
|
|
11049
|
-
});
|
|
11050
|
-
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
11051
|
-
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
11052
|
-
cols.forEach((c, i) => {
|
|
11053
|
-
const x = geo.x + c.x + GAP / 2;
|
|
11054
|
-
const cwid = Math.max(1, c.w - GAP);
|
|
11055
|
-
if (x + cwid < 0 || x > w)
|
|
11554
|
+
};
|
|
11555
|
+
const fail = () => {
|
|
11556
|
+
if (!side.loaded)
|
|
11056
11557
|
return;
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11558
|
+
side.failed = true;
|
|
11559
|
+
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
11560
|
+
side.stall.clear();
|
|
11561
|
+
side.seek = SEEK_IDLE;
|
|
11562
|
+
side.shown = null;
|
|
11563
|
+
setCached(side, false);
|
|
11564
|
+
};
|
|
11565
|
+
if (media instanceof HTMLVideoElement) {
|
|
11566
|
+
const v = media;
|
|
11567
|
+
v.muted = true;
|
|
11568
|
+
v.defaultMuted = true;
|
|
11569
|
+
v.playsInline = true;
|
|
11570
|
+
v.preload = "auto";
|
|
11571
|
+
v.setAttribute("playsinline", "");
|
|
11572
|
+
v.setAttribute("muted", "");
|
|
11573
|
+
on("loadedmetadata", () => {
|
|
11574
|
+
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
11575
|
+
measureDuration();
|
|
11576
|
+
if (auto)
|
|
11577
|
+
measure();
|
|
11578
|
+
if (isPlaying())
|
|
11579
|
+
jumpAll(now());
|
|
11580
|
+
else
|
|
11581
|
+
showFrame(now(), false);
|
|
11582
|
+
requestDraw();
|
|
11583
|
+
});
|
|
11584
|
+
on("resize", () => {
|
|
11585
|
+
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
11586
|
+
requestDraw();
|
|
11587
|
+
});
|
|
11588
|
+
on("loadeddata", () => pushFrame(side));
|
|
11589
|
+
on("seeked", () => {
|
|
11590
|
+
const step = seeked(side.seek, v.currentTime, fps());
|
|
11591
|
+
side.seek = step.state;
|
|
11592
|
+
if (step.apply !== null)
|
|
11593
|
+
seekVideo(side, v, step.apply);
|
|
11594
|
+
else
|
|
11595
|
+
side.stall.clear();
|
|
11596
|
+
pushFrame(side);
|
|
11597
|
+
});
|
|
11598
|
+
on("error", fail);
|
|
11599
|
+
if (v.requestVideoFrameCallback) {
|
|
11600
|
+
let id = 0;
|
|
11601
|
+
const frame = () => {
|
|
11602
|
+
side.frameAt = performance.now();
|
|
11603
|
+
pushFrame(side);
|
|
11604
|
+
id = v.requestVideoFrameCallback(frame);
|
|
11605
|
+
};
|
|
11606
|
+
id = v.requestVideoFrameCallback(frame);
|
|
11607
|
+
offs.push(() => v.cancelVideoFrameCallback?.(id));
|
|
11087
11608
|
}
|
|
11088
|
-
}
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11609
|
+
} else {
|
|
11610
|
+
on("load", () => {
|
|
11611
|
+
side.size = { w: media.naturalWidth, h: media.naturalHeight };
|
|
11612
|
+
if (auto)
|
|
11613
|
+
measure();
|
|
11614
|
+
pushFrame(side);
|
|
11615
|
+
requestDraw();
|
|
11616
|
+
});
|
|
11617
|
+
on("error", fail);
|
|
11618
|
+
media.decoding = "async";
|
|
11094
11619
|
}
|
|
11095
|
-
|
|
11096
|
-
schedule();
|
|
11097
|
-
};
|
|
11098
|
-
const timeAt = (clientX) => {
|
|
11099
|
-
const r = root.getBoundingClientRect();
|
|
11100
|
-
const v = timeline.view;
|
|
11101
|
-
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
11102
|
-
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
11103
|
-
};
|
|
11104
|
-
const columnAt = (clientX) => {
|
|
11105
|
-
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
11106
|
-
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
11107
|
-
return i < 0 ? null : i;
|
|
11620
|
+
return side;
|
|
11108
11621
|
};
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11622
|
+
const loadSide = (s) => {
|
|
11623
|
+
if (s.loaded || disposed)
|
|
11624
|
+
return;
|
|
11625
|
+
s.loaded = true;
|
|
11626
|
+
s.failed = false;
|
|
11627
|
+
s.el.src = s.source.src;
|
|
11628
|
+
if (s.kind === "video" && caching) {
|
|
11629
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
11630
|
+
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
11631
|
+
if (onScreen) {
|
|
11632
|
+
s.frames.visible(true);
|
|
11633
|
+
s.seen = true;
|
|
11634
|
+
}
|
|
11635
|
+
}
|
|
11114
11636
|
};
|
|
11115
|
-
|
|
11116
|
-
if (
|
|
11637
|
+
const unloadSide = (s) => {
|
|
11638
|
+
if (!s.loaded)
|
|
11117
11639
|
return;
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
if (i !== hover) {
|
|
11135
|
-
hover = i;
|
|
11136
|
-
schedule();
|
|
11640
|
+
s.loaded = false;
|
|
11641
|
+
s.el.removeAttribute("src");
|
|
11642
|
+
if (s.el instanceof HTMLVideoElement)
|
|
11643
|
+
s.el.load();
|
|
11644
|
+
s.seek = SEEK_IDLE;
|
|
11645
|
+
s.stall.clear();
|
|
11646
|
+
s.shown = null;
|
|
11647
|
+
setCached(s, false);
|
|
11648
|
+
s.unlisten?.();
|
|
11649
|
+
s.unlisten = null;
|
|
11650
|
+
if (s.frames) {
|
|
11651
|
+
if (s.seen)
|
|
11652
|
+
s.frames.visible(false);
|
|
11653
|
+
s.seen = false;
|
|
11654
|
+
s.frames.release();
|
|
11655
|
+
s.frames = null;
|
|
11137
11656
|
}
|
|
11138
|
-
|
|
11139
|
-
const end = () => {
|
|
11140
|
-
dragging = false;
|
|
11657
|
+
gpu?.clear(s.index);
|
|
11141
11658
|
};
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11659
|
+
const measureDuration = () => {
|
|
11660
|
+
const next = Math.max(0, ...sides.map((s) => {
|
|
11661
|
+
const v = videoOf(s);
|
|
11662
|
+
return v && Number.isFinite(v.duration) ? v.duration : 0;
|
|
11663
|
+
}));
|
|
11664
|
+
if (next <= 0 || next === duration)
|
|
11147
11665
|
return;
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
const
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
const setOnScreen = (on) => {
|
|
11159
|
-
onScreen = on;
|
|
11666
|
+
duration = next;
|
|
11667
|
+
if (ownTimeline && tl)
|
|
11668
|
+
tl.update({ duration, fps: fps() });
|
|
11669
|
+
};
|
|
11670
|
+
const setRenderer = (next) => {
|
|
11671
|
+
pending = false;
|
|
11672
|
+
renderer = next;
|
|
11673
|
+
root.dataset.renderer = next;
|
|
11674
|
+
loupeBtn.hidden = next !== "gpu";
|
|
11675
|
+
canvas.style.display = next === "gpu" ? "" : "none";
|
|
11160
11676
|
for (const s of sides) {
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11677
|
+
const st = s.el.style;
|
|
11678
|
+
st.clipPath = "";
|
|
11679
|
+
st.maskImage = "";
|
|
11680
|
+
st.setProperty("-webkit-mask-image", "");
|
|
11681
|
+
st.mixBlendMode = "";
|
|
11682
|
+
st.opacity = "";
|
|
11683
|
+
s.shown = null;
|
|
11684
|
+
setCached(s, false);
|
|
11165
11685
|
}
|
|
11686
|
+
for (const s of sides)
|
|
11687
|
+
pushFrame(s);
|
|
11688
|
+
requestDraw();
|
|
11166
11689
|
};
|
|
11167
|
-
const
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11690
|
+
const startGpu = async (retry) => {
|
|
11691
|
+
const g = await createCompareGpu(canvas, () => {
|
|
11692
|
+
gpu = null;
|
|
11693
|
+
if (disposed)
|
|
11694
|
+
return;
|
|
11695
|
+
if (retry)
|
|
11696
|
+
win.setTimeout(() => void startGpu(false), 150);
|
|
11697
|
+
else
|
|
11698
|
+
setRenderer("dom");
|
|
11699
|
+
});
|
|
11700
|
+
if (disposed)
|
|
11701
|
+
return g?.dispose();
|
|
11702
|
+
if (!g)
|
|
11703
|
+
return setRenderer("dom");
|
|
11704
|
+
gpu = g;
|
|
11705
|
+
setRenderer("gpu");
|
|
11176
11706
|
};
|
|
11177
|
-
const
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
}
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
next.before ? makeSide(next.before) : was[0],
|
|
11195
|
-
next.after ? makeSide(next.after) : was[1]
|
|
11196
|
-
];
|
|
11197
|
-
for (const s of was)
|
|
11198
|
-
if (!sides.includes(s))
|
|
11199
|
-
unload(s);
|
|
11200
|
-
diffs.clear();
|
|
11201
|
-
pairDiffs.clear();
|
|
11202
|
-
asked = "";
|
|
11203
|
-
if (near)
|
|
11204
|
-
for (const s of sides)
|
|
11205
|
-
load(s);
|
|
11206
|
-
if (onScreen)
|
|
11207
|
-
setOnScreen(true);
|
|
11208
|
-
schedule();
|
|
11209
|
-
},
|
|
11210
|
-
dispose() {
|
|
11211
|
-
disposed = true;
|
|
11212
|
-
if (raf)
|
|
11213
|
-
win.cancelAnimationFrame(raf);
|
|
11214
|
-
unlisten();
|
|
11215
|
-
ro.disconnect();
|
|
11216
|
-
nearIo?.disconnect();
|
|
11217
|
-
screenIo?.disconnect();
|
|
11218
|
-
for (const s of sides)
|
|
11219
|
-
unload(s);
|
|
11220
|
-
root.remove();
|
|
11707
|
+
const syncModeUi = () => {
|
|
11708
|
+
root.dataset.mode = mode;
|
|
11709
|
+
modeList.forEach((m, i) => {
|
|
11710
|
+
modeBtns[i].setAttribute("aria-checked", String(m === mode));
|
|
11711
|
+
});
|
|
11712
|
+
handle.tabIndex = mode === "wipe" ? 0 : -1;
|
|
11713
|
+
modePill?.update();
|
|
11714
|
+
};
|
|
11715
|
+
const setMode = (next, emit = true) => {
|
|
11716
|
+
if (next === mode || !modeList.includes(next))
|
|
11717
|
+
return;
|
|
11718
|
+
mode = next;
|
|
11719
|
+
const reduced = reducedMotion(win);
|
|
11720
|
+
tune(fade, reduced ? FADE_REDUCED : FADE6);
|
|
11721
|
+
if (fade.x >= 0.5) {
|
|
11722
|
+
fromMode = toMode;
|
|
11723
|
+
fade.snap(0);
|
|
11221
11724
|
}
|
|
11725
|
+
toMode = next;
|
|
11726
|
+
fade.set(1);
|
|
11727
|
+
if (reduced)
|
|
11728
|
+
lay.snap(next === "side" ? 0 : 1);
|
|
11729
|
+
else
|
|
11730
|
+
lay.set(next === "side" ? 0 : 1);
|
|
11731
|
+
syncModeUi();
|
|
11732
|
+
if (emit)
|
|
11733
|
+
options.onMode?.(next);
|
|
11734
|
+
requestDraw();
|
|
11222
11735
|
};
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11736
|
+
const setWipe = (next, emit = true, jump = false) => {
|
|
11737
|
+
const w = Math.min(1, Math.max(0, next));
|
|
11738
|
+
if (w === wipe && !jump)
|
|
11739
|
+
return;
|
|
11740
|
+
wipe = w;
|
|
11741
|
+
const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
|
|
11742
|
+
const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
|
|
11743
|
+
wipeCatching = head.catching;
|
|
11744
|
+
if (head.catching) {
|
|
11745
|
+
wipeLine.x = head.x;
|
|
11746
|
+
wipeLine.target = head.target;
|
|
11747
|
+
} else
|
|
11748
|
+
wipeLine.snap(head.x);
|
|
11749
|
+
if (emit)
|
|
11750
|
+
options.onWipe?.(w);
|
|
11751
|
+
if (head.catching)
|
|
11752
|
+
requestDraw();
|
|
11753
|
+
else
|
|
11754
|
+
drawNow();
|
|
11242
11755
|
};
|
|
11243
|
-
const
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
let loupeOn = options.loupe ?? false;
|
|
11248
|
-
let threshold = options.threshold ?? 4 / 255;
|
|
11249
|
-
let fpsOpt = options.fps;
|
|
11250
|
-
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
11251
|
-
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
11252
|
-
const auto = container.clientHeight < 1;
|
|
11253
|
-
const el = (tag, cls) => {
|
|
11254
|
-
const n = doc.createElement(tag);
|
|
11255
|
-
n.className = cls;
|
|
11256
|
-
return n;
|
|
11756
|
+
const setLoupe = (on) => {
|
|
11757
|
+
loupeOn = on;
|
|
11758
|
+
loupeBtn.setAttribute("aria-pressed", String(on));
|
|
11759
|
+
requestDraw();
|
|
11257
11760
|
};
|
|
11258
|
-
const
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
root.dataset.auto = "";
|
|
11268
|
-
const bar = el("div", "knobs-cmp-bar");
|
|
11269
|
-
const modes = el("div", "knobs-cmp-modes");
|
|
11270
|
-
modes.setAttribute("role", "radiogroup");
|
|
11271
|
-
modes.setAttribute("aria-label", "Mode");
|
|
11272
|
-
modes.hidden = modeList.length < 2;
|
|
11273
|
-
const modeBtns = modeList.map((m, i) => {
|
|
11274
|
-
const b = el("button", "knobs-cmp-mode");
|
|
11275
|
-
b.type = "button";
|
|
11276
|
-
b.tabIndex = -1;
|
|
11277
|
-
b.textContent = MODE_LABEL[m].short;
|
|
11278
|
-
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
11279
|
-
b.setAttribute("role", "radio");
|
|
11280
|
-
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
11281
|
-
b.addEventListener("click", () => {
|
|
11282
|
-
setMode(m);
|
|
11283
|
-
root.focus({ preventScroll: true });
|
|
11284
|
-
});
|
|
11285
|
-
modes.append(b);
|
|
11286
|
-
return b;
|
|
11287
|
-
});
|
|
11288
|
-
const toggleBtn = (name, label, title) => {
|
|
11289
|
-
const b = el("button", "knobs-cmp-toggle");
|
|
11290
|
-
b.type = "button";
|
|
11291
|
-
b.tabIndex = -1;
|
|
11292
|
-
b.innerHTML = icon(name);
|
|
11293
|
-
b.setAttribute("aria-label", label);
|
|
11294
|
-
b.title = title;
|
|
11295
|
-
return b;
|
|
11761
|
+
const clampTime = (v, t) => Math.min(Math.max(0, t), Math.max(0, (Number.isFinite(v.duration) ? v.duration : t) - 0.001));
|
|
11762
|
+
const seekVideo = (s, v, t) => {
|
|
11763
|
+
const c = s.frames?.nearest(wantOf(s));
|
|
11764
|
+
const far = !c || Math.abs(c.frame - wantOf(s)) > FAST_SEEK_FRAMES;
|
|
11765
|
+
if (scrubbing && far && v.fastSeek)
|
|
11766
|
+
v.fastSeek(t);
|
|
11767
|
+
else
|
|
11768
|
+
v.currentTime = t;
|
|
11769
|
+
s.stall.arm();
|
|
11296
11770
|
};
|
|
11297
|
-
const
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
api.setZoom(null);
|
|
11324
|
-
root.focus({ preventScroll: true });
|
|
11325
|
-
});
|
|
11326
|
-
zoomPill.append(zoomPct, zoomReset);
|
|
11327
|
-
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
11328
|
-
labels[0].dataset.side = "before";
|
|
11329
|
-
labels[1].dataset.side = "after";
|
|
11330
|
-
const setLabel = (index, text) => {
|
|
11331
|
-
const l = labels[index];
|
|
11332
|
-
if (!l.firstElementChild) {
|
|
11333
|
-
const mark = el("span", "knobs-cmp-mark");
|
|
11334
|
-
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
11335
|
-
l.append(mark, el("span", "knobs-cmp-name"));
|
|
11771
|
+
const showFrame = (t, used) => {
|
|
11772
|
+
for (const s of sides) {
|
|
11773
|
+
const v = videoOf(s);
|
|
11774
|
+
if (!v || v.readyState < 1)
|
|
11775
|
+
continue;
|
|
11776
|
+
if (!v.paused) {
|
|
11777
|
+
v.pause();
|
|
11778
|
+
v.playbackRate = 1;
|
|
11779
|
+
}
|
|
11780
|
+
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
11781
|
+
if (used) {
|
|
11782
|
+
if (scrubbing)
|
|
11783
|
+
s.frames?.follow(want);
|
|
11784
|
+
else
|
|
11785
|
+
s.frames?.focus(want);
|
|
11786
|
+
}
|
|
11787
|
+
if (!(scrubbing && s.frames?.covers(want))) {
|
|
11788
|
+
const at = frameMidpoint(want, fps(), v.duration);
|
|
11789
|
+
const step = requestSeek(s.seek, at, v.currentTime, fps());
|
|
11790
|
+
s.seek = step.state;
|
|
11791
|
+
if (step.apply !== null)
|
|
11792
|
+
seekVideo(s, v, step.apply);
|
|
11793
|
+
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
11794
|
+
pushFrame(s);
|
|
11795
|
+
}
|
|
11796
|
+
refreshCached(s);
|
|
11336
11797
|
}
|
|
11337
|
-
|
|
11338
|
-
if (name.textContent !== text)
|
|
11339
|
-
name.textContent = text;
|
|
11340
|
-
};
|
|
11341
|
-
const seam = el("div", "knobs-cmp-seam");
|
|
11342
|
-
seam.setAttribute("aria-hidden", "true");
|
|
11343
|
-
const handle = el("div", "knobs-cmp-handle");
|
|
11344
|
-
handle.setAttribute("role", "slider");
|
|
11345
|
-
handle.setAttribute("aria-label", "Wipe");
|
|
11346
|
-
handle.setAttribute("aria-valuemin", "0");
|
|
11347
|
-
handle.setAttribute("aria-valuemax", "100");
|
|
11348
|
-
const grip = el("span", "knobs-cmp-grip");
|
|
11349
|
-
grip.setAttribute("aria-hidden", "true");
|
|
11350
|
-
handle.append(grip);
|
|
11351
|
-
const note = el("div", "knobs-cmp-note");
|
|
11352
|
-
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
11353
|
-
root.append(bar, stage);
|
|
11354
|
-
const wantsStrip = options.strip === true;
|
|
11355
|
-
let tl = bound;
|
|
11356
|
-
let ownTimeline = false;
|
|
11357
|
-
let stripHost = null;
|
|
11358
|
-
if (wantsStrip) {
|
|
11359
|
-
stripHost = el("div", "knobs-cmp-strip-host");
|
|
11360
|
-
root.append(stripHost);
|
|
11361
|
-
}
|
|
11362
|
-
if (!tl && (options.timeline === true || wantsStrip)) {
|
|
11363
|
-
const host = el("div", "knobs-cmp-timeline-host");
|
|
11364
|
-
root.append(host);
|
|
11365
|
-
tl = timeline(host, {
|
|
11366
|
-
...look,
|
|
11367
|
-
variant: "line",
|
|
11368
|
-
duration: 1,
|
|
11369
|
-
fps: fps(),
|
|
11370
|
-
arrows: { step: 0.5, shift: 2 },
|
|
11371
|
-
label: "Compare time",
|
|
11372
|
-
...options.hints === false ? { hints: false } : {}
|
|
11373
|
-
});
|
|
11374
|
-
ownTimeline = true;
|
|
11375
|
-
}
|
|
11376
|
-
container.append(root);
|
|
11377
|
-
const sides = [null, null];
|
|
11378
|
-
let duration = 0;
|
|
11379
|
-
let renderer = "dom";
|
|
11380
|
-
let pending = false;
|
|
11381
|
-
let gpu = null;
|
|
11382
|
-
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
|
|
11383
|
-
const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
|
|
11384
|
-
let fromMode = mode;
|
|
11385
|
-
let toMode = mode;
|
|
11386
|
-
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
11387
|
-
let wipeCatching = false;
|
|
11388
|
-
const gripAt = new Spring(0.5, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
|
|
11389
|
-
let loupeHeld = false;
|
|
11390
|
-
let pointer = null;
|
|
11391
|
-
let drag = null;
|
|
11392
|
-
let tap = null;
|
|
11393
|
-
const touches = new Map;
|
|
11394
|
-
let pinch = null;
|
|
11395
|
-
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11396
|
-
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11397
|
-
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11398
|
-
let viewSent = "0.0000,0.0000,1.0000|";
|
|
11399
|
-
let ownLoop = null;
|
|
11400
|
-
let scrubbing = false;
|
|
11401
|
-
let stageW = 1;
|
|
11402
|
-
let stageH = 1;
|
|
11403
|
-
let stageRect = null;
|
|
11404
|
-
let accent = [0.49, 0.81, 0.61];
|
|
11405
|
-
let labelW = [0, 0];
|
|
11406
|
-
let frameRadius = 0;
|
|
11407
|
-
let dirty = true;
|
|
11408
|
-
let disposed = false;
|
|
11409
|
-
let near = options.lazy === false;
|
|
11410
|
-
let onScreen = options.lazy === false;
|
|
11411
|
-
let releaseTimer = 0;
|
|
11412
|
-
let ownTime = 0;
|
|
11413
|
-
let ownPlaying = false;
|
|
11414
|
-
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
11415
|
-
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
11416
|
-
const follower = () => {
|
|
11417
|
-
const m = master();
|
|
11418
|
-
const v = videoOf(sides[0]);
|
|
11419
|
-
return m && v && v !== m ? v : null;
|
|
11420
|
-
};
|
|
11421
|
-
const now = () => tl ? tl.time : ownTime;
|
|
11422
|
-
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
11423
|
-
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
11424
|
-
const sizeOf = (s) => {
|
|
11425
|
-
if (s.size.w > 0)
|
|
11426
|
-
return s.size;
|
|
11427
|
-
const other = sides[s.index === 0 ? 1 : 0];
|
|
11428
|
-
if (other && other.size.w > 0)
|
|
11429
|
-
return other.size;
|
|
11430
|
-
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
11431
|
-
};
|
|
11432
|
-
const wantOf = (s) => {
|
|
11433
|
-
const v = s.el;
|
|
11434
|
-
const f = frameOf(now(), fps());
|
|
11435
|
-
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
11798
|
+
requestDraw();
|
|
11436
11799
|
};
|
|
11437
|
-
const
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11800
|
+
const jumpAll = (t) => {
|
|
11801
|
+
for (const s of sides) {
|
|
11802
|
+
const v = videoOf(s);
|
|
11803
|
+
if (!v || v.readyState < 1)
|
|
11804
|
+
continue;
|
|
11805
|
+
s.seek = SEEK_IDLE;
|
|
11806
|
+
v.currentTime = clampTime(v, t);
|
|
11807
|
+
if (v.paused)
|
|
11808
|
+
v.play().catch(() => {});
|
|
11809
|
+
}
|
|
11441
11810
|
};
|
|
11442
|
-
const
|
|
11443
|
-
const
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11811
|
+
const startVideos = () => {
|
|
11812
|
+
const t = now();
|
|
11813
|
+
for (const s of sides) {
|
|
11814
|
+
const v = videoOf(s);
|
|
11815
|
+
if (!v)
|
|
11816
|
+
continue;
|
|
11817
|
+
s.seek = SEEK_IDLE;
|
|
11818
|
+
v.playbackRate = 1;
|
|
11819
|
+
if (v.readyState >= 1 && Math.abs(v.currentTime - t) > 1.5 / fps())
|
|
11820
|
+
v.currentTime = clampTime(v, t);
|
|
11821
|
+
v.play().catch(() => {});
|
|
11450
11822
|
}
|
|
11451
|
-
|
|
11452
|
-
s.shown = { frame: f, cached: false };
|
|
11453
|
-
setCached(s, false);
|
|
11454
|
-
emitFrame(s, f, "video");
|
|
11455
|
-
requestDraw();
|
|
11823
|
+
loop.wake();
|
|
11456
11824
|
};
|
|
11457
|
-
const
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
} else if (renderer === "dom" && !pending) {
|
|
11464
|
-
const p = s.preview;
|
|
11465
|
-
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
11466
|
-
p.width = c.bitmap.width;
|
|
11467
|
-
p.height = c.bitmap.height;
|
|
11825
|
+
const pauseVideos = () => {
|
|
11826
|
+
for (const s of sides) {
|
|
11827
|
+
const v = videoOf(s);
|
|
11828
|
+
if (v) {
|
|
11829
|
+
v.pause();
|
|
11830
|
+
v.playbackRate = 1;
|
|
11468
11831
|
}
|
|
11469
|
-
|
|
11470
|
-
} else
|
|
11471
|
-
return;
|
|
11472
|
-
s.shown = { frame: c.frame, cached: true };
|
|
11473
|
-
setCached(s, true);
|
|
11474
|
-
emitFrame(s, c.frame, "cache");
|
|
11475
|
-
requestDraw();
|
|
11832
|
+
}
|
|
11476
11833
|
};
|
|
11477
|
-
const
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
gpu?.dispose();
|
|
11481
|
-
gpu = null;
|
|
11482
|
-
setRenderer("dom");
|
|
11834
|
+
const stopVideos = () => {
|
|
11835
|
+
pauseVideos();
|
|
11836
|
+
showFrame(now(), false);
|
|
11483
11837
|
};
|
|
11484
|
-
const
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
return requestDraw();
|
|
11497
|
-
}
|
|
11498
|
-
const v = s.el;
|
|
11499
|
-
if (v.readyState < 2)
|
|
11500
|
-
return;
|
|
11501
|
-
if (isPlaying() || !s.frames)
|
|
11502
|
-
return showVideo(s);
|
|
11503
|
-
const want = wantOf(s);
|
|
11504
|
-
const at = frameOf(v.currentTime, fps());
|
|
11505
|
-
if (at === want) {
|
|
11506
|
-
showVideo(s);
|
|
11507
|
-
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
11508
|
-
return;
|
|
11509
|
-
}
|
|
11510
|
-
const c = s.frames.nearest(want);
|
|
11511
|
-
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
11512
|
-
showCached(s, c);
|
|
11838
|
+
const clock = () => {
|
|
11839
|
+
const m = master();
|
|
11840
|
+
if (!m || !near)
|
|
11841
|
+
return Number.NaN;
|
|
11842
|
+
if (m.readyState < 1 || m.seeking)
|
|
11843
|
+
return tl.time;
|
|
11844
|
+
return m.ended ? Number.POSITIVE_INFINITY : m.currentTime;
|
|
11845
|
+
};
|
|
11846
|
+
const ownSeek = (t) => {
|
|
11847
|
+
ownTime = Math.min(Math.max(0, t), duration);
|
|
11848
|
+
if (ownPlaying)
|
|
11849
|
+
jumpAll(ownTime);
|
|
11513
11850
|
else
|
|
11514
|
-
|
|
11851
|
+
showFrame(ownTime, true);
|
|
11515
11852
|
};
|
|
11516
|
-
const
|
|
11517
|
-
if (
|
|
11518
|
-
return;
|
|
11519
|
-
const want = wantOf(s);
|
|
11520
|
-
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
11853
|
+
const ownPlay = () => {
|
|
11854
|
+
if (ownPlaying || !master())
|
|
11521
11855
|
return;
|
|
11522
|
-
|
|
11523
|
-
|
|
11856
|
+
if (ownLoop && (ownTime < ownLoop.start || ownTime >= ownLoop.end - 0.5 / fps()))
|
|
11857
|
+
ownTime = ownLoop.start;
|
|
11858
|
+
else if (!ownLoop && ownTime >= duration - 0.5 / fps())
|
|
11859
|
+
ownTime = 0;
|
|
11860
|
+
ownPlaying = true;
|
|
11861
|
+
startVideos();
|
|
11862
|
+
};
|
|
11863
|
+
const ownPause = () => {
|
|
11864
|
+
if (!ownPlaying)
|
|
11524
11865
|
return;
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11866
|
+
ownPlaying = false;
|
|
11867
|
+
ownTime = Math.min(duration, frameOf(ownTime, fps()) / fps());
|
|
11868
|
+
stopVideos();
|
|
11528
11869
|
};
|
|
11529
|
-
const
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
const
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
const offs = [];
|
|
11540
|
-
const on = (type, f) => {
|
|
11541
|
-
media.addEventListener(type, f);
|
|
11542
|
-
offs.push(() => media.removeEventListener(type, f));
|
|
11543
|
-
};
|
|
11544
|
-
const side = {
|
|
11545
|
-
index,
|
|
11546
|
-
source,
|
|
11547
|
-
kind,
|
|
11548
|
-
el: media,
|
|
11549
|
-
preview,
|
|
11550
|
-
size: { w: 0, h: 0 },
|
|
11551
|
-
seek: SEEK_IDLE,
|
|
11552
|
-
loaded: false,
|
|
11553
|
-
frames: null,
|
|
11554
|
-
unlisten: null,
|
|
11555
|
-
seen: false,
|
|
11556
|
-
shown: null,
|
|
11557
|
-
frameAt: 0,
|
|
11558
|
-
misses: 0,
|
|
11559
|
-
failed: false,
|
|
11560
|
-
stop: () => {
|
|
11561
|
-
for (const f of offs)
|
|
11562
|
-
f();
|
|
11563
|
-
}
|
|
11564
|
-
};
|
|
11565
|
-
const fail = () => {
|
|
11566
|
-
if (!side.loaded)
|
|
11567
|
-
return;
|
|
11568
|
-
side.failed = true;
|
|
11569
|
-
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
11570
|
-
};
|
|
11571
|
-
if (media instanceof HTMLVideoElement) {
|
|
11572
|
-
const v = media;
|
|
11573
|
-
v.muted = true;
|
|
11574
|
-
v.defaultMuted = true;
|
|
11575
|
-
v.playsInline = true;
|
|
11576
|
-
v.preload = "auto";
|
|
11577
|
-
v.setAttribute("playsinline", "");
|
|
11578
|
-
v.setAttribute("muted", "");
|
|
11579
|
-
on("loadedmetadata", () => {
|
|
11580
|
-
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
11581
|
-
measureDuration();
|
|
11582
|
-
if (auto)
|
|
11583
|
-
measure();
|
|
11584
|
-
if (isPlaying())
|
|
11585
|
-
jumpAll(now());
|
|
11586
|
-
else
|
|
11587
|
-
showFrame(now(), false);
|
|
11588
|
-
requestDraw();
|
|
11589
|
-
});
|
|
11590
|
-
on("resize", () => {
|
|
11591
|
-
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
11592
|
-
requestDraw();
|
|
11593
|
-
});
|
|
11594
|
-
on("loadeddata", () => pushFrame(side));
|
|
11595
|
-
on("seeked", () => {
|
|
11596
|
-
const step = seeked(side.seek, v.currentTime, fps());
|
|
11597
|
-
side.seek = step.state;
|
|
11598
|
-
if (step.apply !== null)
|
|
11599
|
-
seekVideo(side, v, step.apply);
|
|
11600
|
-
pushFrame(side);
|
|
11601
|
-
});
|
|
11602
|
-
on("error", fail);
|
|
11603
|
-
if (v.requestVideoFrameCallback) {
|
|
11604
|
-
let id = 0;
|
|
11605
|
-
const frame = () => {
|
|
11606
|
-
side.frameAt = performance.now();
|
|
11607
|
-
pushFrame(side);
|
|
11608
|
-
id = v.requestVideoFrameCallback(frame);
|
|
11609
|
-
};
|
|
11610
|
-
id = v.requestVideoFrameCallback(frame);
|
|
11611
|
-
offs.push(() => v.cancelVideoFrameCallback?.(id));
|
|
11612
|
-
}
|
|
11613
|
-
} else {
|
|
11614
|
-
on("load", () => {
|
|
11615
|
-
side.size = { w: media.naturalWidth, h: media.naturalHeight };
|
|
11616
|
-
if (auto)
|
|
11617
|
-
measure();
|
|
11618
|
-
pushFrame(side);
|
|
11619
|
-
requestDraw();
|
|
11620
|
-
});
|
|
11621
|
-
on("error", fail);
|
|
11622
|
-
media.decoding = "async";
|
|
11623
|
-
}
|
|
11624
|
-
return side;
|
|
11870
|
+
const zoomNow = () => ({ u: zu.x, v: zv.x, w: zw.x, h: zw.x });
|
|
11871
|
+
const zoomed = () => zw.target < 0.999;
|
|
11872
|
+
const layoutNow = () => {
|
|
11873
|
+
const box = { x: 0, y: 0, w: stageW, h: stageH };
|
|
11874
|
+
const L = compareLayout(box, sizeOf(sides[0]), sizeOf(sides[1]));
|
|
11875
|
+
const k = lay.x;
|
|
11876
|
+
const before = lerpRect(L.side.before, L.overlay.before, k);
|
|
11877
|
+
const after = lerpRect(L.side.after, L.overlay.after, k);
|
|
11878
|
+
const z = zoomNow();
|
|
11879
|
+
return { L, before, after, zBefore: zoomedRect(before, z), zAfter: zoomedRect(after, z) };
|
|
11625
11880
|
};
|
|
11626
|
-
const
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
if (s.kind === "video") {
|
|
11633
|
-
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
11634
|
-
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
11635
|
-
if (onScreen) {
|
|
11636
|
-
s.frames.visible(true);
|
|
11637
|
-
s.seen = true;
|
|
11638
|
-
}
|
|
11639
|
-
}
|
|
11881
|
+
const wipeRect = (r) => {
|
|
11882
|
+
const x = Math.min(r.before.x, r.after.x);
|
|
11883
|
+
const y = Math.min(r.before.y, r.after.y);
|
|
11884
|
+
const x1 = Math.max(r.before.x + r.before.w, r.after.x + r.after.w);
|
|
11885
|
+
const y1 = Math.max(r.before.y + r.before.h, r.after.y + r.after.h);
|
|
11886
|
+
return { x, y, w: x1 - x, h: y1 - y };
|
|
11640
11887
|
};
|
|
11641
|
-
const
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
s.el.removeAttribute("src");
|
|
11646
|
-
if (s.el instanceof HTMLVideoElement)
|
|
11647
|
-
s.el.load();
|
|
11648
|
-
s.seek = SEEK_IDLE;
|
|
11649
|
-
s.shown = null;
|
|
11650
|
-
setCached(s, false);
|
|
11651
|
-
s.unlisten?.();
|
|
11652
|
-
s.unlisten = null;
|
|
11653
|
-
if (s.frames) {
|
|
11654
|
-
if (s.seen)
|
|
11655
|
-
s.frames.visible(false);
|
|
11656
|
-
s.seen = false;
|
|
11657
|
-
s.frames.release();
|
|
11658
|
-
s.frames = null;
|
|
11659
|
-
}
|
|
11660
|
-
gpu?.clear(s.index);
|
|
11888
|
+
const baseAt = (p) => {
|
|
11889
|
+
const r = layoutNow();
|
|
11890
|
+
const inside = (b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
|
|
11891
|
+
return inside(r.after) || !inside(r.before) ? r.after : r.before;
|
|
11661
11892
|
};
|
|
11662
|
-
const
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
return
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
return;
|
|
11669
|
-
duration = next;
|
|
11670
|
-
if (ownTimeline && tl)
|
|
11671
|
-
tl.update({ duration, fps: fps() });
|
|
11893
|
+
const loupeShown = () => renderer === "gpu" && (loupeOn || loupeHeld) && pointer !== null;
|
|
11894
|
+
const loopNow = () => {
|
|
11895
|
+
if (!tl)
|
|
11896
|
+
return ownLoop;
|
|
11897
|
+
const r = tl.ranges.find((x) => x.loop);
|
|
11898
|
+
return r ? { start: r.start, end: r.end } : null;
|
|
11672
11899
|
};
|
|
11673
|
-
const
|
|
11674
|
-
|
|
11675
|
-
renderer = next;
|
|
11676
|
-
root.dataset.renderer = next;
|
|
11677
|
-
loupeBtn.hidden = next !== "gpu";
|
|
11678
|
-
canvas.style.display = next === "gpu" ? "" : "none";
|
|
11679
|
-
for (const s of sides) {
|
|
11680
|
-
const st = s.el.style;
|
|
11681
|
-
st.clipPath = "";
|
|
11682
|
-
st.maskImage = "";
|
|
11683
|
-
st.setProperty("-webkit-mask-image", "");
|
|
11684
|
-
st.mixBlendMode = "";
|
|
11685
|
-
st.opacity = "";
|
|
11686
|
-
s.shown = null;
|
|
11687
|
-
setCached(s, false);
|
|
11688
|
-
}
|
|
11689
|
-
for (const s of sides)
|
|
11690
|
-
pushFrame(s);
|
|
11900
|
+
const syncLoopUi = () => {
|
|
11901
|
+
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
11691
11902
|
requestDraw();
|
|
11692
11903
|
};
|
|
11693
|
-
const
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
});
|
|
11703
|
-
if (disposed)
|
|
11704
|
-
return g?.dispose();
|
|
11705
|
-
if (!g)
|
|
11706
|
-
return setRenderer("dom");
|
|
11707
|
-
gpu = g;
|
|
11708
|
-
setRenderer("gpu");
|
|
11904
|
+
const sendView = () => {
|
|
11905
|
+
if (!options.onView)
|
|
11906
|
+
return;
|
|
11907
|
+
const z = zoomNow();
|
|
11908
|
+
const l = loopNow();
|
|
11909
|
+
const key = `${z.u.toFixed(4)},${z.v.toFixed(4)},${z.w.toFixed(4)}|${l ? `${l.start},${l.end}` : ""}`;
|
|
11910
|
+
if (key === viewSent)
|
|
11911
|
+
return;
|
|
11912
|
+
viewSent = key;
|
|
11913
|
+
options.onView({ zoom: clampZoom(z), loop: l });
|
|
11709
11914
|
};
|
|
11710
|
-
const
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
modeBtns[i].setAttribute("aria-checked", String(m === mode));
|
|
11714
|
-
});
|
|
11715
|
-
handle.tabIndex = mode === "wipe" ? 0 : -1;
|
|
11716
|
-
modePill?.update();
|
|
11915
|
+
const differenceWeight = () => {
|
|
11916
|
+
const f = Math.min(1, Math.max(0, fade.x));
|
|
11917
|
+
return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
|
|
11717
11918
|
};
|
|
11718
|
-
const
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
mode = next;
|
|
11722
|
-
const reduced = reducedMotion(win);
|
|
11723
|
-
tune(fade, reduced ? FADE_REDUCED : FADE6);
|
|
11724
|
-
if (fade.x >= 0.5) {
|
|
11725
|
-
fromMode = toMode;
|
|
11726
|
-
fade.snap(0);
|
|
11727
|
-
}
|
|
11728
|
-
toMode = next;
|
|
11729
|
-
fade.set(1);
|
|
11730
|
-
if (reduced)
|
|
11731
|
-
lay.snap(next === "side" ? 0 : 1);
|
|
11732
|
-
else
|
|
11733
|
-
lay.set(next === "side" ? 0 : 1);
|
|
11734
|
-
syncModeUi();
|
|
11735
|
-
if (emit)
|
|
11736
|
-
options.onMode?.(next);
|
|
11737
|
-
requestDraw();
|
|
11738
|
-
};
|
|
11739
|
-
const setWipe = (next, emit = true, jump = false) => {
|
|
11740
|
-
const w = Math.min(1, Math.max(0, next));
|
|
11741
|
-
if (w === wipe && !jump)
|
|
11742
|
-
return;
|
|
11743
|
-
wipe = w;
|
|
11744
|
-
const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
|
|
11745
|
-
const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
|
|
11746
|
-
wipeCatching = head.catching;
|
|
11747
|
-
if (head.catching) {
|
|
11748
|
-
wipeLine.x = head.x;
|
|
11749
|
-
wipeLine.target = head.target;
|
|
11750
|
-
} else
|
|
11751
|
-
wipeLine.snap(head.x);
|
|
11752
|
-
if (emit)
|
|
11753
|
-
options.onWipe?.(w);
|
|
11754
|
-
if (head.catching)
|
|
11755
|
-
requestDraw();
|
|
11756
|
-
else
|
|
11757
|
-
drawNow();
|
|
11758
|
-
};
|
|
11759
|
-
const setLoupe = (on) => {
|
|
11760
|
-
loupeOn = on;
|
|
11761
|
-
loupeBtn.setAttribute("aria-pressed", String(on));
|
|
11762
|
-
requestDraw();
|
|
11763
|
-
};
|
|
11764
|
-
const clampTime = (v, t) => Math.min(Math.max(0, t), Math.max(0, (Number.isFinite(v.duration) ? v.duration : t) - 0.001));
|
|
11765
|
-
const seekVideo = (s, v, t) => {
|
|
11766
|
-
const c = s.frames?.nearest(wantOf(s));
|
|
11767
|
-
const far = !c || Math.abs(c.frame - wantOf(s)) > FAST_SEEK_FRAMES;
|
|
11768
|
-
if (scrubbing && far && v.fastSeek)
|
|
11769
|
-
v.fastSeek(t);
|
|
11770
|
-
else
|
|
11771
|
-
v.currentTime = t;
|
|
11772
|
-
};
|
|
11773
|
-
const showFrame = (t, used) => {
|
|
11774
|
-
for (const s of sides) {
|
|
11775
|
-
const v = videoOf(s);
|
|
11776
|
-
if (!v || v.readyState < 1)
|
|
11777
|
-
continue;
|
|
11778
|
-
if (!v.paused) {
|
|
11779
|
-
v.pause();
|
|
11780
|
-
v.playbackRate = 1;
|
|
11781
|
-
}
|
|
11782
|
-
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
11783
|
-
const step = requestSeek(s.seek, frameMidpoint(want, fps(), v.duration), v.currentTime, fps());
|
|
11784
|
-
s.seek = step.state;
|
|
11785
|
-
if (step.apply !== null)
|
|
11786
|
-
seekVideo(s, v, step.apply);
|
|
11787
|
-
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
11788
|
-
pushFrame(s);
|
|
11789
|
-
if (used)
|
|
11790
|
-
s.frames?.focus(want);
|
|
11791
|
-
refreshCached(s);
|
|
11792
|
-
}
|
|
11793
|
-
requestDraw();
|
|
11794
|
-
};
|
|
11795
|
-
const jumpAll = (t) => {
|
|
11796
|
-
for (const s of sides) {
|
|
11797
|
-
const v = videoOf(s);
|
|
11798
|
-
if (!v || v.readyState < 1)
|
|
11799
|
-
continue;
|
|
11800
|
-
s.seek = SEEK_IDLE;
|
|
11801
|
-
v.currentTime = clampTime(v, t);
|
|
11802
|
-
if (v.paused)
|
|
11803
|
-
v.play().catch(() => {});
|
|
11804
|
-
}
|
|
11805
|
-
};
|
|
11806
|
-
const startVideos = () => {
|
|
11807
|
-
const t = now();
|
|
11808
|
-
for (const s of sides) {
|
|
11809
|
-
const v = videoOf(s);
|
|
11810
|
-
if (!v)
|
|
11811
|
-
continue;
|
|
11812
|
-
s.seek = SEEK_IDLE;
|
|
11813
|
-
v.playbackRate = 1;
|
|
11814
|
-
if (v.readyState >= 1 && Math.abs(v.currentTime - t) > 1.5 / fps())
|
|
11815
|
-
v.currentTime = clampTime(v, t);
|
|
11816
|
-
v.play().catch(() => {});
|
|
11817
|
-
}
|
|
11818
|
-
loop.wake();
|
|
11819
|
-
};
|
|
11820
|
-
const pauseVideos = () => {
|
|
11821
|
-
for (const s of sides) {
|
|
11822
|
-
const v = videoOf(s);
|
|
11823
|
-
if (v) {
|
|
11824
|
-
v.pause();
|
|
11825
|
-
v.playbackRate = 1;
|
|
11826
|
-
}
|
|
11827
|
-
}
|
|
11828
|
-
};
|
|
11829
|
-
const stopVideos = () => {
|
|
11830
|
-
pauseVideos();
|
|
11831
|
-
showFrame(now(), false);
|
|
11832
|
-
};
|
|
11833
|
-
const clock = () => {
|
|
11834
|
-
const m = master();
|
|
11835
|
-
if (!m || !near)
|
|
11836
|
-
return Number.NaN;
|
|
11837
|
-
if (m.readyState < 1 || m.seeking)
|
|
11838
|
-
return tl.time;
|
|
11839
|
-
return m.ended ? Number.POSITIVE_INFINITY : m.currentTime;
|
|
11840
|
-
};
|
|
11841
|
-
const ownSeek = (t) => {
|
|
11842
|
-
ownTime = Math.min(Math.max(0, t), duration);
|
|
11843
|
-
if (ownPlaying)
|
|
11844
|
-
jumpAll(ownTime);
|
|
11845
|
-
else
|
|
11846
|
-
showFrame(ownTime, true);
|
|
11847
|
-
};
|
|
11848
|
-
const ownPlay = () => {
|
|
11849
|
-
if (ownPlaying || !master())
|
|
11850
|
-
return;
|
|
11851
|
-
if (ownLoop && (ownTime < ownLoop.start || ownTime >= ownLoop.end - 0.5 / fps()))
|
|
11852
|
-
ownTime = ownLoop.start;
|
|
11853
|
-
else if (!ownLoop && ownTime >= duration - 0.5 / fps())
|
|
11854
|
-
ownTime = 0;
|
|
11855
|
-
ownPlaying = true;
|
|
11856
|
-
startVideos();
|
|
11857
|
-
};
|
|
11858
|
-
const ownPause = () => {
|
|
11859
|
-
if (!ownPlaying)
|
|
11860
|
-
return;
|
|
11861
|
-
ownPlaying = false;
|
|
11862
|
-
ownTime = Math.min(duration, frameOf(ownTime, fps()) / fps());
|
|
11863
|
-
stopVideos();
|
|
11864
|
-
};
|
|
11865
|
-
const zoomNow = () => ({ u: zu.x, v: zv.x, w: zw.x, h: zw.x });
|
|
11866
|
-
const zoomed = () => zw.target < 0.999;
|
|
11867
|
-
const layoutNow = () => {
|
|
11868
|
-
const box = { x: 0, y: 0, w: stageW, h: stageH };
|
|
11869
|
-
const L = compareLayout(box, sizeOf(sides[0]), sizeOf(sides[1]));
|
|
11870
|
-
const k = lay.x;
|
|
11871
|
-
const before = lerpRect(L.side.before, L.overlay.before, k);
|
|
11872
|
-
const after = lerpRect(L.side.after, L.overlay.after, k);
|
|
11873
|
-
const z = zoomNow();
|
|
11874
|
-
return { L, before, after, zBefore: zoomedRect(before, z), zAfter: zoomedRect(after, z) };
|
|
11875
|
-
};
|
|
11876
|
-
const wipeRect = (r) => {
|
|
11877
|
-
const x = Math.min(r.before.x, r.after.x);
|
|
11878
|
-
const y = Math.min(r.before.y, r.after.y);
|
|
11879
|
-
const x1 = Math.max(r.before.x + r.before.w, r.after.x + r.after.w);
|
|
11880
|
-
const y1 = Math.max(r.before.y + r.before.h, r.after.y + r.after.h);
|
|
11881
|
-
return { x, y, w: x1 - x, h: y1 - y };
|
|
11882
|
-
};
|
|
11883
|
-
const baseAt = (p) => {
|
|
11884
|
-
const r = layoutNow();
|
|
11885
|
-
const inside = (b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
|
|
11886
|
-
return inside(r.after) || !inside(r.before) ? r.after : r.before;
|
|
11887
|
-
};
|
|
11888
|
-
const loupeShown = () => renderer === "gpu" && (loupeOn || loupeHeld) && pointer !== null;
|
|
11889
|
-
const loopNow = () => {
|
|
11890
|
-
if (!tl)
|
|
11891
|
-
return ownLoop;
|
|
11892
|
-
const r = tl.ranges.find((x) => x.loop);
|
|
11893
|
-
return r ? { start: r.start, end: r.end } : null;
|
|
11894
|
-
};
|
|
11895
|
-
const syncLoopUi = () => {
|
|
11896
|
-
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
11897
|
-
requestDraw();
|
|
11898
|
-
};
|
|
11899
|
-
const sendView = () => {
|
|
11900
|
-
if (!options.onView)
|
|
11901
|
-
return;
|
|
11902
|
-
const z = zoomNow();
|
|
11903
|
-
const l = loopNow();
|
|
11904
|
-
const key = `${z.u.toFixed(4)},${z.v.toFixed(4)},${z.w.toFixed(4)}|${l ? `${l.start},${l.end}` : ""}`;
|
|
11905
|
-
if (key === viewSent)
|
|
11906
|
-
return;
|
|
11907
|
-
viewSent = key;
|
|
11908
|
-
options.onView({ zoom: clampZoom(z), loop: l });
|
|
11909
|
-
};
|
|
11910
|
-
const differenceWeight = () => {
|
|
11911
|
-
const f = Math.min(1, Math.max(0, fade.x));
|
|
11912
|
-
return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
|
|
11913
|
-
};
|
|
11914
|
-
const placeGrip = (wr) => {
|
|
11915
|
-
const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
|
|
11916
|
-
grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
|
|
11919
|
+
const placeGrip = (wr) => {
|
|
11920
|
+
const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
|
|
11921
|
+
grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
|
|
11917
11922
|
};
|
|
11918
11923
|
let folded = 0;
|
|
11919
11924
|
const draw = () => {
|
|
@@ -12418,7 +12423,7 @@ function compare(container, options) {
|
|
|
12418
12423
|
holdLoupe(true);
|
|
12419
12424
|
return;
|
|
12420
12425
|
case "l":
|
|
12421
|
-
if (e.altKey || e.shiftKey)
|
|
12426
|
+
if (e.altKey || e.shiftKey || loopBtn.hidden)
|
|
12422
12427
|
return;
|
|
12423
12428
|
claim();
|
|
12424
12429
|
return api.setLoop(!loopNow());
|
|
@@ -12441,6 +12446,9 @@ function compare(container, options) {
|
|
|
12441
12446
|
return glideZoom(ZOOM_FIT);
|
|
12442
12447
|
}
|
|
12443
12448
|
};
|
|
12449
|
+
const syncLoopButton = () => {
|
|
12450
|
+
loopBtn.hidden = !sides.some((s) => s.kind === "video");
|
|
12451
|
+
};
|
|
12444
12452
|
const replace = (index, source) => {
|
|
12445
12453
|
const old = sides[index];
|
|
12446
12454
|
old.stop();
|
|
@@ -12449,6 +12457,7 @@ function compare(container, options) {
|
|
|
12449
12457
|
old.el.replaceWith(s.el);
|
|
12450
12458
|
old.preview.replaceWith(s.preview);
|
|
12451
12459
|
sides[index] = s;
|
|
12460
|
+
syncLoopButton();
|
|
12452
12461
|
note.textContent = "";
|
|
12453
12462
|
setLabel(index, labelOf(index));
|
|
12454
12463
|
if (near)
|
|
@@ -12463,6 +12472,7 @@ function compare(container, options) {
|
|
|
12463
12472
|
stage.insertBefore(sides[0].preview, labels[0]);
|
|
12464
12473
|
setLabel(0, labelOf(0));
|
|
12465
12474
|
setLabel(1, labelOf(1));
|
|
12475
|
+
syncLoopButton();
|
|
12466
12476
|
loupeBtn.setAttribute("aria-pressed", String(loupeOn));
|
|
12467
12477
|
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
12468
12478
|
const modePill = modeList.length > 1 ? selectionPill(modes, {
|
|
@@ -12541,7 +12551,7 @@ function compare(container, options) {
|
|
|
12541
12551
|
label: MODE_LABEL[m].long,
|
|
12542
12552
|
activate: () => setMode(m)
|
|
12543
12553
|
})) : [],
|
|
12544
|
-
{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
|
|
12554
|
+
...loopBtn.hidden ? [] : [{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) }],
|
|
12545
12555
|
...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
|
|
12546
12556
|
...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
|
|
12547
12557
|
...mode === "wipe" ? [
|
|
@@ -12552,14 +12562,6 @@ function compare(container, options) {
|
|
|
12552
12562
|
}
|
|
12553
12563
|
] : []
|
|
12554
12564
|
], { listen: false, rank: 1 });
|
|
12555
|
-
const strip = wantsStrip && stripHost && tl ? compareStrip(stripHost, {
|
|
12556
|
-
...look,
|
|
12557
|
-
before: options.before,
|
|
12558
|
-
after: options.after,
|
|
12559
|
-
timeline: tl,
|
|
12560
|
-
...fpsOpt ? { fps: fpsOpt } : {},
|
|
12561
|
-
threshold
|
|
12562
|
-
}) : null;
|
|
12563
12565
|
const api = {
|
|
12564
12566
|
element: root,
|
|
12565
12567
|
get mode() {
|
|
@@ -12585,7 +12587,6 @@ function compare(container, options) {
|
|
|
12585
12587
|
return renderer;
|
|
12586
12588
|
},
|
|
12587
12589
|
timeline: tl,
|
|
12588
|
-
strip,
|
|
12589
12590
|
get zoom() {
|
|
12590
12591
|
return clampZoom(zoomNow());
|
|
12591
12592
|
},
|
|
@@ -12607,8 +12608,6 @@ function compare(container, options) {
|
|
|
12607
12608
|
replace(1, after);
|
|
12608
12609
|
else if (next.after)
|
|
12609
12610
|
setLabel(1, next.after.label ?? "After");
|
|
12610
|
-
if (before || after)
|
|
12611
|
-
strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
|
|
12612
12611
|
if (next.before || next.after)
|
|
12613
12612
|
readColors();
|
|
12614
12613
|
if (next.mode !== undefined)
|
|
@@ -12675,7 +12674,6 @@ function compare(container, options) {
|
|
|
12675
12674
|
if (ownTimeline)
|
|
12676
12675
|
tl.dispose();
|
|
12677
12676
|
}
|
|
12678
|
-
strip?.dispose();
|
|
12679
12677
|
for (const s of sides) {
|
|
12680
12678
|
s.stop();
|
|
12681
12679
|
unloadSide(s);
|
|
@@ -12699,7 +12697,6 @@ var init_compare = __esm(() => {
|
|
|
12699
12697
|
init_compare_frames();
|
|
12700
12698
|
init_compare_gpu();
|
|
12701
12699
|
init_compare_model();
|
|
12702
|
-
init_compare_strip();
|
|
12703
12700
|
init_icons();
|
|
12704
12701
|
init_look();
|
|
12705
12702
|
init_rgba();
|
|
@@ -12757,7 +12754,10 @@ function player(container, options) {
|
|
|
12757
12754
|
track.src = options.captions;
|
|
12758
12755
|
v.append(track);
|
|
12759
12756
|
}
|
|
12760
|
-
|
|
12757
|
+
const preview = el("canvas", "knobs-player-preview");
|
|
12758
|
+
preview.setAttribute("aria-hidden", "true");
|
|
12759
|
+
preview.hidden = true;
|
|
12760
|
+
frame.append(v, preview);
|
|
12761
12761
|
const host = el("div", "knobs-player-line");
|
|
12762
12762
|
root.append(frame, host);
|
|
12763
12763
|
container.append(root);
|
|
@@ -12787,6 +12787,57 @@ function player(container, options) {
|
|
|
12787
12787
|
else
|
|
12788
12788
|
v.currentTime = t;
|
|
12789
12789
|
};
|
|
12790
|
+
const fps = options.fps ?? 30;
|
|
12791
|
+
let frames = null;
|
|
12792
|
+
let unlistenFrames = null;
|
|
12793
|
+
let scrubbing = false;
|
|
12794
|
+
let previewing = null;
|
|
12795
|
+
const want = () => frameOf(tl.time, fps);
|
|
12796
|
+
const emitFrame = (frame, source) => root.dispatchEvent(new CustomEvent("knobs-player-frame", { detail: { frame, source } }));
|
|
12797
|
+
const drawPreview = (c) => {
|
|
12798
|
+
if (preview.width !== c.bitmap.width || preview.height !== c.bitmap.height) {
|
|
12799
|
+
preview.width = c.bitmap.width;
|
|
12800
|
+
preview.height = c.bitmap.height;
|
|
12801
|
+
}
|
|
12802
|
+
preview.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
12803
|
+
preview.hidden = false;
|
|
12804
|
+
previewing = c.frame;
|
|
12805
|
+
emitFrame(c.frame, "cache");
|
|
12806
|
+
};
|
|
12807
|
+
const hidePreview = () => {
|
|
12808
|
+
if (previewing === null)
|
|
12809
|
+
return;
|
|
12810
|
+
previewing = null;
|
|
12811
|
+
preview.hidden = true;
|
|
12812
|
+
};
|
|
12813
|
+
const showNearest = () => {
|
|
12814
|
+
if (!frames || !scrubbing)
|
|
12815
|
+
return;
|
|
12816
|
+
const w = want();
|
|
12817
|
+
const c = frames.nearest(w, Number.POSITIVE_INFINITY, true);
|
|
12818
|
+
if (!c || c.frame === previewing)
|
|
12819
|
+
return;
|
|
12820
|
+
const shown = previewing ?? frameOf(v.currentTime, fps);
|
|
12821
|
+
if (Math.abs(c.frame - w) < Math.abs(shown - w))
|
|
12822
|
+
drawPreview(c);
|
|
12823
|
+
};
|
|
12824
|
+
const caching = scrubCaching(options.scrubCache, doc.defaultView);
|
|
12825
|
+
const startFrames = () => {
|
|
12826
|
+
if (frames || !caching)
|
|
12827
|
+
return;
|
|
12828
|
+
frames = acquireFrames(doc, options.src, fps);
|
|
12829
|
+
frames.visible(true);
|
|
12830
|
+
unlistenFrames = frames.listen(showNearest);
|
|
12831
|
+
};
|
|
12832
|
+
const stall = stallGuard(doc.defaultView, () => {
|
|
12833
|
+
pending = null;
|
|
12834
|
+
hidePreview();
|
|
12835
|
+
});
|
|
12836
|
+
const onError = () => {
|
|
12837
|
+
stall.clear();
|
|
12838
|
+
pending = null;
|
|
12839
|
+
hidePreview();
|
|
12840
|
+
};
|
|
12790
12841
|
let reported = 0;
|
|
12791
12842
|
const onSeeked = () => {
|
|
12792
12843
|
if (pending !== null) {
|
|
@@ -12795,6 +12846,11 @@ function player(container, options) {
|
|
|
12795
12846
|
v.currentTime = t;
|
|
12796
12847
|
return;
|
|
12797
12848
|
}
|
|
12849
|
+
stall.clear();
|
|
12850
|
+
const at = frameOf(v.currentTime, fps);
|
|
12851
|
+
emitFrame(at, "video");
|
|
12852
|
+
if (!scrubbing || previewing !== null && Math.abs(at - want()) <= Math.abs(previewing - want()))
|
|
12853
|
+
hidePreview();
|
|
12798
12854
|
if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
|
|
12799
12855
|
return;
|
|
12800
12856
|
reported = v.currentTime;
|
|
@@ -12810,6 +12866,7 @@ function player(container, options) {
|
|
|
12810
12866
|
v.addEventListener("loadedmetadata", onMeta);
|
|
12811
12867
|
v.addEventListener("loadeddata", onFrame);
|
|
12812
12868
|
v.addEventListener("seeked", onSeeked);
|
|
12869
|
+
v.addEventListener("error", onError);
|
|
12813
12870
|
const unlisten = tl.listen({
|
|
12814
12871
|
onSeek(t, source) {
|
|
12815
12872
|
if (source === "play") {
|
|
@@ -12817,14 +12874,24 @@ function player(container, options) {
|
|
|
12817
12874
|
v.currentTime = t;
|
|
12818
12875
|
return;
|
|
12819
12876
|
}
|
|
12820
|
-
if (
|
|
12877
|
+
if (scrubbing && frames) {
|
|
12878
|
+
frames.follow(want());
|
|
12879
|
+
showNearest();
|
|
12880
|
+
if (frames.covers(want()))
|
|
12881
|
+
return;
|
|
12882
|
+
}
|
|
12883
|
+
if (sameFrame(v.currentTime, t, fps)) {
|
|
12884
|
+
if (!scrubbing)
|
|
12885
|
+
hidePreview();
|
|
12821
12886
|
return;
|
|
12887
|
+
}
|
|
12822
12888
|
seekVideo(t);
|
|
12823
12889
|
},
|
|
12824
12890
|
onPlay(on) {
|
|
12825
|
-
if (on)
|
|
12891
|
+
if (on) {
|
|
12892
|
+
hidePreview();
|
|
12826
12893
|
v.play().catch(() => tl.pause());
|
|
12827
|
-
else {
|
|
12894
|
+
} else {
|
|
12828
12895
|
v.pause();
|
|
12829
12896
|
reported = tl.time;
|
|
12830
12897
|
}
|
|
@@ -12832,8 +12899,22 @@ function player(container, options) {
|
|
|
12832
12899
|
options.onPlay?.(on, tl.time);
|
|
12833
12900
|
},
|
|
12834
12901
|
onScrub(on) {
|
|
12835
|
-
|
|
12902
|
+
scrubbing = on;
|
|
12903
|
+
if (on) {
|
|
12836
12904
|
v.pause();
|
|
12905
|
+
startFrames();
|
|
12906
|
+
return;
|
|
12907
|
+
}
|
|
12908
|
+
if (v.readyState < 1)
|
|
12909
|
+
return hidePreview();
|
|
12910
|
+
const at = releaseSeek(v.currentTime, tl.time, fps, v.duration);
|
|
12911
|
+
if (at === null) {
|
|
12912
|
+
pending = null;
|
|
12913
|
+
if (!v.seeking)
|
|
12914
|
+
return hidePreview();
|
|
12915
|
+
} else
|
|
12916
|
+
seekVideo(at);
|
|
12917
|
+
stall.arm();
|
|
12837
12918
|
}
|
|
12838
12919
|
});
|
|
12839
12920
|
tl.drive(() => {
|
|
@@ -12848,6 +12929,7 @@ function player(container, options) {
|
|
|
12848
12929
|
tl.toggle();
|
|
12849
12930
|
};
|
|
12850
12931
|
frame.addEventListener("click", onClick);
|
|
12932
|
+
root.addEventListener("pointerenter", startFrames);
|
|
12851
12933
|
v.src = options.src;
|
|
12852
12934
|
const api = {
|
|
12853
12935
|
element: root,
|
|
@@ -12874,9 +12956,18 @@ function player(container, options) {
|
|
|
12874
12956
|
tl.dispose();
|
|
12875
12957
|
v.pause();
|
|
12876
12958
|
frame.removeEventListener("click", onClick);
|
|
12959
|
+
root.removeEventListener("pointerenter", startFrames);
|
|
12960
|
+
unlistenFrames?.();
|
|
12961
|
+
if (frames) {
|
|
12962
|
+
frames.visible(false);
|
|
12963
|
+
frames.release();
|
|
12964
|
+
frames = null;
|
|
12965
|
+
}
|
|
12877
12966
|
v.removeEventListener("loadedmetadata", onMeta);
|
|
12878
12967
|
v.removeEventListener("loadeddata", onFrame);
|
|
12879
12968
|
v.removeEventListener("seeked", onSeeked);
|
|
12969
|
+
v.removeEventListener("error", onError);
|
|
12970
|
+
stall.clear();
|
|
12880
12971
|
v.removeAttribute("src");
|
|
12881
12972
|
v.load();
|
|
12882
12973
|
root.remove();
|
|
@@ -12885,6 +12976,8 @@ function player(container, options) {
|
|
|
12885
12976
|
return api;
|
|
12886
12977
|
}
|
|
12887
12978
|
var init_player = __esm(() => {
|
|
12979
|
+
init_compare_frames();
|
|
12980
|
+
init_compare_model();
|
|
12888
12981
|
init_look();
|
|
12889
12982
|
init_timeline();
|
|
12890
12983
|
});
|
|
@@ -17322,8 +17415,66 @@ init_page_hints();
|
|
|
17322
17415
|
init_keys();
|
|
17323
17416
|
init_layer();
|
|
17324
17417
|
init_overlay_model();
|
|
17325
|
-
|
|
17418
|
+
|
|
17419
|
+
// src/ui/tooltip-model.ts
|
|
17326
17420
|
var HANDOFF_MS = 400;
|
|
17421
|
+
var TIP_DELAY_MS = 500;
|
|
17422
|
+
function tipSchedule(clock, view) {
|
|
17423
|
+
let shown = null;
|
|
17424
|
+
let pending = null;
|
|
17425
|
+
let timer;
|
|
17426
|
+
let hiddenAt = -Infinity;
|
|
17427
|
+
const cancel = () => {
|
|
17428
|
+
if (pending)
|
|
17429
|
+
clock.clear(timer);
|
|
17430
|
+
pending = null;
|
|
17431
|
+
};
|
|
17432
|
+
const hide = (now = false) => {
|
|
17433
|
+
cancel();
|
|
17434
|
+
if (!shown)
|
|
17435
|
+
return;
|
|
17436
|
+
shown = null;
|
|
17437
|
+
hiddenAt = clock.now();
|
|
17438
|
+
view.hide(now);
|
|
17439
|
+
};
|
|
17440
|
+
return {
|
|
17441
|
+
get shown() {
|
|
17442
|
+
return shown;
|
|
17443
|
+
},
|
|
17444
|
+
get pending() {
|
|
17445
|
+
return pending;
|
|
17446
|
+
},
|
|
17447
|
+
want(tip, options = {}) {
|
|
17448
|
+
if (tip.el === shown?.el || tip.el === pending?.el)
|
|
17449
|
+
return;
|
|
17450
|
+
cancel();
|
|
17451
|
+
const handoff = shown !== null || clock.now() - hiddenAt < HANDOFF_MS;
|
|
17452
|
+
if (handoff || options.now) {
|
|
17453
|
+
shown = tip;
|
|
17454
|
+
view.show(tip, handoff);
|
|
17455
|
+
return;
|
|
17456
|
+
}
|
|
17457
|
+
pending = tip;
|
|
17458
|
+
timer = clock.set(() => {
|
|
17459
|
+
pending = null;
|
|
17460
|
+
if (!view.connected(tip))
|
|
17461
|
+
return;
|
|
17462
|
+
shown = tip;
|
|
17463
|
+
view.show(tip, false);
|
|
17464
|
+
}, options.delay ?? TIP_DELAY_MS);
|
|
17465
|
+
},
|
|
17466
|
+
hide,
|
|
17467
|
+
leave(within) {
|
|
17468
|
+
if (pending && within(pending))
|
|
17469
|
+
cancel();
|
|
17470
|
+
if (shown && within(shown))
|
|
17471
|
+
hide();
|
|
17472
|
+
}
|
|
17473
|
+
};
|
|
17474
|
+
}
|
|
17475
|
+
|
|
17476
|
+
// src/ui/tooltip.ts
|
|
17477
|
+
var tips = new WeakMap;
|
|
17327
17478
|
var controllers = new WeakMap;
|
|
17328
17479
|
function tipOf(start) {
|
|
17329
17480
|
let node = start instanceof Element ? start : null;
|
|
@@ -17357,9 +17508,6 @@ function controllerOf(doc) {
|
|
|
17357
17508
|
let text;
|
|
17358
17509
|
let keys;
|
|
17359
17510
|
let target = null;
|
|
17360
|
-
let pending = null;
|
|
17361
|
-
let timer = 0;
|
|
17362
|
-
let hiddenAt = -Infinity;
|
|
17363
17511
|
let quiet = null;
|
|
17364
17512
|
let keyboard = false;
|
|
17365
17513
|
const build = (look) => {
|
|
@@ -17404,41 +17552,32 @@ function controllerOf(doc) {
|
|
|
17404
17552
|
else
|
|
17405
17553
|
motion.show();
|
|
17406
17554
|
};
|
|
17407
|
-
const
|
|
17408
|
-
win.
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17555
|
+
const schedule = tipSchedule({
|
|
17556
|
+
set: (run, ms) => win.setTimeout(run, ms),
|
|
17557
|
+
clear: (handle) => win.clearTimeout(handle),
|
|
17558
|
+
now: () => performance.now()
|
|
17559
|
+
}, {
|
|
17560
|
+
show: ({ el, tip }, snap) => show(el, tip, snap),
|
|
17561
|
+
hide: (now) => {
|
|
17562
|
+
target?.removeAttribute("aria-describedby");
|
|
17563
|
+
target = null;
|
|
17564
|
+
if (now)
|
|
17565
|
+
motion?.snap(0);
|
|
17566
|
+
else
|
|
17567
|
+
motion?.hide();
|
|
17568
|
+
},
|
|
17569
|
+
connected: ({ el }) => el.isConnected
|
|
17570
|
+
});
|
|
17571
|
+
const hide = (now = false) => schedule.hide(now);
|
|
17572
|
+
const want = (found, now = false) => {
|
|
17421
17573
|
if (!found)
|
|
17422
17574
|
return hide();
|
|
17423
|
-
if (found.el === target || found.el === pending?.el)
|
|
17424
|
-
return;
|
|
17425
17575
|
if (found.el === quiet)
|
|
17426
17576
|
return;
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
show(found.el, found.tip, true);
|
|
17432
|
-
return;
|
|
17433
|
-
}
|
|
17434
|
-
if (target)
|
|
17435
|
-
hide();
|
|
17436
|
-
pending = found;
|
|
17437
|
-
timer = win.setTimeout(() => {
|
|
17438
|
-
pending = null;
|
|
17439
|
-
if (found.el.isConnected)
|
|
17440
|
-
show(found.el, found.tip, false);
|
|
17441
|
-
}, found.tip.opts.delay ?? 500);
|
|
17577
|
+
schedule.want(found, {
|
|
17578
|
+
now,
|
|
17579
|
+
...found.tip.opts.delay !== undefined ? { delay: found.tip.opts.delay } : {}
|
|
17580
|
+
});
|
|
17442
17581
|
};
|
|
17443
17582
|
doc.addEventListener("pointerover", (e) => {
|
|
17444
17583
|
if (e.pointerType === "touch")
|
|
@@ -17472,11 +17611,10 @@ function controllerOf(doc) {
|
|
|
17472
17611
|
doc.addEventListener("focusin", (e) => {
|
|
17473
17612
|
const found = tipOf(e.target);
|
|
17474
17613
|
if (found && keyboard && e.target.matches(":focus-visible"))
|
|
17475
|
-
want(found);
|
|
17614
|
+
want(found, true);
|
|
17476
17615
|
});
|
|
17477
17616
|
doc.addEventListener("focusout", (e) => {
|
|
17478
|
-
|
|
17479
|
-
hide();
|
|
17617
|
+
schedule.leave(({ el }) => el.contains(e.target));
|
|
17480
17618
|
});
|
|
17481
17619
|
doc.addEventListener("scroll", () => target && hide(true), true);
|
|
17482
17620
|
const c = { hide };
|
|
@@ -17775,445 +17913,819 @@ function split(container, options) {
|
|
|
17775
17913
|
out[id] = s.x;
|
|
17776
17914
|
return out;
|
|
17777
17915
|
};
|
|
17778
|
-
const target = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail });
|
|
17779
|
-
const current = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail, fixed: fixed() });
|
|
17780
|
-
const SIDES = { row: ["right", "left"], column: ["bottom", "top"] };
|
|
17781
|
-
const sides = (v) => {
|
|
17782
|
-
const [a, b] = SIDES[direction];
|
|
17783
|
-
return v.edge === "start" ? [a, b] : [b, a];
|
|
17916
|
+
const target = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail });
|
|
17917
|
+
const current = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail, fixed: fixed() });
|
|
17918
|
+
const SIDES = { row: ["right", "left"], column: ["bottom", "top"] };
|
|
17919
|
+
const sides = (v) => {
|
|
17920
|
+
const [a, b] = SIDES[direction];
|
|
17921
|
+
return v.edge === "start" ? [a, b] : [b, a];
|
|
17922
|
+
};
|
|
17923
|
+
const renderFold = (v, size) => {
|
|
17924
|
+
const id = v.spec.id;
|
|
17925
|
+
const isCollapsed = collapsed[id];
|
|
17926
|
+
const folding = isCollapsed || v.folding;
|
|
17927
|
+
const r = rail(id);
|
|
17928
|
+
const open = folding ? Math.max(v.open, r + 1) : size;
|
|
17929
|
+
const p = folding ? foldProgress(size, open, r) : 0;
|
|
17930
|
+
v.faces = foldFaces(v.faces, p);
|
|
17931
|
+
const m = marginOf(v);
|
|
17932
|
+
const settled = !springs.has(id);
|
|
17933
|
+
const content = isCollapsed && settled ? 1 : v.faces.content;
|
|
17934
|
+
put(v.rail ? v.inner : v.body, "will-change", v.folding ? "opacity, filter" : "");
|
|
17935
|
+
if (!v.rail) {
|
|
17936
|
+
put(v.inner, "height", folding ? `${open - m * 2}px` : "");
|
|
17937
|
+
put(v.inner, "bottom", folding ? "auto" : "");
|
|
17938
|
+
fade(v.body, content, CONTENT_BLUR);
|
|
17939
|
+
if (v.toggleIcon)
|
|
17940
|
+
put(v.toggleIcon, "transform", p ? `rotate(${(-90 * p).toFixed(2)}deg)` : "");
|
|
17941
|
+
hide(v.body, isCollapsed && settled);
|
|
17942
|
+
return;
|
|
17943
|
+
}
|
|
17944
|
+
const [side, far] = sides(v);
|
|
17945
|
+
put(v.inner, row ? "width" : "height", folding ? `${open - m * 2}px` : "");
|
|
17946
|
+
put(v.inner, side, folding ? "0" : "");
|
|
17947
|
+
put(v.inner, far, folding ? "auto" : "");
|
|
17948
|
+
fade(v.inner, content, CONTENT_BLUR);
|
|
17949
|
+
hide(v.clip, isCollapsed && settled);
|
|
17950
|
+
const morph = row && Boolean(v.toggleIcon) && !v.el.hasAttribute("data-nested");
|
|
17951
|
+
v.el.toggleAttribute("data-morph", morph);
|
|
17952
|
+
const label = v.railLabel;
|
|
17953
|
+
const travel = morph && style.title === "travel" && Boolean(v.titleEl) && label.textContent === v.titleEl.textContent;
|
|
17954
|
+
v.el.toggleAttribute("data-travel", travel && folding);
|
|
17955
|
+
if (v.countEl)
|
|
17956
|
+
put(v.countEl, "opacity", travel && folding && p > 0 ? Math.max(0, 1 - p / COUNT_CLEAR).toFixed(3) : "");
|
|
17957
|
+
const k = 1 - p;
|
|
17958
|
+
if (travel && !folding) {
|
|
17959
|
+
put(label, "opacity", "0");
|
|
17960
|
+
put(label, "filter", "");
|
|
17961
|
+
put(label, "transform", "");
|
|
17962
|
+
put(label, "color", "");
|
|
17963
|
+
} else if (travel) {
|
|
17964
|
+
const t = titleTravel(v.places, open, r, v.edge, p);
|
|
17965
|
+
put(label, "opacity", "");
|
|
17966
|
+
put(label, "filter", "");
|
|
17967
|
+
put(label, "transform", k === 0 ? "" : `translate(${t.x.toFixed(2)}px,${t.y.toFixed(2)}px) rotate(${t.turn.toFixed(2)}deg)`);
|
|
17968
|
+
put(label, "color", k === 0 ? "" : `color-mix(in srgb,var(--kv-fg) ${(k * 100).toFixed(1)}%,var(--kv-fg-2))`);
|
|
17969
|
+
} else {
|
|
17970
|
+
put(label, "transform", "");
|
|
17971
|
+
put(label, "color", "");
|
|
17972
|
+
}
|
|
17973
|
+
if (morph) {
|
|
17974
|
+
put(v.rail, "opacity", "1");
|
|
17975
|
+
if (!travel)
|
|
17976
|
+
fade(label, v.faces.rail, LABEL_BLUR);
|
|
17977
|
+
const c = v.chev;
|
|
17978
|
+
const dx = c.chevEnd - c.iconEnd + (v.edge === "end" ? open - r : 0);
|
|
17979
|
+
const move = `translate(${(dx * k).toFixed(2)}px,${(c.dy * k).toFixed(2)}px)`;
|
|
17980
|
+
const size = `scale(${(1 + (c.scale - 1) * k).toFixed(4)})`;
|
|
17981
|
+
if (style.chevron === "turn")
|
|
17982
|
+
put(v.turn, "transform", k === 0 ? "" : `${move} rotate(${chevronTurn(p, v.edge).toFixed(2)}deg) ${size}`);
|
|
17983
|
+
else {
|
|
17984
|
+
const turn = chevronFlip(v.flip.x, v.edge);
|
|
17985
|
+
const over = v.flip.resting ? turn === 0 ? "" : "scaleX(-1)" : `perspective(${FLIP_DEPTH}px) rotateY(${turn.toFixed(2)}deg)`;
|
|
17986
|
+
put(v.turn, "transform", k === 0 && !over ? "" : `${move} ${size} ${over}`);
|
|
17987
|
+
}
|
|
17988
|
+
} else {
|
|
17989
|
+
const a = v.faces.rail;
|
|
17990
|
+
put(v.rail, "opacity", a >= 1 ? "1" : a.toFixed(3));
|
|
17991
|
+
put(label, "opacity", "");
|
|
17992
|
+
put(label, "filter", a >= 1 || a <= 0 ? "" : `blur(${(LABEL_BLUR * (1 - a)).toFixed(2)}px)`);
|
|
17993
|
+
put(v.turn, "transform", "");
|
|
17994
|
+
}
|
|
17995
|
+
};
|
|
17996
|
+
let lastReport = "";
|
|
17997
|
+
const render = (shown) => {
|
|
17998
|
+
for (const spec of specs) {
|
|
17999
|
+
const v = views.get(spec.id);
|
|
18000
|
+
const size = Math.max(0, shown[spec.id] ?? 0);
|
|
18001
|
+
v.shown = size;
|
|
18002
|
+
put(v.el, "flex-basis", `${size}px`);
|
|
18003
|
+
put(v.el, row ? "width" : "height", `${size}px`);
|
|
18004
|
+
v.el.toggleAttribute("data-collapsed", collapsed[spec.id]);
|
|
18005
|
+
v.el.toggleAttribute("data-settled", !springs.has(spec.id));
|
|
18006
|
+
if (spec.collapsible)
|
|
18007
|
+
renderFold(v, size);
|
|
18008
|
+
}
|
|
18009
|
+
dividers.forEach((d, i) => {
|
|
18010
|
+
const a = specs[i];
|
|
18011
|
+
const b = specs[i + 1];
|
|
18012
|
+
const now = String(Math.round(views.get(a.id).shown));
|
|
18013
|
+
if (d.getAttribute("aria-valuenow") !== now)
|
|
18014
|
+
d.setAttribute("aria-valuenow", now);
|
|
18015
|
+
if (!d.hasAttribute("aria-controls"))
|
|
18016
|
+
d.setAttribute("aria-controls", `${a.id} ${b.id}`);
|
|
18017
|
+
});
|
|
18018
|
+
const layout = snapshot();
|
|
18019
|
+
const report = JSON.stringify(layout);
|
|
18020
|
+
if (report !== lastReport) {
|
|
18021
|
+
lastReport = report;
|
|
18022
|
+
options.onLayout?.(layout);
|
|
18023
|
+
for (const v of views.values())
|
|
18024
|
+
v.el.dispatchEvent(new Event("knobs-pane-resize"));
|
|
18025
|
+
notifyLayout(doc);
|
|
18026
|
+
}
|
|
18027
|
+
};
|
|
18028
|
+
const snapshot = () => {
|
|
18029
|
+
const s = {};
|
|
18030
|
+
for (const [id, v] of views)
|
|
18031
|
+
s[id] = Math.round(v.shown * 10) / 10;
|
|
18032
|
+
return { sizes: s, collapsed: { ...collapsed } };
|
|
18033
|
+
};
|
|
18034
|
+
const persist = () => {
|
|
18035
|
+
if (!store || !key)
|
|
18036
|
+
return;
|
|
18037
|
+
try {
|
|
18038
|
+
store.setItem(key, serializeLayout({ sizes, collapsed }));
|
|
18039
|
+
} catch {}
|
|
18040
|
+
};
|
|
18041
|
+
const rememberOpen = (shown) => {
|
|
18042
|
+
for (const spec of specs)
|
|
18043
|
+
if (!collapsed[spec.id] && !springs.has(spec.id))
|
|
18044
|
+
views.get(spec.id).open = shown[spec.id];
|
|
18045
|
+
};
|
|
18046
|
+
const loop = frameLoop(win, (dt) => {
|
|
18047
|
+
for (const [id, s] of springs) {
|
|
18048
|
+
s.step(dt);
|
|
18049
|
+
if (s.resting) {
|
|
18050
|
+
springs.delete(id);
|
|
18051
|
+
views.get(id).folding = false;
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
let flipping = false;
|
|
18055
|
+
for (const v of views.values()) {
|
|
18056
|
+
v.flip.step(dt);
|
|
18057
|
+
if (!v.flip.resting)
|
|
18058
|
+
flipping = true;
|
|
18059
|
+
}
|
|
18060
|
+
const shown = current();
|
|
18061
|
+
if (springs.size === 0)
|
|
18062
|
+
rememberOpen(shown);
|
|
18063
|
+
render(shown);
|
|
18064
|
+
return springs.size > 0 || flipping;
|
|
18065
|
+
});
|
|
18066
|
+
const animate = (motion = SETTLE) => {
|
|
18067
|
+
const to = target();
|
|
18068
|
+
for (const v of views.values())
|
|
18069
|
+
if (v.folding && !collapsed[v.spec.id])
|
|
18070
|
+
v.open = to[v.spec.id];
|
|
18071
|
+
if (reducedMotion(win)) {
|
|
18072
|
+
springs.clear();
|
|
18073
|
+
const arriving = [...views.values()].filter((v) => v.folding).map((v) => collapsed[v.spec.id] ? v.rail : v.rail ? v.inner : v.body);
|
|
18074
|
+
for (const v of views.values()) {
|
|
18075
|
+
v.folding = false;
|
|
18076
|
+
v.flip.snap(collapsed[v.spec.id] ? 1 : 0);
|
|
18077
|
+
}
|
|
18078
|
+
rememberOpen(to);
|
|
18079
|
+
render(to);
|
|
18080
|
+
for (const el of arriving)
|
|
18081
|
+
el?.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
18082
|
+
duration: 160,
|
|
18083
|
+
easing: "cubic-bezier(.2,.8,.2,1)"
|
|
18084
|
+
});
|
|
18085
|
+
return;
|
|
18086
|
+
}
|
|
18087
|
+
for (const spec of specs) {
|
|
18088
|
+
const v = views.get(spec.id);
|
|
18089
|
+
const goal = to[spec.id];
|
|
18090
|
+
const s = springs.get(spec.id);
|
|
18091
|
+
if (s) {
|
|
18092
|
+
s.omega = motion.omega;
|
|
18093
|
+
s.zeta = motion.zeta;
|
|
18094
|
+
s.set(goal);
|
|
18095
|
+
} else if (Math.abs(goal - v.shown) > 0.5) {
|
|
18096
|
+
springs.set(spec.id, new Spring(v.shown, motion.omega, motion.zeta, 0.2).set(goal));
|
|
18097
|
+
} else
|
|
18098
|
+
v.folding = false;
|
|
18099
|
+
}
|
|
18100
|
+
loop.wake();
|
|
18101
|
+
};
|
|
18102
|
+
const fold = (id, value) => {
|
|
18103
|
+
const v = views.get(id);
|
|
18104
|
+
if (value && !v.folding)
|
|
18105
|
+
v.open = v.shown;
|
|
18106
|
+
collapsed[id] = value;
|
|
18107
|
+
v.folding = true;
|
|
18108
|
+
v.flip.set(value ? 1 : 0);
|
|
18109
|
+
};
|
|
18110
|
+
const release = (id) => {
|
|
18111
|
+
springs.delete(id);
|
|
18112
|
+
views.get(id).folding = false;
|
|
18113
|
+
};
|
|
18114
|
+
function setCollapsed(id, value, save = true) {
|
|
18115
|
+
const spec = specs.find((p) => p.id === id);
|
|
18116
|
+
if (!spec?.collapsible || collapsed[id] === value)
|
|
18117
|
+
return;
|
|
18118
|
+
fold(id, value);
|
|
18119
|
+
if (save)
|
|
18120
|
+
persist();
|
|
18121
|
+
animate(COLLAPSE);
|
|
18122
|
+
}
|
|
18123
|
+
const setSizes = (next) => {
|
|
18124
|
+
for (const [id, s] of Object.entries(next))
|
|
18125
|
+
sizes[id] = s;
|
|
18126
|
+
};
|
|
18127
|
+
function bindDivider(d, index) {
|
|
18128
|
+
const a = specs[index];
|
|
18129
|
+
const b = specs[index + 1];
|
|
18130
|
+
let start;
|
|
18131
|
+
let from = 0;
|
|
18132
|
+
let pointer = -1;
|
|
18133
|
+
d.addEventListener("pointerdown", (e) => {
|
|
18134
|
+
if (e.button !== 0)
|
|
18135
|
+
return;
|
|
18136
|
+
e.preventDefault();
|
|
18137
|
+
d.focus({ preventScroll: true });
|
|
18138
|
+
const shown = current();
|
|
18139
|
+
for (const id of [a.id, b.id])
|
|
18140
|
+
if (!collapsed[id])
|
|
18141
|
+
release(id);
|
|
18142
|
+
start = { ...shown };
|
|
18143
|
+
for (const id of [a.id, b.id])
|
|
18144
|
+
if (collapsed[id])
|
|
18145
|
+
start[id] = rail(id);
|
|
18146
|
+
from = row ? e.clientX : e.clientY;
|
|
18147
|
+
pointer = e.pointerId;
|
|
18148
|
+
d.setPointerCapture(e.pointerId);
|
|
18149
|
+
d.dataset.active = "";
|
|
18150
|
+
doc.documentElement.dataset.knobsResizing = direction;
|
|
18151
|
+
});
|
|
18152
|
+
d.addEventListener("pointermove", (e) => {
|
|
18153
|
+
if (!start || e.pointerId !== pointer)
|
|
18154
|
+
return;
|
|
18155
|
+
const delta = (row ? e.clientX : e.clientY) - from;
|
|
18156
|
+
const result = drag(specs, start, index, delta, collapsed);
|
|
18157
|
+
if (result.collapse) {
|
|
18158
|
+
const { id, collapsed: now } = result.collapse;
|
|
18159
|
+
if (!now) {
|
|
18160
|
+
for (const [k, s] of Object.entries(result.sizes))
|
|
18161
|
+
if (k === id)
|
|
18162
|
+
sizes[k] = s;
|
|
18163
|
+
}
|
|
18164
|
+
setCollapsed(id, now, false);
|
|
18165
|
+
start = { ...target() };
|
|
18166
|
+
if (now)
|
|
18167
|
+
start[id] = rail(id);
|
|
18168
|
+
from = row ? e.clientX : e.clientY;
|
|
18169
|
+
return;
|
|
18170
|
+
}
|
|
18171
|
+
if (collapsed[a.id] || collapsed[b.id])
|
|
18172
|
+
return;
|
|
18173
|
+
setSizes(result.sizes);
|
|
18174
|
+
for (const id of [a.id, b.id])
|
|
18175
|
+
release(id);
|
|
18176
|
+
const shown = current();
|
|
18177
|
+
render(shown);
|
|
18178
|
+
rememberOpen(shown);
|
|
18179
|
+
});
|
|
18180
|
+
const end = (e) => {
|
|
18181
|
+
if (!start || e.pointerId !== pointer)
|
|
18182
|
+
return;
|
|
18183
|
+
start = undefined;
|
|
18184
|
+
delete d.dataset.active;
|
|
18185
|
+
delete doc.documentElement.dataset.knobsResizing;
|
|
18186
|
+
for (const spec of specs)
|
|
18187
|
+
if (flex.has(spec.id) && !collapsed[spec.id])
|
|
18188
|
+
sizes[spec.id] = views.get(spec.id).shown;
|
|
18189
|
+
persist();
|
|
18190
|
+
};
|
|
18191
|
+
d.addEventListener("pointerup", end);
|
|
18192
|
+
d.addEventListener("pointercancel", end);
|
|
18193
|
+
d.addEventListener("lostpointercapture", end);
|
|
18194
|
+
d.addEventListener("dblclick", (e) => {
|
|
18195
|
+
e.preventDefault();
|
|
18196
|
+
reset(a.id, b.id);
|
|
18197
|
+
});
|
|
18198
|
+
d.addEventListener("keydown", (e) => {
|
|
18199
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
18200
|
+
return;
|
|
18201
|
+
const back = row ? "ArrowLeft" : "ArrowUp";
|
|
18202
|
+
const fwd = row ? "ArrowRight" : "ArrowDown";
|
|
18203
|
+
const step = e.shiftKey ? 64 : 16;
|
|
18204
|
+
let handled = true;
|
|
18205
|
+
if (e.key === back || e.key === fwd) {
|
|
18206
|
+
const goal = target();
|
|
18207
|
+
const shut = [a.id, b.id].find((id) => collapsed[id]);
|
|
18208
|
+
if (shut)
|
|
18209
|
+
setCollapsed(shut, false);
|
|
18210
|
+
else {
|
|
18211
|
+
setSizes(nudge(specs, goal, index, e.key === fwd ? step : -step));
|
|
18212
|
+
persist();
|
|
18213
|
+
animate();
|
|
18214
|
+
}
|
|
18215
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
18216
|
+
const goal = target();
|
|
18217
|
+
setSizes(nudge(specs, goal, index, e.key === "End" ? 1e6 : -1e6));
|
|
18218
|
+
persist();
|
|
18219
|
+
animate();
|
|
18220
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
18221
|
+
const id = collapsibleSide(specs, index);
|
|
18222
|
+
if (id)
|
|
18223
|
+
setCollapsed(id, !collapsed[id]);
|
|
18224
|
+
else
|
|
18225
|
+
handled = false;
|
|
18226
|
+
} else if (e.key === "Escape")
|
|
18227
|
+
d.blur();
|
|
18228
|
+
else
|
|
18229
|
+
handled = false;
|
|
18230
|
+
if (handled) {
|
|
18231
|
+
e.preventDefault();
|
|
18232
|
+
e.stopPropagation();
|
|
18233
|
+
}
|
|
18234
|
+
});
|
|
18235
|
+
}
|
|
18236
|
+
function reset(...ids) {
|
|
18237
|
+
const which = ids.length ? ids : specs.map((p) => p.id);
|
|
18238
|
+
let folds = false;
|
|
18239
|
+
for (const id of which) {
|
|
18240
|
+
delete sizes[id];
|
|
18241
|
+
const spec = specs.find((p) => p.id === id);
|
|
18242
|
+
const shut = Boolean(spec?.collapsed);
|
|
18243
|
+
if (spec?.collapsible && collapsed[id] !== shut) {
|
|
18244
|
+
fold(id, shut);
|
|
18245
|
+
folds = true;
|
|
18246
|
+
}
|
|
18247
|
+
}
|
|
18248
|
+
if (which.some((id) => flex.has(id)))
|
|
18249
|
+
for (const id of flex)
|
|
18250
|
+
delete sizes[id];
|
|
18251
|
+
persist();
|
|
18252
|
+
animate(folds ? COLLAPSE : SETTLE);
|
|
18253
|
+
}
|
|
18254
|
+
{
|
|
18255
|
+
const first = target();
|
|
18256
|
+
for (const spec of specs) {
|
|
18257
|
+
const open = { ...collapsed, [spec.id]: false };
|
|
18258
|
+
views.get(spec.id).open = collapsed[spec.id] ? solve2({ panes: specs, sizes, collapsed: open, total: total(), rail })[spec.id] : first[spec.id];
|
|
18259
|
+
}
|
|
18260
|
+
render(first);
|
|
18261
|
+
measureChevrons();
|
|
18262
|
+
render(first);
|
|
18263
|
+
}
|
|
18264
|
+
const relayout = (remeasure) => {
|
|
18265
|
+
if (remeasure)
|
|
18266
|
+
measure();
|
|
18267
|
+
if (springs.size) {
|
|
18268
|
+
if (remeasure)
|
|
18269
|
+
measureChevrons();
|
|
18270
|
+
return loop.wake();
|
|
18271
|
+
}
|
|
18272
|
+
const shown = target();
|
|
18273
|
+
rememberOpen(shown);
|
|
18274
|
+
render(shown);
|
|
18275
|
+
if (remeasure) {
|
|
18276
|
+
measureChevrons();
|
|
18277
|
+
render(shown);
|
|
18278
|
+
}
|
|
18279
|
+
};
|
|
18280
|
+
const ro = new ResizeObserver(() => relayout(!measured));
|
|
18281
|
+
ro.observe(root);
|
|
18282
|
+
const restyled = () => relayout(true);
|
|
18283
|
+
root.addEventListener("knobs-restyle", restyled);
|
|
18284
|
+
const paneApi = (id) => {
|
|
18285
|
+
const v = views.get(id);
|
|
18286
|
+
if (!v)
|
|
18287
|
+
throw new Error(`No pane "${id}"`);
|
|
18288
|
+
return {
|
|
18289
|
+
id,
|
|
18290
|
+
element: v.el,
|
|
18291
|
+
body: v.body,
|
|
18292
|
+
header: v.head,
|
|
18293
|
+
get size() {
|
|
18294
|
+
return v.shown;
|
|
18295
|
+
},
|
|
18296
|
+
get collapsed() {
|
|
18297
|
+
return collapsed[id];
|
|
18298
|
+
},
|
|
18299
|
+
collapse: (value = !collapsed[id]) => setCollapsed(id, value),
|
|
18300
|
+
resize: (size) => api.resize(id, size),
|
|
18301
|
+
title(text, count) {
|
|
18302
|
+
if (v.titleEl)
|
|
18303
|
+
v.titleEl.textContent = text;
|
|
18304
|
+
if (v.countEl && count !== undefined)
|
|
18305
|
+
v.countEl.textContent = String(count);
|
|
18306
|
+
if (v.railLabel)
|
|
18307
|
+
v.railLabel.textContent = text;
|
|
18308
|
+
measureChevrons(v);
|
|
18309
|
+
}
|
|
18310
|
+
};
|
|
18311
|
+
};
|
|
18312
|
+
const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [...views.values()].flatMap((v) => [
|
|
18313
|
+
...v.head?.querySelectorAll(":scope > .knobs-pane-btn") ?? [],
|
|
18314
|
+
...v.rail && collapsed[v.spec.id] ? [v.rail] : []
|
|
18315
|
+
]), { listen: false, rank: 1 });
|
|
18316
|
+
const api = {
|
|
18317
|
+
element: root,
|
|
18318
|
+
pane: paneApi,
|
|
18319
|
+
layout: snapshot,
|
|
18320
|
+
collapse: (id, value) => setCollapsed(id, value ?? !collapsed[id]),
|
|
18321
|
+
resize(id, size) {
|
|
18322
|
+
const i = specs.findIndex((p) => p.id === id);
|
|
18323
|
+
if (i < 0)
|
|
18324
|
+
return;
|
|
18325
|
+
if (collapsed[id])
|
|
18326
|
+
setCollapsed(id, false);
|
|
18327
|
+
if (!flex.has(id))
|
|
18328
|
+
sizes[id] = size;
|
|
18329
|
+
else {
|
|
18330
|
+
const goal = target();
|
|
18331
|
+
const j = i > 0 ? i - 1 : i;
|
|
18332
|
+
const delta = size - goal[id];
|
|
18333
|
+
setSizes(nudge(specs, goal, j, j === i ? delta : -delta));
|
|
18334
|
+
}
|
|
18335
|
+
persist();
|
|
18336
|
+
animate();
|
|
18337
|
+
},
|
|
18338
|
+
reset: (id) => id ? reset(id) : reset(),
|
|
18339
|
+
dispose() {
|
|
18340
|
+
unregisterHints?.();
|
|
18341
|
+
loop.stop();
|
|
18342
|
+
ro.disconnect();
|
|
18343
|
+
root.removeEventListener("knobs-restyle", restyled);
|
|
18344
|
+
host?.removeAttribute("data-nested");
|
|
18345
|
+
root.remove();
|
|
18346
|
+
}
|
|
17784
18347
|
};
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
18348
|
+
return api;
|
|
18349
|
+
}
|
|
18350
|
+
|
|
18351
|
+
// src/ui/react.tsx
|
|
18352
|
+
init_timeline();
|
|
18353
|
+
init_compare();
|
|
18354
|
+
|
|
18355
|
+
// src/ui/compare-strip.ts
|
|
18356
|
+
init_compare_frames();
|
|
18357
|
+
init_compare_model();
|
|
18358
|
+
init_look();
|
|
18359
|
+
init_rgba();
|
|
18360
|
+
init_timeline_model();
|
|
18361
|
+
var BAR = 7;
|
|
18362
|
+
var GAP = 2;
|
|
18363
|
+
var DIFF = { w: 32, h: 18 };
|
|
18364
|
+
var DIFFS_PER_DRAW = 6;
|
|
18365
|
+
function compareStrip(container, options) {
|
|
18366
|
+
const doc = container.ownerDocument;
|
|
18367
|
+
const win = doc.defaultView;
|
|
18368
|
+
const timeline = options.timeline;
|
|
18369
|
+
const fps = () => options.fps ?? timeline.fps;
|
|
18370
|
+
const align = options.align ?? "track";
|
|
18371
|
+
const threshold = options.threshold ?? 4 / 255;
|
|
18372
|
+
const root = doc.createElement("div");
|
|
18373
|
+
root.className = "knobs-cmp-strip";
|
|
18374
|
+
root.setAttribute("role", "img");
|
|
18375
|
+
root.setAttribute("aria-label", "Frame strip: Before over After, with how much they differ");
|
|
18376
|
+
applyLook2(root, options);
|
|
18377
|
+
const canvas = doc.createElement("canvas");
|
|
18378
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
18379
|
+
root.append(canvas);
|
|
18380
|
+
container.append(root);
|
|
18381
|
+
const diffs = new Map;
|
|
18382
|
+
const pairDiffs = new Map;
|
|
18383
|
+
let disposed = false;
|
|
18384
|
+
let near = false;
|
|
18385
|
+
let onScreen = false;
|
|
18386
|
+
const makeSide = (source) => ({
|
|
18387
|
+
source,
|
|
18388
|
+
kind: mediaKind(source.src, source.kind),
|
|
18389
|
+
frames: null,
|
|
18390
|
+
still: null,
|
|
18391
|
+
seen: false
|
|
18392
|
+
});
|
|
18393
|
+
let sides = [makeSide(options.before), makeSide(options.after)];
|
|
18394
|
+
const offs = new Map;
|
|
18395
|
+
const load = (s) => {
|
|
18396
|
+
if (s.frames || s.still)
|
|
17804
18397
|
return;
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
hide(v.clip, isCollapsed && settled);
|
|
17812
|
-
const morph = row && Boolean(v.toggleIcon) && !v.el.hasAttribute("data-nested");
|
|
17813
|
-
v.el.toggleAttribute("data-morph", morph);
|
|
17814
|
-
const label = v.railLabel;
|
|
17815
|
-
const travel = morph && style.title === "travel" && Boolean(v.titleEl) && label.textContent === v.titleEl.textContent;
|
|
17816
|
-
v.el.toggleAttribute("data-travel", travel && folding);
|
|
17817
|
-
if (v.countEl)
|
|
17818
|
-
put(v.countEl, "opacity", travel && folding && p > 0 ? Math.max(0, 1 - p / COUNT_CLEAR).toFixed(3) : "");
|
|
17819
|
-
const k = 1 - p;
|
|
17820
|
-
if (travel && !folding) {
|
|
17821
|
-
put(label, "opacity", "0");
|
|
17822
|
-
put(label, "filter", "");
|
|
17823
|
-
put(label, "transform", "");
|
|
17824
|
-
put(label, "color", "");
|
|
17825
|
-
} else if (travel) {
|
|
17826
|
-
const t = titleTravel(v.places, open, r, v.edge, p);
|
|
17827
|
-
put(label, "opacity", "");
|
|
17828
|
-
put(label, "filter", "");
|
|
17829
|
-
put(label, "transform", k === 0 ? "" : `translate(${t.x.toFixed(2)}px,${t.y.toFixed(2)}px) rotate(${t.turn.toFixed(2)}deg)`);
|
|
17830
|
-
put(label, "color", k === 0 ? "" : `color-mix(in srgb,var(--kv-fg) ${(k * 100).toFixed(1)}%,var(--kv-fg-2))`);
|
|
17831
|
-
} else {
|
|
17832
|
-
put(label, "transform", "");
|
|
17833
|
-
put(label, "color", "");
|
|
17834
|
-
}
|
|
17835
|
-
if (morph) {
|
|
17836
|
-
put(v.rail, "opacity", "1");
|
|
17837
|
-
if (!travel)
|
|
17838
|
-
fade(label, v.faces.rail, LABEL_BLUR);
|
|
17839
|
-
const c = v.chev;
|
|
17840
|
-
const dx = c.chevEnd - c.iconEnd + (v.edge === "end" ? open - r : 0);
|
|
17841
|
-
const move = `translate(${(dx * k).toFixed(2)}px,${(c.dy * k).toFixed(2)}px)`;
|
|
17842
|
-
const size = `scale(${(1 + (c.scale - 1) * k).toFixed(4)})`;
|
|
17843
|
-
if (style.chevron === "turn")
|
|
17844
|
-
put(v.turn, "transform", k === 0 ? "" : `${move} rotate(${chevronTurn(p, v.edge).toFixed(2)}deg) ${size}`);
|
|
17845
|
-
else {
|
|
17846
|
-
const turn = chevronFlip(v.flip.x, v.edge);
|
|
17847
|
-
const over = v.flip.resting ? turn === 0 ? "" : "scaleX(-1)" : `perspective(${FLIP_DEPTH}px) rotateY(${turn.toFixed(2)}deg)`;
|
|
17848
|
-
put(v.turn, "transform", k === 0 && !over ? "" : `${move} ${size} ${over}`);
|
|
18398
|
+
if (s.kind === "video") {
|
|
18399
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
18400
|
+
offs.set(s, s.frames.listen(schedule));
|
|
18401
|
+
if (onScreen) {
|
|
18402
|
+
s.frames.visible(true);
|
|
18403
|
+
s.seen = true;
|
|
17849
18404
|
}
|
|
17850
18405
|
} else {
|
|
17851
|
-
const
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
18406
|
+
const img = doc.createElement("img");
|
|
18407
|
+
img.crossOrigin = "anonymous";
|
|
18408
|
+
img.decoding = "async";
|
|
18409
|
+
img.addEventListener("load", () => {
|
|
18410
|
+
pairDiffs.clear();
|
|
18411
|
+
schedule();
|
|
18412
|
+
});
|
|
18413
|
+
img.src = s.source.src;
|
|
18414
|
+
s.still = img;
|
|
17856
18415
|
}
|
|
17857
18416
|
};
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
put(v.el, row ? "width" : "height", `${size}px`);
|
|
17866
|
-
v.el.toggleAttribute("data-collapsed", collapsed[spec.id]);
|
|
17867
|
-
v.el.toggleAttribute("data-settled", !springs.has(spec.id));
|
|
17868
|
-
if (spec.collapsible)
|
|
17869
|
-
renderFold(v, size);
|
|
18417
|
+
const unload = (s) => {
|
|
18418
|
+
offs.get(s)?.();
|
|
18419
|
+
offs.delete(s);
|
|
18420
|
+
if (s.frames) {
|
|
18421
|
+
if (s.seen)
|
|
18422
|
+
s.frames.visible(false);
|
|
18423
|
+
s.frames.release();
|
|
17870
18424
|
}
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
if (
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
18425
|
+
s.frames = null;
|
|
18426
|
+
s.seen = false;
|
|
18427
|
+
if (s.still)
|
|
18428
|
+
s.still.removeAttribute("src");
|
|
18429
|
+
s.still = null;
|
|
18430
|
+
};
|
|
18431
|
+
let w = 1;
|
|
18432
|
+
let h = 1;
|
|
18433
|
+
let geo = { x: 0, w: 1 };
|
|
18434
|
+
const rowH = () => Math.max(4, (h - BAR - GAP * 2) / 2);
|
|
18435
|
+
const aspectOf = (s) => {
|
|
18436
|
+
if (s.still?.naturalWidth)
|
|
18437
|
+
return s.still.naturalWidth / s.still.naturalHeight;
|
|
18438
|
+
const c = s.frames?.nearest(0);
|
|
18439
|
+
return c ? c.bitmap.width / c.bitmap.height : 0;
|
|
18440
|
+
};
|
|
18441
|
+
const columns = () => stripColumns(timeline.view, geo.w, {
|
|
18442
|
+
duration: timeline.duration,
|
|
18443
|
+
fps: fps(),
|
|
18444
|
+
minWidth: rowH() * Math.max(16 / 9, ...sides.map(aspectOf)) + GAP
|
|
18445
|
+
});
|
|
18446
|
+
const measure = () => {
|
|
18447
|
+
const r = root.getBoundingClientRect();
|
|
18448
|
+
w = Math.max(1, r.width);
|
|
18449
|
+
h = Math.max(1, r.height);
|
|
18450
|
+
geo = { x: 0, w };
|
|
18451
|
+
if (align === "track") {
|
|
18452
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
18453
|
+
const t = track?.getBoundingClientRect();
|
|
18454
|
+
if (t && t.width > 0)
|
|
18455
|
+
geo = { x: t.left - r.left, w: t.width };
|
|
17888
18456
|
}
|
|
18457
|
+
readColors();
|
|
18458
|
+
schedule();
|
|
17889
18459
|
};
|
|
17890
|
-
const
|
|
17891
|
-
const
|
|
17892
|
-
for (const
|
|
17893
|
-
s
|
|
17894
|
-
return { sizes: s, collapsed: { ...collapsed } };
|
|
18460
|
+
const ask = (cols) => {
|
|
18461
|
+
const points = cols.map((c) => ({ frame: c.frame, tolerance: Math.floor((c.to - c.from) / 2) }));
|
|
18462
|
+
for (const s of sides)
|
|
18463
|
+
s.frames?.want(points);
|
|
17895
18464
|
};
|
|
17896
|
-
const
|
|
17897
|
-
if (
|
|
18465
|
+
const picOf = (s, c) => {
|
|
18466
|
+
if (s.still?.naturalWidth)
|
|
18467
|
+
return { img: s.still, w: s.still.naturalWidth, h: s.still.naturalHeight, key: "still" };
|
|
18468
|
+
const f = s.frames?.nearest(c.frame, Math.max(1, c.to - c.from));
|
|
18469
|
+
return f ? { img: f.bitmap, w: f.bitmap.width, h: f.bitmap.height, key: String(f.frame) } : null;
|
|
18470
|
+
};
|
|
18471
|
+
const probe = doc.createElement("canvas");
|
|
18472
|
+
probe.width = DIFF.w;
|
|
18473
|
+
probe.height = DIFF.h;
|
|
18474
|
+
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
18475
|
+
const differ = (a, b) => {
|
|
18476
|
+
if (!probeCtx)
|
|
17898
18477
|
return;
|
|
17899
18478
|
try {
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
|
|
17905
|
-
|
|
17906
|
-
views.get(spec.id).open = shown[spec.id];
|
|
17907
|
-
};
|
|
17908
|
-
const loop = frameLoop(win, (dt) => {
|
|
17909
|
-
for (const [id, s] of springs) {
|
|
17910
|
-
s.step(dt);
|
|
17911
|
-
if (s.resting) {
|
|
17912
|
-
springs.delete(id);
|
|
17913
|
-
views.get(id).folding = false;
|
|
17914
|
-
}
|
|
17915
|
-
}
|
|
17916
|
-
let flipping = false;
|
|
17917
|
-
for (const v of views.values()) {
|
|
17918
|
-
v.flip.step(dt);
|
|
17919
|
-
if (!v.flip.resting)
|
|
17920
|
-
flipping = true;
|
|
17921
|
-
}
|
|
17922
|
-
const shown = current();
|
|
17923
|
-
if (springs.size === 0)
|
|
17924
|
-
rememberOpen(shown);
|
|
17925
|
-
render(shown);
|
|
17926
|
-
return springs.size > 0 || flipping;
|
|
17927
|
-
});
|
|
17928
|
-
const animate = (motion = SETTLE) => {
|
|
17929
|
-
const to = target();
|
|
17930
|
-
for (const v of views.values())
|
|
17931
|
-
if (v.folding && !collapsed[v.spec.id])
|
|
17932
|
-
v.open = to[v.spec.id];
|
|
17933
|
-
if (reducedMotion(win)) {
|
|
17934
|
-
springs.clear();
|
|
17935
|
-
const arriving = [...views.values()].filter((v) => v.folding).map((v) => collapsed[v.spec.id] ? v.rail : v.rail ? v.inner : v.body);
|
|
17936
|
-
for (const v of views.values()) {
|
|
17937
|
-
v.folding = false;
|
|
17938
|
-
v.flip.snap(collapsed[v.spec.id] ? 1 : 0);
|
|
17939
|
-
}
|
|
17940
|
-
rememberOpen(to);
|
|
17941
|
-
render(to);
|
|
17942
|
-
for (const el of arriving)
|
|
17943
|
-
el?.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
17944
|
-
duration: 160,
|
|
17945
|
-
easing: "cubic-bezier(.2,.8,.2,1)"
|
|
17946
|
-
});
|
|
18479
|
+
probeCtx.drawImage(a.img, 0, 0, DIFF.w, DIFF.h);
|
|
18480
|
+
const pa = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
18481
|
+
probeCtx.drawImage(b.img, 0, 0, DIFF.w, DIFF.h);
|
|
18482
|
+
const pb = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
18483
|
+
return meanAbsDiff(pa, pb);
|
|
18484
|
+
} catch {
|
|
17947
18485
|
return;
|
|
17948
18486
|
}
|
|
17949
|
-
for (const spec of specs) {
|
|
17950
|
-
const v = views.get(spec.id);
|
|
17951
|
-
const goal = to[spec.id];
|
|
17952
|
-
const s = springs.get(spec.id);
|
|
17953
|
-
if (s) {
|
|
17954
|
-
s.omega = motion.omega;
|
|
17955
|
-
s.zeta = motion.zeta;
|
|
17956
|
-
s.set(goal);
|
|
17957
|
-
} else if (Math.abs(goal - v.shown) > 0.5) {
|
|
17958
|
-
springs.set(spec.id, new Spring(v.shown, motion.omega, motion.zeta, 0.2).set(goal));
|
|
17959
|
-
} else
|
|
17960
|
-
v.folding = false;
|
|
17961
|
-
}
|
|
17962
|
-
loop.wake();
|
|
17963
18487
|
};
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
collapsed[id] = value;
|
|
17969
|
-
v.folding = true;
|
|
17970
|
-
v.flip.set(value ? 1 : 0);
|
|
18488
|
+
let colors = { surface: "", accent: "", diff: "", wash: "" };
|
|
18489
|
+
const rgba = (raw, fallback) => {
|
|
18490
|
+
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
18491
|
+
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
17971
18492
|
};
|
|
17972
|
-
const
|
|
17973
|
-
|
|
17974
|
-
|
|
18493
|
+
const readColors = () => {
|
|
18494
|
+
const cs = win.getComputedStyle(root);
|
|
18495
|
+
colors = {
|
|
18496
|
+
surface: rgba(tokenCss(cs, "--kv-fill", ""), [1, 1, 1, 0.055]),
|
|
18497
|
+
accent: rgba(tokenCss(cs, "--kv-accent", ""), [1, 1, 1, 1]),
|
|
18498
|
+
diff: rgba(tokenCss(cs, "--_ku-cmp-diff", ""), [0.49, 0.81, 0.61, 1]),
|
|
18499
|
+
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
18500
|
+
};
|
|
17975
18501
|
};
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
persist();
|
|
17983
|
-
animate(COLLAPSE);
|
|
18502
|
+
let hover = null;
|
|
18503
|
+
let raf = 0;
|
|
18504
|
+
let asked = "";
|
|
18505
|
+
function schedule() {
|
|
18506
|
+
if (!raf && !disposed)
|
|
18507
|
+
raf = win.requestAnimationFrame(draw);
|
|
17984
18508
|
}
|
|
17985
|
-
const
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
const
|
|
17991
|
-
const
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
if (!start || e.pointerId !== pointer)
|
|
18509
|
+
const draw = () => {
|
|
18510
|
+
raf = 0;
|
|
18511
|
+
if (!near)
|
|
18512
|
+
return;
|
|
18513
|
+
const dpr = Math.min(3, Math.max(1, win.devicePixelRatio || 1));
|
|
18514
|
+
const cw = Math.round(w * dpr);
|
|
18515
|
+
const ch = Math.round(h * dpr);
|
|
18516
|
+
if (canvas.width !== cw || canvas.height !== ch) {
|
|
18517
|
+
canvas.width = cw;
|
|
18518
|
+
canvas.height = ch;
|
|
18519
|
+
}
|
|
18520
|
+
const ctx = canvas.getContext("2d");
|
|
18521
|
+
if (!ctx)
|
|
18522
|
+
return;
|
|
18523
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
18524
|
+
ctx.clearRect(0, 0, w, h);
|
|
18525
|
+
const cols = columns();
|
|
18526
|
+
const key = cols.map((c) => c.frame).join(",");
|
|
18527
|
+
if (key !== asked) {
|
|
18528
|
+
asked = key;
|
|
18529
|
+
ask(cols);
|
|
18530
|
+
}
|
|
18531
|
+
const rh = rowH();
|
|
18532
|
+
const radius = 3;
|
|
18533
|
+
let budget = DIFFS_PER_DRAW;
|
|
18534
|
+
let more = false;
|
|
18535
|
+
const ds = cols.map((c) => {
|
|
18536
|
+
const a = picOf(sides[0], c);
|
|
18537
|
+
const b = picOf(sides[1], c);
|
|
18538
|
+
if (!a || !b)
|
|
18016
18539
|
return;
|
|
18017
|
-
const
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
for (const [k, s] of Object.entries(result.sizes))
|
|
18023
|
-
if (k === id)
|
|
18024
|
-
sizes[k] = s;
|
|
18540
|
+
const k = `${a.key}|${b.key}`;
|
|
18541
|
+
if (!pairDiffs.has(k)) {
|
|
18542
|
+
if (budget-- <= 0) {
|
|
18543
|
+
more = true;
|
|
18544
|
+
return;
|
|
18025
18545
|
}
|
|
18026
|
-
|
|
18027
|
-
start = { ...target() };
|
|
18028
|
-
if (now)
|
|
18029
|
-
start[id] = rail(id);
|
|
18030
|
-
from = row ? e.clientX : e.clientY;
|
|
18031
|
-
return;
|
|
18546
|
+
pairDiffs.set(k, differ(a, b));
|
|
18032
18547
|
}
|
|
18033
|
-
|
|
18034
|
-
|
|
18035
|
-
|
|
18036
|
-
|
|
18037
|
-
release(id);
|
|
18038
|
-
const shown = current();
|
|
18039
|
-
render(shown);
|
|
18040
|
-
rememberOpen(shown);
|
|
18041
|
-
});
|
|
18042
|
-
const end = (e) => {
|
|
18043
|
-
if (!start || e.pointerId !== pointer)
|
|
18044
|
-
return;
|
|
18045
|
-
start = undefined;
|
|
18046
|
-
delete d.dataset.active;
|
|
18047
|
-
delete doc.documentElement.dataset.knobsResizing;
|
|
18048
|
-
for (const spec of specs)
|
|
18049
|
-
if (flex.has(spec.id) && !collapsed[spec.id])
|
|
18050
|
-
sizes[spec.id] = views.get(spec.id).shown;
|
|
18051
|
-
persist();
|
|
18052
|
-
};
|
|
18053
|
-
d.addEventListener("pointerup", end);
|
|
18054
|
-
d.addEventListener("pointercancel", end);
|
|
18055
|
-
d.addEventListener("lostpointercapture", end);
|
|
18056
|
-
d.addEventListener("dblclick", (e) => {
|
|
18057
|
-
e.preventDefault();
|
|
18058
|
-
reset(a.id, b.id);
|
|
18548
|
+
const d = pairDiffs.get(k);
|
|
18549
|
+
if (d !== undefined)
|
|
18550
|
+
diffs.set(c.frame, d);
|
|
18551
|
+
return d;
|
|
18059
18552
|
});
|
|
18060
|
-
|
|
18061
|
-
|
|
18553
|
+
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
18554
|
+
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
18555
|
+
cols.forEach((c, i) => {
|
|
18556
|
+
const x = geo.x + c.x + GAP / 2;
|
|
18557
|
+
const cwid = Math.max(1, c.w - GAP);
|
|
18558
|
+
if (x + cwid < 0 || x > w)
|
|
18062
18559
|
return;
|
|
18063
|
-
const
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
} else if (e.key === "Home" || e.key === "End") {
|
|
18078
|
-
const goal = target();
|
|
18079
|
-
setSizes(nudge(specs, goal, index, e.key === "End" ? 1e6 : -1e6));
|
|
18080
|
-
persist();
|
|
18081
|
-
animate();
|
|
18082
|
-
} else if (e.key === "Enter" || e.key === " ") {
|
|
18083
|
-
const id = collapsibleSide(specs, index);
|
|
18084
|
-
if (id)
|
|
18085
|
-
setCollapsed(id, !collapsed[id]);
|
|
18086
|
-
else
|
|
18087
|
-
handled = false;
|
|
18088
|
-
} else if (e.key === "Escape")
|
|
18089
|
-
d.blur();
|
|
18090
|
-
else
|
|
18091
|
-
handled = false;
|
|
18092
|
-
if (handled) {
|
|
18093
|
-
e.preventDefault();
|
|
18094
|
-
e.stopPropagation();
|
|
18560
|
+
for (const row of [0, 1]) {
|
|
18561
|
+
const y = row * (rh + GAP);
|
|
18562
|
+
const pic = picOf(sides[row], c);
|
|
18563
|
+
ctx.save();
|
|
18564
|
+
ctx.beginPath();
|
|
18565
|
+
ctx.roundRect(x, y, cwid, rh, radius);
|
|
18566
|
+
ctx.fillStyle = colors.surface;
|
|
18567
|
+
ctx.fill();
|
|
18568
|
+
if (pic) {
|
|
18569
|
+
ctx.clip();
|
|
18570
|
+
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
18571
|
+
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
18572
|
+
}
|
|
18573
|
+
ctx.restore();
|
|
18095
18574
|
}
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
for (const id of which) {
|
|
18102
|
-
delete sizes[id];
|
|
18103
|
-
const spec = specs.find((p) => p.id === id);
|
|
18104
|
-
const shut = Boolean(spec?.collapsed);
|
|
18105
|
-
if (spec?.collapsible && collapsed[id] !== shut) {
|
|
18106
|
-
fold(id, shut);
|
|
18107
|
-
folds = true;
|
|
18575
|
+
if (hover === i) {
|
|
18576
|
+
ctx.fillStyle = colors.wash;
|
|
18577
|
+
ctx.beginPath();
|
|
18578
|
+
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
18579
|
+
ctx.fill();
|
|
18108
18580
|
}
|
|
18581
|
+
const d = ds[i];
|
|
18582
|
+
if (d !== undefined && d > 0) {
|
|
18583
|
+
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
18584
|
+
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
18585
|
+
ctx.fillStyle = colors.diff;
|
|
18586
|
+
ctx.beginPath();
|
|
18587
|
+
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
18588
|
+
ctx.fill();
|
|
18589
|
+
ctx.globalAlpha = 1;
|
|
18590
|
+
}
|
|
18591
|
+
});
|
|
18592
|
+
const v = timeline.view;
|
|
18593
|
+
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
18594
|
+
if (px >= -1 && px <= w + 1) {
|
|
18595
|
+
ctx.fillStyle = colors.accent;
|
|
18596
|
+
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
18109
18597
|
}
|
|
18110
|
-
if (
|
|
18111
|
-
|
|
18112
|
-
|
|
18113
|
-
|
|
18114
|
-
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
return
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18598
|
+
if (more)
|
|
18599
|
+
schedule();
|
|
18600
|
+
};
|
|
18601
|
+
const timeAt = (clientX) => {
|
|
18602
|
+
const r = root.getBoundingClientRect();
|
|
18603
|
+
const v = timeline.view;
|
|
18604
|
+
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
18605
|
+
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
18606
|
+
};
|
|
18607
|
+
const columnAt = (clientX) => {
|
|
18608
|
+
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
18609
|
+
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
18610
|
+
return i < 0 ? null : i;
|
|
18611
|
+
};
|
|
18612
|
+
let dragging = false;
|
|
18613
|
+
const pick = (clientX) => {
|
|
18614
|
+
const t = timeAt(clientX);
|
|
18615
|
+
timeline.seek(t);
|
|
18616
|
+
options.onPick?.(t);
|
|
18617
|
+
};
|
|
18618
|
+
root.addEventListener("pointerdown", (e) => {
|
|
18619
|
+
if (e.button !== 0)
|
|
18620
|
+
return;
|
|
18621
|
+
dragging = true;
|
|
18622
|
+
root.setPointerCapture(e.pointerId);
|
|
18623
|
+
const host = root.closest(".knobs-compare");
|
|
18624
|
+
if (host)
|
|
18625
|
+
host.focus({ preventScroll: true });
|
|
18626
|
+
else
|
|
18627
|
+
timeline.focus();
|
|
18628
|
+
if (timeline.playing)
|
|
18629
|
+
timeline.pause();
|
|
18630
|
+
pick(e.clientX);
|
|
18631
|
+
e.preventDefault();
|
|
18632
|
+
});
|
|
18633
|
+
root.addEventListener("pointermove", (e) => {
|
|
18634
|
+
if (dragging)
|
|
18635
|
+
pick(e.clientX);
|
|
18636
|
+
const i = columnAt(e.clientX);
|
|
18637
|
+
if (i !== hover) {
|
|
18638
|
+
hover = i;
|
|
18639
|
+
schedule();
|
|
18140
18640
|
}
|
|
18641
|
+
});
|
|
18642
|
+
const end = () => {
|
|
18643
|
+
dragging = false;
|
|
18141
18644
|
};
|
|
18142
|
-
|
|
18645
|
+
root.addEventListener("pointerup", end);
|
|
18646
|
+
root.addEventListener("pointercancel", end);
|
|
18647
|
+
root.addEventListener("lostpointercapture", end);
|
|
18648
|
+
root.addEventListener("pointerleave", () => {
|
|
18649
|
+
if (hover === null)
|
|
18650
|
+
return;
|
|
18651
|
+
hover = null;
|
|
18652
|
+
schedule();
|
|
18653
|
+
});
|
|
18654
|
+
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
18655
|
+
const ro = new win.ResizeObserver(measure);
|
|
18143
18656
|
ro.observe(root);
|
|
18144
|
-
const
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
return v.shown;
|
|
18157
|
-
},
|
|
18158
|
-
get collapsed() {
|
|
18159
|
-
return collapsed[id];
|
|
18160
|
-
},
|
|
18161
|
-
collapse: (value = !collapsed[id]) => setCollapsed(id, value),
|
|
18162
|
-
resize: (size) => api.resize(id, size),
|
|
18163
|
-
title(text, count) {
|
|
18164
|
-
if (v.titleEl)
|
|
18165
|
-
v.titleEl.textContent = text;
|
|
18166
|
-
if (v.countEl && count !== undefined)
|
|
18167
|
-
v.countEl.textContent = String(count);
|
|
18168
|
-
if (v.railLabel)
|
|
18169
|
-
v.railLabel.textContent = text;
|
|
18170
|
-
measureChevrons(v);
|
|
18171
|
-
}
|
|
18172
|
-
};
|
|
18657
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
18658
|
+
if (track)
|
|
18659
|
+
ro.observe(track);
|
|
18660
|
+
root.addEventListener("knobs-restyle", measure);
|
|
18661
|
+
const setOnScreen = (on) => {
|
|
18662
|
+
onScreen = on;
|
|
18663
|
+
for (const s of sides) {
|
|
18664
|
+
if (!s.frames || s.seen === on)
|
|
18665
|
+
continue;
|
|
18666
|
+
s.seen = on;
|
|
18667
|
+
s.frames.visible(on);
|
|
18668
|
+
}
|
|
18173
18669
|
};
|
|
18174
|
-
const
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18670
|
+
const setNear = (on) => {
|
|
18671
|
+
near = on;
|
|
18672
|
+
if (on) {
|
|
18673
|
+
for (const s of sides)
|
|
18674
|
+
load(s);
|
|
18675
|
+
schedule();
|
|
18676
|
+
} else
|
|
18677
|
+
for (const s of sides)
|
|
18678
|
+
unload(s);
|
|
18679
|
+
};
|
|
18680
|
+
const IO = win.IntersectionObserver;
|
|
18681
|
+
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
18682
|
+
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
18683
|
+
measure();
|
|
18684
|
+
if (nearIo && screenIo) {
|
|
18685
|
+
nearIo.observe(root);
|
|
18686
|
+
screenIo.observe(root);
|
|
18687
|
+
} else {
|
|
18688
|
+
setNear(true);
|
|
18689
|
+
setOnScreen(true);
|
|
18690
|
+
}
|
|
18691
|
+
return {
|
|
18179
18692
|
element: root,
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
const
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
|
|
18693
|
+
diffs,
|
|
18694
|
+
update(next) {
|
|
18695
|
+
const was = sides;
|
|
18696
|
+
sides = [
|
|
18697
|
+
next.before ? makeSide(next.before) : was[0],
|
|
18698
|
+
next.after ? makeSide(next.after) : was[1]
|
|
18699
|
+
];
|
|
18700
|
+
for (const s of was)
|
|
18701
|
+
if (!sides.includes(s))
|
|
18702
|
+
unload(s);
|
|
18703
|
+
diffs.clear();
|
|
18704
|
+
pairDiffs.clear();
|
|
18705
|
+
asked = "";
|
|
18706
|
+
if (near)
|
|
18707
|
+
for (const s of sides)
|
|
18708
|
+
load(s);
|
|
18709
|
+
if (onScreen)
|
|
18710
|
+
setOnScreen(true);
|
|
18711
|
+
schedule();
|
|
18199
18712
|
},
|
|
18200
|
-
reset: (id) => id ? reset(id) : reset(),
|
|
18201
18713
|
dispose() {
|
|
18202
|
-
|
|
18203
|
-
|
|
18714
|
+
disposed = true;
|
|
18715
|
+
if (raf)
|
|
18716
|
+
win.cancelAnimationFrame(raf);
|
|
18717
|
+
unlisten();
|
|
18204
18718
|
ro.disconnect();
|
|
18205
|
-
|
|
18206
|
-
|
|
18719
|
+
nearIo?.disconnect();
|
|
18720
|
+
screenIo?.disconnect();
|
|
18721
|
+
for (const s of sides)
|
|
18722
|
+
unload(s);
|
|
18207
18723
|
root.remove();
|
|
18208
18724
|
}
|
|
18209
18725
|
};
|
|
18210
|
-
return api;
|
|
18211
18726
|
}
|
|
18212
18727
|
|
|
18213
18728
|
// src/ui/react.tsx
|
|
18214
|
-
init_timeline();
|
|
18215
|
-
init_compare();
|
|
18216
|
-
init_compare_strip();
|
|
18217
18729
|
init_tree();
|
|
18218
18730
|
|
|
18219
18731
|
// src/ui/walkthrough.ts
|
|
@@ -21509,9 +22021,8 @@ var walkthroughCss = `${foundationCss}
|
|
|
21509
22021
|
.knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
|
|
21510
22022
|
/* A compare is as tall as its two pictures side by side at this width, in every mode (Wipe fuses
|
|
21511
22023
|
them at one side's size, so nothing grows or re-measures), plus its mode bar, the line and gaps
|
|
21512
|
-
(92 px)
|
|
22024
|
+
(92 px). */
|
|
21513
22025
|
.knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
|
|
21514
|
-
.knobs-wt-fit[data-strip]{--kw-chrome:158px}
|
|
21515
22026
|
.knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
|
|
21516
22027
|
.knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:var(--kw-frame-r);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
|
|
21517
22028
|
.knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
|
|
@@ -21524,16 +22035,15 @@ var walkthroughCss = `${foundationCss}
|
|
|
21524
22035
|
/* The diff inside it: its header row 28 high on the container's padding, a hairline under it, and
|
|
21525
22036
|
no line of its own under the code (the container's ring closes it). */
|
|
21526
22037
|
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-head:28px;--kd-x:0px}
|
|
21527
|
-
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"])::after{display:none}
|
|
21528
|
-
.knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--
|
|
22038
|
+
.knobs-wt-slot[data-kind="diff"] :is(.knobs-ui.knobs-diff:not([data-on="pane"]),.knobs-diff-review)::after{display:none}
|
|
22039
|
+
.knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--kw-frame-r);background:var(--_k-fill);color:var(--_k-fg);
|
|
21529
22040
|
font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
|
|
21530
|
-
.knobs-surface
|
|
22041
|
+
.knobs-surface .knobs-wt-slot[data-kind="diff"]>.knobs-wt-body{padding:0;border-radius:0;background:transparent;box-shadow:inset 0 -1px 0 var(--_k-border-subtle)}
|
|
21531
22042
|
.knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
21532
22043
|
.knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
|
|
21533
22044
|
.knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
|
|
21534
22045
|
.knobs-wt-out{color:var(--_k-fg-2)}
|
|
21535
22046
|
.knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
|
|
21536
|
-
.knobs-wt-mlabel{color:var(--_k-fg-2)}
|
|
21537
22047
|
.knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-feature-settings:var(--_kr-data)}
|
|
21538
22048
|
.knobs-wt-mfrom{color:var(--_k-fg-3)}
|
|
21539
22049
|
.knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
|
|
@@ -21552,6 +22062,9 @@ var walkthroughCss = `${foundationCss}
|
|
|
21552
22062
|
.knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
|
|
21553
22063
|
.knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
|
|
21554
22064
|
.knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
|
|
22065
|
+
/* A diff holds its room with its own well's edge, no fill: the diff reads the colour behind it
|
|
22066
|
+
as it mounts, and a loading fill there would stay under its code. */
|
|
22067
|
+
.knobs-wt-slot[data-kind="diff"]>.knobs-wt-hold{background:transparent}
|
|
21555
22068
|
.knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
|
|
21556
22069
|
|
|
21557
22070
|
/* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
|
|
@@ -21801,8 +22314,6 @@ var side = (s, fallback) => ({
|
|
|
21801
22314
|
async function mountCompare(host, m, look, alive, onEvent) {
|
|
21802
22315
|
const doc = host.ownerDocument;
|
|
21803
22316
|
const fit = div(doc, "knobs-wt-fit");
|
|
21804
|
-
if (m.strip)
|
|
21805
|
-
fit.dataset.strip = "";
|
|
21806
22317
|
const { before, after } = m;
|
|
21807
22318
|
const known = parseAspect(m.aspect);
|
|
21808
22319
|
fit.style.setProperty("--kw-aspect", String(known ?? DEFAULT_ASPECT));
|
|
@@ -21829,7 +22340,6 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
21829
22340
|
timeline: mediaKind(before.src) === "video" || mediaKind(after.src) === "video",
|
|
21830
22341
|
lazy: false,
|
|
21831
22342
|
...aspect ? { aspect } : {},
|
|
21832
|
-
...m.strip ? { strip: true } : {},
|
|
21833
22343
|
onMode: (next) => {
|
|
21834
22344
|
onEvent?.({ what: "mode", detail: next });
|
|
21835
22345
|
const layer = marks?.element;
|
|
@@ -21891,11 +22401,11 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
21891
22401
|
pills?.sync();
|
|
21892
22402
|
marking?.kick();
|
|
21893
22403
|
if (source !== "play")
|
|
21894
|
-
onEvent?.({ what: "seek", time:
|
|
22404
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
21895
22405
|
},
|
|
21896
22406
|
onPlay: (on) => {
|
|
21897
22407
|
marking?.kick();
|
|
21898
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
22408
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(c.time) });
|
|
21899
22409
|
}
|
|
21900
22410
|
});
|
|
21901
22411
|
requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
|
|
@@ -21913,13 +22423,13 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
21913
22423
|
const zoom = zoomedIn(c.zoom) ? c.zoom : null;
|
|
21914
22424
|
return {
|
|
21915
22425
|
kind: "compare",
|
|
21916
|
-
time:
|
|
21917
|
-
duration:
|
|
22426
|
+
time: round3(c.time),
|
|
22427
|
+
duration: round3(c.duration),
|
|
21918
22428
|
playing: c.playing,
|
|
21919
22429
|
mode: c.mode,
|
|
21920
22430
|
...pills?.current ? { chapter: pills.current.label } : {},
|
|
21921
|
-
...loop ? { loop: { start:
|
|
21922
|
-
...zoom ? { zoom: { u:
|
|
22431
|
+
...loop ? { loop: { start: round3(loop.start), end: round3(loop.end) } } : {},
|
|
22432
|
+
...zoom ? { zoom: { u: round3(zoom.u), v: round3(zoom.v), w: round3(zoom.w), h: round3(zoom.h) } } : {}
|
|
21923
22433
|
};
|
|
21924
22434
|
},
|
|
21925
22435
|
dispose: () => {
|
|
@@ -21932,7 +22442,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
21932
22442
|
}
|
|
21933
22443
|
};
|
|
21934
22444
|
}
|
|
21935
|
-
var
|
|
22445
|
+
var round3 = (v) => Math.round(v * 100) / 100;
|
|
21936
22446
|
var MARK_GLIDE = 450;
|
|
21937
22447
|
var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
|
|
21938
22448
|
async function mountVideo(host, m, look, alive, onEvent) {
|
|
@@ -21959,12 +22469,12 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
21959
22469
|
} : {},
|
|
21960
22470
|
onPlay: (on, time) => {
|
|
21961
22471
|
marking?.kick();
|
|
21962
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
22472
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(time) });
|
|
21963
22473
|
},
|
|
21964
22474
|
onSeek: (time) => {
|
|
21965
22475
|
pills?.sync();
|
|
21966
22476
|
marking?.kick();
|
|
21967
|
-
onEvent?.({ what: "seek", time:
|
|
22477
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
21968
22478
|
}
|
|
21969
22479
|
});
|
|
21970
22480
|
const v = p.video;
|
|
@@ -21997,8 +22507,8 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
21997
22507
|
seekBy: (d) => p.seekBy(d),
|
|
21998
22508
|
context: () => ({
|
|
21999
22509
|
kind: "video",
|
|
22000
|
-
time:
|
|
22001
|
-
...p.duration ? { duration:
|
|
22510
|
+
time: round3(p.time),
|
|
22511
|
+
...p.duration ? { duration: round3(p.duration) } : {},
|
|
22002
22512
|
playing: p.playing,
|
|
22003
22513
|
...pills?.current ? { chapter: pills.current.label } : {}
|
|
22004
22514
|
}),
|
|
@@ -22142,8 +22652,6 @@ var fmt = (v, unit = "") => `${v}${unit}`;
|
|
|
22142
22652
|
function mountMetric(host, m) {
|
|
22143
22653
|
const doc = host.ownerDocument;
|
|
22144
22654
|
const box = div(doc, "knobs-wt-metric");
|
|
22145
|
-
const label = div(doc, "knobs-wt-mlabel");
|
|
22146
|
-
label.textContent = m.label;
|
|
22147
22655
|
const value = div(doc, "knobs-wt-mvalue");
|
|
22148
22656
|
const span = (cls, text) => {
|
|
22149
22657
|
const s = doc.createElement("span");
|
|
@@ -22163,7 +22671,7 @@ function mountMetric(host, m) {
|
|
|
22163
22671
|
delta.dataset.tone = d > 0 === (better === "up") ? "good" : "bad";
|
|
22164
22672
|
value.append(delta);
|
|
22165
22673
|
}
|
|
22166
|
-
box.append(
|
|
22674
|
+
box.append(value);
|
|
22167
22675
|
const total = num(m.of);
|
|
22168
22676
|
if (Number.isFinite(total) && total > 0 && Number.isFinite(a) && Number.isFinite(b)) {
|
|
22169
22677
|
const track = div(doc, "knobs-wt-mtrack");
|
|
@@ -22217,7 +22725,7 @@ function mountDecision(host, m, slot) {
|
|
|
22217
22725
|
}
|
|
22218
22726
|
var HEAVY = new Set(["compare", "video", "diff", "mount"]);
|
|
22219
22727
|
var RELEASES = new Set(["compare", "video"]);
|
|
22220
|
-
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "
|
|
22728
|
+
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "history" ? m.label : undefined;
|
|
22221
22729
|
function mediaSlot(container, given) {
|
|
22222
22730
|
const doc = container.ownerDocument;
|
|
22223
22731
|
injectWalkthroughStyles(doc);
|
|
@@ -22231,7 +22739,7 @@ function mediaSlot(container, given) {
|
|
|
22231
22739
|
root.dataset.kind = m.kind;
|
|
22232
22740
|
applyLook2(root, options);
|
|
22233
22741
|
const label = labelOf2(m);
|
|
22234
|
-
if (label
|
|
22742
|
+
if (label) {
|
|
22235
22743
|
const l = doc.createElement("span");
|
|
22236
22744
|
l.className = "knobs-wt-slot-label";
|
|
22237
22745
|
l.textContent = label;
|
|
@@ -24039,7 +24547,7 @@ function Compare(props) {
|
|
|
24039
24547
|
c.dispose();
|
|
24040
24548
|
handle.current = null;
|
|
24041
24549
|
};
|
|
24042
|
-
}, [props.timeline, props.
|
|
24550
|
+
}, [props.timeline, props.renderer, props.scrubCache]);
|
|
24043
24551
|
useFollow({
|
|
24044
24552
|
before: props.before,
|
|
24045
24553
|
after: props.after,
|