@kitlangton/knobs 0.4.0-next.16 → 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 +28 -9
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-9y93net8.js → chunk-3vag4n3q.js} +39 -9
- package/dist/dev/{chunk-e5hbs8fe.js → chunk-5cv3g7ca.js} +205 -64
- package/dist/dev/{chunk-exaya50m.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +292 -83
- 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 +7 -1
- 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/page-hints.d.ts +3 -2
- package/dist/types/panel/position.d.ts +23 -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.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(",");
|
|
@@ -3565,12 +3584,24 @@ function createRegistry(doc) {
|
|
|
3565
3584
|
return;
|
|
3566
3585
|
if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
|
|
3567
3586
|
return;
|
|
3568
|
-
if (isTyping(e.target) || !wide())
|
|
3587
|
+
if (isTyping(e.target) || !wide(e.target))
|
|
3569
3588
|
return;
|
|
3570
3589
|
if (registry.start())
|
|
3571
3590
|
e.preventDefault();
|
|
3572
3591
|
};
|
|
3573
|
-
const
|
|
3592
|
+
const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
|
|
3593
|
+
const wide = (target) => {
|
|
3594
|
+
if (!listening)
|
|
3595
|
+
return false;
|
|
3596
|
+
if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
|
|
3597
|
+
return true;
|
|
3598
|
+
if (sources.length < 2)
|
|
3599
|
+
return false;
|
|
3600
|
+
if (target === undefined)
|
|
3601
|
+
return true;
|
|
3602
|
+
const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
|
|
3603
|
+
return Boolean(piece?.classList.contains("knobs-ui"));
|
|
3604
|
+
};
|
|
3574
3605
|
const relisten = () => {
|
|
3575
3606
|
const next = sources.some((s) => s.listen);
|
|
3576
3607
|
if (next === listening)
|
|
@@ -6716,6 +6747,31 @@ function seeked(s, current, fps) {
|
|
|
6716
6747
|
return { state: { seeking: true, pending: null }, apply: s.pending, settled: false };
|
|
6717
6748
|
return { state: SEEK_IDLE, apply: null, settled: true };
|
|
6718
6749
|
}
|
|
6750
|
+
function releaseSeek(current, t, fps, duration) {
|
|
6751
|
+
const at = seekTime(t, fps, duration);
|
|
6752
|
+
return sameFrame(current, at, fps) ? null : at;
|
|
6753
|
+
}
|
|
6754
|
+
function stallGuard(timers, onStall, ms = SEEK_STALL_MS) {
|
|
6755
|
+
let id = 0;
|
|
6756
|
+
const clear = () => {
|
|
6757
|
+
if (id)
|
|
6758
|
+
timers.clearTimeout(id);
|
|
6759
|
+
id = 0;
|
|
6760
|
+
};
|
|
6761
|
+
return {
|
|
6762
|
+
arm() {
|
|
6763
|
+
clear();
|
|
6764
|
+
id = timers.setTimeout(() => {
|
|
6765
|
+
id = 0;
|
|
6766
|
+
onStall();
|
|
6767
|
+
}, ms);
|
|
6768
|
+
},
|
|
6769
|
+
clear,
|
|
6770
|
+
get armed() {
|
|
6771
|
+
return id !== 0;
|
|
6772
|
+
}
|
|
6773
|
+
};
|
|
6774
|
+
}
|
|
6719
6775
|
function letterbox(media, box) {
|
|
6720
6776
|
if (media.w <= 0 || media.h <= 0 || box.w <= 0 || box.h <= 0)
|
|
6721
6777
|
return box;
|
|
@@ -6836,7 +6892,7 @@ function stripPeaks(diffs, o = {}) {
|
|
|
6836
6892
|
});
|
|
6837
6893
|
return out;
|
|
6838
6894
|
}
|
|
6839
|
-
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, FUSE, fuseCorners = (r, k) => {
|
|
6895
|
+
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) => {
|
|
6840
6896
|
const inner = r * Math.min(1, Math.max(0, k));
|
|
6841
6897
|
return { before: { left: r, right: inner }, after: { left: inner, right: r } };
|
|
6842
6898
|
}, labelFold = (fold, mark, blur = 3) => {
|
|
@@ -7083,6 +7139,25 @@ var init_compare_mp4 = __esm(() => {
|
|
|
7083
7139
|
});
|
|
7084
7140
|
|
|
7085
7141
|
// src/ui/compare-frames.ts
|
|
7142
|
+
function scrubCaching(option, win) {
|
|
7143
|
+
if (option === false)
|
|
7144
|
+
return false;
|
|
7145
|
+
if (option === true)
|
|
7146
|
+
return true;
|
|
7147
|
+
const net = win?.navigator?.connection;
|
|
7148
|
+
if (net?.saveData)
|
|
7149
|
+
return false;
|
|
7150
|
+
return !win?.matchMedia?.("(prefers-reduced-data: reduce)").matches;
|
|
7151
|
+
}
|
|
7152
|
+
function proxyStride(bytes, count, total, budget) {
|
|
7153
|
+
if (count <= 0 || total <= 0)
|
|
7154
|
+
return 1;
|
|
7155
|
+
const each = bytes / count;
|
|
7156
|
+
let stride = 1;
|
|
7157
|
+
while (total / stride * each > budget && stride < total)
|
|
7158
|
+
stride *= 2;
|
|
7159
|
+
return stride;
|
|
7160
|
+
}
|
|
7086
7161
|
function gridPoints(total, count = GRID) {
|
|
7087
7162
|
if (total <= 0)
|
|
7088
7163
|
return [];
|
|
@@ -7113,6 +7188,24 @@ function nearestKey(keys, frame, within = Infinity) {
|
|
|
7113
7188
|
}
|
|
7114
7189
|
return best !== null && bestD <= within ? best : null;
|
|
7115
7190
|
}
|
|
7191
|
+
function claim(core) {
|
|
7192
|
+
if (!round2) {
|
|
7193
|
+
const r = new Set;
|
|
7194
|
+
round2 = r;
|
|
7195
|
+
queueMicrotask(() => {
|
|
7196
|
+
for (const c of actives)
|
|
7197
|
+
if (!r.has(c)) {
|
|
7198
|
+
c.active = false;
|
|
7199
|
+
c.dropFull();
|
|
7200
|
+
}
|
|
7201
|
+
actives = r;
|
|
7202
|
+
round2 = null;
|
|
7203
|
+
});
|
|
7204
|
+
}
|
|
7205
|
+
round2.add(core);
|
|
7206
|
+
actives.add(core);
|
|
7207
|
+
core.active = true;
|
|
7208
|
+
}
|
|
7116
7209
|
function pump(win) {
|
|
7117
7210
|
if (running)
|
|
7118
7211
|
return;
|
|
@@ -7120,12 +7213,14 @@ function pump(win) {
|
|
|
7120
7213
|
(async () => {
|
|
7121
7214
|
try {
|
|
7122
7215
|
for (;; ) {
|
|
7123
|
-
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active));
|
|
7216
|
+
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active) || a.ran - b.ran);
|
|
7124
7217
|
let unit = null;
|
|
7125
7218
|
for (const c of order) {
|
|
7126
7219
|
unit = c.work();
|
|
7127
|
-
if (unit)
|
|
7220
|
+
if (unit) {
|
|
7221
|
+
c.ran = ++turns;
|
|
7128
7222
|
break;
|
|
7223
|
+
}
|
|
7129
7224
|
}
|
|
7130
7225
|
if (!unit)
|
|
7131
7226
|
break;
|
|
@@ -7155,12 +7250,99 @@ async function toBitmap(doc, source, w, h, cap) {
|
|
|
7155
7250
|
return createImageBitmap(c);
|
|
7156
7251
|
}
|
|
7157
7252
|
}
|
|
7253
|
+
function holdEncoders() {
|
|
7254
|
+
encoderUsers++;
|
|
7255
|
+
}
|
|
7256
|
+
function dropEncoders() {
|
|
7257
|
+
encoderUsers = Math.max(0, encoderUsers - 1);
|
|
7258
|
+
if (encoderUsers > 0)
|
|
7259
|
+
return;
|
|
7260
|
+
endEncoders("released");
|
|
7261
|
+
encoders = undefined;
|
|
7262
|
+
}
|
|
7263
|
+
function encodersOf(host) {
|
|
7264
|
+
if (encoders !== undefined)
|
|
7265
|
+
return encoders;
|
|
7266
|
+
encoders = null;
|
|
7267
|
+
const W = host.Worker;
|
|
7268
|
+
if (!W || !host.OffscreenCanvas)
|
|
7269
|
+
return null;
|
|
7270
|
+
const workers = [];
|
|
7271
|
+
let url = null;
|
|
7272
|
+
try {
|
|
7273
|
+
url = host.URL.createObjectURL(new host.Blob([ENCODER], { type: "text/javascript" }));
|
|
7274
|
+
for (let i = 0;i < ENCODERS; i++)
|
|
7275
|
+
workers.push(new W(url));
|
|
7276
|
+
} catch {
|
|
7277
|
+
for (const w of workers)
|
|
7278
|
+
w.terminate();
|
|
7279
|
+
return null;
|
|
7280
|
+
} finally {
|
|
7281
|
+
if (url)
|
|
7282
|
+
host.URL.revokeObjectURL(url);
|
|
7283
|
+
}
|
|
7284
|
+
const pool = { workers, waiting: new Map, next: 0 };
|
|
7285
|
+
for (const w of workers) {
|
|
7286
|
+
w.onmessage = (e) => {
|
|
7287
|
+
const job = pool.waiting.get(e.data.id);
|
|
7288
|
+
pool.waiting.delete(e.data.id);
|
|
7289
|
+
if (e.data.blob)
|
|
7290
|
+
job?.done(e.data.blob);
|
|
7291
|
+
else
|
|
7292
|
+
job?.fail(new Error(e.data.error));
|
|
7293
|
+
};
|
|
7294
|
+
w.onerror = () => {
|
|
7295
|
+
if (encoders !== pool)
|
|
7296
|
+
return;
|
|
7297
|
+
endEncoders("encoder");
|
|
7298
|
+
encoders = null;
|
|
7299
|
+
};
|
|
7300
|
+
}
|
|
7301
|
+
encoders = pool;
|
|
7302
|
+
return pool;
|
|
7303
|
+
}
|
|
7304
|
+
function encodeOn(pool, bitmap, quality) {
|
|
7305
|
+
const id = pool.next++;
|
|
7306
|
+
const worker = pool.workers[id % pool.workers.length];
|
|
7307
|
+
return new Promise((done, fail) => {
|
|
7308
|
+
pool.waiting.set(id, { done, fail });
|
|
7309
|
+
try {
|
|
7310
|
+
worker.postMessage({ id, bitmap, quality }, [bitmap]);
|
|
7311
|
+
} catch (err) {
|
|
7312
|
+
pool.waiting.delete(id);
|
|
7313
|
+
bitmap.close();
|
|
7314
|
+
fail(err);
|
|
7315
|
+
}
|
|
7316
|
+
});
|
|
7317
|
+
}
|
|
7318
|
+
async function toProxy(doc, source, w, h) {
|
|
7319
|
+
const bm = await toBitmap(doc, source, w, h, PROXY.size);
|
|
7320
|
+
const pool = encodersOf(doc.defaultView);
|
|
7321
|
+
if (pool)
|
|
7322
|
+
return encodeOn(pool, bm, PROXY.quality);
|
|
7323
|
+
try {
|
|
7324
|
+
const Off = doc.defaultView?.OffscreenCanvas;
|
|
7325
|
+
if (Off) {
|
|
7326
|
+
const c = new Off(bm.width, bm.height);
|
|
7327
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
7328
|
+
return await c.convertToBlob({ type: "image/jpeg", quality: PROXY.quality });
|
|
7329
|
+
}
|
|
7330
|
+
const c = doc.createElement("canvas");
|
|
7331
|
+
c.width = bm.width;
|
|
7332
|
+
c.height = bm.height;
|
|
7333
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
7334
|
+
return await new Promise((done, fail) => c.toBlob((b) => b ? done(b) : fail(new Error("toBlob")), "image/jpeg", PROXY.quality));
|
|
7335
|
+
} finally {
|
|
7336
|
+
bm.close();
|
|
7337
|
+
}
|
|
7338
|
+
}
|
|
7158
7339
|
function acquireFrames(doc, src, fps) {
|
|
7159
7340
|
const key = `${fps}|${src}`;
|
|
7160
7341
|
let core = cores.get(key);
|
|
7161
7342
|
if (!core) {
|
|
7162
7343
|
core = makeCore(doc, key, src, fps);
|
|
7163
7344
|
cores.set(key, core);
|
|
7345
|
+
holdEncoders();
|
|
7164
7346
|
}
|
|
7165
7347
|
core.refs++;
|
|
7166
7348
|
return core.cache;
|
|
@@ -7181,6 +7363,71 @@ function makeCore(doc, key, src, fps) {
|
|
|
7181
7363
|
let generation = 0;
|
|
7182
7364
|
let tick = 0;
|
|
7183
7365
|
let notifyRaf = 0;
|
|
7366
|
+
const proxies = new Map;
|
|
7367
|
+
let proxyBytes = 0;
|
|
7368
|
+
let stride = 1;
|
|
7369
|
+
const decoded = new Map;
|
|
7370
|
+
const decoding = new Set;
|
|
7371
|
+
let aim = null;
|
|
7372
|
+
let lastAsked = null;
|
|
7373
|
+
const storeProxy = (frame, blob) => {
|
|
7374
|
+
if (disposed || frame % stride || proxies.has(frame))
|
|
7375
|
+
return;
|
|
7376
|
+
proxies.set(frame, blob);
|
|
7377
|
+
proxyBytes += blob.size;
|
|
7378
|
+
const next = proxyStride(proxyBytes, proxies.size, total, PROXY.budget);
|
|
7379
|
+
if (next <= stride)
|
|
7380
|
+
return;
|
|
7381
|
+
stride = next;
|
|
7382
|
+
for (const [f, b] of proxies)
|
|
7383
|
+
if (f % stride) {
|
|
7384
|
+
proxies.delete(f);
|
|
7385
|
+
proxyBytes -= b.size;
|
|
7386
|
+
}
|
|
7387
|
+
};
|
|
7388
|
+
const proxyNear = (frame) => {
|
|
7389
|
+
const at = Math.round(frame / stride) * stride;
|
|
7390
|
+
let best = null;
|
|
7391
|
+
for (const f of [at, at - stride, at + stride])
|
|
7392
|
+
if (proxies.has(f) && (best === null || Math.abs(f - frame) < Math.abs(best - frame)))
|
|
7393
|
+
best = f;
|
|
7394
|
+
return best;
|
|
7395
|
+
};
|
|
7396
|
+
const proxyTodo = () => {
|
|
7397
|
+
const out = [];
|
|
7398
|
+
for (let f = 0;f < total; f += stride)
|
|
7399
|
+
if (!proxies.has(f))
|
|
7400
|
+
out.push(f);
|
|
7401
|
+
return out;
|
|
7402
|
+
};
|
|
7403
|
+
const decodeProxy = (frame) => {
|
|
7404
|
+
if (frame === null || decoded.has(frame) || decoding.has(frame) || disposed)
|
|
7405
|
+
return;
|
|
7406
|
+
const blob = proxies.get(frame);
|
|
7407
|
+
if (!blob)
|
|
7408
|
+
return;
|
|
7409
|
+
decoding.add(frame);
|
|
7410
|
+
createImageBitmap(blob).then((bitmap) => {
|
|
7411
|
+
decoding.delete(frame);
|
|
7412
|
+
if (disposed)
|
|
7413
|
+
return bitmap.close();
|
|
7414
|
+
decoded.set(frame, { bitmap, used: ++tick });
|
|
7415
|
+
if (decoded.size > PROXY.decoded) {
|
|
7416
|
+
const [oldest] = [...decoded].sort((a, b) => a[1].used - b[1].used);
|
|
7417
|
+
if (oldest && oldest[0] !== frame) {
|
|
7418
|
+
oldest[1].bitmap.close();
|
|
7419
|
+
decoded.delete(oldest[0]);
|
|
7420
|
+
}
|
|
7421
|
+
}
|
|
7422
|
+
for (const f of listeners)
|
|
7423
|
+
f();
|
|
7424
|
+
}, () => decoding.delete(frame));
|
|
7425
|
+
};
|
|
7426
|
+
const dropDecoded = () => {
|
|
7427
|
+
for (const d of decoded.values())
|
|
7428
|
+
d.bitmap.close();
|
|
7429
|
+
decoded.clear();
|
|
7430
|
+
};
|
|
7184
7431
|
const notify = () => {
|
|
7185
7432
|
if (notifyRaf)
|
|
7186
7433
|
return;
|
|
@@ -7311,18 +7558,50 @@ function makeCore(doc, key, src, fps) {
|
|
|
7311
7558
|
return;
|
|
7312
7559
|
const forFocus = todo[0].kind === "focus";
|
|
7313
7560
|
const gen = generation;
|
|
7314
|
-
|
|
7561
|
+
await decodeRun(t, data, run, forFocus ? () => gen !== generation : () => false, (vf, f) => {
|
|
7562
|
+
const want = plan.get(f);
|
|
7563
|
+
if (!want)
|
|
7564
|
+
return null;
|
|
7565
|
+
plan.delete(f);
|
|
7566
|
+
return toBitmap(doc, vf, vf.displayWidth, vf.displayHeight, want.full ? FULL : PREVIEW).then((bm) => store(f, bm, want.full, want.kind));
|
|
7567
|
+
});
|
|
7568
|
+
};
|
|
7569
|
+
const proxyUnit = async () => {
|
|
7570
|
+
const todo = proxyTodo();
|
|
7571
|
+
if (!todo.length || !track || !buf)
|
|
7572
|
+
return;
|
|
7573
|
+
const runs = decodeRuns(track.samples, new Set(todo), (pts) => frameOf(pts, fps));
|
|
7574
|
+
const at = aim ?? focusAt ?? 0;
|
|
7575
|
+
let run = runs[0];
|
|
7576
|
+
let best = Number.POSITIVE_INFINITY;
|
|
7577
|
+
for (const r of runs) {
|
|
7578
|
+
const d = Math.min(...r.frames.map((f) => Math.abs(f - at)));
|
|
7579
|
+
if (d < best) {
|
|
7580
|
+
best = d;
|
|
7581
|
+
run = r;
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
if (!run)
|
|
7585
|
+
return;
|
|
7586
|
+
const gen = generation;
|
|
7587
|
+
let made = 0;
|
|
7588
|
+
await decodeRun(track, buf, run, () => gen !== generation || made >= PROXY.turn, (vf, f) => {
|
|
7589
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
7590
|
+
return null;
|
|
7591
|
+
made++;
|
|
7592
|
+
return toProxy(doc, vf, vf.displayWidth, vf.displayHeight).then((b) => storeProxy(f, b));
|
|
7593
|
+
});
|
|
7594
|
+
};
|
|
7595
|
+
const decodeRun = async (t, data, run, stale, take) => {
|
|
7315
7596
|
let pending = 0;
|
|
7316
7597
|
let failed = false;
|
|
7317
7598
|
const decoder = new VideoDecoder({
|
|
7318
7599
|
output: (vf) => {
|
|
7319
|
-
const
|
|
7320
|
-
|
|
7321
|
-
if (!want || stale() || disposed)
|
|
7600
|
+
const work = stale() || disposed ? null : take(vf, frameOf(vf.timestamp / 1e6, fps));
|
|
7601
|
+
if (!work)
|
|
7322
7602
|
return vf.close();
|
|
7323
|
-
plan.delete(f);
|
|
7324
7603
|
pending++;
|
|
7325
|
-
|
|
7604
|
+
work.catch(() => {}).finally(() => {
|
|
7326
7605
|
vf.close();
|
|
7327
7606
|
pending--;
|
|
7328
7607
|
});
|
|
@@ -7436,6 +7715,25 @@ function makeCore(doc, key, src, fps) {
|
|
|
7436
7715
|
return points.some((p) => p.frame + p.tolerance >= f && !covered(p));
|
|
7437
7716
|
}, 6000, false);
|
|
7438
7717
|
};
|
|
7718
|
+
const videoProxyUnit = async () => {
|
|
7719
|
+
const v = sampler;
|
|
7720
|
+
const todo = proxyTodo();
|
|
7721
|
+
if (!v || !todo.length || !v.videoWidth)
|
|
7722
|
+
return;
|
|
7723
|
+
const at = aim ?? focusAt ?? 0;
|
|
7724
|
+
const first = todo.find((f) => f >= at) ?? todo[0];
|
|
7725
|
+
const grab = async (f) => {
|
|
7726
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
7727
|
+
return;
|
|
7728
|
+
storeProxy(f, await toProxy(doc, v, v.videoWidth, v.videoHeight));
|
|
7729
|
+
};
|
|
7730
|
+
await seekTo(v, (first + 0.5) / fps);
|
|
7731
|
+
await grab(frameOf(v.currentTime, fps));
|
|
7732
|
+
await playThrough(v, 1, async (f) => {
|
|
7733
|
+
await grab(f);
|
|
7734
|
+
return proxyTodo().length > 0;
|
|
7735
|
+
}, 4000, true);
|
|
7736
|
+
};
|
|
7439
7737
|
const cache = {
|
|
7440
7738
|
src,
|
|
7441
7739
|
get backend() {
|
|
@@ -7444,29 +7742,56 @@ function makeCore(doc, key, src, fps) {
|
|
|
7444
7742
|
get size() {
|
|
7445
7743
|
return entries.size;
|
|
7446
7744
|
},
|
|
7447
|
-
|
|
7745
|
+
get proxies() {
|
|
7746
|
+
return { count: proxies.size, stride, bytes: proxyBytes };
|
|
7747
|
+
},
|
|
7748
|
+
nearest(frame, within = Infinity, decode = false) {
|
|
7448
7749
|
const k = nearestKey(entries.keys(), frame, within);
|
|
7750
|
+
const kd = nearestKey(decoded.keys(), frame, within);
|
|
7751
|
+
const dk = k === null ? Number.POSITIVE_INFINITY : Math.abs(k - frame);
|
|
7752
|
+
const dd = kd === null ? Number.POSITIVE_INFINITY : Math.abs(kd - frame);
|
|
7753
|
+
if (decode) {
|
|
7754
|
+
const p = proxyNear(frame);
|
|
7755
|
+
if (p !== null && Math.abs(p - frame) < Math.min(dk, dd))
|
|
7756
|
+
decodeProxy(p);
|
|
7757
|
+
const ahead = aheadOf(frame, lastAsked, total);
|
|
7758
|
+
if (ahead !== null)
|
|
7759
|
+
decodeProxy(proxyNear(ahead));
|
|
7760
|
+
lastAsked = frame;
|
|
7761
|
+
}
|
|
7762
|
+
if (kd !== null && (dd < dk || dd === dk && !entries.get(k)?.full)) {
|
|
7763
|
+
const d = decoded.get(kd);
|
|
7764
|
+
d.used = ++tick;
|
|
7765
|
+
return { frame: kd, bitmap: d.bitmap, full: false };
|
|
7766
|
+
}
|
|
7449
7767
|
if (k === null)
|
|
7450
7768
|
return null;
|
|
7451
7769
|
const e = entries.get(k);
|
|
7452
7770
|
e.used = ++tick;
|
|
7453
7771
|
return { frame: k, bitmap: e.bitmap, full: e.full };
|
|
7454
7772
|
},
|
|
7773
|
+
covers(frame) {
|
|
7774
|
+
if (entries.has(frame) || decoded.has(frame))
|
|
7775
|
+
return true;
|
|
7776
|
+
const p = proxyNear(frame);
|
|
7777
|
+
return p !== null && Math.abs(p - frame) <= stride / 2;
|
|
7778
|
+
},
|
|
7455
7779
|
focus(frame) {
|
|
7456
|
-
|
|
7457
|
-
active?.dropFull();
|
|
7458
|
-
if (active)
|
|
7459
|
-
active.active = false;
|
|
7460
|
-
active = core;
|
|
7461
|
-
core.active = true;
|
|
7462
|
-
}
|
|
7780
|
+
claim(core);
|
|
7463
7781
|
const moved = focusAt === null || Math.abs(frame - focusAt) > FOCUS_RADIUS;
|
|
7464
7782
|
focusAt = frame;
|
|
7783
|
+
aim = null;
|
|
7465
7784
|
if (moved)
|
|
7466
7785
|
generation++;
|
|
7467
7786
|
start();
|
|
7468
7787
|
pump(win);
|
|
7469
7788
|
},
|
|
7789
|
+
follow(frame) {
|
|
7790
|
+
claim(core);
|
|
7791
|
+
aim = frame;
|
|
7792
|
+
start();
|
|
7793
|
+
pump(win);
|
|
7794
|
+
},
|
|
7470
7795
|
want(points) {
|
|
7471
7796
|
wants = points;
|
|
7472
7797
|
if (core.shown > 0)
|
|
@@ -7504,12 +7829,15 @@ function makeCore(doc, key, src, fps) {
|
|
|
7504
7829
|
refs: 0,
|
|
7505
7830
|
shown: 0,
|
|
7506
7831
|
active: false,
|
|
7832
|
+
ran: 0,
|
|
7507
7833
|
work() {
|
|
7508
7834
|
if (disposed || backend === "pending")
|
|
7509
7835
|
return null;
|
|
7510
|
-
if (
|
|
7511
|
-
return
|
|
7512
|
-
|
|
7836
|
+
if (missing().length)
|
|
7837
|
+
return backend === "webcodecs" ? decodeUnit() : videoUnit();
|
|
7838
|
+
if (proxyTodo().length)
|
|
7839
|
+
return backend === "webcodecs" ? proxyUnit() : videoProxyUnit();
|
|
7840
|
+
return null;
|
|
7513
7841
|
},
|
|
7514
7842
|
dropFull() {
|
|
7515
7843
|
for (const [f, e] of entries)
|
|
@@ -7518,8 +7846,13 @@ function makeCore(doc, key, src, fps) {
|
|
|
7518
7846
|
entries.delete(f);
|
|
7519
7847
|
}
|
|
7520
7848
|
focusAt = null;
|
|
7849
|
+
aim = null;
|
|
7850
|
+
lastAsked = null;
|
|
7851
|
+
dropDecoded();
|
|
7521
7852
|
},
|
|
7522
7853
|
dispose() {
|
|
7854
|
+
if (disposed)
|
|
7855
|
+
return;
|
|
7523
7856
|
disposed = true;
|
|
7524
7857
|
generation++;
|
|
7525
7858
|
if (notifyRaf)
|
|
@@ -7527,6 +7860,8 @@ function makeCore(doc, key, src, fps) {
|
|
|
7527
7860
|
for (const e of entries.values())
|
|
7528
7861
|
e.bitmap.close();
|
|
7529
7862
|
entries.clear();
|
|
7863
|
+
dropDecoded();
|
|
7864
|
+
proxies.clear();
|
|
7530
7865
|
listeners.clear();
|
|
7531
7866
|
if (sampler) {
|
|
7532
7867
|
sampler.removeAttribute("src");
|
|
@@ -7534,24 +7869,45 @@ function makeCore(doc, key, src, fps) {
|
|
|
7534
7869
|
}
|
|
7535
7870
|
buf = null;
|
|
7536
7871
|
track = null;
|
|
7537
|
-
|
|
7538
|
-
|
|
7872
|
+
actives.delete(core);
|
|
7873
|
+
round2?.delete(core);
|
|
7539
7874
|
cores.delete(key);
|
|
7875
|
+
dropEncoders();
|
|
7540
7876
|
}
|
|
7541
7877
|
};
|
|
7542
7878
|
return core;
|
|
7543
7879
|
}
|
|
7544
|
-
var FOCUS_RADIUS = 6, GRID = 48, SEEN = 12, WANTED = 48, PREVIEW = 384, FULL = 1280, cores,
|
|
7880
|
+
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) => {
|
|
7545
7881
|
const ric = win.requestIdleCallback;
|
|
7546
7882
|
if (ric)
|
|
7547
7883
|
ric(() => done(), { timeout: 60 });
|
|
7548
7884
|
else
|
|
7549
7885
|
win.setTimeout(done, 4);
|
|
7550
|
-
}), sleep = (win, ms) => new Promise((done) => win.setTimeout(done, ms));
|
|
7886
|
+
}), 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) => {
|
|
7887
|
+
const pool = encoders;
|
|
7888
|
+
if (!pool)
|
|
7889
|
+
return;
|
|
7890
|
+
for (const w of pool.workers) {
|
|
7891
|
+
w.onmessage = null;
|
|
7892
|
+
w.onerror = null;
|
|
7893
|
+
w.terminate();
|
|
7894
|
+
}
|
|
7895
|
+
for (const j of pool.waiting.values())
|
|
7896
|
+
j.fail(new Error(why));
|
|
7897
|
+
pool.waiting.clear();
|
|
7898
|
+
};
|
|
7551
7899
|
var init_compare_frames = __esm(() => {
|
|
7552
7900
|
init_compare_model();
|
|
7553
7901
|
init_compare_mp4();
|
|
7902
|
+
PROXY = {
|
|
7903
|
+
size: 960,
|
|
7904
|
+
quality: 0.8,
|
|
7905
|
+
budget: 48 * 2 ** 20,
|
|
7906
|
+
decoded: 8,
|
|
7907
|
+
turn: 90
|
|
7908
|
+
};
|
|
7554
7909
|
cores = new Map;
|
|
7910
|
+
actives = new Set;
|
|
7555
7911
|
});
|
|
7556
7912
|
|
|
7557
7913
|
// src/ui/compare-gpu.ts
|
|
@@ -7817,1046 +8173,695 @@ var init_compare_gpu = __esm(() => {
|
|
|
7817
8173
|
lostListeners = new Set;
|
|
7818
8174
|
});
|
|
7819
8175
|
|
|
7820
|
-
// src/ui/compare
|
|
7821
|
-
function
|
|
8176
|
+
// src/ui/compare.ts
|
|
8177
|
+
function compare(container, options) {
|
|
7822
8178
|
const doc = container.ownerDocument;
|
|
7823
8179
|
const win = doc.defaultView;
|
|
7824
|
-
const
|
|
7825
|
-
const
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
8180
|
+
const caching = scrubCaching(options.scrubCache, win);
|
|
8181
|
+
const look = {
|
|
8182
|
+
...options.preset ? { preset: options.preset } : {},
|
|
8183
|
+
...options.theme ? { theme: options.theme } : {},
|
|
8184
|
+
...options.appearance ? { appearance: options.appearance } : {}
|
|
8185
|
+
};
|
|
8186
|
+
const offered = compareModes(options.modes, options.mode);
|
|
8187
|
+
const modeList = offered.modes;
|
|
8188
|
+
let mode = offered.mode;
|
|
8189
|
+
let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
|
|
8190
|
+
let loupeOn = options.loupe ?? false;
|
|
8191
|
+
let threshold = options.threshold ?? 4 / 255;
|
|
8192
|
+
let fpsOpt = options.fps;
|
|
8193
|
+
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
8194
|
+
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
8195
|
+
const auto = container.clientHeight < 1;
|
|
8196
|
+
const el = (tag, cls) => {
|
|
8197
|
+
const n = doc.createElement(tag);
|
|
8198
|
+
n.className = cls;
|
|
8199
|
+
return n;
|
|
8200
|
+
};
|
|
8201
|
+
const root = el("div", "knobs-compare");
|
|
8202
|
+
root.tabIndex = 0;
|
|
8203
|
+
root.setAttribute("role", "group");
|
|
8204
|
+
root.setAttribute("aria-roledescription", "compare");
|
|
8205
|
+
root.setAttribute("aria-label", options.label ?? "Compare");
|
|
8206
|
+
root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
|
|
7832
8207
|
applyLook2(root, options);
|
|
7833
|
-
|
|
8208
|
+
root.dataset.mode = mode;
|
|
8209
|
+
if (auto)
|
|
8210
|
+
root.dataset.auto = "";
|
|
8211
|
+
const bar = el("div", "knobs-cmp-bar");
|
|
8212
|
+
const modes = el("div", "knobs-cmp-modes");
|
|
8213
|
+
modes.setAttribute("role", "radiogroup");
|
|
8214
|
+
modes.setAttribute("aria-label", "Mode");
|
|
8215
|
+
modes.hidden = modeList.length < 2;
|
|
8216
|
+
const modeBtns = modeList.map((m, i) => {
|
|
8217
|
+
const b = el("button", "knobs-cmp-mode");
|
|
8218
|
+
b.type = "button";
|
|
8219
|
+
b.tabIndex = -1;
|
|
8220
|
+
b.textContent = MODE_LABEL[m].short;
|
|
8221
|
+
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
8222
|
+
b.setAttribute("role", "radio");
|
|
8223
|
+
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
8224
|
+
b.addEventListener("click", () => {
|
|
8225
|
+
setMode(m);
|
|
8226
|
+
root.focus({ preventScroll: true });
|
|
8227
|
+
});
|
|
8228
|
+
modes.append(b);
|
|
8229
|
+
return b;
|
|
8230
|
+
});
|
|
8231
|
+
const toggleBtn = (name, label, title) => {
|
|
8232
|
+
const b = el("button", "knobs-cmp-toggle");
|
|
8233
|
+
b.type = "button";
|
|
8234
|
+
b.tabIndex = -1;
|
|
8235
|
+
b.innerHTML = icon(name);
|
|
8236
|
+
b.setAttribute("aria-label", label);
|
|
8237
|
+
b.title = title;
|
|
8238
|
+
return b;
|
|
8239
|
+
};
|
|
8240
|
+
const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
|
|
8241
|
+
loupeBtn.addEventListener("click", () => {
|
|
8242
|
+
setLoupe(!loupeOn);
|
|
8243
|
+
root.focus({ preventScroll: true });
|
|
8244
|
+
});
|
|
8245
|
+
const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
|
|
8246
|
+
loopBtn.setAttribute("aria-keyshortcuts", "L");
|
|
8247
|
+
loopBtn.addEventListener("click", () => {
|
|
8248
|
+
api.setLoop(!loopNow());
|
|
8249
|
+
root.focus({ preventScroll: true });
|
|
8250
|
+
});
|
|
8251
|
+
bar.append(modes, el("span", "knobs-cmp-fill"), loopBtn, loupeBtn);
|
|
8252
|
+
const stage = el("div", "knobs-cmp-stage");
|
|
8253
|
+
const canvas = el("canvas", "knobs-cmp-canvas");
|
|
7834
8254
|
canvas.setAttribute("aria-hidden", "true");
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
const
|
|
7838
|
-
const
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
8255
|
+
const split = el("div", "knobs-cmp-split");
|
|
8256
|
+
split.setAttribute("aria-hidden", "true");
|
|
8257
|
+
const zoomPill = el("div", "knobs-cmp-zoom");
|
|
8258
|
+
const zoomPct = el("span", "knobs-cmp-zoom-pct");
|
|
8259
|
+
const zoomReset = el("button", "knobs-cmp-zoom-reset");
|
|
8260
|
+
zoomReset.type = "button";
|
|
8261
|
+
zoomReset.tabIndex = -1;
|
|
8262
|
+
zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
|
|
8263
|
+
zoomReset.setAttribute("aria-label", "Reset zoom");
|
|
8264
|
+
zoomReset.title = "Fit · 0 or double-click";
|
|
8265
|
+
zoomReset.addEventListener("click", () => {
|
|
8266
|
+
api.setZoom(null);
|
|
8267
|
+
root.focus({ preventScroll: true });
|
|
7848
8268
|
});
|
|
7849
|
-
|
|
7850
|
-
const
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
s.seen = true;
|
|
7860
|
-
}
|
|
7861
|
-
} else {
|
|
7862
|
-
const img = doc.createElement("img");
|
|
7863
|
-
img.crossOrigin = "anonymous";
|
|
7864
|
-
img.decoding = "async";
|
|
7865
|
-
img.addEventListener("load", () => {
|
|
7866
|
-
pairDiffs.clear();
|
|
7867
|
-
schedule();
|
|
7868
|
-
});
|
|
7869
|
-
img.src = s.source.src;
|
|
7870
|
-
s.still = img;
|
|
8269
|
+
zoomPill.append(zoomPct, zoomReset);
|
|
8270
|
+
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
8271
|
+
labels[0].dataset.side = "before";
|
|
8272
|
+
labels[1].dataset.side = "after";
|
|
8273
|
+
const setLabel = (index, text) => {
|
|
8274
|
+
const l = labels[index];
|
|
8275
|
+
if (!l.firstElementChild) {
|
|
8276
|
+
const mark = el("span", "knobs-cmp-mark");
|
|
8277
|
+
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
8278
|
+
l.append(mark, el("span", "knobs-cmp-name"));
|
|
7871
8279
|
}
|
|
8280
|
+
const name = l.lastElementChild;
|
|
8281
|
+
if (name.textContent !== text)
|
|
8282
|
+
name.textContent = text;
|
|
7872
8283
|
};
|
|
7873
|
-
const
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
8284
|
+
const seam = el("div", "knobs-cmp-seam");
|
|
8285
|
+
seam.setAttribute("aria-hidden", "true");
|
|
8286
|
+
const handle = el("div", "knobs-cmp-handle");
|
|
8287
|
+
handle.setAttribute("role", "slider");
|
|
8288
|
+
handle.setAttribute("aria-label", "Wipe");
|
|
8289
|
+
handle.setAttribute("aria-valuemin", "0");
|
|
8290
|
+
handle.setAttribute("aria-valuemax", "100");
|
|
8291
|
+
const grip = el("span", "knobs-cmp-grip");
|
|
8292
|
+
grip.setAttribute("aria-hidden", "true");
|
|
8293
|
+
handle.append(grip);
|
|
8294
|
+
const note = el("div", "knobs-cmp-note");
|
|
8295
|
+
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
8296
|
+
root.append(bar, stage);
|
|
8297
|
+
let tl = bound;
|
|
8298
|
+
let ownTimeline = false;
|
|
8299
|
+
if (!tl && options.timeline === true) {
|
|
8300
|
+
const host = el("div", "knobs-cmp-timeline-host");
|
|
8301
|
+
root.append(host);
|
|
8302
|
+
tl = timeline(host, {
|
|
8303
|
+
...look,
|
|
8304
|
+
variant: "line",
|
|
8305
|
+
duration: 1,
|
|
8306
|
+
fps: fps(),
|
|
8307
|
+
arrows: { step: 0.5, shift: 2 },
|
|
8308
|
+
label: "Compare time",
|
|
8309
|
+
...options.hints === false ? { hints: false } : {}
|
|
8310
|
+
});
|
|
8311
|
+
ownTimeline = true;
|
|
8312
|
+
}
|
|
8313
|
+
container.append(root);
|
|
8314
|
+
const sides = [null, null];
|
|
8315
|
+
let duration = 0;
|
|
8316
|
+
let renderer = "dom";
|
|
8317
|
+
let pending = false;
|
|
8318
|
+
let gpu = null;
|
|
8319
|
+
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
8320
|
+
const fade = new Spring(1, FADE2.omega, FADE2.zeta, 0.001);
|
|
8321
|
+
let fromMode = mode;
|
|
8322
|
+
let toMode = mode;
|
|
8323
|
+
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
8324
|
+
let wipeCatching = false;
|
|
8325
|
+
const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
8326
|
+
let loupeHeld = false;
|
|
8327
|
+
let pointer = null;
|
|
8328
|
+
let drag = null;
|
|
8329
|
+
let tap = null;
|
|
8330
|
+
const touches = new Map;
|
|
8331
|
+
let pinch = null;
|
|
8332
|
+
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8333
|
+
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8334
|
+
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8335
|
+
let viewSent = "0.0000,0.0000,1.0000|";
|
|
8336
|
+
let ownLoop = null;
|
|
8337
|
+
let scrubbing = false;
|
|
8338
|
+
let stageW = 1;
|
|
8339
|
+
let stageH = 1;
|
|
8340
|
+
let stageRect = null;
|
|
8341
|
+
let accent = [0.49, 0.81, 0.61];
|
|
8342
|
+
let labelW = [0, 0];
|
|
8343
|
+
let frameRadius = 0;
|
|
8344
|
+
let dirty = true;
|
|
8345
|
+
let disposed = false;
|
|
8346
|
+
let near = options.lazy === false;
|
|
8347
|
+
let onScreen = options.lazy === false;
|
|
8348
|
+
let releaseTimer = 0;
|
|
8349
|
+
let ownTime = 0;
|
|
8350
|
+
let ownPlaying = false;
|
|
8351
|
+
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
8352
|
+
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
8353
|
+
const follower = () => {
|
|
8354
|
+
const m = master();
|
|
8355
|
+
const v = videoOf(sides[0]);
|
|
8356
|
+
return m && v && v !== m ? v : null;
|
|
7886
8357
|
};
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
const
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
8358
|
+
const now = () => tl ? tl.time : ownTime;
|
|
8359
|
+
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
8360
|
+
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
8361
|
+
const sizeOf = (s) => {
|
|
8362
|
+
if (s.size.w > 0)
|
|
8363
|
+
return s.size;
|
|
8364
|
+
const other = sides[s.index === 0 ? 1 : 0];
|
|
8365
|
+
if (other && other.size.w > 0)
|
|
8366
|
+
return other.size;
|
|
8367
|
+
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
7896
8368
|
};
|
|
7897
|
-
const
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
});
|
|
7902
|
-
const measure = () => {
|
|
7903
|
-
const r = root.getBoundingClientRect();
|
|
7904
|
-
w = Math.max(1, r.width);
|
|
7905
|
-
h = Math.max(1, r.height);
|
|
7906
|
-
geo = { x: 0, w };
|
|
7907
|
-
if (align === "track") {
|
|
7908
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
7909
|
-
const t = track?.getBoundingClientRect();
|
|
7910
|
-
if (t && t.width > 0)
|
|
7911
|
-
geo = { x: t.left - r.left, w: t.width };
|
|
7912
|
-
}
|
|
7913
|
-
readColors();
|
|
7914
|
-
schedule();
|
|
8369
|
+
const wantOf = (s) => {
|
|
8370
|
+
const v = s.el;
|
|
8371
|
+
const f = frameOf(now(), fps());
|
|
8372
|
+
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
7915
8373
|
};
|
|
7916
|
-
const
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
s.
|
|
8374
|
+
const emitFrame = (s, frame, source) => root.dispatchEvent(new CustomEvent("knobs-compare-frame", { detail: { side: s.index, frame, source } }));
|
|
8375
|
+
const setCached = (s, on) => {
|
|
8376
|
+
if (renderer === "dom")
|
|
8377
|
+
s.preview.style.display = on ? "" : "none";
|
|
7920
8378
|
};
|
|
7921
|
-
const
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
8379
|
+
const showVideo = (s) => {
|
|
8380
|
+
const v = s.el;
|
|
8381
|
+
if (renderer === "gpu") {
|
|
8382
|
+
if (!gpu)
|
|
8383
|
+
return;
|
|
8384
|
+
if (!gpu.upload(s.index, v, v.videoWidth, v.videoHeight))
|
|
8385
|
+
return miss(s);
|
|
8386
|
+
s.misses = 0;
|
|
8387
|
+
}
|
|
8388
|
+
const f = frameOf(v.currentTime, fps());
|
|
8389
|
+
s.shown = { frame: f, cached: false };
|
|
8390
|
+
setCached(s, false);
|
|
8391
|
+
emitFrame(s, f, "video");
|
|
8392
|
+
requestDraw();
|
|
7926
8393
|
};
|
|
7927
|
-
const
|
|
7928
|
-
|
|
7929
|
-
probe.height = DIFF.h;
|
|
7930
|
-
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
7931
|
-
const differ = (a, b) => {
|
|
7932
|
-
if (!probeCtx)
|
|
8394
|
+
const showCached = (s, c) => {
|
|
8395
|
+
if (s.shown?.cached && s.shown.frame === c.frame)
|
|
7933
8396
|
return;
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
const
|
|
7939
|
-
|
|
7940
|
-
|
|
8397
|
+
if (renderer === "gpu") {
|
|
8398
|
+
if (!gpu || !gpu.upload(2 + s.index, c.bitmap, c.bitmap.width, c.bitmap.height))
|
|
8399
|
+
return;
|
|
8400
|
+
} else if (renderer === "dom" && !pending) {
|
|
8401
|
+
const p = s.preview;
|
|
8402
|
+
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
8403
|
+
p.width = c.bitmap.width;
|
|
8404
|
+
p.height = c.bitmap.height;
|
|
8405
|
+
}
|
|
8406
|
+
p.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
8407
|
+
} else
|
|
7941
8408
|
return;
|
|
7942
|
-
}
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
7947
|
-
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
8409
|
+
s.shown = { frame: c.frame, cached: true };
|
|
8410
|
+
setCached(s, true);
|
|
8411
|
+
emitFrame(s, c.frame, "cache");
|
|
8412
|
+
requestDraw();
|
|
7948
8413
|
};
|
|
7949
|
-
const
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
7956
|
-
};
|
|
8414
|
+
const miss = (s) => {
|
|
8415
|
+
if (s.failed || ++s.misses < 3)
|
|
8416
|
+
return;
|
|
8417
|
+
gpu?.dispose();
|
|
8418
|
+
gpu = null;
|
|
8419
|
+
setRenderer("dom");
|
|
7957
8420
|
};
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
let asked = "";
|
|
7961
|
-
function schedule() {
|
|
7962
|
-
if (!raf && !disposed)
|
|
7963
|
-
raf = win.requestAnimationFrame(draw);
|
|
7964
|
-
}
|
|
7965
|
-
const draw = () => {
|
|
7966
|
-
raf = 0;
|
|
7967
|
-
if (!near)
|
|
8421
|
+
const pushFrame = (s) => {
|
|
8422
|
+
if (!s.loaded || disposed)
|
|
7968
8423
|
return;
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8424
|
+
if (s.kind === "image") {
|
|
8425
|
+
const img = s.el;
|
|
8426
|
+
if (!(img.complete && img.naturalWidth))
|
|
8427
|
+
return;
|
|
8428
|
+
if (renderer === "gpu" && gpu) {
|
|
8429
|
+
if (!gpu.upload(s.index, img, img.naturalWidth, img.naturalHeight))
|
|
8430
|
+
return miss(s);
|
|
8431
|
+
}
|
|
8432
|
+
s.shown = { frame: 0, cached: false };
|
|
8433
|
+
return requestDraw();
|
|
7975
8434
|
}
|
|
7976
|
-
const
|
|
7977
|
-
if (
|
|
8435
|
+
const v = s.el;
|
|
8436
|
+
if (v.readyState < 2)
|
|
8437
|
+
return;
|
|
8438
|
+
if (isPlaying() || !s.frames)
|
|
8439
|
+
return showVideo(s);
|
|
8440
|
+
const want = wantOf(s);
|
|
8441
|
+
const at = frameOf(v.currentTime, fps());
|
|
8442
|
+
if (at === want) {
|
|
8443
|
+
showVideo(s);
|
|
8444
|
+
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
7978
8445
|
return;
|
|
7979
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
7980
|
-
ctx.clearRect(0, 0, w, h);
|
|
7981
|
-
const cols = columns();
|
|
7982
|
-
const key = cols.map((c) => c.frame).join(",");
|
|
7983
|
-
if (key !== asked) {
|
|
7984
|
-
asked = key;
|
|
7985
|
-
ask(cols);
|
|
7986
8446
|
}
|
|
7987
|
-
const
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8447
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
8448
|
+
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
8449
|
+
showCached(s, c);
|
|
8450
|
+
else
|
|
8451
|
+
showVideo(s);
|
|
8452
|
+
};
|
|
8453
|
+
const refreshCached = (s) => {
|
|
8454
|
+
if (!s.loaded || !s.frames || isPlaying())
|
|
8455
|
+
return;
|
|
8456
|
+
const want = wantOf(s);
|
|
8457
|
+
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
8458
|
+
return;
|
|
8459
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
8460
|
+
if (!c)
|
|
8461
|
+
return;
|
|
8462
|
+
const was = s.shown ? Math.abs(s.shown.frame - want) : Number.POSITIVE_INFINITY;
|
|
8463
|
+
if (Math.abs(c.frame - want) < was)
|
|
8464
|
+
showCached(s, c);
|
|
8465
|
+
};
|
|
8466
|
+
const giveUp = (s) => {
|
|
8467
|
+
s.seek = SEEK_IDLE;
|
|
8468
|
+
if (!s.loaded || disposed)
|
|
8469
|
+
return;
|
|
8470
|
+
if (s.el.readyState >= 2)
|
|
8471
|
+
return showVideo(s);
|
|
8472
|
+
s.shown = null;
|
|
8473
|
+
setCached(s, false);
|
|
8474
|
+
requestDraw();
|
|
8475
|
+
};
|
|
8476
|
+
const makeSide = (index, source) => {
|
|
8477
|
+
const kind = mediaKind(source.src, source.kind);
|
|
8478
|
+
const media = el(kind === "video" ? "video" : "img", "knobs-cmp-media");
|
|
8479
|
+
media.dataset.side = index === 0 ? "before" : "after";
|
|
8480
|
+
media.crossOrigin = "anonymous";
|
|
8481
|
+
media.setAttribute("aria-hidden", "true");
|
|
8482
|
+
const preview = el("canvas", "knobs-cmp-media knobs-cmp-preview");
|
|
8483
|
+
preview.dataset.side = media.dataset.side;
|
|
8484
|
+
preview.style.display = "none";
|
|
8485
|
+
preview.setAttribute("aria-hidden", "true");
|
|
8486
|
+
const offs = [];
|
|
8487
|
+
const on = (type, f) => {
|
|
8488
|
+
media.addEventListener(type, f);
|
|
8489
|
+
offs.push(() => media.removeEventListener(type, f));
|
|
8490
|
+
};
|
|
8491
|
+
const side = {
|
|
8492
|
+
index,
|
|
8493
|
+
source,
|
|
8494
|
+
kind,
|
|
8495
|
+
el: media,
|
|
8496
|
+
preview,
|
|
8497
|
+
size: { w: 0, h: 0 },
|
|
8498
|
+
seek: SEEK_IDLE,
|
|
8499
|
+
loaded: false,
|
|
8500
|
+
frames: null,
|
|
8501
|
+
unlisten: null,
|
|
8502
|
+
seen: false,
|
|
8503
|
+
shown: null,
|
|
8504
|
+
frameAt: 0,
|
|
8505
|
+
misses: 0,
|
|
8506
|
+
failed: false,
|
|
8507
|
+
stall: stallGuard(win, () => giveUp(side)),
|
|
8508
|
+
stop: () => {
|
|
8509
|
+
side.stall.clear();
|
|
8510
|
+
for (const f of offs)
|
|
8511
|
+
f();
|
|
8003
8512
|
}
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
return d;
|
|
8008
|
-
});
|
|
8009
|
-
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
8010
|
-
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
8011
|
-
cols.forEach((c, i) => {
|
|
8012
|
-
const x = geo.x + c.x + GAP / 2;
|
|
8013
|
-
const cwid = Math.max(1, c.w - GAP);
|
|
8014
|
-
if (x + cwid < 0 || x > w)
|
|
8513
|
+
};
|
|
8514
|
+
const fail = () => {
|
|
8515
|
+
if (!side.loaded)
|
|
8015
8516
|
return;
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8517
|
+
side.failed = true;
|
|
8518
|
+
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
8519
|
+
side.stall.clear();
|
|
8520
|
+
side.seek = SEEK_IDLE;
|
|
8521
|
+
side.shown = null;
|
|
8522
|
+
setCached(side, false);
|
|
8523
|
+
};
|
|
8524
|
+
if (media instanceof HTMLVideoElement) {
|
|
8525
|
+
const v = media;
|
|
8526
|
+
v.muted = true;
|
|
8527
|
+
v.defaultMuted = true;
|
|
8528
|
+
v.playsInline = true;
|
|
8529
|
+
v.preload = "auto";
|
|
8530
|
+
v.setAttribute("playsinline", "");
|
|
8531
|
+
v.setAttribute("muted", "");
|
|
8532
|
+
on("loadedmetadata", () => {
|
|
8533
|
+
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
8534
|
+
measureDuration();
|
|
8535
|
+
if (auto)
|
|
8536
|
+
measure();
|
|
8537
|
+
if (isPlaying())
|
|
8538
|
+
jumpAll(now());
|
|
8539
|
+
else
|
|
8540
|
+
showFrame(now(), false);
|
|
8541
|
+
requestDraw();
|
|
8542
|
+
});
|
|
8543
|
+
on("resize", () => {
|
|
8544
|
+
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
8545
|
+
requestDraw();
|
|
8546
|
+
});
|
|
8547
|
+
on("loadeddata", () => pushFrame(side));
|
|
8548
|
+
on("seeked", () => {
|
|
8549
|
+
const step = seeked(side.seek, v.currentTime, fps());
|
|
8550
|
+
side.seek = step.state;
|
|
8551
|
+
if (step.apply !== null)
|
|
8552
|
+
seekVideo(side, v, step.apply);
|
|
8553
|
+
else
|
|
8554
|
+
side.stall.clear();
|
|
8555
|
+
pushFrame(side);
|
|
8556
|
+
});
|
|
8557
|
+
on("error", fail);
|
|
8558
|
+
if (v.requestVideoFrameCallback) {
|
|
8559
|
+
let id = 0;
|
|
8560
|
+
const frame = () => {
|
|
8561
|
+
side.frameAt = performance.now();
|
|
8562
|
+
pushFrame(side);
|
|
8563
|
+
id = v.requestVideoFrameCallback(frame);
|
|
8564
|
+
};
|
|
8565
|
+
id = v.requestVideoFrameCallback(frame);
|
|
8566
|
+
offs.push(() => v.cancelVideoFrameCallback?.(id));
|
|
8046
8567
|
}
|
|
8047
|
-
}
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8568
|
+
} else {
|
|
8569
|
+
on("load", () => {
|
|
8570
|
+
side.size = { w: media.naturalWidth, h: media.naturalHeight };
|
|
8571
|
+
if (auto)
|
|
8572
|
+
measure();
|
|
8573
|
+
pushFrame(side);
|
|
8574
|
+
requestDraw();
|
|
8575
|
+
});
|
|
8576
|
+
on("error", fail);
|
|
8577
|
+
media.decoding = "async";
|
|
8053
8578
|
}
|
|
8054
|
-
|
|
8055
|
-
schedule();
|
|
8056
|
-
};
|
|
8057
|
-
const timeAt = (clientX) => {
|
|
8058
|
-
const r = root.getBoundingClientRect();
|
|
8059
|
-
const v = timeline.view;
|
|
8060
|
-
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
8061
|
-
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
8062
|
-
};
|
|
8063
|
-
const columnAt = (clientX) => {
|
|
8064
|
-
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
8065
|
-
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
8066
|
-
return i < 0 ? null : i;
|
|
8579
|
+
return side;
|
|
8067
8580
|
};
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8581
|
+
const loadSide = (s) => {
|
|
8582
|
+
if (s.loaded || disposed)
|
|
8583
|
+
return;
|
|
8584
|
+
s.loaded = true;
|
|
8585
|
+
s.failed = false;
|
|
8586
|
+
s.el.src = s.source.src;
|
|
8587
|
+
if (s.kind === "video" && caching) {
|
|
8588
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
8589
|
+
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
8590
|
+
if (onScreen) {
|
|
8591
|
+
s.frames.visible(true);
|
|
8592
|
+
s.seen = true;
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8073
8595
|
};
|
|
8074
|
-
|
|
8075
|
-
if (
|
|
8596
|
+
const unloadSide = (s) => {
|
|
8597
|
+
if (!s.loaded)
|
|
8076
8598
|
return;
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
if (i !== hover) {
|
|
8094
|
-
hover = i;
|
|
8095
|
-
schedule();
|
|
8599
|
+
s.loaded = false;
|
|
8600
|
+
s.el.removeAttribute("src");
|
|
8601
|
+
if (s.el instanceof HTMLVideoElement)
|
|
8602
|
+
s.el.load();
|
|
8603
|
+
s.seek = SEEK_IDLE;
|
|
8604
|
+
s.stall.clear();
|
|
8605
|
+
s.shown = null;
|
|
8606
|
+
setCached(s, false);
|
|
8607
|
+
s.unlisten?.();
|
|
8608
|
+
s.unlisten = null;
|
|
8609
|
+
if (s.frames) {
|
|
8610
|
+
if (s.seen)
|
|
8611
|
+
s.frames.visible(false);
|
|
8612
|
+
s.seen = false;
|
|
8613
|
+
s.frames.release();
|
|
8614
|
+
s.frames = null;
|
|
8096
8615
|
}
|
|
8097
|
-
|
|
8098
|
-
const end = () => {
|
|
8099
|
-
dragging = false;
|
|
8616
|
+
gpu?.clear(s.index);
|
|
8100
8617
|
};
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8618
|
+
const measureDuration = () => {
|
|
8619
|
+
const next = Math.max(0, ...sides.map((s) => {
|
|
8620
|
+
const v = videoOf(s);
|
|
8621
|
+
return v && Number.isFinite(v.duration) ? v.duration : 0;
|
|
8622
|
+
}));
|
|
8623
|
+
if (next <= 0 || next === duration)
|
|
8106
8624
|
return;
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
const
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
const setOnScreen = (on) => {
|
|
8118
|
-
onScreen = on;
|
|
8625
|
+
duration = next;
|
|
8626
|
+
if (ownTimeline && tl)
|
|
8627
|
+
tl.update({ duration, fps: fps() });
|
|
8628
|
+
};
|
|
8629
|
+
const setRenderer = (next) => {
|
|
8630
|
+
pending = false;
|
|
8631
|
+
renderer = next;
|
|
8632
|
+
root.dataset.renderer = next;
|
|
8633
|
+
loupeBtn.hidden = next !== "gpu";
|
|
8634
|
+
canvas.style.display = next === "gpu" ? "" : "none";
|
|
8119
8635
|
for (const s of sides) {
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8636
|
+
const st = s.el.style;
|
|
8637
|
+
st.clipPath = "";
|
|
8638
|
+
st.maskImage = "";
|
|
8639
|
+
st.setProperty("-webkit-mask-image", "");
|
|
8640
|
+
st.mixBlendMode = "";
|
|
8641
|
+
st.opacity = "";
|
|
8642
|
+
s.shown = null;
|
|
8643
|
+
setCached(s, false);
|
|
8124
8644
|
}
|
|
8645
|
+
for (const s of sides)
|
|
8646
|
+
pushFrame(s);
|
|
8647
|
+
requestDraw();
|
|
8125
8648
|
};
|
|
8126
|
-
const
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8649
|
+
const startGpu = async (retry) => {
|
|
8650
|
+
const g = await createCompareGpu(canvas, () => {
|
|
8651
|
+
gpu = null;
|
|
8652
|
+
if (disposed)
|
|
8653
|
+
return;
|
|
8654
|
+
if (retry)
|
|
8655
|
+
win.setTimeout(() => void startGpu(false), 150);
|
|
8656
|
+
else
|
|
8657
|
+
setRenderer("dom");
|
|
8658
|
+
});
|
|
8659
|
+
if (disposed)
|
|
8660
|
+
return g?.dispose();
|
|
8661
|
+
if (!g)
|
|
8662
|
+
return setRenderer("dom");
|
|
8663
|
+
gpu = g;
|
|
8664
|
+
setRenderer("gpu");
|
|
8135
8665
|
};
|
|
8136
|
-
const
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
}
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
next.before ? makeSide(next.before) : was[0],
|
|
8154
|
-
next.after ? makeSide(next.after) : was[1]
|
|
8155
|
-
];
|
|
8156
|
-
for (const s of was)
|
|
8157
|
-
if (!sides.includes(s))
|
|
8158
|
-
unload(s);
|
|
8159
|
-
diffs.clear();
|
|
8160
|
-
pairDiffs.clear();
|
|
8161
|
-
asked = "";
|
|
8162
|
-
if (near)
|
|
8163
|
-
for (const s of sides)
|
|
8164
|
-
load(s);
|
|
8165
|
-
if (onScreen)
|
|
8166
|
-
setOnScreen(true);
|
|
8167
|
-
schedule();
|
|
8168
|
-
},
|
|
8169
|
-
dispose() {
|
|
8170
|
-
disposed = true;
|
|
8171
|
-
if (raf)
|
|
8172
|
-
win.cancelAnimationFrame(raf);
|
|
8173
|
-
unlisten();
|
|
8174
|
-
ro.disconnect();
|
|
8175
|
-
nearIo?.disconnect();
|
|
8176
|
-
screenIo?.disconnect();
|
|
8177
|
-
for (const s of sides)
|
|
8178
|
-
unload(s);
|
|
8179
|
-
root.remove();
|
|
8666
|
+
const syncModeUi = () => {
|
|
8667
|
+
root.dataset.mode = mode;
|
|
8668
|
+
modeList.forEach((m, i) => {
|
|
8669
|
+
modeBtns[i].setAttribute("aria-checked", String(m === mode));
|
|
8670
|
+
});
|
|
8671
|
+
handle.tabIndex = mode === "wipe" ? 0 : -1;
|
|
8672
|
+
modePill?.update();
|
|
8673
|
+
};
|
|
8674
|
+
const setMode = (next, emit = true) => {
|
|
8675
|
+
if (next === mode || !modeList.includes(next))
|
|
8676
|
+
return;
|
|
8677
|
+
mode = next;
|
|
8678
|
+
const reduced = reducedMotion(win);
|
|
8679
|
+
tune(fade, reduced ? FADE_REDUCED : FADE2);
|
|
8680
|
+
if (fade.x >= 0.5) {
|
|
8681
|
+
fromMode = toMode;
|
|
8682
|
+
fade.snap(0);
|
|
8180
8683
|
}
|
|
8684
|
+
toMode = next;
|
|
8685
|
+
fade.set(1);
|
|
8686
|
+
if (reduced)
|
|
8687
|
+
lay.snap(next === "side" ? 0 : 1);
|
|
8688
|
+
else
|
|
8689
|
+
lay.set(next === "side" ? 0 : 1);
|
|
8690
|
+
syncModeUi();
|
|
8691
|
+
if (emit)
|
|
8692
|
+
options.onMode?.(next);
|
|
8693
|
+
requestDraw();
|
|
8181
8694
|
};
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8695
|
+
const setWipe = (next, emit = true, jump = false) => {
|
|
8696
|
+
const w = Math.min(1, Math.max(0, next));
|
|
8697
|
+
if (w === wipe && !jump)
|
|
8698
|
+
return;
|
|
8699
|
+
wipe = w;
|
|
8700
|
+
const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
|
|
8701
|
+
const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
|
|
8702
|
+
wipeCatching = head.catching;
|
|
8703
|
+
if (head.catching) {
|
|
8704
|
+
wipeLine.x = head.x;
|
|
8705
|
+
wipeLine.target = head.target;
|
|
8706
|
+
} else
|
|
8707
|
+
wipeLine.snap(head.x);
|
|
8708
|
+
if (emit)
|
|
8709
|
+
options.onWipe?.(w);
|
|
8710
|
+
if (head.catching)
|
|
8711
|
+
requestDraw();
|
|
8712
|
+
else
|
|
8713
|
+
drawNow();
|
|
8201
8714
|
};
|
|
8202
|
-
const
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
let loupeOn = options.loupe ?? false;
|
|
8207
|
-
let threshold = options.threshold ?? 4 / 255;
|
|
8208
|
-
let fpsOpt = options.fps;
|
|
8209
|
-
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
8210
|
-
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
8211
|
-
const auto = container.clientHeight < 1;
|
|
8212
|
-
const el = (tag, cls) => {
|
|
8213
|
-
const n = doc.createElement(tag);
|
|
8214
|
-
n.className = cls;
|
|
8215
|
-
return n;
|
|
8715
|
+
const setLoupe = (on) => {
|
|
8716
|
+
loupeOn = on;
|
|
8717
|
+
loupeBtn.setAttribute("aria-pressed", String(on));
|
|
8718
|
+
requestDraw();
|
|
8216
8719
|
};
|
|
8217
|
-
const
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
root.dataset.auto = "";
|
|
8227
|
-
const bar = el("div", "knobs-cmp-bar");
|
|
8228
|
-
const modes = el("div", "knobs-cmp-modes");
|
|
8229
|
-
modes.setAttribute("role", "radiogroup");
|
|
8230
|
-
modes.setAttribute("aria-label", "Mode");
|
|
8231
|
-
modes.hidden = modeList.length < 2;
|
|
8232
|
-
const modeBtns = modeList.map((m, i) => {
|
|
8233
|
-
const b = el("button", "knobs-cmp-mode");
|
|
8234
|
-
b.type = "button";
|
|
8235
|
-
b.tabIndex = -1;
|
|
8236
|
-
b.textContent = MODE_LABEL[m].short;
|
|
8237
|
-
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
8238
|
-
b.setAttribute("role", "radio");
|
|
8239
|
-
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
8240
|
-
b.addEventListener("click", () => {
|
|
8241
|
-
setMode(m);
|
|
8242
|
-
root.focus({ preventScroll: true });
|
|
8243
|
-
});
|
|
8244
|
-
modes.append(b);
|
|
8245
|
-
return b;
|
|
8246
|
-
});
|
|
8247
|
-
const toggleBtn = (name, label, title) => {
|
|
8248
|
-
const b = el("button", "knobs-cmp-toggle");
|
|
8249
|
-
b.type = "button";
|
|
8250
|
-
b.tabIndex = -1;
|
|
8251
|
-
b.innerHTML = icon(name);
|
|
8252
|
-
b.setAttribute("aria-label", label);
|
|
8253
|
-
b.title = title;
|
|
8254
|
-
return b;
|
|
8720
|
+
const clampTime = (v, t) => Math.min(Math.max(0, t), Math.max(0, (Number.isFinite(v.duration) ? v.duration : t) - 0.001));
|
|
8721
|
+
const seekVideo = (s, v, t) => {
|
|
8722
|
+
const c = s.frames?.nearest(wantOf(s));
|
|
8723
|
+
const far = !c || Math.abs(c.frame - wantOf(s)) > FAST_SEEK_FRAMES;
|
|
8724
|
+
if (scrubbing && far && v.fastSeek)
|
|
8725
|
+
v.fastSeek(t);
|
|
8726
|
+
else
|
|
8727
|
+
v.currentTime = t;
|
|
8728
|
+
s.stall.arm();
|
|
8255
8729
|
};
|
|
8256
|
-
const
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
api.setZoom(null);
|
|
8283
|
-
root.focus({ preventScroll: true });
|
|
8284
|
-
});
|
|
8285
|
-
zoomPill.append(zoomPct, zoomReset);
|
|
8286
|
-
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
8287
|
-
labels[0].dataset.side = "before";
|
|
8288
|
-
labels[1].dataset.side = "after";
|
|
8289
|
-
const setLabel = (index, text) => {
|
|
8290
|
-
const l = labels[index];
|
|
8291
|
-
if (!l.firstElementChild) {
|
|
8292
|
-
const mark = el("span", "knobs-cmp-mark");
|
|
8293
|
-
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
8294
|
-
l.append(mark, el("span", "knobs-cmp-name"));
|
|
8730
|
+
const showFrame = (t, used) => {
|
|
8731
|
+
for (const s of sides) {
|
|
8732
|
+
const v = videoOf(s);
|
|
8733
|
+
if (!v || v.readyState < 1)
|
|
8734
|
+
continue;
|
|
8735
|
+
if (!v.paused) {
|
|
8736
|
+
v.pause();
|
|
8737
|
+
v.playbackRate = 1;
|
|
8738
|
+
}
|
|
8739
|
+
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
8740
|
+
if (used) {
|
|
8741
|
+
if (scrubbing)
|
|
8742
|
+
s.frames?.follow(want);
|
|
8743
|
+
else
|
|
8744
|
+
s.frames?.focus(want);
|
|
8745
|
+
}
|
|
8746
|
+
if (!(scrubbing && s.frames?.covers(want))) {
|
|
8747
|
+
const at = frameMidpoint(want, fps(), v.duration);
|
|
8748
|
+
const step = requestSeek(s.seek, at, v.currentTime, fps());
|
|
8749
|
+
s.seek = step.state;
|
|
8750
|
+
if (step.apply !== null)
|
|
8751
|
+
seekVideo(s, v, step.apply);
|
|
8752
|
+
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
8753
|
+
pushFrame(s);
|
|
8754
|
+
}
|
|
8755
|
+
refreshCached(s);
|
|
8295
8756
|
}
|
|
8296
|
-
|
|
8297
|
-
if (name.textContent !== text)
|
|
8298
|
-
name.textContent = text;
|
|
8299
|
-
};
|
|
8300
|
-
const seam = el("div", "knobs-cmp-seam");
|
|
8301
|
-
seam.setAttribute("aria-hidden", "true");
|
|
8302
|
-
const handle = el("div", "knobs-cmp-handle");
|
|
8303
|
-
handle.setAttribute("role", "slider");
|
|
8304
|
-
handle.setAttribute("aria-label", "Wipe");
|
|
8305
|
-
handle.setAttribute("aria-valuemin", "0");
|
|
8306
|
-
handle.setAttribute("aria-valuemax", "100");
|
|
8307
|
-
const grip = el("span", "knobs-cmp-grip");
|
|
8308
|
-
grip.setAttribute("aria-hidden", "true");
|
|
8309
|
-
handle.append(grip);
|
|
8310
|
-
const note = el("div", "knobs-cmp-note");
|
|
8311
|
-
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
8312
|
-
root.append(bar, stage);
|
|
8313
|
-
const wantsStrip = options.strip === true;
|
|
8314
|
-
let tl = bound;
|
|
8315
|
-
let ownTimeline = false;
|
|
8316
|
-
let stripHost = null;
|
|
8317
|
-
if (wantsStrip) {
|
|
8318
|
-
stripHost = el("div", "knobs-cmp-strip-host");
|
|
8319
|
-
root.append(stripHost);
|
|
8320
|
-
}
|
|
8321
|
-
if (!tl && (options.timeline === true || wantsStrip)) {
|
|
8322
|
-
const host = el("div", "knobs-cmp-timeline-host");
|
|
8323
|
-
root.append(host);
|
|
8324
|
-
tl = timeline(host, {
|
|
8325
|
-
...look,
|
|
8326
|
-
variant: "line",
|
|
8327
|
-
duration: 1,
|
|
8328
|
-
fps: fps(),
|
|
8329
|
-
arrows: { step: 0.5, shift: 2 },
|
|
8330
|
-
label: "Compare time",
|
|
8331
|
-
...options.hints === false ? { hints: false } : {}
|
|
8332
|
-
});
|
|
8333
|
-
ownTimeline = true;
|
|
8334
|
-
}
|
|
8335
|
-
container.append(root);
|
|
8336
|
-
const sides = [null, null];
|
|
8337
|
-
let duration = 0;
|
|
8338
|
-
let renderer = "dom";
|
|
8339
|
-
let pending = false;
|
|
8340
|
-
let gpu = null;
|
|
8341
|
-
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
8342
|
-
const fade = new Spring(1, FADE2.omega, FADE2.zeta, 0.001);
|
|
8343
|
-
let fromMode = mode;
|
|
8344
|
-
let toMode = mode;
|
|
8345
|
-
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
8346
|
-
let wipeCatching = false;
|
|
8347
|
-
const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
8348
|
-
let loupeHeld = false;
|
|
8349
|
-
let pointer = null;
|
|
8350
|
-
let drag = null;
|
|
8351
|
-
let tap = null;
|
|
8352
|
-
const touches = new Map;
|
|
8353
|
-
let pinch = null;
|
|
8354
|
-
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8355
|
-
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8356
|
-
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
8357
|
-
let viewSent = "0.0000,0.0000,1.0000|";
|
|
8358
|
-
let ownLoop = null;
|
|
8359
|
-
let scrubbing = false;
|
|
8360
|
-
let stageW = 1;
|
|
8361
|
-
let stageH = 1;
|
|
8362
|
-
let stageRect = null;
|
|
8363
|
-
let accent = [0.49, 0.81, 0.61];
|
|
8364
|
-
let labelW = [0, 0];
|
|
8365
|
-
let frameRadius = 0;
|
|
8366
|
-
let dirty = true;
|
|
8367
|
-
let disposed = false;
|
|
8368
|
-
let near = options.lazy === false;
|
|
8369
|
-
let onScreen = options.lazy === false;
|
|
8370
|
-
let releaseTimer = 0;
|
|
8371
|
-
let ownTime = 0;
|
|
8372
|
-
let ownPlaying = false;
|
|
8373
|
-
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
8374
|
-
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
8375
|
-
const follower = () => {
|
|
8376
|
-
const m = master();
|
|
8377
|
-
const v = videoOf(sides[0]);
|
|
8378
|
-
return m && v && v !== m ? v : null;
|
|
8379
|
-
};
|
|
8380
|
-
const now = () => tl ? tl.time : ownTime;
|
|
8381
|
-
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
8382
|
-
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
8383
|
-
const sizeOf = (s) => {
|
|
8384
|
-
if (s.size.w > 0)
|
|
8385
|
-
return s.size;
|
|
8386
|
-
const other = sides[s.index === 0 ? 1 : 0];
|
|
8387
|
-
if (other && other.size.w > 0)
|
|
8388
|
-
return other.size;
|
|
8389
|
-
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
8390
|
-
};
|
|
8391
|
-
const wantOf = (s) => {
|
|
8392
|
-
const v = s.el;
|
|
8393
|
-
const f = frameOf(now(), fps());
|
|
8394
|
-
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
8757
|
+
requestDraw();
|
|
8395
8758
|
};
|
|
8396
|
-
const
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8759
|
+
const jumpAll = (t) => {
|
|
8760
|
+
for (const s of sides) {
|
|
8761
|
+
const v = videoOf(s);
|
|
8762
|
+
if (!v || v.readyState < 1)
|
|
8763
|
+
continue;
|
|
8764
|
+
s.seek = SEEK_IDLE;
|
|
8765
|
+
v.currentTime = clampTime(v, t);
|
|
8766
|
+
if (v.paused)
|
|
8767
|
+
v.play().catch(() => {});
|
|
8768
|
+
}
|
|
8400
8769
|
};
|
|
8401
|
-
const
|
|
8402
|
-
const
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8770
|
+
const startVideos = () => {
|
|
8771
|
+
const t = now();
|
|
8772
|
+
for (const s of sides) {
|
|
8773
|
+
const v = videoOf(s);
|
|
8774
|
+
if (!v)
|
|
8775
|
+
continue;
|
|
8776
|
+
s.seek = SEEK_IDLE;
|
|
8777
|
+
v.playbackRate = 1;
|
|
8778
|
+
if (v.readyState >= 1 && Math.abs(v.currentTime - t) > 1.5 / fps())
|
|
8779
|
+
v.currentTime = clampTime(v, t);
|
|
8780
|
+
v.play().catch(() => {});
|
|
8409
8781
|
}
|
|
8410
|
-
|
|
8411
|
-
s.shown = { frame: f, cached: false };
|
|
8412
|
-
setCached(s, false);
|
|
8413
|
-
emitFrame(s, f, "video");
|
|
8414
|
-
requestDraw();
|
|
8782
|
+
loop.wake();
|
|
8415
8783
|
};
|
|
8416
|
-
const
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
} else if (renderer === "dom" && !pending) {
|
|
8423
|
-
const p = s.preview;
|
|
8424
|
-
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
8425
|
-
p.width = c.bitmap.width;
|
|
8426
|
-
p.height = c.bitmap.height;
|
|
8784
|
+
const pauseVideos = () => {
|
|
8785
|
+
for (const s of sides) {
|
|
8786
|
+
const v = videoOf(s);
|
|
8787
|
+
if (v) {
|
|
8788
|
+
v.pause();
|
|
8789
|
+
v.playbackRate = 1;
|
|
8427
8790
|
}
|
|
8428
|
-
|
|
8429
|
-
} else
|
|
8430
|
-
return;
|
|
8431
|
-
s.shown = { frame: c.frame, cached: true };
|
|
8432
|
-
setCached(s, true);
|
|
8433
|
-
emitFrame(s, c.frame, "cache");
|
|
8434
|
-
requestDraw();
|
|
8791
|
+
}
|
|
8435
8792
|
};
|
|
8436
|
-
const
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
gpu?.dispose();
|
|
8440
|
-
gpu = null;
|
|
8441
|
-
setRenderer("dom");
|
|
8793
|
+
const stopVideos = () => {
|
|
8794
|
+
pauseVideos();
|
|
8795
|
+
showFrame(now(), false);
|
|
8442
8796
|
};
|
|
8443
|
-
const
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
return requestDraw();
|
|
8456
|
-
}
|
|
8457
|
-
const v = s.el;
|
|
8458
|
-
if (v.readyState < 2)
|
|
8459
|
-
return;
|
|
8460
|
-
if (isPlaying() || !s.frames)
|
|
8461
|
-
return showVideo(s);
|
|
8462
|
-
const want = wantOf(s);
|
|
8463
|
-
const at = frameOf(v.currentTime, fps());
|
|
8464
|
-
if (at === want) {
|
|
8465
|
-
showVideo(s);
|
|
8466
|
-
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
8467
|
-
return;
|
|
8468
|
-
}
|
|
8469
|
-
const c = s.frames.nearest(want);
|
|
8470
|
-
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
8471
|
-
showCached(s, c);
|
|
8797
|
+
const clock = () => {
|
|
8798
|
+
const m = master();
|
|
8799
|
+
if (!m || !near)
|
|
8800
|
+
return Number.NaN;
|
|
8801
|
+
if (m.readyState < 1 || m.seeking)
|
|
8802
|
+
return tl.time;
|
|
8803
|
+
return m.ended ? Number.POSITIVE_INFINITY : m.currentTime;
|
|
8804
|
+
};
|
|
8805
|
+
const ownSeek = (t) => {
|
|
8806
|
+
ownTime = Math.min(Math.max(0, t), duration);
|
|
8807
|
+
if (ownPlaying)
|
|
8808
|
+
jumpAll(ownTime);
|
|
8472
8809
|
else
|
|
8473
|
-
|
|
8810
|
+
showFrame(ownTime, true);
|
|
8474
8811
|
};
|
|
8475
|
-
const
|
|
8476
|
-
if (
|
|
8477
|
-
return;
|
|
8478
|
-
const want = wantOf(s);
|
|
8479
|
-
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
8480
|
-
return;
|
|
8481
|
-
const c = s.frames.nearest(want);
|
|
8482
|
-
if (!c)
|
|
8812
|
+
const ownPlay = () => {
|
|
8813
|
+
if (ownPlaying || !master())
|
|
8483
8814
|
return;
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8815
|
+
if (ownLoop && (ownTime < ownLoop.start || ownTime >= ownLoop.end - 0.5 / fps()))
|
|
8816
|
+
ownTime = ownLoop.start;
|
|
8817
|
+
else if (!ownLoop && ownTime >= duration - 0.5 / fps())
|
|
8818
|
+
ownTime = 0;
|
|
8819
|
+
ownPlaying = true;
|
|
8820
|
+
startVideos();
|
|
8487
8821
|
};
|
|
8488
|
-
const
|
|
8489
|
-
|
|
8490
|
-
const media = el(kind === "video" ? "video" : "img", "knobs-cmp-media");
|
|
8491
|
-
media.dataset.side = index === 0 ? "before" : "after";
|
|
8492
|
-
media.crossOrigin = "anonymous";
|
|
8493
|
-
media.setAttribute("aria-hidden", "true");
|
|
8494
|
-
const preview = el("canvas", "knobs-cmp-media knobs-cmp-preview");
|
|
8495
|
-
preview.dataset.side = media.dataset.side;
|
|
8496
|
-
preview.style.display = "none";
|
|
8497
|
-
preview.setAttribute("aria-hidden", "true");
|
|
8498
|
-
const offs = [];
|
|
8499
|
-
const on = (type, f) => {
|
|
8500
|
-
media.addEventListener(type, f);
|
|
8501
|
-
offs.push(() => media.removeEventListener(type, f));
|
|
8502
|
-
};
|
|
8503
|
-
const side = {
|
|
8504
|
-
index,
|
|
8505
|
-
source,
|
|
8506
|
-
kind,
|
|
8507
|
-
el: media,
|
|
8508
|
-
preview,
|
|
8509
|
-
size: { w: 0, h: 0 },
|
|
8510
|
-
seek: SEEK_IDLE,
|
|
8511
|
-
loaded: false,
|
|
8512
|
-
frames: null,
|
|
8513
|
-
unlisten: null,
|
|
8514
|
-
seen: false,
|
|
8515
|
-
shown: null,
|
|
8516
|
-
frameAt: 0,
|
|
8517
|
-
misses: 0,
|
|
8518
|
-
failed: false,
|
|
8519
|
-
stop: () => {
|
|
8520
|
-
for (const f of offs)
|
|
8521
|
-
f();
|
|
8522
|
-
}
|
|
8523
|
-
};
|
|
8524
|
-
const fail = () => {
|
|
8525
|
-
if (!side.loaded)
|
|
8526
|
-
return;
|
|
8527
|
-
side.failed = true;
|
|
8528
|
-
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
8529
|
-
};
|
|
8530
|
-
if (media instanceof HTMLVideoElement) {
|
|
8531
|
-
const v = media;
|
|
8532
|
-
v.muted = true;
|
|
8533
|
-
v.defaultMuted = true;
|
|
8534
|
-
v.playsInline = true;
|
|
8535
|
-
v.preload = "auto";
|
|
8536
|
-
v.setAttribute("playsinline", "");
|
|
8537
|
-
v.setAttribute("muted", "");
|
|
8538
|
-
on("loadedmetadata", () => {
|
|
8539
|
-
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
8540
|
-
measureDuration();
|
|
8541
|
-
if (auto)
|
|
8542
|
-
measure();
|
|
8543
|
-
if (isPlaying())
|
|
8544
|
-
jumpAll(now());
|
|
8545
|
-
else
|
|
8546
|
-
showFrame(now(), false);
|
|
8547
|
-
requestDraw();
|
|
8548
|
-
});
|
|
8549
|
-
on("resize", () => {
|
|
8550
|
-
side.size = { w: v.videoWidth, h: v.videoHeight };
|
|
8551
|
-
requestDraw();
|
|
8552
|
-
});
|
|
8553
|
-
on("loadeddata", () => pushFrame(side));
|
|
8554
|
-
on("seeked", () => {
|
|
8555
|
-
const step = seeked(side.seek, v.currentTime, fps());
|
|
8556
|
-
side.seek = step.state;
|
|
8557
|
-
if (step.apply !== null)
|
|
8558
|
-
seekVideo(side, v, step.apply);
|
|
8559
|
-
pushFrame(side);
|
|
8560
|
-
});
|
|
8561
|
-
on("error", fail);
|
|
8562
|
-
if (v.requestVideoFrameCallback) {
|
|
8563
|
-
let id = 0;
|
|
8564
|
-
const frame = () => {
|
|
8565
|
-
side.frameAt = performance.now();
|
|
8566
|
-
pushFrame(side);
|
|
8567
|
-
id = v.requestVideoFrameCallback(frame);
|
|
8568
|
-
};
|
|
8569
|
-
id = v.requestVideoFrameCallback(frame);
|
|
8570
|
-
offs.push(() => v.cancelVideoFrameCallback?.(id));
|
|
8571
|
-
}
|
|
8572
|
-
} else {
|
|
8573
|
-
on("load", () => {
|
|
8574
|
-
side.size = { w: media.naturalWidth, h: media.naturalHeight };
|
|
8575
|
-
if (auto)
|
|
8576
|
-
measure();
|
|
8577
|
-
pushFrame(side);
|
|
8578
|
-
requestDraw();
|
|
8579
|
-
});
|
|
8580
|
-
on("error", fail);
|
|
8581
|
-
media.decoding = "async";
|
|
8582
|
-
}
|
|
8583
|
-
return side;
|
|
8584
|
-
};
|
|
8585
|
-
const loadSide = (s) => {
|
|
8586
|
-
if (s.loaded || disposed)
|
|
8587
|
-
return;
|
|
8588
|
-
s.loaded = true;
|
|
8589
|
-
s.failed = false;
|
|
8590
|
-
s.el.src = s.source.src;
|
|
8591
|
-
if (s.kind === "video") {
|
|
8592
|
-
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
8593
|
-
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
8594
|
-
if (onScreen) {
|
|
8595
|
-
s.frames.visible(true);
|
|
8596
|
-
s.seen = true;
|
|
8597
|
-
}
|
|
8598
|
-
}
|
|
8599
|
-
};
|
|
8600
|
-
const unloadSide = (s) => {
|
|
8601
|
-
if (!s.loaded)
|
|
8822
|
+
const ownPause = () => {
|
|
8823
|
+
if (!ownPlaying)
|
|
8602
8824
|
return;
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
s.el.load();
|
|
8607
|
-
s.seek = SEEK_IDLE;
|
|
8608
|
-
s.shown = null;
|
|
8609
|
-
setCached(s, false);
|
|
8610
|
-
s.unlisten?.();
|
|
8611
|
-
s.unlisten = null;
|
|
8612
|
-
if (s.frames) {
|
|
8613
|
-
if (s.seen)
|
|
8614
|
-
s.frames.visible(false);
|
|
8615
|
-
s.seen = false;
|
|
8616
|
-
s.frames.release();
|
|
8617
|
-
s.frames = null;
|
|
8618
|
-
}
|
|
8619
|
-
gpu?.clear(s.index);
|
|
8825
|
+
ownPlaying = false;
|
|
8826
|
+
ownTime = Math.min(duration, frameOf(ownTime, fps()) / fps());
|
|
8827
|
+
stopVideos();
|
|
8620
8828
|
};
|
|
8621
|
-
const
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8829
|
+
const zoomNow = () => ({ u: zu.x, v: zv.x, w: zw.x, h: zw.x });
|
|
8830
|
+
const zoomed = () => zw.target < 0.999;
|
|
8831
|
+
const layoutNow = () => {
|
|
8832
|
+
const box = { x: 0, y: 0, w: stageW, h: stageH };
|
|
8833
|
+
const L = compareLayout(box, sizeOf(sides[0]), sizeOf(sides[1]));
|
|
8834
|
+
const k = lay.x;
|
|
8835
|
+
const before = lerpRect(L.side.before, L.overlay.before, k);
|
|
8836
|
+
const after = lerpRect(L.side.after, L.overlay.after, k);
|
|
8837
|
+
const z = zoomNow();
|
|
8838
|
+
return { L, before, after, zBefore: zoomedRect(before, z), zAfter: zoomedRect(after, z) };
|
|
8631
8839
|
};
|
|
8632
|
-
const
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
for (const s of sides) {
|
|
8639
|
-
const st = s.el.style;
|
|
8640
|
-
st.clipPath = "";
|
|
8641
|
-
st.maskImage = "";
|
|
8642
|
-
st.setProperty("-webkit-mask-image", "");
|
|
8643
|
-
st.mixBlendMode = "";
|
|
8644
|
-
st.opacity = "";
|
|
8645
|
-
s.shown = null;
|
|
8646
|
-
setCached(s, false);
|
|
8647
|
-
}
|
|
8648
|
-
for (const s of sides)
|
|
8649
|
-
pushFrame(s);
|
|
8650
|
-
requestDraw();
|
|
8840
|
+
const wipeRect = (r) => {
|
|
8841
|
+
const x = Math.min(r.before.x, r.after.x);
|
|
8842
|
+
const y = Math.min(r.before.y, r.after.y);
|
|
8843
|
+
const x1 = Math.max(r.before.x + r.before.w, r.after.x + r.after.w);
|
|
8844
|
+
const y1 = Math.max(r.before.y + r.before.h, r.after.y + r.after.h);
|
|
8845
|
+
return { x, y, w: x1 - x, h: y1 - y };
|
|
8651
8846
|
};
|
|
8652
|
-
const
|
|
8653
|
-
const
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
return;
|
|
8657
|
-
if (retry)
|
|
8658
|
-
win.setTimeout(() => void startGpu(false), 150);
|
|
8659
|
-
else
|
|
8660
|
-
setRenderer("dom");
|
|
8661
|
-
});
|
|
8662
|
-
if (disposed)
|
|
8663
|
-
return g?.dispose();
|
|
8664
|
-
if (!g)
|
|
8665
|
-
return setRenderer("dom");
|
|
8666
|
-
gpu = g;
|
|
8667
|
-
setRenderer("gpu");
|
|
8847
|
+
const baseAt = (p) => {
|
|
8848
|
+
const r = layoutNow();
|
|
8849
|
+
const inside = (b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
|
|
8850
|
+
return inside(r.after) || !inside(r.before) ? r.after : r.before;
|
|
8668
8851
|
};
|
|
8669
|
-
const
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
modePill?.update();
|
|
8852
|
+
const loupeShown = () => renderer === "gpu" && (loupeOn || loupeHeld) && pointer !== null;
|
|
8853
|
+
const loopNow = () => {
|
|
8854
|
+
if (!tl)
|
|
8855
|
+
return ownLoop;
|
|
8856
|
+
const r = tl.ranges.find((x) => x.loop);
|
|
8857
|
+
return r ? { start: r.start, end: r.end } : null;
|
|
8676
8858
|
};
|
|
8677
|
-
const
|
|
8678
|
-
|
|
8679
|
-
return;
|
|
8680
|
-
mode = next;
|
|
8681
|
-
const reduced = reducedMotion(win);
|
|
8682
|
-
tune(fade, reduced ? FADE_REDUCED : FADE2);
|
|
8683
|
-
if (fade.x >= 0.5) {
|
|
8684
|
-
fromMode = toMode;
|
|
8685
|
-
fade.snap(0);
|
|
8686
|
-
}
|
|
8687
|
-
toMode = next;
|
|
8688
|
-
fade.set(1);
|
|
8689
|
-
if (reduced)
|
|
8690
|
-
lay.snap(next === "side" ? 0 : 1);
|
|
8691
|
-
else
|
|
8692
|
-
lay.set(next === "side" ? 0 : 1);
|
|
8693
|
-
syncModeUi();
|
|
8694
|
-
if (emit)
|
|
8695
|
-
options.onMode?.(next);
|
|
8859
|
+
const syncLoopUi = () => {
|
|
8860
|
+
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
8696
8861
|
requestDraw();
|
|
8697
8862
|
};
|
|
8698
|
-
const
|
|
8699
|
-
|
|
8700
|
-
if (w === wipe && !jump)
|
|
8701
|
-
return;
|
|
8702
|
-
wipe = w;
|
|
8703
|
-
const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
|
|
8704
|
-
const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
|
|
8705
|
-
wipeCatching = head.catching;
|
|
8706
|
-
if (head.catching) {
|
|
8707
|
-
wipeLine.x = head.x;
|
|
8708
|
-
wipeLine.target = head.target;
|
|
8709
|
-
} else
|
|
8710
|
-
wipeLine.snap(head.x);
|
|
8711
|
-
if (emit)
|
|
8712
|
-
options.onWipe?.(w);
|
|
8713
|
-
if (head.catching)
|
|
8714
|
-
requestDraw();
|
|
8715
|
-
else
|
|
8716
|
-
drawNow();
|
|
8717
|
-
};
|
|
8718
|
-
const setLoupe = (on) => {
|
|
8719
|
-
loupeOn = on;
|
|
8720
|
-
loupeBtn.setAttribute("aria-pressed", String(on));
|
|
8721
|
-
requestDraw();
|
|
8722
|
-
};
|
|
8723
|
-
const clampTime = (v, t) => Math.min(Math.max(0, t), Math.max(0, (Number.isFinite(v.duration) ? v.duration : t) - 0.001));
|
|
8724
|
-
const seekVideo = (s, v, t) => {
|
|
8725
|
-
const c = s.frames?.nearest(wantOf(s));
|
|
8726
|
-
const far = !c || Math.abs(c.frame - wantOf(s)) > FAST_SEEK_FRAMES;
|
|
8727
|
-
if (scrubbing && far && v.fastSeek)
|
|
8728
|
-
v.fastSeek(t);
|
|
8729
|
-
else
|
|
8730
|
-
v.currentTime = t;
|
|
8731
|
-
};
|
|
8732
|
-
const showFrame = (t, used) => {
|
|
8733
|
-
for (const s of sides) {
|
|
8734
|
-
const v = videoOf(s);
|
|
8735
|
-
if (!v || v.readyState < 1)
|
|
8736
|
-
continue;
|
|
8737
|
-
if (!v.paused) {
|
|
8738
|
-
v.pause();
|
|
8739
|
-
v.playbackRate = 1;
|
|
8740
|
-
}
|
|
8741
|
-
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
8742
|
-
const step = requestSeek(s.seek, frameMidpoint(want, fps(), v.duration), v.currentTime, fps());
|
|
8743
|
-
s.seek = step.state;
|
|
8744
|
-
if (step.apply !== null)
|
|
8745
|
-
seekVideo(s, v, step.apply);
|
|
8746
|
-
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
8747
|
-
pushFrame(s);
|
|
8748
|
-
if (used)
|
|
8749
|
-
s.frames?.focus(want);
|
|
8750
|
-
refreshCached(s);
|
|
8751
|
-
}
|
|
8752
|
-
requestDraw();
|
|
8753
|
-
};
|
|
8754
|
-
const jumpAll = (t) => {
|
|
8755
|
-
for (const s of sides) {
|
|
8756
|
-
const v = videoOf(s);
|
|
8757
|
-
if (!v || v.readyState < 1)
|
|
8758
|
-
continue;
|
|
8759
|
-
s.seek = SEEK_IDLE;
|
|
8760
|
-
v.currentTime = clampTime(v, t);
|
|
8761
|
-
if (v.paused)
|
|
8762
|
-
v.play().catch(() => {});
|
|
8763
|
-
}
|
|
8764
|
-
};
|
|
8765
|
-
const startVideos = () => {
|
|
8766
|
-
const t = now();
|
|
8767
|
-
for (const s of sides) {
|
|
8768
|
-
const v = videoOf(s);
|
|
8769
|
-
if (!v)
|
|
8770
|
-
continue;
|
|
8771
|
-
s.seek = SEEK_IDLE;
|
|
8772
|
-
v.playbackRate = 1;
|
|
8773
|
-
if (v.readyState >= 1 && Math.abs(v.currentTime - t) > 1.5 / fps())
|
|
8774
|
-
v.currentTime = clampTime(v, t);
|
|
8775
|
-
v.play().catch(() => {});
|
|
8776
|
-
}
|
|
8777
|
-
loop.wake();
|
|
8778
|
-
};
|
|
8779
|
-
const pauseVideos = () => {
|
|
8780
|
-
for (const s of sides) {
|
|
8781
|
-
const v = videoOf(s);
|
|
8782
|
-
if (v) {
|
|
8783
|
-
v.pause();
|
|
8784
|
-
v.playbackRate = 1;
|
|
8785
|
-
}
|
|
8786
|
-
}
|
|
8787
|
-
};
|
|
8788
|
-
const stopVideos = () => {
|
|
8789
|
-
pauseVideos();
|
|
8790
|
-
showFrame(now(), false);
|
|
8791
|
-
};
|
|
8792
|
-
const clock = () => {
|
|
8793
|
-
const m = master();
|
|
8794
|
-
if (!m || !near)
|
|
8795
|
-
return Number.NaN;
|
|
8796
|
-
if (m.readyState < 1 || m.seeking)
|
|
8797
|
-
return tl.time;
|
|
8798
|
-
return m.ended ? Number.POSITIVE_INFINITY : m.currentTime;
|
|
8799
|
-
};
|
|
8800
|
-
const ownSeek = (t) => {
|
|
8801
|
-
ownTime = Math.min(Math.max(0, t), duration);
|
|
8802
|
-
if (ownPlaying)
|
|
8803
|
-
jumpAll(ownTime);
|
|
8804
|
-
else
|
|
8805
|
-
showFrame(ownTime, true);
|
|
8806
|
-
};
|
|
8807
|
-
const ownPlay = () => {
|
|
8808
|
-
if (ownPlaying || !master())
|
|
8809
|
-
return;
|
|
8810
|
-
if (ownLoop && (ownTime < ownLoop.start || ownTime >= ownLoop.end - 0.5 / fps()))
|
|
8811
|
-
ownTime = ownLoop.start;
|
|
8812
|
-
else if (!ownLoop && ownTime >= duration - 0.5 / fps())
|
|
8813
|
-
ownTime = 0;
|
|
8814
|
-
ownPlaying = true;
|
|
8815
|
-
startVideos();
|
|
8816
|
-
};
|
|
8817
|
-
const ownPause = () => {
|
|
8818
|
-
if (!ownPlaying)
|
|
8819
|
-
return;
|
|
8820
|
-
ownPlaying = false;
|
|
8821
|
-
ownTime = Math.min(duration, frameOf(ownTime, fps()) / fps());
|
|
8822
|
-
stopVideos();
|
|
8823
|
-
};
|
|
8824
|
-
const zoomNow = () => ({ u: zu.x, v: zv.x, w: zw.x, h: zw.x });
|
|
8825
|
-
const zoomed = () => zw.target < 0.999;
|
|
8826
|
-
const layoutNow = () => {
|
|
8827
|
-
const box = { x: 0, y: 0, w: stageW, h: stageH };
|
|
8828
|
-
const L = compareLayout(box, sizeOf(sides[0]), sizeOf(sides[1]));
|
|
8829
|
-
const k = lay.x;
|
|
8830
|
-
const before = lerpRect(L.side.before, L.overlay.before, k);
|
|
8831
|
-
const after = lerpRect(L.side.after, L.overlay.after, k);
|
|
8832
|
-
const z = zoomNow();
|
|
8833
|
-
return { L, before, after, zBefore: zoomedRect(before, z), zAfter: zoomedRect(after, z) };
|
|
8834
|
-
};
|
|
8835
|
-
const wipeRect = (r) => {
|
|
8836
|
-
const x = Math.min(r.before.x, r.after.x);
|
|
8837
|
-
const y = Math.min(r.before.y, r.after.y);
|
|
8838
|
-
const x1 = Math.max(r.before.x + r.before.w, r.after.x + r.after.w);
|
|
8839
|
-
const y1 = Math.max(r.before.y + r.before.h, r.after.y + r.after.h);
|
|
8840
|
-
return { x, y, w: x1 - x, h: y1 - y };
|
|
8841
|
-
};
|
|
8842
|
-
const baseAt = (p) => {
|
|
8843
|
-
const r = layoutNow();
|
|
8844
|
-
const inside = (b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
|
|
8845
|
-
return inside(r.after) || !inside(r.before) ? r.after : r.before;
|
|
8846
|
-
};
|
|
8847
|
-
const loupeShown = () => renderer === "gpu" && (loupeOn || loupeHeld) && pointer !== null;
|
|
8848
|
-
const loopNow = () => {
|
|
8849
|
-
if (!tl)
|
|
8850
|
-
return ownLoop;
|
|
8851
|
-
const r = tl.ranges.find((x) => x.loop);
|
|
8852
|
-
return r ? { start: r.start, end: r.end } : null;
|
|
8853
|
-
};
|
|
8854
|
-
const syncLoopUi = () => {
|
|
8855
|
-
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
8856
|
-
requestDraw();
|
|
8857
|
-
};
|
|
8858
|
-
const sendView = () => {
|
|
8859
|
-
if (!options.onView)
|
|
8863
|
+
const sendView = () => {
|
|
8864
|
+
if (!options.onView)
|
|
8860
8865
|
return;
|
|
8861
8866
|
const z = zoomNow();
|
|
8862
8867
|
const l = loopNow();
|
|
@@ -9377,7 +9382,7 @@ function compare(container, options) {
|
|
|
9377
9382
|
holdLoupe(true);
|
|
9378
9383
|
return;
|
|
9379
9384
|
case "l":
|
|
9380
|
-
if (e.altKey || e.shiftKey)
|
|
9385
|
+
if (e.altKey || e.shiftKey || loopBtn.hidden)
|
|
9381
9386
|
return;
|
|
9382
9387
|
claim();
|
|
9383
9388
|
return api.setLoop(!loopNow());
|
|
@@ -9400,6 +9405,9 @@ function compare(container, options) {
|
|
|
9400
9405
|
return glideZoom(ZOOM_FIT);
|
|
9401
9406
|
}
|
|
9402
9407
|
};
|
|
9408
|
+
const syncLoopButton = () => {
|
|
9409
|
+
loopBtn.hidden = !sides.some((s) => s.kind === "video");
|
|
9410
|
+
};
|
|
9403
9411
|
const replace = (index, source) => {
|
|
9404
9412
|
const old = sides[index];
|
|
9405
9413
|
old.stop();
|
|
@@ -9408,6 +9416,7 @@ function compare(container, options) {
|
|
|
9408
9416
|
old.el.replaceWith(s.el);
|
|
9409
9417
|
old.preview.replaceWith(s.preview);
|
|
9410
9418
|
sides[index] = s;
|
|
9419
|
+
syncLoopButton();
|
|
9411
9420
|
note.textContent = "";
|
|
9412
9421
|
setLabel(index, labelOf(index));
|
|
9413
9422
|
if (near)
|
|
@@ -9422,6 +9431,7 @@ function compare(container, options) {
|
|
|
9422
9431
|
stage.insertBefore(sides[0].preview, labels[0]);
|
|
9423
9432
|
setLabel(0, labelOf(0));
|
|
9424
9433
|
setLabel(1, labelOf(1));
|
|
9434
|
+
syncLoopButton();
|
|
9425
9435
|
loupeBtn.setAttribute("aria-pressed", String(loupeOn));
|
|
9426
9436
|
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
9427
9437
|
const modePill = modeList.length > 1 ? selectionPill(modes, {
|
|
@@ -9500,7 +9510,7 @@ function compare(container, options) {
|
|
|
9500
9510
|
label: MODE_LABEL[m].long,
|
|
9501
9511
|
activate: () => setMode(m)
|
|
9502
9512
|
})) : [],
|
|
9503
|
-
{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
|
|
9513
|
+
...loopBtn.hidden ? [] : [{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) }],
|
|
9504
9514
|
...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
|
|
9505
9515
|
...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
|
|
9506
9516
|
...mode === "wipe" ? [
|
|
@@ -9511,14 +9521,6 @@ function compare(container, options) {
|
|
|
9511
9521
|
}
|
|
9512
9522
|
] : []
|
|
9513
9523
|
], { listen: false, rank: 1 });
|
|
9514
|
-
const strip = wantsStrip && stripHost && tl ? compareStrip(stripHost, {
|
|
9515
|
-
...look,
|
|
9516
|
-
before: options.before,
|
|
9517
|
-
after: options.after,
|
|
9518
|
-
timeline: tl,
|
|
9519
|
-
...fpsOpt ? { fps: fpsOpt } : {},
|
|
9520
|
-
threshold
|
|
9521
|
-
}) : null;
|
|
9522
9524
|
const api = {
|
|
9523
9525
|
element: root,
|
|
9524
9526
|
get mode() {
|
|
@@ -9544,7 +9546,6 @@ function compare(container, options) {
|
|
|
9544
9546
|
return renderer;
|
|
9545
9547
|
},
|
|
9546
9548
|
timeline: tl,
|
|
9547
|
-
strip,
|
|
9548
9549
|
get zoom() {
|
|
9549
9550
|
return clampZoom(zoomNow());
|
|
9550
9551
|
},
|
|
@@ -9566,8 +9567,6 @@ function compare(container, options) {
|
|
|
9566
9567
|
replace(1, after);
|
|
9567
9568
|
else if (next.after)
|
|
9568
9569
|
setLabel(1, next.after.label ?? "After");
|
|
9569
|
-
if (before || after)
|
|
9570
|
-
strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
|
|
9571
9570
|
if (next.before || next.after)
|
|
9572
9571
|
readColors();
|
|
9573
9572
|
if (next.mode !== undefined)
|
|
@@ -9634,7 +9633,6 @@ function compare(container, options) {
|
|
|
9634
9633
|
if (ownTimeline)
|
|
9635
9634
|
tl.dispose();
|
|
9636
9635
|
}
|
|
9637
|
-
strip?.dispose();
|
|
9638
9636
|
for (const s of sides) {
|
|
9639
9637
|
s.stop();
|
|
9640
9638
|
unloadSide(s);
|
|
@@ -9658,7 +9656,6 @@ var init_compare = __esm(() => {
|
|
|
9658
9656
|
init_compare_frames();
|
|
9659
9657
|
init_compare_gpu();
|
|
9660
9658
|
init_compare_model();
|
|
9661
|
-
init_compare_strip();
|
|
9662
9659
|
init_icons();
|
|
9663
9660
|
init_look();
|
|
9664
9661
|
init_rgba();
|
|
@@ -9716,7 +9713,10 @@ function player(container, options) {
|
|
|
9716
9713
|
track.src = options.captions;
|
|
9717
9714
|
v.append(track);
|
|
9718
9715
|
}
|
|
9719
|
-
|
|
9716
|
+
const preview = el("canvas", "knobs-player-preview");
|
|
9717
|
+
preview.setAttribute("aria-hidden", "true");
|
|
9718
|
+
preview.hidden = true;
|
|
9719
|
+
frame.append(v, preview);
|
|
9720
9720
|
const host = el("div", "knobs-player-line");
|
|
9721
9721
|
root.append(frame, host);
|
|
9722
9722
|
container.append(root);
|
|
@@ -9746,6 +9746,57 @@ function player(container, options) {
|
|
|
9746
9746
|
else
|
|
9747
9747
|
v.currentTime = t;
|
|
9748
9748
|
};
|
|
9749
|
+
const fps = options.fps ?? 30;
|
|
9750
|
+
let frames = null;
|
|
9751
|
+
let unlistenFrames = null;
|
|
9752
|
+
let scrubbing = false;
|
|
9753
|
+
let previewing = null;
|
|
9754
|
+
const want = () => frameOf(tl.time, fps);
|
|
9755
|
+
const emitFrame = (frame, source) => root.dispatchEvent(new CustomEvent("knobs-player-frame", { detail: { frame, source } }));
|
|
9756
|
+
const drawPreview = (c) => {
|
|
9757
|
+
if (preview.width !== c.bitmap.width || preview.height !== c.bitmap.height) {
|
|
9758
|
+
preview.width = c.bitmap.width;
|
|
9759
|
+
preview.height = c.bitmap.height;
|
|
9760
|
+
}
|
|
9761
|
+
preview.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
9762
|
+
preview.hidden = false;
|
|
9763
|
+
previewing = c.frame;
|
|
9764
|
+
emitFrame(c.frame, "cache");
|
|
9765
|
+
};
|
|
9766
|
+
const hidePreview = () => {
|
|
9767
|
+
if (previewing === null)
|
|
9768
|
+
return;
|
|
9769
|
+
previewing = null;
|
|
9770
|
+
preview.hidden = true;
|
|
9771
|
+
};
|
|
9772
|
+
const showNearest = () => {
|
|
9773
|
+
if (!frames || !scrubbing)
|
|
9774
|
+
return;
|
|
9775
|
+
const w = want();
|
|
9776
|
+
const c = frames.nearest(w, Number.POSITIVE_INFINITY, true);
|
|
9777
|
+
if (!c || c.frame === previewing)
|
|
9778
|
+
return;
|
|
9779
|
+
const shown = previewing ?? frameOf(v.currentTime, fps);
|
|
9780
|
+
if (Math.abs(c.frame - w) < Math.abs(shown - w))
|
|
9781
|
+
drawPreview(c);
|
|
9782
|
+
};
|
|
9783
|
+
const caching = scrubCaching(options.scrubCache, doc.defaultView);
|
|
9784
|
+
const startFrames = () => {
|
|
9785
|
+
if (frames || !caching)
|
|
9786
|
+
return;
|
|
9787
|
+
frames = acquireFrames(doc, options.src, fps);
|
|
9788
|
+
frames.visible(true);
|
|
9789
|
+
unlistenFrames = frames.listen(showNearest);
|
|
9790
|
+
};
|
|
9791
|
+
const stall = stallGuard(doc.defaultView, () => {
|
|
9792
|
+
pending = null;
|
|
9793
|
+
hidePreview();
|
|
9794
|
+
});
|
|
9795
|
+
const onError = () => {
|
|
9796
|
+
stall.clear();
|
|
9797
|
+
pending = null;
|
|
9798
|
+
hidePreview();
|
|
9799
|
+
};
|
|
9749
9800
|
let reported = 0;
|
|
9750
9801
|
const onSeeked = () => {
|
|
9751
9802
|
if (pending !== null) {
|
|
@@ -9754,6 +9805,11 @@ function player(container, options) {
|
|
|
9754
9805
|
v.currentTime = t;
|
|
9755
9806
|
return;
|
|
9756
9807
|
}
|
|
9808
|
+
stall.clear();
|
|
9809
|
+
const at = frameOf(v.currentTime, fps);
|
|
9810
|
+
emitFrame(at, "video");
|
|
9811
|
+
if (!scrubbing || previewing !== null && Math.abs(at - want()) <= Math.abs(previewing - want()))
|
|
9812
|
+
hidePreview();
|
|
9757
9813
|
if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
|
|
9758
9814
|
return;
|
|
9759
9815
|
reported = v.currentTime;
|
|
@@ -9769,6 +9825,7 @@ function player(container, options) {
|
|
|
9769
9825
|
v.addEventListener("loadedmetadata", onMeta);
|
|
9770
9826
|
v.addEventListener("loadeddata", onFrame);
|
|
9771
9827
|
v.addEventListener("seeked", onSeeked);
|
|
9828
|
+
v.addEventListener("error", onError);
|
|
9772
9829
|
const unlisten = tl.listen({
|
|
9773
9830
|
onSeek(t, source) {
|
|
9774
9831
|
if (source === "play") {
|
|
@@ -9776,14 +9833,24 @@ function player(container, options) {
|
|
|
9776
9833
|
v.currentTime = t;
|
|
9777
9834
|
return;
|
|
9778
9835
|
}
|
|
9779
|
-
if (
|
|
9836
|
+
if (scrubbing && frames) {
|
|
9837
|
+
frames.follow(want());
|
|
9838
|
+
showNearest();
|
|
9839
|
+
if (frames.covers(want()))
|
|
9840
|
+
return;
|
|
9841
|
+
}
|
|
9842
|
+
if (sameFrame(v.currentTime, t, fps)) {
|
|
9843
|
+
if (!scrubbing)
|
|
9844
|
+
hidePreview();
|
|
9780
9845
|
return;
|
|
9846
|
+
}
|
|
9781
9847
|
seekVideo(t);
|
|
9782
9848
|
},
|
|
9783
9849
|
onPlay(on) {
|
|
9784
|
-
if (on)
|
|
9850
|
+
if (on) {
|
|
9851
|
+
hidePreview();
|
|
9785
9852
|
v.play().catch(() => tl.pause());
|
|
9786
|
-
else {
|
|
9853
|
+
} else {
|
|
9787
9854
|
v.pause();
|
|
9788
9855
|
reported = tl.time;
|
|
9789
9856
|
}
|
|
@@ -9791,8 +9858,22 @@ function player(container, options) {
|
|
|
9791
9858
|
options.onPlay?.(on, tl.time);
|
|
9792
9859
|
},
|
|
9793
9860
|
onScrub(on) {
|
|
9794
|
-
|
|
9861
|
+
scrubbing = on;
|
|
9862
|
+
if (on) {
|
|
9795
9863
|
v.pause();
|
|
9864
|
+
startFrames();
|
|
9865
|
+
return;
|
|
9866
|
+
}
|
|
9867
|
+
if (v.readyState < 1)
|
|
9868
|
+
return hidePreview();
|
|
9869
|
+
const at = releaseSeek(v.currentTime, tl.time, fps, v.duration);
|
|
9870
|
+
if (at === null) {
|
|
9871
|
+
pending = null;
|
|
9872
|
+
if (!v.seeking)
|
|
9873
|
+
return hidePreview();
|
|
9874
|
+
} else
|
|
9875
|
+
seekVideo(at);
|
|
9876
|
+
stall.arm();
|
|
9796
9877
|
}
|
|
9797
9878
|
});
|
|
9798
9879
|
tl.drive(() => {
|
|
@@ -9807,6 +9888,7 @@ function player(container, options) {
|
|
|
9807
9888
|
tl.toggle();
|
|
9808
9889
|
};
|
|
9809
9890
|
frame.addEventListener("click", onClick);
|
|
9891
|
+
root.addEventListener("pointerenter", startFrames);
|
|
9810
9892
|
v.src = options.src;
|
|
9811
9893
|
const api = {
|
|
9812
9894
|
element: root,
|
|
@@ -9833,9 +9915,18 @@ function player(container, options) {
|
|
|
9833
9915
|
tl.dispose();
|
|
9834
9916
|
v.pause();
|
|
9835
9917
|
frame.removeEventListener("click", onClick);
|
|
9918
|
+
root.removeEventListener("pointerenter", startFrames);
|
|
9919
|
+
unlistenFrames?.();
|
|
9920
|
+
if (frames) {
|
|
9921
|
+
frames.visible(false);
|
|
9922
|
+
frames.release();
|
|
9923
|
+
frames = null;
|
|
9924
|
+
}
|
|
9836
9925
|
v.removeEventListener("loadedmetadata", onMeta);
|
|
9837
9926
|
v.removeEventListener("loadeddata", onFrame);
|
|
9838
9927
|
v.removeEventListener("seeked", onSeeked);
|
|
9928
|
+
v.removeEventListener("error", onError);
|
|
9929
|
+
stall.clear();
|
|
9839
9930
|
v.removeAttribute("src");
|
|
9840
9931
|
v.load();
|
|
9841
9932
|
root.remove();
|
|
@@ -9844,6 +9935,8 @@ function player(container, options) {
|
|
|
9844
9935
|
return api;
|
|
9845
9936
|
}
|
|
9846
9937
|
var init_player = __esm(() => {
|
|
9938
|
+
init_compare_frames();
|
|
9939
|
+
init_compare_model();
|
|
9847
9940
|
init_look();
|
|
9848
9941
|
init_timeline();
|
|
9849
9942
|
});
|
|
@@ -15687,8 +15780,66 @@ init_selection_pill();
|
|
|
15687
15780
|
init_keys();
|
|
15688
15781
|
init_layer();
|
|
15689
15782
|
init_overlay_model();
|
|
15690
|
-
|
|
15783
|
+
|
|
15784
|
+
// src/ui/tooltip-model.ts
|
|
15691
15785
|
var HANDOFF_MS = 400;
|
|
15786
|
+
var TIP_DELAY_MS = 500;
|
|
15787
|
+
function tipSchedule(clock, view) {
|
|
15788
|
+
let shown = null;
|
|
15789
|
+
let pending = null;
|
|
15790
|
+
let timer;
|
|
15791
|
+
let hiddenAt = -Infinity;
|
|
15792
|
+
const cancel = () => {
|
|
15793
|
+
if (pending)
|
|
15794
|
+
clock.clear(timer);
|
|
15795
|
+
pending = null;
|
|
15796
|
+
};
|
|
15797
|
+
const hide = (now = false) => {
|
|
15798
|
+
cancel();
|
|
15799
|
+
if (!shown)
|
|
15800
|
+
return;
|
|
15801
|
+
shown = null;
|
|
15802
|
+
hiddenAt = clock.now();
|
|
15803
|
+
view.hide(now);
|
|
15804
|
+
};
|
|
15805
|
+
return {
|
|
15806
|
+
get shown() {
|
|
15807
|
+
return shown;
|
|
15808
|
+
},
|
|
15809
|
+
get pending() {
|
|
15810
|
+
return pending;
|
|
15811
|
+
},
|
|
15812
|
+
want(tip, options = {}) {
|
|
15813
|
+
if (tip.el === shown?.el || tip.el === pending?.el)
|
|
15814
|
+
return;
|
|
15815
|
+
cancel();
|
|
15816
|
+
const handoff = shown !== null || clock.now() - hiddenAt < HANDOFF_MS;
|
|
15817
|
+
if (handoff || options.now) {
|
|
15818
|
+
shown = tip;
|
|
15819
|
+
view.show(tip, handoff);
|
|
15820
|
+
return;
|
|
15821
|
+
}
|
|
15822
|
+
pending = tip;
|
|
15823
|
+
timer = clock.set(() => {
|
|
15824
|
+
pending = null;
|
|
15825
|
+
if (!view.connected(tip))
|
|
15826
|
+
return;
|
|
15827
|
+
shown = tip;
|
|
15828
|
+
view.show(tip, false);
|
|
15829
|
+
}, options.delay ?? TIP_DELAY_MS);
|
|
15830
|
+
},
|
|
15831
|
+
hide,
|
|
15832
|
+
leave(within) {
|
|
15833
|
+
if (pending && within(pending))
|
|
15834
|
+
cancel();
|
|
15835
|
+
if (shown && within(shown))
|
|
15836
|
+
hide();
|
|
15837
|
+
}
|
|
15838
|
+
};
|
|
15839
|
+
}
|
|
15840
|
+
|
|
15841
|
+
// src/ui/tooltip.ts
|
|
15842
|
+
var tips = new WeakMap;
|
|
15692
15843
|
var controllers = new WeakMap;
|
|
15693
15844
|
function tipOf(start) {
|
|
15694
15845
|
let node = start instanceof Element ? start : null;
|
|
@@ -15722,9 +15873,6 @@ function controllerOf(doc) {
|
|
|
15722
15873
|
let text;
|
|
15723
15874
|
let keys;
|
|
15724
15875
|
let target = null;
|
|
15725
|
-
let pending = null;
|
|
15726
|
-
let timer = 0;
|
|
15727
|
-
let hiddenAt = -Infinity;
|
|
15728
15876
|
let quiet = null;
|
|
15729
15877
|
let keyboard = false;
|
|
15730
15878
|
const build = (look) => {
|
|
@@ -15769,41 +15917,32 @@ function controllerOf(doc) {
|
|
|
15769
15917
|
else
|
|
15770
15918
|
motion.show();
|
|
15771
15919
|
};
|
|
15772
|
-
const
|
|
15773
|
-
win.
|
|
15774
|
-
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
15785
|
-
|
|
15920
|
+
const schedule = tipSchedule({
|
|
15921
|
+
set: (run, ms) => win.setTimeout(run, ms),
|
|
15922
|
+
clear: (handle) => win.clearTimeout(handle),
|
|
15923
|
+
now: () => performance.now()
|
|
15924
|
+
}, {
|
|
15925
|
+
show: ({ el, tip }, snap) => show(el, tip, snap),
|
|
15926
|
+
hide: (now) => {
|
|
15927
|
+
target?.removeAttribute("aria-describedby");
|
|
15928
|
+
target = null;
|
|
15929
|
+
if (now)
|
|
15930
|
+
motion?.snap(0);
|
|
15931
|
+
else
|
|
15932
|
+
motion?.hide();
|
|
15933
|
+
},
|
|
15934
|
+
connected: ({ el }) => el.isConnected
|
|
15935
|
+
});
|
|
15936
|
+
const hide = (now = false) => schedule.hide(now);
|
|
15937
|
+
const want = (found, now = false) => {
|
|
15786
15938
|
if (!found)
|
|
15787
15939
|
return hide();
|
|
15788
|
-
if (found.el === target || found.el === pending?.el)
|
|
15789
|
-
return;
|
|
15790
15940
|
if (found.el === quiet)
|
|
15791
15941
|
return;
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
show(found.el, found.tip, true);
|
|
15797
|
-
return;
|
|
15798
|
-
}
|
|
15799
|
-
if (target)
|
|
15800
|
-
hide();
|
|
15801
|
-
pending = found;
|
|
15802
|
-
timer = win.setTimeout(() => {
|
|
15803
|
-
pending = null;
|
|
15804
|
-
if (found.el.isConnected)
|
|
15805
|
-
show(found.el, found.tip, false);
|
|
15806
|
-
}, found.tip.opts.delay ?? 500);
|
|
15942
|
+
schedule.want(found, {
|
|
15943
|
+
now,
|
|
15944
|
+
...found.tip.opts.delay !== undefined ? { delay: found.tip.opts.delay } : {}
|
|
15945
|
+
});
|
|
15807
15946
|
};
|
|
15808
15947
|
doc.addEventListener("pointerover", (e) => {
|
|
15809
15948
|
if (e.pointerType === "touch")
|
|
@@ -15837,11 +15976,10 @@ function controllerOf(doc) {
|
|
|
15837
15976
|
doc.addEventListener("focusin", (e) => {
|
|
15838
15977
|
const found = tipOf(e.target);
|
|
15839
15978
|
if (found && keyboard && e.target.matches(":focus-visible"))
|
|
15840
|
-
want(found);
|
|
15979
|
+
want(found, true);
|
|
15841
15980
|
});
|
|
15842
15981
|
doc.addEventListener("focusout", (e) => {
|
|
15843
|
-
|
|
15844
|
-
hide();
|
|
15982
|
+
schedule.leave(({ el }) => el.contains(e.target));
|
|
15845
15983
|
});
|
|
15846
15984
|
doc.addEventListener("scroll", () => target && hide(true), true);
|
|
15847
15985
|
const c = { hide };
|
|
@@ -16134,458 +16272,832 @@ function split(container, options) {
|
|
|
16134
16272
|
};
|
|
16135
16273
|
}
|
|
16136
16274
|
};
|
|
16137
|
-
measure();
|
|
16138
|
-
const rail = (id) => {
|
|
16139
|
-
const v = views.get(id);
|
|
16140
|
-
return !row && v.head ? headerSize(v) : railPx;
|
|
16141
|
-
};
|
|
16142
|
-
const fixed = () => {
|
|
16143
|
-
const out = {};
|
|
16144
|
-
for (const [id, s] of springs)
|
|
16145
|
-
out[id] = s.x;
|
|
16146
|
-
return out;
|
|
16147
|
-
};
|
|
16148
|
-
const target = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail });
|
|
16149
|
-
const current = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail, fixed: fixed() });
|
|
16150
|
-
const SIDES = { row: ["right", "left"], column: ["bottom", "top"] };
|
|
16151
|
-
const sides = (v) => {
|
|
16152
|
-
const [a, b] = SIDES[direction];
|
|
16153
|
-
return v.edge === "start" ? [a, b] : [b, a];
|
|
16154
|
-
};
|
|
16155
|
-
const renderFold = (v, size) => {
|
|
16156
|
-
const id = v.spec.id;
|
|
16157
|
-
const isCollapsed = collapsed[id];
|
|
16158
|
-
const folding = isCollapsed || v.folding;
|
|
16159
|
-
const r = rail(id);
|
|
16160
|
-
const open = folding ? Math.max(v.open, r + 1) : size;
|
|
16161
|
-
const p = folding ? foldProgress(size, open, r) : 0;
|
|
16162
|
-
v.faces = foldFaces(v.faces, p);
|
|
16163
|
-
const m = marginOf(v);
|
|
16164
|
-
const settled = !springs.has(id);
|
|
16165
|
-
const content = isCollapsed && settled ? 1 : v.faces.content;
|
|
16166
|
-
put(v.rail ? v.inner : v.body, "will-change", v.folding ? "opacity, filter" : "");
|
|
16167
|
-
if (!v.rail) {
|
|
16168
|
-
put(v.inner, "height", folding ? `${open - m * 2}px` : "");
|
|
16169
|
-
put(v.inner, "bottom", folding ? "auto" : "");
|
|
16170
|
-
fade(v.body, content, CONTENT_BLUR);
|
|
16171
|
-
if (v.toggleIcon)
|
|
16172
|
-
put(v.toggleIcon, "transform", p ? `rotate(${(-90 * p).toFixed(2)}deg)` : "");
|
|
16173
|
-
hide(v.body, isCollapsed && settled);
|
|
16275
|
+
measure();
|
|
16276
|
+
const rail = (id) => {
|
|
16277
|
+
const v = views.get(id);
|
|
16278
|
+
return !row && v.head ? headerSize(v) : railPx;
|
|
16279
|
+
};
|
|
16280
|
+
const fixed = () => {
|
|
16281
|
+
const out = {};
|
|
16282
|
+
for (const [id, s] of springs)
|
|
16283
|
+
out[id] = s.x;
|
|
16284
|
+
return out;
|
|
16285
|
+
};
|
|
16286
|
+
const target = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail });
|
|
16287
|
+
const current = () => solve2({ panes: specs, sizes, collapsed, total: total(), rail, fixed: fixed() });
|
|
16288
|
+
const SIDES = { row: ["right", "left"], column: ["bottom", "top"] };
|
|
16289
|
+
const sides = (v) => {
|
|
16290
|
+
const [a, b] = SIDES[direction];
|
|
16291
|
+
return v.edge === "start" ? [a, b] : [b, a];
|
|
16292
|
+
};
|
|
16293
|
+
const renderFold = (v, size) => {
|
|
16294
|
+
const id = v.spec.id;
|
|
16295
|
+
const isCollapsed = collapsed[id];
|
|
16296
|
+
const folding = isCollapsed || v.folding;
|
|
16297
|
+
const r = rail(id);
|
|
16298
|
+
const open = folding ? Math.max(v.open, r + 1) : size;
|
|
16299
|
+
const p = folding ? foldProgress(size, open, r) : 0;
|
|
16300
|
+
v.faces = foldFaces(v.faces, p);
|
|
16301
|
+
const m = marginOf(v);
|
|
16302
|
+
const settled = !springs.has(id);
|
|
16303
|
+
const content = isCollapsed && settled ? 1 : v.faces.content;
|
|
16304
|
+
put(v.rail ? v.inner : v.body, "will-change", v.folding ? "opacity, filter" : "");
|
|
16305
|
+
if (!v.rail) {
|
|
16306
|
+
put(v.inner, "height", folding ? `${open - m * 2}px` : "");
|
|
16307
|
+
put(v.inner, "bottom", folding ? "auto" : "");
|
|
16308
|
+
fade(v.body, content, CONTENT_BLUR);
|
|
16309
|
+
if (v.toggleIcon)
|
|
16310
|
+
put(v.toggleIcon, "transform", p ? `rotate(${(-90 * p).toFixed(2)}deg)` : "");
|
|
16311
|
+
hide(v.body, isCollapsed && settled);
|
|
16312
|
+
return;
|
|
16313
|
+
}
|
|
16314
|
+
const [side, far] = sides(v);
|
|
16315
|
+
put(v.inner, row ? "width" : "height", folding ? `${open - m * 2}px` : "");
|
|
16316
|
+
put(v.inner, side, folding ? "0" : "");
|
|
16317
|
+
put(v.inner, far, folding ? "auto" : "");
|
|
16318
|
+
fade(v.inner, content, CONTENT_BLUR);
|
|
16319
|
+
hide(v.clip, isCollapsed && settled);
|
|
16320
|
+
const morph = row && Boolean(v.toggleIcon) && !v.el.hasAttribute("data-nested");
|
|
16321
|
+
v.el.toggleAttribute("data-morph", morph);
|
|
16322
|
+
const label = v.railLabel;
|
|
16323
|
+
const travel = morph && style.title === "travel" && Boolean(v.titleEl) && label.textContent === v.titleEl.textContent;
|
|
16324
|
+
v.el.toggleAttribute("data-travel", travel && folding);
|
|
16325
|
+
if (v.countEl)
|
|
16326
|
+
put(v.countEl, "opacity", travel && folding && p > 0 ? Math.max(0, 1 - p / COUNT_CLEAR).toFixed(3) : "");
|
|
16327
|
+
const k = 1 - p;
|
|
16328
|
+
if (travel && !folding) {
|
|
16329
|
+
put(label, "opacity", "0");
|
|
16330
|
+
put(label, "filter", "");
|
|
16331
|
+
put(label, "transform", "");
|
|
16332
|
+
put(label, "color", "");
|
|
16333
|
+
} else if (travel) {
|
|
16334
|
+
const t = titleTravel(v.places, open, r, v.edge, p);
|
|
16335
|
+
put(label, "opacity", "");
|
|
16336
|
+
put(label, "filter", "");
|
|
16337
|
+
put(label, "transform", k === 0 ? "" : `translate(${t.x.toFixed(2)}px,${t.y.toFixed(2)}px) rotate(${t.turn.toFixed(2)}deg)`);
|
|
16338
|
+
put(label, "color", k === 0 ? "" : `color-mix(in srgb,var(--kv-fg) ${(k * 100).toFixed(1)}%,var(--kv-fg-2))`);
|
|
16339
|
+
} else {
|
|
16340
|
+
put(label, "transform", "");
|
|
16341
|
+
put(label, "color", "");
|
|
16342
|
+
}
|
|
16343
|
+
if (morph) {
|
|
16344
|
+
put(v.rail, "opacity", "1");
|
|
16345
|
+
if (!travel)
|
|
16346
|
+
fade(label, v.faces.rail, LABEL_BLUR);
|
|
16347
|
+
const c = v.chev;
|
|
16348
|
+
const dx = c.chevEnd - c.iconEnd + (v.edge === "end" ? open - r : 0);
|
|
16349
|
+
const move = `translate(${(dx * k).toFixed(2)}px,${(c.dy * k).toFixed(2)}px)`;
|
|
16350
|
+
const size = `scale(${(1 + (c.scale - 1) * k).toFixed(4)})`;
|
|
16351
|
+
if (style.chevron === "turn")
|
|
16352
|
+
put(v.turn, "transform", k === 0 ? "" : `${move} rotate(${chevronTurn(p, v.edge).toFixed(2)}deg) ${size}`);
|
|
16353
|
+
else {
|
|
16354
|
+
const turn = chevronFlip(v.flip.x, v.edge);
|
|
16355
|
+
const over = v.flip.resting ? turn === 0 ? "" : "scaleX(-1)" : `perspective(${FLIP_DEPTH}px) rotateY(${turn.toFixed(2)}deg)`;
|
|
16356
|
+
put(v.turn, "transform", k === 0 && !over ? "" : `${move} ${size} ${over}`);
|
|
16357
|
+
}
|
|
16358
|
+
} else {
|
|
16359
|
+
const a = v.faces.rail;
|
|
16360
|
+
put(v.rail, "opacity", a >= 1 ? "1" : a.toFixed(3));
|
|
16361
|
+
put(label, "opacity", "");
|
|
16362
|
+
put(label, "filter", a >= 1 || a <= 0 ? "" : `blur(${(LABEL_BLUR * (1 - a)).toFixed(2)}px)`);
|
|
16363
|
+
put(v.turn, "transform", "");
|
|
16364
|
+
}
|
|
16365
|
+
};
|
|
16366
|
+
let lastReport = "";
|
|
16367
|
+
const render = (shown) => {
|
|
16368
|
+
for (const spec of specs) {
|
|
16369
|
+
const v = views.get(spec.id);
|
|
16370
|
+
const size = Math.max(0, shown[spec.id] ?? 0);
|
|
16371
|
+
v.shown = size;
|
|
16372
|
+
put(v.el, "flex-basis", `${size}px`);
|
|
16373
|
+
put(v.el, row ? "width" : "height", `${size}px`);
|
|
16374
|
+
v.el.toggleAttribute("data-collapsed", collapsed[spec.id]);
|
|
16375
|
+
v.el.toggleAttribute("data-settled", !springs.has(spec.id));
|
|
16376
|
+
if (spec.collapsible)
|
|
16377
|
+
renderFold(v, size);
|
|
16378
|
+
}
|
|
16379
|
+
dividers.forEach((d, i) => {
|
|
16380
|
+
const a = specs[i];
|
|
16381
|
+
const b = specs[i + 1];
|
|
16382
|
+
const now = String(Math.round(views.get(a.id).shown));
|
|
16383
|
+
if (d.getAttribute("aria-valuenow") !== now)
|
|
16384
|
+
d.setAttribute("aria-valuenow", now);
|
|
16385
|
+
if (!d.hasAttribute("aria-controls"))
|
|
16386
|
+
d.setAttribute("aria-controls", `${a.id} ${b.id}`);
|
|
16387
|
+
});
|
|
16388
|
+
const layout = snapshot();
|
|
16389
|
+
const report = JSON.stringify(layout);
|
|
16390
|
+
if (report !== lastReport) {
|
|
16391
|
+
lastReport = report;
|
|
16392
|
+
options.onLayout?.(layout);
|
|
16393
|
+
for (const v of views.values())
|
|
16394
|
+
v.el.dispatchEvent(new Event("knobs-pane-resize"));
|
|
16395
|
+
notifyLayout(doc);
|
|
16396
|
+
}
|
|
16397
|
+
};
|
|
16398
|
+
const snapshot = () => {
|
|
16399
|
+
const s = {};
|
|
16400
|
+
for (const [id, v] of views)
|
|
16401
|
+
s[id] = Math.round(v.shown * 10) / 10;
|
|
16402
|
+
return { sizes: s, collapsed: { ...collapsed } };
|
|
16403
|
+
};
|
|
16404
|
+
const persist = () => {
|
|
16405
|
+
if (!store || !key)
|
|
16406
|
+
return;
|
|
16407
|
+
try {
|
|
16408
|
+
store.setItem(key, serializeLayout({ sizes, collapsed }));
|
|
16409
|
+
} catch {}
|
|
16410
|
+
};
|
|
16411
|
+
const rememberOpen = (shown) => {
|
|
16412
|
+
for (const spec of specs)
|
|
16413
|
+
if (!collapsed[spec.id] && !springs.has(spec.id))
|
|
16414
|
+
views.get(spec.id).open = shown[spec.id];
|
|
16415
|
+
};
|
|
16416
|
+
const loop = frameLoop(win, (dt) => {
|
|
16417
|
+
for (const [id, s] of springs) {
|
|
16418
|
+
s.step(dt);
|
|
16419
|
+
if (s.resting) {
|
|
16420
|
+
springs.delete(id);
|
|
16421
|
+
views.get(id).folding = false;
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
let flipping = false;
|
|
16425
|
+
for (const v of views.values()) {
|
|
16426
|
+
v.flip.step(dt);
|
|
16427
|
+
if (!v.flip.resting)
|
|
16428
|
+
flipping = true;
|
|
16429
|
+
}
|
|
16430
|
+
const shown = current();
|
|
16431
|
+
if (springs.size === 0)
|
|
16432
|
+
rememberOpen(shown);
|
|
16433
|
+
render(shown);
|
|
16434
|
+
return springs.size > 0 || flipping;
|
|
16435
|
+
});
|
|
16436
|
+
const animate = (motion = SETTLE) => {
|
|
16437
|
+
const to = target();
|
|
16438
|
+
for (const v of views.values())
|
|
16439
|
+
if (v.folding && !collapsed[v.spec.id])
|
|
16440
|
+
v.open = to[v.spec.id];
|
|
16441
|
+
if (reducedMotion(win)) {
|
|
16442
|
+
springs.clear();
|
|
16443
|
+
const arriving = [...views.values()].filter((v) => v.folding).map((v) => collapsed[v.spec.id] ? v.rail : v.rail ? v.inner : v.body);
|
|
16444
|
+
for (const v of views.values()) {
|
|
16445
|
+
v.folding = false;
|
|
16446
|
+
v.flip.snap(collapsed[v.spec.id] ? 1 : 0);
|
|
16447
|
+
}
|
|
16448
|
+
rememberOpen(to);
|
|
16449
|
+
render(to);
|
|
16450
|
+
for (const el of arriving)
|
|
16451
|
+
el?.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
16452
|
+
duration: 160,
|
|
16453
|
+
easing: "cubic-bezier(.2,.8,.2,1)"
|
|
16454
|
+
});
|
|
16455
|
+
return;
|
|
16456
|
+
}
|
|
16457
|
+
for (const spec of specs) {
|
|
16458
|
+
const v = views.get(spec.id);
|
|
16459
|
+
const goal = to[spec.id];
|
|
16460
|
+
const s = springs.get(spec.id);
|
|
16461
|
+
if (s) {
|
|
16462
|
+
s.omega = motion.omega;
|
|
16463
|
+
s.zeta = motion.zeta;
|
|
16464
|
+
s.set(goal);
|
|
16465
|
+
} else if (Math.abs(goal - v.shown) > 0.5) {
|
|
16466
|
+
springs.set(spec.id, new Spring(v.shown, motion.omega, motion.zeta, 0.2).set(goal));
|
|
16467
|
+
} else
|
|
16468
|
+
v.folding = false;
|
|
16469
|
+
}
|
|
16470
|
+
loop.wake();
|
|
16471
|
+
};
|
|
16472
|
+
const fold = (id, value) => {
|
|
16473
|
+
const v = views.get(id);
|
|
16474
|
+
if (value && !v.folding)
|
|
16475
|
+
v.open = v.shown;
|
|
16476
|
+
collapsed[id] = value;
|
|
16477
|
+
v.folding = true;
|
|
16478
|
+
v.flip.set(value ? 1 : 0);
|
|
16479
|
+
};
|
|
16480
|
+
const release = (id) => {
|
|
16481
|
+
springs.delete(id);
|
|
16482
|
+
views.get(id).folding = false;
|
|
16483
|
+
};
|
|
16484
|
+
function setCollapsed(id, value, save = true) {
|
|
16485
|
+
const spec = specs.find((p) => p.id === id);
|
|
16486
|
+
if (!spec?.collapsible || collapsed[id] === value)
|
|
16487
|
+
return;
|
|
16488
|
+
fold(id, value);
|
|
16489
|
+
if (save)
|
|
16490
|
+
persist();
|
|
16491
|
+
animate(COLLAPSE);
|
|
16492
|
+
}
|
|
16493
|
+
const setSizes = (next) => {
|
|
16494
|
+
for (const [id, s] of Object.entries(next))
|
|
16495
|
+
sizes[id] = s;
|
|
16496
|
+
};
|
|
16497
|
+
function bindDivider(d, index) {
|
|
16498
|
+
const a = specs[index];
|
|
16499
|
+
const b = specs[index + 1];
|
|
16500
|
+
let start;
|
|
16501
|
+
let from = 0;
|
|
16502
|
+
let pointer = -1;
|
|
16503
|
+
d.addEventListener("pointerdown", (e) => {
|
|
16504
|
+
if (e.button !== 0)
|
|
16505
|
+
return;
|
|
16506
|
+
e.preventDefault();
|
|
16507
|
+
d.focus({ preventScroll: true });
|
|
16508
|
+
const shown = current();
|
|
16509
|
+
for (const id of [a.id, b.id])
|
|
16510
|
+
if (!collapsed[id])
|
|
16511
|
+
release(id);
|
|
16512
|
+
start = { ...shown };
|
|
16513
|
+
for (const id of [a.id, b.id])
|
|
16514
|
+
if (collapsed[id])
|
|
16515
|
+
start[id] = rail(id);
|
|
16516
|
+
from = row ? e.clientX : e.clientY;
|
|
16517
|
+
pointer = e.pointerId;
|
|
16518
|
+
d.setPointerCapture(e.pointerId);
|
|
16519
|
+
d.dataset.active = "";
|
|
16520
|
+
doc.documentElement.dataset.knobsResizing = direction;
|
|
16521
|
+
});
|
|
16522
|
+
d.addEventListener("pointermove", (e) => {
|
|
16523
|
+
if (!start || e.pointerId !== pointer)
|
|
16524
|
+
return;
|
|
16525
|
+
const delta = (row ? e.clientX : e.clientY) - from;
|
|
16526
|
+
const result = drag(specs, start, index, delta, collapsed);
|
|
16527
|
+
if (result.collapse) {
|
|
16528
|
+
const { id, collapsed: now } = result.collapse;
|
|
16529
|
+
if (!now) {
|
|
16530
|
+
for (const [k, s] of Object.entries(result.sizes))
|
|
16531
|
+
if (k === id)
|
|
16532
|
+
sizes[k] = s;
|
|
16533
|
+
}
|
|
16534
|
+
setCollapsed(id, now, false);
|
|
16535
|
+
start = { ...target() };
|
|
16536
|
+
if (now)
|
|
16537
|
+
start[id] = rail(id);
|
|
16538
|
+
from = row ? e.clientX : e.clientY;
|
|
16539
|
+
return;
|
|
16540
|
+
}
|
|
16541
|
+
if (collapsed[a.id] || collapsed[b.id])
|
|
16542
|
+
return;
|
|
16543
|
+
setSizes(result.sizes);
|
|
16544
|
+
for (const id of [a.id, b.id])
|
|
16545
|
+
release(id);
|
|
16546
|
+
const shown = current();
|
|
16547
|
+
render(shown);
|
|
16548
|
+
rememberOpen(shown);
|
|
16549
|
+
});
|
|
16550
|
+
const end = (e) => {
|
|
16551
|
+
if (!start || e.pointerId !== pointer)
|
|
16552
|
+
return;
|
|
16553
|
+
start = undefined;
|
|
16554
|
+
delete d.dataset.active;
|
|
16555
|
+
delete doc.documentElement.dataset.knobsResizing;
|
|
16556
|
+
for (const spec of specs)
|
|
16557
|
+
if (flex.has(spec.id) && !collapsed[spec.id])
|
|
16558
|
+
sizes[spec.id] = views.get(spec.id).shown;
|
|
16559
|
+
persist();
|
|
16560
|
+
};
|
|
16561
|
+
d.addEventListener("pointerup", end);
|
|
16562
|
+
d.addEventListener("pointercancel", end);
|
|
16563
|
+
d.addEventListener("lostpointercapture", end);
|
|
16564
|
+
d.addEventListener("dblclick", (e) => {
|
|
16565
|
+
e.preventDefault();
|
|
16566
|
+
reset(a.id, b.id);
|
|
16567
|
+
});
|
|
16568
|
+
d.addEventListener("keydown", (e) => {
|
|
16569
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
16570
|
+
return;
|
|
16571
|
+
const back = row ? "ArrowLeft" : "ArrowUp";
|
|
16572
|
+
const fwd = row ? "ArrowRight" : "ArrowDown";
|
|
16573
|
+
const step = e.shiftKey ? 64 : 16;
|
|
16574
|
+
let handled = true;
|
|
16575
|
+
if (e.key === back || e.key === fwd) {
|
|
16576
|
+
const goal = target();
|
|
16577
|
+
const shut = [a.id, b.id].find((id) => collapsed[id]);
|
|
16578
|
+
if (shut)
|
|
16579
|
+
setCollapsed(shut, false);
|
|
16580
|
+
else {
|
|
16581
|
+
setSizes(nudge(specs, goal, index, e.key === fwd ? step : -step));
|
|
16582
|
+
persist();
|
|
16583
|
+
animate();
|
|
16584
|
+
}
|
|
16585
|
+
} else if (e.key === "Home" || e.key === "End") {
|
|
16586
|
+
const goal = target();
|
|
16587
|
+
setSizes(nudge(specs, goal, index, e.key === "End" ? 1e6 : -1e6));
|
|
16588
|
+
persist();
|
|
16589
|
+
animate();
|
|
16590
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
16591
|
+
const id = collapsibleSide(specs, index);
|
|
16592
|
+
if (id)
|
|
16593
|
+
setCollapsed(id, !collapsed[id]);
|
|
16594
|
+
else
|
|
16595
|
+
handled = false;
|
|
16596
|
+
} else if (e.key === "Escape")
|
|
16597
|
+
d.blur();
|
|
16598
|
+
else
|
|
16599
|
+
handled = false;
|
|
16600
|
+
if (handled) {
|
|
16601
|
+
e.preventDefault();
|
|
16602
|
+
e.stopPropagation();
|
|
16603
|
+
}
|
|
16604
|
+
});
|
|
16605
|
+
}
|
|
16606
|
+
function reset(...ids) {
|
|
16607
|
+
const which = ids.length ? ids : specs.map((p) => p.id);
|
|
16608
|
+
let folds = false;
|
|
16609
|
+
for (const id of which) {
|
|
16610
|
+
delete sizes[id];
|
|
16611
|
+
const spec = specs.find((p) => p.id === id);
|
|
16612
|
+
const shut = Boolean(spec?.collapsed);
|
|
16613
|
+
if (spec?.collapsible && collapsed[id] !== shut) {
|
|
16614
|
+
fold(id, shut);
|
|
16615
|
+
folds = true;
|
|
16616
|
+
}
|
|
16617
|
+
}
|
|
16618
|
+
if (which.some((id) => flex.has(id)))
|
|
16619
|
+
for (const id of flex)
|
|
16620
|
+
delete sizes[id];
|
|
16621
|
+
persist();
|
|
16622
|
+
animate(folds ? COLLAPSE : SETTLE);
|
|
16623
|
+
}
|
|
16624
|
+
{
|
|
16625
|
+
const first = target();
|
|
16626
|
+
for (const spec of specs) {
|
|
16627
|
+
const open = { ...collapsed, [spec.id]: false };
|
|
16628
|
+
views.get(spec.id).open = collapsed[spec.id] ? solve2({ panes: specs, sizes, collapsed: open, total: total(), rail })[spec.id] : first[spec.id];
|
|
16629
|
+
}
|
|
16630
|
+
render(first);
|
|
16631
|
+
measureChevrons();
|
|
16632
|
+
render(first);
|
|
16633
|
+
}
|
|
16634
|
+
const relayout = (remeasure) => {
|
|
16635
|
+
if (remeasure)
|
|
16636
|
+
measure();
|
|
16637
|
+
if (springs.size) {
|
|
16638
|
+
if (remeasure)
|
|
16639
|
+
measureChevrons();
|
|
16640
|
+
return loop.wake();
|
|
16641
|
+
}
|
|
16642
|
+
const shown = target();
|
|
16643
|
+
rememberOpen(shown);
|
|
16644
|
+
render(shown);
|
|
16645
|
+
if (remeasure) {
|
|
16646
|
+
measureChevrons();
|
|
16647
|
+
render(shown);
|
|
16648
|
+
}
|
|
16649
|
+
};
|
|
16650
|
+
const ro = new ResizeObserver(() => relayout(!measured));
|
|
16651
|
+
ro.observe(root);
|
|
16652
|
+
const restyled = () => relayout(true);
|
|
16653
|
+
root.addEventListener("knobs-restyle", restyled);
|
|
16654
|
+
const paneApi = (id) => {
|
|
16655
|
+
const v = views.get(id);
|
|
16656
|
+
if (!v)
|
|
16657
|
+
throw new Error(`No pane "${id}"`);
|
|
16658
|
+
return {
|
|
16659
|
+
id,
|
|
16660
|
+
element: v.el,
|
|
16661
|
+
body: v.body,
|
|
16662
|
+
header: v.head,
|
|
16663
|
+
get size() {
|
|
16664
|
+
return v.shown;
|
|
16665
|
+
},
|
|
16666
|
+
get collapsed() {
|
|
16667
|
+
return collapsed[id];
|
|
16668
|
+
},
|
|
16669
|
+
collapse: (value = !collapsed[id]) => setCollapsed(id, value),
|
|
16670
|
+
resize: (size) => api.resize(id, size),
|
|
16671
|
+
title(text, count) {
|
|
16672
|
+
if (v.titleEl)
|
|
16673
|
+
v.titleEl.textContent = text;
|
|
16674
|
+
if (v.countEl && count !== undefined)
|
|
16675
|
+
v.countEl.textContent = String(count);
|
|
16676
|
+
if (v.railLabel)
|
|
16677
|
+
v.railLabel.textContent = text;
|
|
16678
|
+
measureChevrons(v);
|
|
16679
|
+
}
|
|
16680
|
+
};
|
|
16681
|
+
};
|
|
16682
|
+
const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [...views.values()].flatMap((v) => [
|
|
16683
|
+
...v.head?.querySelectorAll(":scope > .knobs-pane-btn") ?? [],
|
|
16684
|
+
...v.rail && collapsed[v.spec.id] ? [v.rail] : []
|
|
16685
|
+
]), { listen: false, rank: 1 });
|
|
16686
|
+
const api = {
|
|
16687
|
+
element: root,
|
|
16688
|
+
pane: paneApi,
|
|
16689
|
+
layout: snapshot,
|
|
16690
|
+
collapse: (id, value) => setCollapsed(id, value ?? !collapsed[id]),
|
|
16691
|
+
resize(id, size) {
|
|
16692
|
+
const i = specs.findIndex((p) => p.id === id);
|
|
16693
|
+
if (i < 0)
|
|
16694
|
+
return;
|
|
16695
|
+
if (collapsed[id])
|
|
16696
|
+
setCollapsed(id, false);
|
|
16697
|
+
if (!flex.has(id))
|
|
16698
|
+
sizes[id] = size;
|
|
16699
|
+
else {
|
|
16700
|
+
const goal = target();
|
|
16701
|
+
const j = i > 0 ? i - 1 : i;
|
|
16702
|
+
const delta = size - goal[id];
|
|
16703
|
+
setSizes(nudge(specs, goal, j, j === i ? delta : -delta));
|
|
16704
|
+
}
|
|
16705
|
+
persist();
|
|
16706
|
+
animate();
|
|
16707
|
+
},
|
|
16708
|
+
reset: (id) => id ? reset(id) : reset(),
|
|
16709
|
+
dispose() {
|
|
16710
|
+
unregisterHints?.();
|
|
16711
|
+
loop.stop();
|
|
16712
|
+
ro.disconnect();
|
|
16713
|
+
root.removeEventListener("knobs-restyle", restyled);
|
|
16714
|
+
host?.removeAttribute("data-nested");
|
|
16715
|
+
root.remove();
|
|
16716
|
+
}
|
|
16717
|
+
};
|
|
16718
|
+
return api;
|
|
16719
|
+
}
|
|
16720
|
+
|
|
16721
|
+
// src/ui/index.ts
|
|
16722
|
+
init_split_model();
|
|
16723
|
+
init_timeline();
|
|
16724
|
+
init_timeline_model();
|
|
16725
|
+
init_compare();
|
|
16726
|
+
|
|
16727
|
+
// src/ui/compare-strip.ts
|
|
16728
|
+
init_compare_frames();
|
|
16729
|
+
init_compare_model();
|
|
16730
|
+
init_look();
|
|
16731
|
+
init_rgba();
|
|
16732
|
+
init_timeline_model();
|
|
16733
|
+
var BAR = 7;
|
|
16734
|
+
var GAP = 2;
|
|
16735
|
+
var DIFF = { w: 32, h: 18 };
|
|
16736
|
+
var DIFFS_PER_DRAW = 6;
|
|
16737
|
+
function compareStrip(container, options) {
|
|
16738
|
+
const doc = container.ownerDocument;
|
|
16739
|
+
const win = doc.defaultView;
|
|
16740
|
+
const timeline = options.timeline;
|
|
16741
|
+
const fps = () => options.fps ?? timeline.fps;
|
|
16742
|
+
const align = options.align ?? "track";
|
|
16743
|
+
const threshold = options.threshold ?? 4 / 255;
|
|
16744
|
+
const root = doc.createElement("div");
|
|
16745
|
+
root.className = "knobs-cmp-strip";
|
|
16746
|
+
root.setAttribute("role", "img");
|
|
16747
|
+
root.setAttribute("aria-label", "Frame strip: Before over After, with how much they differ");
|
|
16748
|
+
applyLook2(root, options);
|
|
16749
|
+
const canvas = doc.createElement("canvas");
|
|
16750
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
16751
|
+
root.append(canvas);
|
|
16752
|
+
container.append(root);
|
|
16753
|
+
const diffs = new Map;
|
|
16754
|
+
const pairDiffs = new Map;
|
|
16755
|
+
let disposed = false;
|
|
16756
|
+
let near = false;
|
|
16757
|
+
let onScreen = false;
|
|
16758
|
+
const makeSide = (source) => ({
|
|
16759
|
+
source,
|
|
16760
|
+
kind: mediaKind(source.src, source.kind),
|
|
16761
|
+
frames: null,
|
|
16762
|
+
still: null,
|
|
16763
|
+
seen: false
|
|
16764
|
+
});
|
|
16765
|
+
let sides = [makeSide(options.before), makeSide(options.after)];
|
|
16766
|
+
const offs = new Map;
|
|
16767
|
+
const load = (s) => {
|
|
16768
|
+
if (s.frames || s.still)
|
|
16174
16769
|
return;
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
|
|
16181
|
-
hide(v.clip, isCollapsed && settled);
|
|
16182
|
-
const morph = row && Boolean(v.toggleIcon) && !v.el.hasAttribute("data-nested");
|
|
16183
|
-
v.el.toggleAttribute("data-morph", morph);
|
|
16184
|
-
const label = v.railLabel;
|
|
16185
|
-
const travel = morph && style.title === "travel" && Boolean(v.titleEl) && label.textContent === v.titleEl.textContent;
|
|
16186
|
-
v.el.toggleAttribute("data-travel", travel && folding);
|
|
16187
|
-
if (v.countEl)
|
|
16188
|
-
put(v.countEl, "opacity", travel && folding && p > 0 ? Math.max(0, 1 - p / COUNT_CLEAR).toFixed(3) : "");
|
|
16189
|
-
const k = 1 - p;
|
|
16190
|
-
if (travel && !folding) {
|
|
16191
|
-
put(label, "opacity", "0");
|
|
16192
|
-
put(label, "filter", "");
|
|
16193
|
-
put(label, "transform", "");
|
|
16194
|
-
put(label, "color", "");
|
|
16195
|
-
} else if (travel) {
|
|
16196
|
-
const t = titleTravel(v.places, open, r, v.edge, p);
|
|
16197
|
-
put(label, "opacity", "");
|
|
16198
|
-
put(label, "filter", "");
|
|
16199
|
-
put(label, "transform", k === 0 ? "" : `translate(${t.x.toFixed(2)}px,${t.y.toFixed(2)}px) rotate(${t.turn.toFixed(2)}deg)`);
|
|
16200
|
-
put(label, "color", k === 0 ? "" : `color-mix(in srgb,var(--kv-fg) ${(k * 100).toFixed(1)}%,var(--kv-fg-2))`);
|
|
16201
|
-
} else {
|
|
16202
|
-
put(label, "transform", "");
|
|
16203
|
-
put(label, "color", "");
|
|
16204
|
-
}
|
|
16205
|
-
if (morph) {
|
|
16206
|
-
put(v.rail, "opacity", "1");
|
|
16207
|
-
if (!travel)
|
|
16208
|
-
fade(label, v.faces.rail, LABEL_BLUR);
|
|
16209
|
-
const c = v.chev;
|
|
16210
|
-
const dx = c.chevEnd - c.iconEnd + (v.edge === "end" ? open - r : 0);
|
|
16211
|
-
const move = `translate(${(dx * k).toFixed(2)}px,${(c.dy * k).toFixed(2)}px)`;
|
|
16212
|
-
const size = `scale(${(1 + (c.scale - 1) * k).toFixed(4)})`;
|
|
16213
|
-
if (style.chevron === "turn")
|
|
16214
|
-
put(v.turn, "transform", k === 0 ? "" : `${move} rotate(${chevronTurn(p, v.edge).toFixed(2)}deg) ${size}`);
|
|
16215
|
-
else {
|
|
16216
|
-
const turn = chevronFlip(v.flip.x, v.edge);
|
|
16217
|
-
const over = v.flip.resting ? turn === 0 ? "" : "scaleX(-1)" : `perspective(${FLIP_DEPTH}px) rotateY(${turn.toFixed(2)}deg)`;
|
|
16218
|
-
put(v.turn, "transform", k === 0 && !over ? "" : `${move} ${size} ${over}`);
|
|
16770
|
+
if (s.kind === "video") {
|
|
16771
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
16772
|
+
offs.set(s, s.frames.listen(schedule));
|
|
16773
|
+
if (onScreen) {
|
|
16774
|
+
s.frames.visible(true);
|
|
16775
|
+
s.seen = true;
|
|
16219
16776
|
}
|
|
16220
16777
|
} else {
|
|
16221
|
-
const
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16778
|
+
const img = doc.createElement("img");
|
|
16779
|
+
img.crossOrigin = "anonymous";
|
|
16780
|
+
img.decoding = "async";
|
|
16781
|
+
img.addEventListener("load", () => {
|
|
16782
|
+
pairDiffs.clear();
|
|
16783
|
+
schedule();
|
|
16784
|
+
});
|
|
16785
|
+
img.src = s.source.src;
|
|
16786
|
+
s.still = img;
|
|
16226
16787
|
}
|
|
16227
16788
|
};
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
put(v.el, row ? "width" : "height", `${size}px`);
|
|
16236
|
-
v.el.toggleAttribute("data-collapsed", collapsed[spec.id]);
|
|
16237
|
-
v.el.toggleAttribute("data-settled", !springs.has(spec.id));
|
|
16238
|
-
if (spec.collapsible)
|
|
16239
|
-
renderFold(v, size);
|
|
16789
|
+
const unload = (s) => {
|
|
16790
|
+
offs.get(s)?.();
|
|
16791
|
+
offs.delete(s);
|
|
16792
|
+
if (s.frames) {
|
|
16793
|
+
if (s.seen)
|
|
16794
|
+
s.frames.visible(false);
|
|
16795
|
+
s.frames.release();
|
|
16240
16796
|
}
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
if (
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16797
|
+
s.frames = null;
|
|
16798
|
+
s.seen = false;
|
|
16799
|
+
if (s.still)
|
|
16800
|
+
s.still.removeAttribute("src");
|
|
16801
|
+
s.still = null;
|
|
16802
|
+
};
|
|
16803
|
+
let w = 1;
|
|
16804
|
+
let h = 1;
|
|
16805
|
+
let geo = { x: 0, w: 1 };
|
|
16806
|
+
const rowH = () => Math.max(4, (h - BAR - GAP * 2) / 2);
|
|
16807
|
+
const aspectOf = (s) => {
|
|
16808
|
+
if (s.still?.naturalWidth)
|
|
16809
|
+
return s.still.naturalWidth / s.still.naturalHeight;
|
|
16810
|
+
const c = s.frames?.nearest(0);
|
|
16811
|
+
return c ? c.bitmap.width / c.bitmap.height : 0;
|
|
16812
|
+
};
|
|
16813
|
+
const columns = () => stripColumns(timeline.view, geo.w, {
|
|
16814
|
+
duration: timeline.duration,
|
|
16815
|
+
fps: fps(),
|
|
16816
|
+
minWidth: rowH() * Math.max(16 / 9, ...sides.map(aspectOf)) + GAP
|
|
16817
|
+
});
|
|
16818
|
+
const measure = () => {
|
|
16819
|
+
const r = root.getBoundingClientRect();
|
|
16820
|
+
w = Math.max(1, r.width);
|
|
16821
|
+
h = Math.max(1, r.height);
|
|
16822
|
+
geo = { x: 0, w };
|
|
16823
|
+
if (align === "track") {
|
|
16824
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
16825
|
+
const t = track?.getBoundingClientRect();
|
|
16826
|
+
if (t && t.width > 0)
|
|
16827
|
+
geo = { x: t.left - r.left, w: t.width };
|
|
16258
16828
|
}
|
|
16829
|
+
readColors();
|
|
16830
|
+
schedule();
|
|
16259
16831
|
};
|
|
16260
|
-
const
|
|
16261
|
-
const
|
|
16262
|
-
for (const
|
|
16263
|
-
s
|
|
16264
|
-
return { sizes: s, collapsed: { ...collapsed } };
|
|
16832
|
+
const ask = (cols) => {
|
|
16833
|
+
const points = cols.map((c) => ({ frame: c.frame, tolerance: Math.floor((c.to - c.from) / 2) }));
|
|
16834
|
+
for (const s of sides)
|
|
16835
|
+
s.frames?.want(points);
|
|
16265
16836
|
};
|
|
16266
|
-
const
|
|
16267
|
-
if (
|
|
16837
|
+
const picOf = (s, c) => {
|
|
16838
|
+
if (s.still?.naturalWidth)
|
|
16839
|
+
return { img: s.still, w: s.still.naturalWidth, h: s.still.naturalHeight, key: "still" };
|
|
16840
|
+
const f = s.frames?.nearest(c.frame, Math.max(1, c.to - c.from));
|
|
16841
|
+
return f ? { img: f.bitmap, w: f.bitmap.width, h: f.bitmap.height, key: String(f.frame) } : null;
|
|
16842
|
+
};
|
|
16843
|
+
const probe = doc.createElement("canvas");
|
|
16844
|
+
probe.width = DIFF.w;
|
|
16845
|
+
probe.height = DIFF.h;
|
|
16846
|
+
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
16847
|
+
const differ = (a, b) => {
|
|
16848
|
+
if (!probeCtx)
|
|
16268
16849
|
return;
|
|
16269
16850
|
try {
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
views.get(spec.id).open = shown[spec.id];
|
|
16277
|
-
};
|
|
16278
|
-
const loop = frameLoop(win, (dt) => {
|
|
16279
|
-
for (const [id, s] of springs) {
|
|
16280
|
-
s.step(dt);
|
|
16281
|
-
if (s.resting) {
|
|
16282
|
-
springs.delete(id);
|
|
16283
|
-
views.get(id).folding = false;
|
|
16284
|
-
}
|
|
16285
|
-
}
|
|
16286
|
-
let flipping = false;
|
|
16287
|
-
for (const v of views.values()) {
|
|
16288
|
-
v.flip.step(dt);
|
|
16289
|
-
if (!v.flip.resting)
|
|
16290
|
-
flipping = true;
|
|
16291
|
-
}
|
|
16292
|
-
const shown = current();
|
|
16293
|
-
if (springs.size === 0)
|
|
16294
|
-
rememberOpen(shown);
|
|
16295
|
-
render(shown);
|
|
16296
|
-
return springs.size > 0 || flipping;
|
|
16297
|
-
});
|
|
16298
|
-
const animate = (motion = SETTLE) => {
|
|
16299
|
-
const to = target();
|
|
16300
|
-
for (const v of views.values())
|
|
16301
|
-
if (v.folding && !collapsed[v.spec.id])
|
|
16302
|
-
v.open = to[v.spec.id];
|
|
16303
|
-
if (reducedMotion(win)) {
|
|
16304
|
-
springs.clear();
|
|
16305
|
-
const arriving = [...views.values()].filter((v) => v.folding).map((v) => collapsed[v.spec.id] ? v.rail : v.rail ? v.inner : v.body);
|
|
16306
|
-
for (const v of views.values()) {
|
|
16307
|
-
v.folding = false;
|
|
16308
|
-
v.flip.snap(collapsed[v.spec.id] ? 1 : 0);
|
|
16309
|
-
}
|
|
16310
|
-
rememberOpen(to);
|
|
16311
|
-
render(to);
|
|
16312
|
-
for (const el of arriving)
|
|
16313
|
-
el?.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
16314
|
-
duration: 160,
|
|
16315
|
-
easing: "cubic-bezier(.2,.8,.2,1)"
|
|
16316
|
-
});
|
|
16851
|
+
probeCtx.drawImage(a.img, 0, 0, DIFF.w, DIFF.h);
|
|
16852
|
+
const pa = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
16853
|
+
probeCtx.drawImage(b.img, 0, 0, DIFF.w, DIFF.h);
|
|
16854
|
+
const pb = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
16855
|
+
return meanAbsDiff(pa, pb);
|
|
16856
|
+
} catch {
|
|
16317
16857
|
return;
|
|
16318
16858
|
}
|
|
16319
|
-
for (const spec of specs) {
|
|
16320
|
-
const v = views.get(spec.id);
|
|
16321
|
-
const goal = to[spec.id];
|
|
16322
|
-
const s = springs.get(spec.id);
|
|
16323
|
-
if (s) {
|
|
16324
|
-
s.omega = motion.omega;
|
|
16325
|
-
s.zeta = motion.zeta;
|
|
16326
|
-
s.set(goal);
|
|
16327
|
-
} else if (Math.abs(goal - v.shown) > 0.5) {
|
|
16328
|
-
springs.set(spec.id, new Spring(v.shown, motion.omega, motion.zeta, 0.2).set(goal));
|
|
16329
|
-
} else
|
|
16330
|
-
v.folding = false;
|
|
16331
|
-
}
|
|
16332
|
-
loop.wake();
|
|
16333
16859
|
};
|
|
16334
|
-
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
collapsed[id] = value;
|
|
16339
|
-
v.folding = true;
|
|
16340
|
-
v.flip.set(value ? 1 : 0);
|
|
16860
|
+
let colors = { surface: "", accent: "", diff: "", wash: "" };
|
|
16861
|
+
const rgba = (raw, fallback) => {
|
|
16862
|
+
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
16863
|
+
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
16341
16864
|
};
|
|
16342
|
-
const
|
|
16343
|
-
|
|
16344
|
-
|
|
16865
|
+
const readColors = () => {
|
|
16866
|
+
const cs = win.getComputedStyle(root);
|
|
16867
|
+
colors = {
|
|
16868
|
+
surface: rgba(tokenCss(cs, "--kv-fill", ""), [1, 1, 1, 0.055]),
|
|
16869
|
+
accent: rgba(tokenCss(cs, "--kv-accent", ""), [1, 1, 1, 1]),
|
|
16870
|
+
diff: rgba(tokenCss(cs, "--_ku-cmp-diff", ""), [0.49, 0.81, 0.61, 1]),
|
|
16871
|
+
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
16872
|
+
};
|
|
16345
16873
|
};
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
persist();
|
|
16353
|
-
animate(COLLAPSE);
|
|
16874
|
+
let hover = null;
|
|
16875
|
+
let raf = 0;
|
|
16876
|
+
let asked = "";
|
|
16877
|
+
function schedule() {
|
|
16878
|
+
if (!raf && !disposed)
|
|
16879
|
+
raf = win.requestAnimationFrame(draw);
|
|
16354
16880
|
}
|
|
16355
|
-
const
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
const
|
|
16361
|
-
const
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
if (!start || e.pointerId !== pointer)
|
|
16881
|
+
const draw = () => {
|
|
16882
|
+
raf = 0;
|
|
16883
|
+
if (!near)
|
|
16884
|
+
return;
|
|
16885
|
+
const dpr = Math.min(3, Math.max(1, win.devicePixelRatio || 1));
|
|
16886
|
+
const cw = Math.round(w * dpr);
|
|
16887
|
+
const ch = Math.round(h * dpr);
|
|
16888
|
+
if (canvas.width !== cw || canvas.height !== ch) {
|
|
16889
|
+
canvas.width = cw;
|
|
16890
|
+
canvas.height = ch;
|
|
16891
|
+
}
|
|
16892
|
+
const ctx = canvas.getContext("2d");
|
|
16893
|
+
if (!ctx)
|
|
16894
|
+
return;
|
|
16895
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
16896
|
+
ctx.clearRect(0, 0, w, h);
|
|
16897
|
+
const cols = columns();
|
|
16898
|
+
const key = cols.map((c) => c.frame).join(",");
|
|
16899
|
+
if (key !== asked) {
|
|
16900
|
+
asked = key;
|
|
16901
|
+
ask(cols);
|
|
16902
|
+
}
|
|
16903
|
+
const rh = rowH();
|
|
16904
|
+
const radius = 3;
|
|
16905
|
+
let budget = DIFFS_PER_DRAW;
|
|
16906
|
+
let more = false;
|
|
16907
|
+
const ds = cols.map((c) => {
|
|
16908
|
+
const a = picOf(sides[0], c);
|
|
16909
|
+
const b = picOf(sides[1], c);
|
|
16910
|
+
if (!a || !b)
|
|
16386
16911
|
return;
|
|
16387
|
-
const
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
|
|
16392
|
-
for (const [k, s] of Object.entries(result.sizes))
|
|
16393
|
-
if (k === id)
|
|
16394
|
-
sizes[k] = s;
|
|
16912
|
+
const k = `${a.key}|${b.key}`;
|
|
16913
|
+
if (!pairDiffs.has(k)) {
|
|
16914
|
+
if (budget-- <= 0) {
|
|
16915
|
+
more = true;
|
|
16916
|
+
return;
|
|
16395
16917
|
}
|
|
16396
|
-
|
|
16397
|
-
start = { ...target() };
|
|
16398
|
-
if (now)
|
|
16399
|
-
start[id] = rail(id);
|
|
16400
|
-
from = row ? e.clientX : e.clientY;
|
|
16401
|
-
return;
|
|
16918
|
+
pairDiffs.set(k, differ(a, b));
|
|
16402
16919
|
}
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
|
|
16407
|
-
release(id);
|
|
16408
|
-
const shown = current();
|
|
16409
|
-
render(shown);
|
|
16410
|
-
rememberOpen(shown);
|
|
16411
|
-
});
|
|
16412
|
-
const end = (e) => {
|
|
16413
|
-
if (!start || e.pointerId !== pointer)
|
|
16414
|
-
return;
|
|
16415
|
-
start = undefined;
|
|
16416
|
-
delete d.dataset.active;
|
|
16417
|
-
delete doc.documentElement.dataset.knobsResizing;
|
|
16418
|
-
for (const spec of specs)
|
|
16419
|
-
if (flex.has(spec.id) && !collapsed[spec.id])
|
|
16420
|
-
sizes[spec.id] = views.get(spec.id).shown;
|
|
16421
|
-
persist();
|
|
16422
|
-
};
|
|
16423
|
-
d.addEventListener("pointerup", end);
|
|
16424
|
-
d.addEventListener("pointercancel", end);
|
|
16425
|
-
d.addEventListener("lostpointercapture", end);
|
|
16426
|
-
d.addEventListener("dblclick", (e) => {
|
|
16427
|
-
e.preventDefault();
|
|
16428
|
-
reset(a.id, b.id);
|
|
16920
|
+
const d = pairDiffs.get(k);
|
|
16921
|
+
if (d !== undefined)
|
|
16922
|
+
diffs.set(c.frame, d);
|
|
16923
|
+
return d;
|
|
16429
16924
|
});
|
|
16430
|
-
|
|
16431
|
-
|
|
16925
|
+
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
16926
|
+
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
16927
|
+
cols.forEach((c, i) => {
|
|
16928
|
+
const x = geo.x + c.x + GAP / 2;
|
|
16929
|
+
const cwid = Math.max(1, c.w - GAP);
|
|
16930
|
+
if (x + cwid < 0 || x > w)
|
|
16432
16931
|
return;
|
|
16433
|
-
const
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
} else if (e.key === "Home" || e.key === "End") {
|
|
16448
|
-
const goal = target();
|
|
16449
|
-
setSizes(nudge(specs, goal, index, e.key === "End" ? 1e6 : -1e6));
|
|
16450
|
-
persist();
|
|
16451
|
-
animate();
|
|
16452
|
-
} else if (e.key === "Enter" || e.key === " ") {
|
|
16453
|
-
const id = collapsibleSide(specs, index);
|
|
16454
|
-
if (id)
|
|
16455
|
-
setCollapsed(id, !collapsed[id]);
|
|
16456
|
-
else
|
|
16457
|
-
handled = false;
|
|
16458
|
-
} else if (e.key === "Escape")
|
|
16459
|
-
d.blur();
|
|
16460
|
-
else
|
|
16461
|
-
handled = false;
|
|
16462
|
-
if (handled) {
|
|
16463
|
-
e.preventDefault();
|
|
16464
|
-
e.stopPropagation();
|
|
16932
|
+
for (const row of [0, 1]) {
|
|
16933
|
+
const y = row * (rh + GAP);
|
|
16934
|
+
const pic = picOf(sides[row], c);
|
|
16935
|
+
ctx.save();
|
|
16936
|
+
ctx.beginPath();
|
|
16937
|
+
ctx.roundRect(x, y, cwid, rh, radius);
|
|
16938
|
+
ctx.fillStyle = colors.surface;
|
|
16939
|
+
ctx.fill();
|
|
16940
|
+
if (pic) {
|
|
16941
|
+
ctx.clip();
|
|
16942
|
+
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
16943
|
+
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
16944
|
+
}
|
|
16945
|
+
ctx.restore();
|
|
16465
16946
|
}
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
for (const id of which) {
|
|
16472
|
-
delete sizes[id];
|
|
16473
|
-
const spec = specs.find((p) => p.id === id);
|
|
16474
|
-
const shut = Boolean(spec?.collapsed);
|
|
16475
|
-
if (spec?.collapsible && collapsed[id] !== shut) {
|
|
16476
|
-
fold(id, shut);
|
|
16477
|
-
folds = true;
|
|
16947
|
+
if (hover === i) {
|
|
16948
|
+
ctx.fillStyle = colors.wash;
|
|
16949
|
+
ctx.beginPath();
|
|
16950
|
+
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
16951
|
+
ctx.fill();
|
|
16478
16952
|
}
|
|
16953
|
+
const d = ds[i];
|
|
16954
|
+
if (d !== undefined && d > 0) {
|
|
16955
|
+
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
16956
|
+
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
16957
|
+
ctx.fillStyle = colors.diff;
|
|
16958
|
+
ctx.beginPath();
|
|
16959
|
+
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
16960
|
+
ctx.fill();
|
|
16961
|
+
ctx.globalAlpha = 1;
|
|
16962
|
+
}
|
|
16963
|
+
});
|
|
16964
|
+
const v = timeline.view;
|
|
16965
|
+
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
16966
|
+
if (px >= -1 && px <= w + 1) {
|
|
16967
|
+
ctx.fillStyle = colors.accent;
|
|
16968
|
+
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
16479
16969
|
}
|
|
16480
|
-
if (
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
return
|
|
16503
|
-
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16970
|
+
if (more)
|
|
16971
|
+
schedule();
|
|
16972
|
+
};
|
|
16973
|
+
const timeAt = (clientX) => {
|
|
16974
|
+
const r = root.getBoundingClientRect();
|
|
16975
|
+
const v = timeline.view;
|
|
16976
|
+
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
16977
|
+
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
16978
|
+
};
|
|
16979
|
+
const columnAt = (clientX) => {
|
|
16980
|
+
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
16981
|
+
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
16982
|
+
return i < 0 ? null : i;
|
|
16983
|
+
};
|
|
16984
|
+
let dragging = false;
|
|
16985
|
+
const pick = (clientX) => {
|
|
16986
|
+
const t = timeAt(clientX);
|
|
16987
|
+
timeline.seek(t);
|
|
16988
|
+
options.onPick?.(t);
|
|
16989
|
+
};
|
|
16990
|
+
root.addEventListener("pointerdown", (e) => {
|
|
16991
|
+
if (e.button !== 0)
|
|
16992
|
+
return;
|
|
16993
|
+
dragging = true;
|
|
16994
|
+
root.setPointerCapture(e.pointerId);
|
|
16995
|
+
const host = root.closest(".knobs-compare");
|
|
16996
|
+
if (host)
|
|
16997
|
+
host.focus({ preventScroll: true });
|
|
16998
|
+
else
|
|
16999
|
+
timeline.focus();
|
|
17000
|
+
if (timeline.playing)
|
|
17001
|
+
timeline.pause();
|
|
17002
|
+
pick(e.clientX);
|
|
17003
|
+
e.preventDefault();
|
|
17004
|
+
});
|
|
17005
|
+
root.addEventListener("pointermove", (e) => {
|
|
17006
|
+
if (dragging)
|
|
17007
|
+
pick(e.clientX);
|
|
17008
|
+
const i = columnAt(e.clientX);
|
|
17009
|
+
if (i !== hover) {
|
|
17010
|
+
hover = i;
|
|
17011
|
+
schedule();
|
|
16510
17012
|
}
|
|
17013
|
+
});
|
|
17014
|
+
const end = () => {
|
|
17015
|
+
dragging = false;
|
|
16511
17016
|
};
|
|
16512
|
-
|
|
17017
|
+
root.addEventListener("pointerup", end);
|
|
17018
|
+
root.addEventListener("pointercancel", end);
|
|
17019
|
+
root.addEventListener("lostpointercapture", end);
|
|
17020
|
+
root.addEventListener("pointerleave", () => {
|
|
17021
|
+
if (hover === null)
|
|
17022
|
+
return;
|
|
17023
|
+
hover = null;
|
|
17024
|
+
schedule();
|
|
17025
|
+
});
|
|
17026
|
+
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
17027
|
+
const ro = new win.ResizeObserver(measure);
|
|
16513
17028
|
ro.observe(root);
|
|
16514
|
-
const
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
return v.shown;
|
|
16527
|
-
},
|
|
16528
|
-
get collapsed() {
|
|
16529
|
-
return collapsed[id];
|
|
16530
|
-
},
|
|
16531
|
-
collapse: (value = !collapsed[id]) => setCollapsed(id, value),
|
|
16532
|
-
resize: (size) => api.resize(id, size),
|
|
16533
|
-
title(text, count) {
|
|
16534
|
-
if (v.titleEl)
|
|
16535
|
-
v.titleEl.textContent = text;
|
|
16536
|
-
if (v.countEl && count !== undefined)
|
|
16537
|
-
v.countEl.textContent = String(count);
|
|
16538
|
-
if (v.railLabel)
|
|
16539
|
-
v.railLabel.textContent = text;
|
|
16540
|
-
measureChevrons(v);
|
|
16541
|
-
}
|
|
16542
|
-
};
|
|
17029
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
17030
|
+
if (track)
|
|
17031
|
+
ro.observe(track);
|
|
17032
|
+
root.addEventListener("knobs-restyle", measure);
|
|
17033
|
+
const setOnScreen = (on) => {
|
|
17034
|
+
onScreen = on;
|
|
17035
|
+
for (const s of sides) {
|
|
17036
|
+
if (!s.frames || s.seen === on)
|
|
17037
|
+
continue;
|
|
17038
|
+
s.seen = on;
|
|
17039
|
+
s.frames.visible(on);
|
|
17040
|
+
}
|
|
16543
17041
|
};
|
|
16544
|
-
const
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
17042
|
+
const setNear = (on) => {
|
|
17043
|
+
near = on;
|
|
17044
|
+
if (on) {
|
|
17045
|
+
for (const s of sides)
|
|
17046
|
+
load(s);
|
|
17047
|
+
schedule();
|
|
17048
|
+
} else
|
|
17049
|
+
for (const s of sides)
|
|
17050
|
+
unload(s);
|
|
17051
|
+
};
|
|
17052
|
+
const IO = win.IntersectionObserver;
|
|
17053
|
+
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
17054
|
+
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
17055
|
+
measure();
|
|
17056
|
+
if (nearIo && screenIo) {
|
|
17057
|
+
nearIo.observe(root);
|
|
17058
|
+
screenIo.observe(root);
|
|
17059
|
+
} else {
|
|
17060
|
+
setNear(true);
|
|
17061
|
+
setOnScreen(true);
|
|
17062
|
+
}
|
|
17063
|
+
return {
|
|
16549
17064
|
element: root,
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
|
|
16555
|
-
|
|
16556
|
-
|
|
16557
|
-
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
const
|
|
16565
|
-
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
17065
|
+
diffs,
|
|
17066
|
+
update(next) {
|
|
17067
|
+
const was = sides;
|
|
17068
|
+
sides = [
|
|
17069
|
+
next.before ? makeSide(next.before) : was[0],
|
|
17070
|
+
next.after ? makeSide(next.after) : was[1]
|
|
17071
|
+
];
|
|
17072
|
+
for (const s of was)
|
|
17073
|
+
if (!sides.includes(s))
|
|
17074
|
+
unload(s);
|
|
17075
|
+
diffs.clear();
|
|
17076
|
+
pairDiffs.clear();
|
|
17077
|
+
asked = "";
|
|
17078
|
+
if (near)
|
|
17079
|
+
for (const s of sides)
|
|
17080
|
+
load(s);
|
|
17081
|
+
if (onScreen)
|
|
17082
|
+
setOnScreen(true);
|
|
17083
|
+
schedule();
|
|
16569
17084
|
},
|
|
16570
|
-
reset: (id) => id ? reset(id) : reset(),
|
|
16571
17085
|
dispose() {
|
|
16572
|
-
|
|
16573
|
-
|
|
17086
|
+
disposed = true;
|
|
17087
|
+
if (raf)
|
|
17088
|
+
win.cancelAnimationFrame(raf);
|
|
17089
|
+
unlisten();
|
|
16574
17090
|
ro.disconnect();
|
|
16575
|
-
|
|
16576
|
-
|
|
17091
|
+
nearIo?.disconnect();
|
|
17092
|
+
screenIo?.disconnect();
|
|
17093
|
+
for (const s of sides)
|
|
17094
|
+
unload(s);
|
|
16577
17095
|
root.remove();
|
|
16578
17096
|
}
|
|
16579
17097
|
};
|
|
16580
|
-
return api;
|
|
16581
17098
|
}
|
|
16582
17099
|
|
|
16583
17100
|
// src/ui/index.ts
|
|
16584
|
-
init_split_model();
|
|
16585
|
-
init_timeline();
|
|
16586
|
-
init_timeline_model();
|
|
16587
|
-
init_compare();
|
|
16588
|
-
init_compare_strip();
|
|
16589
17101
|
init_player();
|
|
16590
17102
|
|
|
16591
17103
|
// src/ui/history.ts
|
|
@@ -24131,9 +24643,8 @@ var walkthroughCss = `${foundationCss}
|
|
|
24131
24643
|
.knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
|
|
24132
24644
|
/* A compare is as tall as its two pictures side by side at this width, in every mode (Wipe fuses
|
|
24133
24645
|
them at one side's size, so nothing grows or re-measures), plus its mode bar, the line and gaps
|
|
24134
|
-
(92 px)
|
|
24646
|
+
(92 px). */
|
|
24135
24647
|
.knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
|
|
24136
|
-
.knobs-wt-fit[data-strip]{--kw-chrome:158px}
|
|
24137
24648
|
.knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
|
|
24138
24649
|
.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)}
|
|
24139
24650
|
.knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
|
|
@@ -24146,16 +24657,15 @@ var walkthroughCss = `${foundationCss}
|
|
|
24146
24657
|
/* The diff inside it: its header row 28 high on the container's padding, a hairline under it, and
|
|
24147
24658
|
no line of its own under the code (the container's ring closes it). */
|
|
24148
24659
|
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-head:28px;--kd-x:0px}
|
|
24149
|
-
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"])::after{display:none}
|
|
24150
|
-
.knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--
|
|
24660
|
+
.knobs-wt-slot[data-kind="diff"] :is(.knobs-ui.knobs-diff:not([data-on="pane"]),.knobs-diff-review)::after{display:none}
|
|
24661
|
+
.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);
|
|
24151
24662
|
font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
|
|
24152
|
-
.knobs-surface
|
|
24663
|
+
.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)}
|
|
24153
24664
|
.knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
24154
24665
|
.knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
|
|
24155
24666
|
.knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
|
|
24156
24667
|
.knobs-wt-out{color:var(--_k-fg-2)}
|
|
24157
24668
|
.knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
|
|
24158
|
-
.knobs-wt-mlabel{color:var(--_k-fg-2)}
|
|
24159
24669
|
.knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-feature-settings:var(--_kr-data)}
|
|
24160
24670
|
.knobs-wt-mfrom{color:var(--_k-fg-3)}
|
|
24161
24671
|
.knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
|
|
@@ -24174,6 +24684,9 @@ var walkthroughCss = `${foundationCss}
|
|
|
24174
24684
|
.knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
|
|
24175
24685
|
.knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
|
|
24176
24686
|
.knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
|
|
24687
|
+
/* A diff holds its room with its own well's edge, no fill: the diff reads the colour behind it
|
|
24688
|
+
as it mounts, and a loading fill there would stay under its code. */
|
|
24689
|
+
.knobs-wt-slot[data-kind="diff"]>.knobs-wt-hold{background:transparent}
|
|
24177
24690
|
.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)}
|
|
24178
24691
|
|
|
24179
24692
|
/* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
|
|
@@ -24423,8 +24936,6 @@ var side = (s, fallback) => ({
|
|
|
24423
24936
|
async function mountCompare(host, m, look, alive, onEvent) {
|
|
24424
24937
|
const doc = host.ownerDocument;
|
|
24425
24938
|
const fit = div(doc, "knobs-wt-fit");
|
|
24426
|
-
if (m.strip)
|
|
24427
|
-
fit.dataset.strip = "";
|
|
24428
24939
|
const { before, after } = m;
|
|
24429
24940
|
const known = parseAspect(m.aspect);
|
|
24430
24941
|
fit.style.setProperty("--kw-aspect", String(known ?? DEFAULT_ASPECT));
|
|
@@ -24451,7 +24962,6 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
24451
24962
|
timeline: mediaKind(before.src) === "video" || mediaKind(after.src) === "video",
|
|
24452
24963
|
lazy: false,
|
|
24453
24964
|
...aspect ? { aspect } : {},
|
|
24454
|
-
...m.strip ? { strip: true } : {},
|
|
24455
24965
|
onMode: (next) => {
|
|
24456
24966
|
onEvent?.({ what: "mode", detail: next });
|
|
24457
24967
|
const layer = marks?.element;
|
|
@@ -24513,11 +25023,11 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
24513
25023
|
pills?.sync();
|
|
24514
25024
|
marking?.kick();
|
|
24515
25025
|
if (source !== "play")
|
|
24516
|
-
onEvent?.({ what: "seek", time:
|
|
25026
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
24517
25027
|
},
|
|
24518
25028
|
onPlay: (on) => {
|
|
24519
25029
|
marking?.kick();
|
|
24520
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
25030
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(c.time) });
|
|
24521
25031
|
}
|
|
24522
25032
|
});
|
|
24523
25033
|
requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
|
|
@@ -24535,13 +25045,13 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
24535
25045
|
const zoom = zoomedIn(c.zoom) ? c.zoom : null;
|
|
24536
25046
|
return {
|
|
24537
25047
|
kind: "compare",
|
|
24538
|
-
time:
|
|
24539
|
-
duration:
|
|
25048
|
+
time: round3(c.time),
|
|
25049
|
+
duration: round3(c.duration),
|
|
24540
25050
|
playing: c.playing,
|
|
24541
25051
|
mode: c.mode,
|
|
24542
25052
|
...pills?.current ? { chapter: pills.current.label } : {},
|
|
24543
|
-
...loop ? { loop: { start:
|
|
24544
|
-
...zoom ? { zoom: { u:
|
|
25053
|
+
...loop ? { loop: { start: round3(loop.start), end: round3(loop.end) } } : {},
|
|
25054
|
+
...zoom ? { zoom: { u: round3(zoom.u), v: round3(zoom.v), w: round3(zoom.w), h: round3(zoom.h) } } : {}
|
|
24545
25055
|
};
|
|
24546
25056
|
},
|
|
24547
25057
|
dispose: () => {
|
|
@@ -24554,7 +25064,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
24554
25064
|
}
|
|
24555
25065
|
};
|
|
24556
25066
|
}
|
|
24557
|
-
var
|
|
25067
|
+
var round3 = (v) => Math.round(v * 100) / 100;
|
|
24558
25068
|
var MARK_GLIDE = 450;
|
|
24559
25069
|
var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
|
|
24560
25070
|
async function mountVideo(host, m, look, alive, onEvent) {
|
|
@@ -24581,12 +25091,12 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
24581
25091
|
} : {},
|
|
24582
25092
|
onPlay: (on, time) => {
|
|
24583
25093
|
marking?.kick();
|
|
24584
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
25094
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(time) });
|
|
24585
25095
|
},
|
|
24586
25096
|
onSeek: (time) => {
|
|
24587
25097
|
pills?.sync();
|
|
24588
25098
|
marking?.kick();
|
|
24589
|
-
onEvent?.({ what: "seek", time:
|
|
25099
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
24590
25100
|
}
|
|
24591
25101
|
});
|
|
24592
25102
|
const v = p.video;
|
|
@@ -24619,8 +25129,8 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
24619
25129
|
seekBy: (d) => p.seekBy(d),
|
|
24620
25130
|
context: () => ({
|
|
24621
25131
|
kind: "video",
|
|
24622
|
-
time:
|
|
24623
|
-
...p.duration ? { duration:
|
|
25132
|
+
time: round3(p.time),
|
|
25133
|
+
...p.duration ? { duration: round3(p.duration) } : {},
|
|
24624
25134
|
playing: p.playing,
|
|
24625
25135
|
...pills?.current ? { chapter: pills.current.label } : {}
|
|
24626
25136
|
}),
|
|
@@ -24764,8 +25274,6 @@ var fmt = (v, unit = "") => `${v}${unit}`;
|
|
|
24764
25274
|
function mountMetric(host, m) {
|
|
24765
25275
|
const doc = host.ownerDocument;
|
|
24766
25276
|
const box = div(doc, "knobs-wt-metric");
|
|
24767
|
-
const label = div(doc, "knobs-wt-mlabel");
|
|
24768
|
-
label.textContent = m.label;
|
|
24769
25277
|
const value = div(doc, "knobs-wt-mvalue");
|
|
24770
25278
|
const span = (cls, text) => {
|
|
24771
25279
|
const s = doc.createElement("span");
|
|
@@ -24785,7 +25293,7 @@ function mountMetric(host, m) {
|
|
|
24785
25293
|
delta.dataset.tone = d > 0 === (better === "up") ? "good" : "bad";
|
|
24786
25294
|
value.append(delta);
|
|
24787
25295
|
}
|
|
24788
|
-
box.append(
|
|
25296
|
+
box.append(value);
|
|
24789
25297
|
const total = num(m.of);
|
|
24790
25298
|
if (Number.isFinite(total) && total > 0 && Number.isFinite(a) && Number.isFinite(b)) {
|
|
24791
25299
|
const track = div(doc, "knobs-wt-mtrack");
|
|
@@ -24839,7 +25347,7 @@ function mountDecision(host, m, slot) {
|
|
|
24839
25347
|
}
|
|
24840
25348
|
var HEAVY = new Set(["compare", "video", "diff", "mount"]);
|
|
24841
25349
|
var RELEASES = new Set(["compare", "video"]);
|
|
24842
|
-
var labelOf3 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "
|
|
25350
|
+
var labelOf3 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "history" ? m.label : undefined;
|
|
24843
25351
|
function mediaSlot(container, given) {
|
|
24844
25352
|
const doc = container.ownerDocument;
|
|
24845
25353
|
injectWalkthroughStyles(doc);
|
|
@@ -24853,7 +25361,7 @@ function mediaSlot(container, given) {
|
|
|
24853
25361
|
root.dataset.kind = m.kind;
|
|
24854
25362
|
applyLook2(root, options);
|
|
24855
25363
|
const label = labelOf3(m);
|
|
24856
|
-
if (label
|
|
25364
|
+
if (label) {
|
|
24857
25365
|
const l = doc.createElement("span");
|
|
24858
25366
|
l.className = "knobs-wt-slot-label";
|
|
24859
25367
|
l.textContent = label;
|