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