@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/review-react.js
CHANGED
|
@@ -648,7 +648,6 @@ var compareCss = `
|
|
|
648
648
|
:where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}
|
|
649
649
|
.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
|
|
650
650
|
.knobs-cmp-note:empty{display:none}
|
|
651
|
-
.knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
|
|
652
651
|
.knobs-cmp-timeline-host{flex:none;min-height:0}
|
|
653
652
|
/* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
|
|
654
653
|
.knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
|
|
@@ -668,6 +667,9 @@ var playerCss = `
|
|
|
668
667
|
.knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
|
|
669
668
|
opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
|
|
670
669
|
.knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
|
|
670
|
+
/* Mid-scrub, the cached frame under the hand, over the video until it lands. */
|
|
671
|
+
.knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
|
|
672
|
+
.knobs-player-preview[hidden]{display:none}
|
|
671
673
|
.knobs-player-line{flex:none;min-height:0}
|
|
672
674
|
/* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
|
|
673
675
|
about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
|
|
@@ -1449,6 +1451,14 @@ var init_schema = __esm(() => {
|
|
|
1449
1451
|
doc: "Panel and card radius, concentric with controls.",
|
|
1450
1452
|
deprecated: ["--k-panel-radius", "--ku-panel-radius"]
|
|
1451
1453
|
},
|
|
1454
|
+
{
|
|
1455
|
+
name: "radius-frame",
|
|
1456
|
+
tier: "semantic",
|
|
1457
|
+
type: "length",
|
|
1458
|
+
group: "Shape",
|
|
1459
|
+
source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
|
|
1460
|
+
doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
|
|
1461
|
+
},
|
|
1452
1462
|
{
|
|
1453
1463
|
name: "round",
|
|
1454
1464
|
tier: "semantic",
|
|
@@ -2567,7 +2577,16 @@ var init_glide = __esm(() => {
|
|
|
2567
2577
|
"border-subtle",
|
|
2568
2578
|
"bg-page"
|
|
2569
2579
|
];
|
|
2570
|
-
LENGTH_TOKENS = [
|
|
2580
|
+
LENGTH_TOKENS = [
|
|
2581
|
+
"pad",
|
|
2582
|
+
"inset",
|
|
2583
|
+
"row",
|
|
2584
|
+
"gap",
|
|
2585
|
+
"radius",
|
|
2586
|
+
"radius-panel",
|
|
2587
|
+
"radius-frame",
|
|
2588
|
+
"font-size"
|
|
2589
|
+
];
|
|
2571
2590
|
NUMBER_TOKENS = ["weight-label", "blur", "glow", "glass", "glass-tone"];
|
|
2572
2591
|
ANIMATED = [...COLOR_TOKENS, ...LENGTH_TOKENS, ...NUMBER_TOKENS, "width"];
|
|
2573
2592
|
TOKEN_TRANSITIONS = ANIMATED.map((t) => `--kt-${t} var(--k-token-t)`).join(",");
|
|
@@ -4605,12 +4624,24 @@ function createRegistry(doc) {
|
|
|
4605
4624
|
return;
|
|
4606
4625
|
if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
|
|
4607
4626
|
return;
|
|
4608
|
-
if (isTyping(e.target) || !wide())
|
|
4627
|
+
if (isTyping(e.target) || !wide(e.target))
|
|
4609
4628
|
return;
|
|
4610
4629
|
if (registry.start())
|
|
4611
4630
|
e.preventDefault();
|
|
4612
4631
|
};
|
|
4613
|
-
const
|
|
4632
|
+
const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
|
|
4633
|
+
const wide = (target) => {
|
|
4634
|
+
if (!listening)
|
|
4635
|
+
return false;
|
|
4636
|
+
if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
|
|
4637
|
+
return true;
|
|
4638
|
+
if (sources.length < 2)
|
|
4639
|
+
return false;
|
|
4640
|
+
if (target === undefined)
|
|
4641
|
+
return true;
|
|
4642
|
+
const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
|
|
4643
|
+
return Boolean(piece?.classList.contains("knobs-ui"));
|
|
4644
|
+
};
|
|
4614
4645
|
const relisten = () => {
|
|
4615
4646
|
const next = sources.some((s) => s.listen);
|
|
4616
4647
|
if (next === listening)
|
|
@@ -9575,6 +9606,31 @@ function seeked(s, current, fps) {
|
|
|
9575
9606
|
return { state: { seeking: true, pending: null }, apply: s.pending, settled: false };
|
|
9576
9607
|
return { state: SEEK_IDLE, apply: null, settled: true };
|
|
9577
9608
|
}
|
|
9609
|
+
function releaseSeek(current, t, fps, duration) {
|
|
9610
|
+
const at = seekTime(t, fps, duration);
|
|
9611
|
+
return sameFrame(current, at, fps) ? null : at;
|
|
9612
|
+
}
|
|
9613
|
+
function stallGuard(timers, onStall, ms = SEEK_STALL_MS) {
|
|
9614
|
+
let id = 0;
|
|
9615
|
+
const clear = () => {
|
|
9616
|
+
if (id)
|
|
9617
|
+
timers.clearTimeout(id);
|
|
9618
|
+
id = 0;
|
|
9619
|
+
};
|
|
9620
|
+
return {
|
|
9621
|
+
arm() {
|
|
9622
|
+
clear();
|
|
9623
|
+
id = timers.setTimeout(() => {
|
|
9624
|
+
id = 0;
|
|
9625
|
+
onStall();
|
|
9626
|
+
}, ms);
|
|
9627
|
+
},
|
|
9628
|
+
clear,
|
|
9629
|
+
get armed() {
|
|
9630
|
+
return id !== 0;
|
|
9631
|
+
}
|
|
9632
|
+
};
|
|
9633
|
+
}
|
|
9578
9634
|
function letterbox(media, box) {
|
|
9579
9635
|
if (media.w <= 0 || media.h <= 0 || box.w <= 0 || box.h <= 0)
|
|
9580
9636
|
return box;
|
|
@@ -9695,7 +9751,7 @@ function stripPeaks(diffs, o = {}) {
|
|
|
9695
9751
|
});
|
|
9696
9752
|
return out;
|
|
9697
9753
|
}
|
|
9698
|
-
var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, fuseCorners = (r, k) => {
|
|
9754
|
+
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) => {
|
|
9699
9755
|
const inner = r * Math.min(1, Math.max(0, k));
|
|
9700
9756
|
return { before: { left: r, right: inner }, after: { left: inner, right: r } };
|
|
9701
9757
|
}, labelFold = (fold, mark, blur = 3) => {
|
|
@@ -9942,6 +9998,25 @@ var init_compare_mp4 = __esm(() => {
|
|
|
9942
9998
|
});
|
|
9943
9999
|
|
|
9944
10000
|
// src/ui/compare-frames.ts
|
|
10001
|
+
function scrubCaching(option, win) {
|
|
10002
|
+
if (option === false)
|
|
10003
|
+
return false;
|
|
10004
|
+
if (option === true)
|
|
10005
|
+
return true;
|
|
10006
|
+
const net = win?.navigator?.connection;
|
|
10007
|
+
if (net?.saveData)
|
|
10008
|
+
return false;
|
|
10009
|
+
return !win?.matchMedia?.("(prefers-reduced-data: reduce)").matches;
|
|
10010
|
+
}
|
|
10011
|
+
function proxyStride(bytes, count, total, budget) {
|
|
10012
|
+
if (count <= 0 || total <= 0)
|
|
10013
|
+
return 1;
|
|
10014
|
+
const each = bytes / count;
|
|
10015
|
+
let stride = 1;
|
|
10016
|
+
while (total / stride * each > budget && stride < total)
|
|
10017
|
+
stride *= 2;
|
|
10018
|
+
return stride;
|
|
10019
|
+
}
|
|
9945
10020
|
function gridPoints(total, count = GRID) {
|
|
9946
10021
|
if (total <= 0)
|
|
9947
10022
|
return [];
|
|
@@ -9972,6 +10047,24 @@ function nearestKey(keys, frame, within = Infinity) {
|
|
|
9972
10047
|
}
|
|
9973
10048
|
return best !== null && bestD <= within ? best : null;
|
|
9974
10049
|
}
|
|
10050
|
+
function claim(core) {
|
|
10051
|
+
if (!round2) {
|
|
10052
|
+
const r = new Set;
|
|
10053
|
+
round2 = r;
|
|
10054
|
+
queueMicrotask(() => {
|
|
10055
|
+
for (const c of actives)
|
|
10056
|
+
if (!r.has(c)) {
|
|
10057
|
+
c.active = false;
|
|
10058
|
+
c.dropFull();
|
|
10059
|
+
}
|
|
10060
|
+
actives = r;
|
|
10061
|
+
round2 = null;
|
|
10062
|
+
});
|
|
10063
|
+
}
|
|
10064
|
+
round2.add(core);
|
|
10065
|
+
actives.add(core);
|
|
10066
|
+
core.active = true;
|
|
10067
|
+
}
|
|
9975
10068
|
function pump(win) {
|
|
9976
10069
|
if (running)
|
|
9977
10070
|
return;
|
|
@@ -9979,12 +10072,14 @@ function pump(win) {
|
|
|
9979
10072
|
(async () => {
|
|
9980
10073
|
try {
|
|
9981
10074
|
for (;; ) {
|
|
9982
|
-
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active));
|
|
10075
|
+
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active) || a.ran - b.ran);
|
|
9983
10076
|
let unit = null;
|
|
9984
10077
|
for (const c of order) {
|
|
9985
10078
|
unit = c.work();
|
|
9986
|
-
if (unit)
|
|
10079
|
+
if (unit) {
|
|
10080
|
+
c.ran = ++turns;
|
|
9987
10081
|
break;
|
|
10082
|
+
}
|
|
9988
10083
|
}
|
|
9989
10084
|
if (!unit)
|
|
9990
10085
|
break;
|
|
@@ -10014,12 +10109,99 @@ async function toBitmap(doc, source, w, h, cap) {
|
|
|
10014
10109
|
return createImageBitmap(c);
|
|
10015
10110
|
}
|
|
10016
10111
|
}
|
|
10112
|
+
function holdEncoders() {
|
|
10113
|
+
encoderUsers++;
|
|
10114
|
+
}
|
|
10115
|
+
function dropEncoders() {
|
|
10116
|
+
encoderUsers = Math.max(0, encoderUsers - 1);
|
|
10117
|
+
if (encoderUsers > 0)
|
|
10118
|
+
return;
|
|
10119
|
+
endEncoders("released");
|
|
10120
|
+
encoders = undefined;
|
|
10121
|
+
}
|
|
10122
|
+
function encodersOf(host) {
|
|
10123
|
+
if (encoders !== undefined)
|
|
10124
|
+
return encoders;
|
|
10125
|
+
encoders = null;
|
|
10126
|
+
const W = host.Worker;
|
|
10127
|
+
if (!W || !host.OffscreenCanvas)
|
|
10128
|
+
return null;
|
|
10129
|
+
const workers = [];
|
|
10130
|
+
let url = null;
|
|
10131
|
+
try {
|
|
10132
|
+
url = host.URL.createObjectURL(new host.Blob([ENCODER], { type: "text/javascript" }));
|
|
10133
|
+
for (let i = 0;i < ENCODERS; i++)
|
|
10134
|
+
workers.push(new W(url));
|
|
10135
|
+
} catch {
|
|
10136
|
+
for (const w of workers)
|
|
10137
|
+
w.terminate();
|
|
10138
|
+
return null;
|
|
10139
|
+
} finally {
|
|
10140
|
+
if (url)
|
|
10141
|
+
host.URL.revokeObjectURL(url);
|
|
10142
|
+
}
|
|
10143
|
+
const pool = { workers, waiting: new Map, next: 0 };
|
|
10144
|
+
for (const w of workers) {
|
|
10145
|
+
w.onmessage = (e) => {
|
|
10146
|
+
const job = pool.waiting.get(e.data.id);
|
|
10147
|
+
pool.waiting.delete(e.data.id);
|
|
10148
|
+
if (e.data.blob)
|
|
10149
|
+
job?.done(e.data.blob);
|
|
10150
|
+
else
|
|
10151
|
+
job?.fail(new Error(e.data.error));
|
|
10152
|
+
};
|
|
10153
|
+
w.onerror = () => {
|
|
10154
|
+
if (encoders !== pool)
|
|
10155
|
+
return;
|
|
10156
|
+
endEncoders("encoder");
|
|
10157
|
+
encoders = null;
|
|
10158
|
+
};
|
|
10159
|
+
}
|
|
10160
|
+
encoders = pool;
|
|
10161
|
+
return pool;
|
|
10162
|
+
}
|
|
10163
|
+
function encodeOn(pool, bitmap, quality) {
|
|
10164
|
+
const id = pool.next++;
|
|
10165
|
+
const worker = pool.workers[id % pool.workers.length];
|
|
10166
|
+
return new Promise((done, fail) => {
|
|
10167
|
+
pool.waiting.set(id, { done, fail });
|
|
10168
|
+
try {
|
|
10169
|
+
worker.postMessage({ id, bitmap, quality }, [bitmap]);
|
|
10170
|
+
} catch (err) {
|
|
10171
|
+
pool.waiting.delete(id);
|
|
10172
|
+
bitmap.close();
|
|
10173
|
+
fail(err);
|
|
10174
|
+
}
|
|
10175
|
+
});
|
|
10176
|
+
}
|
|
10177
|
+
async function toProxy(doc, source, w, h) {
|
|
10178
|
+
const bm = await toBitmap(doc, source, w, h, PROXY.size);
|
|
10179
|
+
const pool = encodersOf(doc.defaultView);
|
|
10180
|
+
if (pool)
|
|
10181
|
+
return encodeOn(pool, bm, PROXY.quality);
|
|
10182
|
+
try {
|
|
10183
|
+
const Off = doc.defaultView?.OffscreenCanvas;
|
|
10184
|
+
if (Off) {
|
|
10185
|
+
const c = new Off(bm.width, bm.height);
|
|
10186
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
10187
|
+
return await c.convertToBlob({ type: "image/jpeg", quality: PROXY.quality });
|
|
10188
|
+
}
|
|
10189
|
+
const c = doc.createElement("canvas");
|
|
10190
|
+
c.width = bm.width;
|
|
10191
|
+
c.height = bm.height;
|
|
10192
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
10193
|
+
return await new Promise((done, fail) => c.toBlob((b) => b ? done(b) : fail(new Error("toBlob")), "image/jpeg", PROXY.quality));
|
|
10194
|
+
} finally {
|
|
10195
|
+
bm.close();
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10017
10198
|
function acquireFrames(doc, src, fps) {
|
|
10018
10199
|
const key = `${fps}|${src}`;
|
|
10019
10200
|
let core = cores.get(key);
|
|
10020
10201
|
if (!core) {
|
|
10021
10202
|
core = makeCore(doc, key, src, fps);
|
|
10022
10203
|
cores.set(key, core);
|
|
10204
|
+
holdEncoders();
|
|
10023
10205
|
}
|
|
10024
10206
|
core.refs++;
|
|
10025
10207
|
return core.cache;
|
|
@@ -10040,6 +10222,71 @@ function makeCore(doc, key, src, fps) {
|
|
|
10040
10222
|
let generation = 0;
|
|
10041
10223
|
let tick = 0;
|
|
10042
10224
|
let notifyRaf = 0;
|
|
10225
|
+
const proxies = new Map;
|
|
10226
|
+
let proxyBytes = 0;
|
|
10227
|
+
let stride = 1;
|
|
10228
|
+
const decoded = new Map;
|
|
10229
|
+
const decoding = new Set;
|
|
10230
|
+
let aim = null;
|
|
10231
|
+
let lastAsked = null;
|
|
10232
|
+
const storeProxy = (frame, blob) => {
|
|
10233
|
+
if (disposed || frame % stride || proxies.has(frame))
|
|
10234
|
+
return;
|
|
10235
|
+
proxies.set(frame, blob);
|
|
10236
|
+
proxyBytes += blob.size;
|
|
10237
|
+
const next = proxyStride(proxyBytes, proxies.size, total, PROXY.budget);
|
|
10238
|
+
if (next <= stride)
|
|
10239
|
+
return;
|
|
10240
|
+
stride = next;
|
|
10241
|
+
for (const [f, b] of proxies)
|
|
10242
|
+
if (f % stride) {
|
|
10243
|
+
proxies.delete(f);
|
|
10244
|
+
proxyBytes -= b.size;
|
|
10245
|
+
}
|
|
10246
|
+
};
|
|
10247
|
+
const proxyNear = (frame) => {
|
|
10248
|
+
const at = Math.round(frame / stride) * stride;
|
|
10249
|
+
let best = null;
|
|
10250
|
+
for (const f of [at, at - stride, at + stride])
|
|
10251
|
+
if (proxies.has(f) && (best === null || Math.abs(f - frame) < Math.abs(best - frame)))
|
|
10252
|
+
best = f;
|
|
10253
|
+
return best;
|
|
10254
|
+
};
|
|
10255
|
+
const proxyTodo = () => {
|
|
10256
|
+
const out = [];
|
|
10257
|
+
for (let f = 0;f < total; f += stride)
|
|
10258
|
+
if (!proxies.has(f))
|
|
10259
|
+
out.push(f);
|
|
10260
|
+
return out;
|
|
10261
|
+
};
|
|
10262
|
+
const decodeProxy = (frame) => {
|
|
10263
|
+
if (frame === null || decoded.has(frame) || decoding.has(frame) || disposed)
|
|
10264
|
+
return;
|
|
10265
|
+
const blob = proxies.get(frame);
|
|
10266
|
+
if (!blob)
|
|
10267
|
+
return;
|
|
10268
|
+
decoding.add(frame);
|
|
10269
|
+
createImageBitmap(blob).then((bitmap) => {
|
|
10270
|
+
decoding.delete(frame);
|
|
10271
|
+
if (disposed)
|
|
10272
|
+
return bitmap.close();
|
|
10273
|
+
decoded.set(frame, { bitmap, used: ++tick });
|
|
10274
|
+
if (decoded.size > PROXY.decoded) {
|
|
10275
|
+
const [oldest] = [...decoded].sort((a, b) => a[1].used - b[1].used);
|
|
10276
|
+
if (oldest && oldest[0] !== frame) {
|
|
10277
|
+
oldest[1].bitmap.close();
|
|
10278
|
+
decoded.delete(oldest[0]);
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
10281
|
+
for (const f of listeners)
|
|
10282
|
+
f();
|
|
10283
|
+
}, () => decoding.delete(frame));
|
|
10284
|
+
};
|
|
10285
|
+
const dropDecoded = () => {
|
|
10286
|
+
for (const d of decoded.values())
|
|
10287
|
+
d.bitmap.close();
|
|
10288
|
+
decoded.clear();
|
|
10289
|
+
};
|
|
10043
10290
|
const notify = () => {
|
|
10044
10291
|
if (notifyRaf)
|
|
10045
10292
|
return;
|
|
@@ -10170,18 +10417,50 @@ function makeCore(doc, key, src, fps) {
|
|
|
10170
10417
|
return;
|
|
10171
10418
|
const forFocus = todo[0].kind === "focus";
|
|
10172
10419
|
const gen = generation;
|
|
10173
|
-
|
|
10420
|
+
await decodeRun(t, data, run, forFocus ? () => gen !== generation : () => false, (vf, f) => {
|
|
10421
|
+
const want = plan.get(f);
|
|
10422
|
+
if (!want)
|
|
10423
|
+
return null;
|
|
10424
|
+
plan.delete(f);
|
|
10425
|
+
return toBitmap(doc, vf, vf.displayWidth, vf.displayHeight, want.full ? FULL : PREVIEW).then((bm) => store(f, bm, want.full, want.kind));
|
|
10426
|
+
});
|
|
10427
|
+
};
|
|
10428
|
+
const proxyUnit = async () => {
|
|
10429
|
+
const todo = proxyTodo();
|
|
10430
|
+
if (!todo.length || !track || !buf)
|
|
10431
|
+
return;
|
|
10432
|
+
const runs = decodeRuns(track.samples, new Set(todo), (pts) => frameOf(pts, fps));
|
|
10433
|
+
const at = aim ?? focusAt ?? 0;
|
|
10434
|
+
let run = runs[0];
|
|
10435
|
+
let best = Number.POSITIVE_INFINITY;
|
|
10436
|
+
for (const r of runs) {
|
|
10437
|
+
const d = Math.min(...r.frames.map((f) => Math.abs(f - at)));
|
|
10438
|
+
if (d < best) {
|
|
10439
|
+
best = d;
|
|
10440
|
+
run = r;
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
10443
|
+
if (!run)
|
|
10444
|
+
return;
|
|
10445
|
+
const gen = generation;
|
|
10446
|
+
let made = 0;
|
|
10447
|
+
await decodeRun(track, buf, run, () => gen !== generation || made >= PROXY.turn, (vf, f) => {
|
|
10448
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
10449
|
+
return null;
|
|
10450
|
+
made++;
|
|
10451
|
+
return toProxy(doc, vf, vf.displayWidth, vf.displayHeight).then((b) => storeProxy(f, b));
|
|
10452
|
+
});
|
|
10453
|
+
};
|
|
10454
|
+
const decodeRun = async (t, data, run, stale, take) => {
|
|
10174
10455
|
let pending = 0;
|
|
10175
10456
|
let failed = false;
|
|
10176
10457
|
const decoder = new VideoDecoder({
|
|
10177
10458
|
output: (vf) => {
|
|
10178
|
-
const
|
|
10179
|
-
|
|
10180
|
-
if (!want || stale() || disposed)
|
|
10459
|
+
const work = stale() || disposed ? null : take(vf, frameOf(vf.timestamp / 1e6, fps));
|
|
10460
|
+
if (!work)
|
|
10181
10461
|
return vf.close();
|
|
10182
|
-
plan.delete(f);
|
|
10183
10462
|
pending++;
|
|
10184
|
-
|
|
10463
|
+
work.catch(() => {}).finally(() => {
|
|
10185
10464
|
vf.close();
|
|
10186
10465
|
pending--;
|
|
10187
10466
|
});
|
|
@@ -10295,6 +10574,25 @@ function makeCore(doc, key, src, fps) {
|
|
|
10295
10574
|
return points.some((p) => p.frame + p.tolerance >= f && !covered(p));
|
|
10296
10575
|
}, 6000, false);
|
|
10297
10576
|
};
|
|
10577
|
+
const videoProxyUnit = async () => {
|
|
10578
|
+
const v = sampler;
|
|
10579
|
+
const todo = proxyTodo();
|
|
10580
|
+
if (!v || !todo.length || !v.videoWidth)
|
|
10581
|
+
return;
|
|
10582
|
+
const at = aim ?? focusAt ?? 0;
|
|
10583
|
+
const first = todo.find((f) => f >= at) ?? todo[0];
|
|
10584
|
+
const grab = async (f) => {
|
|
10585
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
10586
|
+
return;
|
|
10587
|
+
storeProxy(f, await toProxy(doc, v, v.videoWidth, v.videoHeight));
|
|
10588
|
+
};
|
|
10589
|
+
await seekTo(v, (first + 0.5) / fps);
|
|
10590
|
+
await grab(frameOf(v.currentTime, fps));
|
|
10591
|
+
await playThrough(v, 1, async (f) => {
|
|
10592
|
+
await grab(f);
|
|
10593
|
+
return proxyTodo().length > 0;
|
|
10594
|
+
}, 4000, true);
|
|
10595
|
+
};
|
|
10298
10596
|
const cache = {
|
|
10299
10597
|
src,
|
|
10300
10598
|
get backend() {
|
|
@@ -10303,29 +10601,56 @@ function makeCore(doc, key, src, fps) {
|
|
|
10303
10601
|
get size() {
|
|
10304
10602
|
return entries.size;
|
|
10305
10603
|
},
|
|
10306
|
-
|
|
10604
|
+
get proxies() {
|
|
10605
|
+
return { count: proxies.size, stride, bytes: proxyBytes };
|
|
10606
|
+
},
|
|
10607
|
+
nearest(frame, within = Infinity, decode = false) {
|
|
10307
10608
|
const k = nearestKey(entries.keys(), frame, within);
|
|
10609
|
+
const kd = nearestKey(decoded.keys(), frame, within);
|
|
10610
|
+
const dk = k === null ? Number.POSITIVE_INFINITY : Math.abs(k - frame);
|
|
10611
|
+
const dd = kd === null ? Number.POSITIVE_INFINITY : Math.abs(kd - frame);
|
|
10612
|
+
if (decode) {
|
|
10613
|
+
const p = proxyNear(frame);
|
|
10614
|
+
if (p !== null && Math.abs(p - frame) < Math.min(dk, dd))
|
|
10615
|
+
decodeProxy(p);
|
|
10616
|
+
const ahead = aheadOf(frame, lastAsked, total);
|
|
10617
|
+
if (ahead !== null)
|
|
10618
|
+
decodeProxy(proxyNear(ahead));
|
|
10619
|
+
lastAsked = frame;
|
|
10620
|
+
}
|
|
10621
|
+
if (kd !== null && (dd < dk || dd === dk && !entries.get(k)?.full)) {
|
|
10622
|
+
const d = decoded.get(kd);
|
|
10623
|
+
d.used = ++tick;
|
|
10624
|
+
return { frame: kd, bitmap: d.bitmap, full: false };
|
|
10625
|
+
}
|
|
10308
10626
|
if (k === null)
|
|
10309
10627
|
return null;
|
|
10310
10628
|
const e = entries.get(k);
|
|
10311
10629
|
e.used = ++tick;
|
|
10312
10630
|
return { frame: k, bitmap: e.bitmap, full: e.full };
|
|
10313
10631
|
},
|
|
10632
|
+
covers(frame) {
|
|
10633
|
+
if (entries.has(frame) || decoded.has(frame))
|
|
10634
|
+
return true;
|
|
10635
|
+
const p = proxyNear(frame);
|
|
10636
|
+
return p !== null && Math.abs(p - frame) <= stride / 2;
|
|
10637
|
+
},
|
|
10314
10638
|
focus(frame) {
|
|
10315
|
-
|
|
10316
|
-
active?.dropFull();
|
|
10317
|
-
if (active)
|
|
10318
|
-
active.active = false;
|
|
10319
|
-
active = core;
|
|
10320
|
-
core.active = true;
|
|
10321
|
-
}
|
|
10639
|
+
claim(core);
|
|
10322
10640
|
const moved = focusAt === null || Math.abs(frame - focusAt) > FOCUS_RADIUS;
|
|
10323
10641
|
focusAt = frame;
|
|
10642
|
+
aim = null;
|
|
10324
10643
|
if (moved)
|
|
10325
10644
|
generation++;
|
|
10326
10645
|
start();
|
|
10327
10646
|
pump(win);
|
|
10328
10647
|
},
|
|
10648
|
+
follow(frame) {
|
|
10649
|
+
claim(core);
|
|
10650
|
+
aim = frame;
|
|
10651
|
+
start();
|
|
10652
|
+
pump(win);
|
|
10653
|
+
},
|
|
10329
10654
|
want(points) {
|
|
10330
10655
|
wants = points;
|
|
10331
10656
|
if (core.shown > 0)
|
|
@@ -10363,12 +10688,15 @@ function makeCore(doc, key, src, fps) {
|
|
|
10363
10688
|
refs: 0,
|
|
10364
10689
|
shown: 0,
|
|
10365
10690
|
active: false,
|
|
10691
|
+
ran: 0,
|
|
10366
10692
|
work() {
|
|
10367
10693
|
if (disposed || backend === "pending")
|
|
10368
10694
|
return null;
|
|
10369
|
-
if (
|
|
10370
|
-
return
|
|
10371
|
-
|
|
10695
|
+
if (missing().length)
|
|
10696
|
+
return backend === "webcodecs" ? decodeUnit() : videoUnit();
|
|
10697
|
+
if (proxyTodo().length)
|
|
10698
|
+
return backend === "webcodecs" ? proxyUnit() : videoProxyUnit();
|
|
10699
|
+
return null;
|
|
10372
10700
|
},
|
|
10373
10701
|
dropFull() {
|
|
10374
10702
|
for (const [f, e] of entries)
|
|
@@ -10377,8 +10705,13 @@ function makeCore(doc, key, src, fps) {
|
|
|
10377
10705
|
entries.delete(f);
|
|
10378
10706
|
}
|
|
10379
10707
|
focusAt = null;
|
|
10708
|
+
aim = null;
|
|
10709
|
+
lastAsked = null;
|
|
10710
|
+
dropDecoded();
|
|
10380
10711
|
},
|
|
10381
10712
|
dispose() {
|
|
10713
|
+
if (disposed)
|
|
10714
|
+
return;
|
|
10382
10715
|
disposed = true;
|
|
10383
10716
|
generation++;
|
|
10384
10717
|
if (notifyRaf)
|
|
@@ -10386,6 +10719,8 @@ function makeCore(doc, key, src, fps) {
|
|
|
10386
10719
|
for (const e of entries.values())
|
|
10387
10720
|
e.bitmap.close();
|
|
10388
10721
|
entries.clear();
|
|
10722
|
+
dropDecoded();
|
|
10723
|
+
proxies.clear();
|
|
10389
10724
|
listeners.clear();
|
|
10390
10725
|
if (sampler) {
|
|
10391
10726
|
sampler.removeAttribute("src");
|
|
@@ -10393,24 +10728,45 @@ function makeCore(doc, key, src, fps) {
|
|
|
10393
10728
|
}
|
|
10394
10729
|
buf = null;
|
|
10395
10730
|
track = null;
|
|
10396
|
-
|
|
10397
|
-
|
|
10731
|
+
actives.delete(core);
|
|
10732
|
+
round2?.delete(core);
|
|
10398
10733
|
cores.delete(key);
|
|
10734
|
+
dropEncoders();
|
|
10399
10735
|
}
|
|
10400
10736
|
};
|
|
10401
10737
|
return core;
|
|
10402
10738
|
}
|
|
10403
|
-
var FOCUS_RADIUS = 6, GRID = 48, SEEN = 12, WANTED = 48, PREVIEW = 384, FULL = 1280, cores,
|
|
10739
|
+
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) => {
|
|
10404
10740
|
const ric = win.requestIdleCallback;
|
|
10405
10741
|
if (ric)
|
|
10406
10742
|
ric(() => done(), { timeout: 60 });
|
|
10407
10743
|
else
|
|
10408
10744
|
win.setTimeout(done, 4);
|
|
10409
|
-
}), sleep = (win, ms) => new Promise((done) => win.setTimeout(done, ms));
|
|
10745
|
+
}), 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) => {
|
|
10746
|
+
const pool = encoders;
|
|
10747
|
+
if (!pool)
|
|
10748
|
+
return;
|
|
10749
|
+
for (const w of pool.workers) {
|
|
10750
|
+
w.onmessage = null;
|
|
10751
|
+
w.onerror = null;
|
|
10752
|
+
w.terminate();
|
|
10753
|
+
}
|
|
10754
|
+
for (const j of pool.waiting.values())
|
|
10755
|
+
j.fail(new Error(why));
|
|
10756
|
+
pool.waiting.clear();
|
|
10757
|
+
};
|
|
10410
10758
|
var init_compare_frames = __esm(() => {
|
|
10411
10759
|
init_compare_model();
|
|
10412
10760
|
init_compare_mp4();
|
|
10761
|
+
PROXY = {
|
|
10762
|
+
size: 960,
|
|
10763
|
+
quality: 0.8,
|
|
10764
|
+
budget: 48 * 2 ** 20,
|
|
10765
|
+
decoded: 8,
|
|
10766
|
+
turn: 90
|
|
10767
|
+
};
|
|
10413
10768
|
cores = new Map;
|
|
10769
|
+
actives = new Set;
|
|
10414
10770
|
});
|
|
10415
10771
|
|
|
10416
10772
|
// src/ui/compare-gpu.ts
|
|
@@ -10676,656 +11032,278 @@ var init_compare_gpu = __esm(() => {
|
|
|
10676
11032
|
lostListeners = new Set;
|
|
10677
11033
|
});
|
|
10678
11034
|
|
|
10679
|
-
// src/ui/compare
|
|
10680
|
-
function
|
|
11035
|
+
// src/ui/compare.ts
|
|
11036
|
+
function compare(container, options) {
|
|
10681
11037
|
const doc = container.ownerDocument;
|
|
10682
11038
|
const win = doc.defaultView;
|
|
10683
|
-
const
|
|
10684
|
-
const
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
11039
|
+
const caching = scrubCaching(options.scrubCache, win);
|
|
11040
|
+
const look = {
|
|
11041
|
+
...options.preset ? { preset: options.preset } : {},
|
|
11042
|
+
...options.theme ? { theme: options.theme } : {},
|
|
11043
|
+
...options.appearance ? { appearance: options.appearance } : {}
|
|
11044
|
+
};
|
|
11045
|
+
const offered = compareModes(options.modes, options.mode);
|
|
11046
|
+
const modeList = offered.modes;
|
|
11047
|
+
let mode = offered.mode;
|
|
11048
|
+
let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
|
|
11049
|
+
let loupeOn = options.loupe ?? false;
|
|
11050
|
+
let threshold = options.threshold ?? 4 / 255;
|
|
11051
|
+
let fpsOpt = options.fps;
|
|
11052
|
+
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
11053
|
+
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
11054
|
+
const auto = container.clientHeight < 1;
|
|
11055
|
+
const el = (tag, cls) => {
|
|
11056
|
+
const n = doc.createElement(tag);
|
|
11057
|
+
n.className = cls;
|
|
11058
|
+
return n;
|
|
11059
|
+
};
|
|
11060
|
+
const root = el("div", "knobs-compare");
|
|
11061
|
+
root.tabIndex = 0;
|
|
11062
|
+
root.setAttribute("role", "group");
|
|
11063
|
+
root.setAttribute("aria-roledescription", "compare");
|
|
11064
|
+
root.setAttribute("aria-label", options.label ?? "Compare");
|
|
11065
|
+
root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
|
|
10691
11066
|
applyLook2(root, options);
|
|
10692
|
-
|
|
11067
|
+
root.dataset.mode = mode;
|
|
11068
|
+
if (auto)
|
|
11069
|
+
root.dataset.auto = "";
|
|
11070
|
+
const bar = el("div", "knobs-cmp-bar");
|
|
11071
|
+
const modes = el("div", "knobs-cmp-modes");
|
|
11072
|
+
modes.setAttribute("role", "radiogroup");
|
|
11073
|
+
modes.setAttribute("aria-label", "Mode");
|
|
11074
|
+
modes.hidden = modeList.length < 2;
|
|
11075
|
+
const modeBtns = modeList.map((m, i) => {
|
|
11076
|
+
const b = el("button", "knobs-cmp-mode");
|
|
11077
|
+
b.type = "button";
|
|
11078
|
+
b.tabIndex = -1;
|
|
11079
|
+
b.textContent = MODE_LABEL[m].short;
|
|
11080
|
+
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
11081
|
+
b.setAttribute("role", "radio");
|
|
11082
|
+
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
11083
|
+
b.addEventListener("click", () => {
|
|
11084
|
+
setMode(m);
|
|
11085
|
+
root.focus({ preventScroll: true });
|
|
11086
|
+
});
|
|
11087
|
+
modes.append(b);
|
|
11088
|
+
return b;
|
|
11089
|
+
});
|
|
11090
|
+
const toggleBtn = (name, label, title) => {
|
|
11091
|
+
const b = el("button", "knobs-cmp-toggle");
|
|
11092
|
+
b.type = "button";
|
|
11093
|
+
b.tabIndex = -1;
|
|
11094
|
+
b.innerHTML = icon(name);
|
|
11095
|
+
b.setAttribute("aria-label", label);
|
|
11096
|
+
b.title = title;
|
|
11097
|
+
return b;
|
|
11098
|
+
};
|
|
11099
|
+
const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
|
|
11100
|
+
loupeBtn.addEventListener("click", () => {
|
|
11101
|
+
setLoupe(!loupeOn);
|
|
11102
|
+
root.focus({ preventScroll: true });
|
|
11103
|
+
});
|
|
11104
|
+
const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
|
|
11105
|
+
loopBtn.setAttribute("aria-keyshortcuts", "L");
|
|
11106
|
+
loopBtn.addEventListener("click", () => {
|
|
11107
|
+
api.setLoop(!loopNow());
|
|
11108
|
+
root.focus({ preventScroll: true });
|
|
11109
|
+
});
|
|
11110
|
+
bar.append(modes, el("span", "knobs-cmp-fill"), loopBtn, loupeBtn);
|
|
11111
|
+
const stage = el("div", "knobs-cmp-stage");
|
|
11112
|
+
const canvas = el("canvas", "knobs-cmp-canvas");
|
|
10693
11113
|
canvas.setAttribute("aria-hidden", "true");
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
const
|
|
10697
|
-
const
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
11114
|
+
const split = el("div", "knobs-cmp-split");
|
|
11115
|
+
split.setAttribute("aria-hidden", "true");
|
|
11116
|
+
const zoomPill = el("div", "knobs-cmp-zoom");
|
|
11117
|
+
const zoomPct = el("span", "knobs-cmp-zoom-pct");
|
|
11118
|
+
const zoomReset = el("button", "knobs-cmp-zoom-reset");
|
|
11119
|
+
zoomReset.type = "button";
|
|
11120
|
+
zoomReset.tabIndex = -1;
|
|
11121
|
+
zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
|
|
11122
|
+
zoomReset.setAttribute("aria-label", "Reset zoom");
|
|
11123
|
+
zoomReset.title = "Fit · 0 or double-click";
|
|
11124
|
+
zoomReset.addEventListener("click", () => {
|
|
11125
|
+
api.setZoom(null);
|
|
11126
|
+
root.focus({ preventScroll: true });
|
|
10707
11127
|
});
|
|
10708
|
-
|
|
10709
|
-
const
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
s.seen = true;
|
|
10719
|
-
}
|
|
10720
|
-
} else {
|
|
10721
|
-
const img = doc.createElement("img");
|
|
10722
|
-
img.crossOrigin = "anonymous";
|
|
10723
|
-
img.decoding = "async";
|
|
10724
|
-
img.addEventListener("load", () => {
|
|
10725
|
-
pairDiffs.clear();
|
|
10726
|
-
schedule();
|
|
10727
|
-
});
|
|
10728
|
-
img.src = s.source.src;
|
|
10729
|
-
s.still = img;
|
|
11128
|
+
zoomPill.append(zoomPct, zoomReset);
|
|
11129
|
+
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
11130
|
+
labels[0].dataset.side = "before";
|
|
11131
|
+
labels[1].dataset.side = "after";
|
|
11132
|
+
const setLabel = (index, text) => {
|
|
11133
|
+
const l = labels[index];
|
|
11134
|
+
if (!l.firstElementChild) {
|
|
11135
|
+
const mark = el("span", "knobs-cmp-mark");
|
|
11136
|
+
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
11137
|
+
l.append(mark, el("span", "knobs-cmp-name"));
|
|
10730
11138
|
}
|
|
11139
|
+
const name = l.lastElementChild;
|
|
11140
|
+
if (name.textContent !== text)
|
|
11141
|
+
name.textContent = text;
|
|
10731
11142
|
};
|
|
10732
|
-
const
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
11143
|
+
const seam = el("div", "knobs-cmp-seam");
|
|
11144
|
+
seam.setAttribute("aria-hidden", "true");
|
|
11145
|
+
const handle = el("div", "knobs-cmp-handle");
|
|
11146
|
+
handle.setAttribute("role", "slider");
|
|
11147
|
+
handle.setAttribute("aria-label", "Wipe");
|
|
11148
|
+
handle.setAttribute("aria-valuemin", "0");
|
|
11149
|
+
handle.setAttribute("aria-valuemax", "100");
|
|
11150
|
+
const grip = el("span", "knobs-cmp-grip");
|
|
11151
|
+
grip.setAttribute("aria-hidden", "true");
|
|
11152
|
+
handle.append(grip);
|
|
11153
|
+
const note = el("div", "knobs-cmp-note");
|
|
11154
|
+
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
11155
|
+
root.append(bar, stage);
|
|
11156
|
+
let tl = bound;
|
|
11157
|
+
let ownTimeline = false;
|
|
11158
|
+
if (!tl && options.timeline === true) {
|
|
11159
|
+
const host = el("div", "knobs-cmp-timeline-host");
|
|
11160
|
+
root.append(host);
|
|
11161
|
+
tl = timeline(host, {
|
|
11162
|
+
...look,
|
|
11163
|
+
variant: "line",
|
|
11164
|
+
duration: 1,
|
|
11165
|
+
fps: fps(),
|
|
11166
|
+
arrows: { step: 0.5, shift: 2 },
|
|
11167
|
+
label: "Compare time",
|
|
11168
|
+
...options.hints === false ? { hints: false } : {}
|
|
11169
|
+
});
|
|
11170
|
+
ownTimeline = true;
|
|
11171
|
+
}
|
|
11172
|
+
container.append(root);
|
|
11173
|
+
const sides = [null, null];
|
|
11174
|
+
let duration = 0;
|
|
11175
|
+
let renderer = "dom";
|
|
11176
|
+
let pending = false;
|
|
11177
|
+
let gpu = null;
|
|
11178
|
+
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
11179
|
+
const fade = new Spring(1, FADE3.omega, FADE3.zeta, 0.001);
|
|
11180
|
+
let fromMode = mode;
|
|
11181
|
+
let toMode = mode;
|
|
11182
|
+
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
11183
|
+
let wipeCatching = false;
|
|
11184
|
+
const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
11185
|
+
let loupeHeld = false;
|
|
11186
|
+
let pointer = null;
|
|
11187
|
+
let drag = null;
|
|
11188
|
+
let tap = null;
|
|
11189
|
+
const touches = new Map;
|
|
11190
|
+
let pinch = null;
|
|
11191
|
+
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11192
|
+
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11193
|
+
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11194
|
+
let viewSent = "0.0000,0.0000,1.0000|";
|
|
11195
|
+
let ownLoop = null;
|
|
11196
|
+
let scrubbing = false;
|
|
11197
|
+
let stageW = 1;
|
|
11198
|
+
let stageH = 1;
|
|
11199
|
+
let stageRect = null;
|
|
11200
|
+
let accent = [0.49, 0.81, 0.61];
|
|
11201
|
+
let labelW = [0, 0];
|
|
11202
|
+
let frameRadius = 0;
|
|
11203
|
+
let dirty = true;
|
|
11204
|
+
let disposed = false;
|
|
11205
|
+
let near = options.lazy === false;
|
|
11206
|
+
let onScreen = options.lazy === false;
|
|
11207
|
+
let releaseTimer = 0;
|
|
11208
|
+
let ownTime = 0;
|
|
11209
|
+
let ownPlaying = false;
|
|
11210
|
+
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
11211
|
+
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
11212
|
+
const follower = () => {
|
|
11213
|
+
const m = master();
|
|
11214
|
+
const v = videoOf(sides[0]);
|
|
11215
|
+
return m && v && v !== m ? v : null;
|
|
10745
11216
|
};
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
const
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
11217
|
+
const now = () => tl ? tl.time : ownTime;
|
|
11218
|
+
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
11219
|
+
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
11220
|
+
const sizeOf = (s) => {
|
|
11221
|
+
if (s.size.w > 0)
|
|
11222
|
+
return s.size;
|
|
11223
|
+
const other = sides[s.index === 0 ? 1 : 0];
|
|
11224
|
+
if (other && other.size.w > 0)
|
|
11225
|
+
return other.size;
|
|
11226
|
+
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
10755
11227
|
};
|
|
10756
|
-
const
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
});
|
|
10761
|
-
const measure = () => {
|
|
10762
|
-
const r = root.getBoundingClientRect();
|
|
10763
|
-
w = Math.max(1, r.width);
|
|
10764
|
-
h = Math.max(1, r.height);
|
|
10765
|
-
geo = { x: 0, w };
|
|
10766
|
-
if (align === "track") {
|
|
10767
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
10768
|
-
const t = track?.getBoundingClientRect();
|
|
10769
|
-
if (t && t.width > 0)
|
|
10770
|
-
geo = { x: t.left - r.left, w: t.width };
|
|
10771
|
-
}
|
|
10772
|
-
readColors();
|
|
10773
|
-
schedule();
|
|
11228
|
+
const wantOf = (s) => {
|
|
11229
|
+
const v = s.el;
|
|
11230
|
+
const f = frameOf(now(), fps());
|
|
11231
|
+
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
10774
11232
|
};
|
|
10775
|
-
const
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
s.
|
|
11233
|
+
const emitFrame = (s, frame, source) => root.dispatchEvent(new CustomEvent("knobs-compare-frame", { detail: { side: s.index, frame, source } }));
|
|
11234
|
+
const setCached = (s, on) => {
|
|
11235
|
+
if (renderer === "dom")
|
|
11236
|
+
s.preview.style.display = on ? "" : "none";
|
|
10779
11237
|
};
|
|
10780
|
-
const
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
11238
|
+
const showVideo = (s) => {
|
|
11239
|
+
const v = s.el;
|
|
11240
|
+
if (renderer === "gpu") {
|
|
11241
|
+
if (!gpu)
|
|
11242
|
+
return;
|
|
11243
|
+
if (!gpu.upload(s.index, v, v.videoWidth, v.videoHeight))
|
|
11244
|
+
return miss(s);
|
|
11245
|
+
s.misses = 0;
|
|
11246
|
+
}
|
|
11247
|
+
const f = frameOf(v.currentTime, fps());
|
|
11248
|
+
s.shown = { frame: f, cached: false };
|
|
11249
|
+
setCached(s, false);
|
|
11250
|
+
emitFrame(s, f, "video");
|
|
11251
|
+
requestDraw();
|
|
10785
11252
|
};
|
|
10786
|
-
const
|
|
10787
|
-
|
|
10788
|
-
probe.height = DIFF.h;
|
|
10789
|
-
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
10790
|
-
const differ = (a, b) => {
|
|
10791
|
-
if (!probeCtx)
|
|
11253
|
+
const showCached = (s, c) => {
|
|
11254
|
+
if (s.shown?.cached && s.shown.frame === c.frame)
|
|
10792
11255
|
return;
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
const
|
|
10798
|
-
|
|
10799
|
-
|
|
11256
|
+
if (renderer === "gpu") {
|
|
11257
|
+
if (!gpu || !gpu.upload(2 + s.index, c.bitmap, c.bitmap.width, c.bitmap.height))
|
|
11258
|
+
return;
|
|
11259
|
+
} else if (renderer === "dom" && !pending) {
|
|
11260
|
+
const p = s.preview;
|
|
11261
|
+
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
11262
|
+
p.width = c.bitmap.width;
|
|
11263
|
+
p.height = c.bitmap.height;
|
|
11264
|
+
}
|
|
11265
|
+
p.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
11266
|
+
} else
|
|
10800
11267
|
return;
|
|
10801
|
-
}
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
10806
|
-
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
11268
|
+
s.shown = { frame: c.frame, cached: true };
|
|
11269
|
+
setCached(s, true);
|
|
11270
|
+
emitFrame(s, c.frame, "cache");
|
|
11271
|
+
requestDraw();
|
|
10807
11272
|
};
|
|
10808
|
-
const
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
10815
|
-
};
|
|
11273
|
+
const miss = (s) => {
|
|
11274
|
+
if (s.failed || ++s.misses < 3)
|
|
11275
|
+
return;
|
|
11276
|
+
gpu?.dispose();
|
|
11277
|
+
gpu = null;
|
|
11278
|
+
setRenderer("dom");
|
|
10816
11279
|
};
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
let asked = "";
|
|
10820
|
-
function schedule() {
|
|
10821
|
-
if (!raf && !disposed)
|
|
10822
|
-
raf = win.requestAnimationFrame(draw);
|
|
10823
|
-
}
|
|
10824
|
-
const draw = () => {
|
|
10825
|
-
raf = 0;
|
|
10826
|
-
if (!near)
|
|
11280
|
+
const pushFrame = (s) => {
|
|
11281
|
+
if (!s.loaded || disposed)
|
|
10827
11282
|
return;
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
11283
|
+
if (s.kind === "image") {
|
|
11284
|
+
const img = s.el;
|
|
11285
|
+
if (!(img.complete && img.naturalWidth))
|
|
11286
|
+
return;
|
|
11287
|
+
if (renderer === "gpu" && gpu) {
|
|
11288
|
+
if (!gpu.upload(s.index, img, img.naturalWidth, img.naturalHeight))
|
|
11289
|
+
return miss(s);
|
|
11290
|
+
}
|
|
11291
|
+
s.shown = { frame: 0, cached: false };
|
|
11292
|
+
return requestDraw();
|
|
10834
11293
|
}
|
|
10835
|
-
const
|
|
10836
|
-
if (
|
|
11294
|
+
const v = s.el;
|
|
11295
|
+
if (v.readyState < 2)
|
|
11296
|
+
return;
|
|
11297
|
+
if (isPlaying() || !s.frames)
|
|
11298
|
+
return showVideo(s);
|
|
11299
|
+
const want = wantOf(s);
|
|
11300
|
+
const at = frameOf(v.currentTime, fps());
|
|
11301
|
+
if (at === want) {
|
|
11302
|
+
showVideo(s);
|
|
11303
|
+
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
10837
11304
|
return;
|
|
10838
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
10839
|
-
ctx.clearRect(0, 0, w, h);
|
|
10840
|
-
const cols = columns();
|
|
10841
|
-
const key = cols.map((c) => c.frame).join(",");
|
|
10842
|
-
if (key !== asked) {
|
|
10843
|
-
asked = key;
|
|
10844
|
-
ask(cols);
|
|
10845
11305
|
}
|
|
10846
|
-
const
|
|
10847
|
-
const radius = 3;
|
|
10848
|
-
let budget = DIFFS_PER_DRAW;
|
|
10849
|
-
let more = false;
|
|
10850
|
-
const ds = cols.map((c) => {
|
|
10851
|
-
const a = picOf(sides[0], c);
|
|
10852
|
-
const b = picOf(sides[1], c);
|
|
10853
|
-
if (!a || !b)
|
|
10854
|
-
return;
|
|
10855
|
-
const k = `${a.key}|${b.key}`;
|
|
10856
|
-
if (!pairDiffs.has(k)) {
|
|
10857
|
-
if (budget-- <= 0) {
|
|
10858
|
-
more = true;
|
|
10859
|
-
return;
|
|
10860
|
-
}
|
|
10861
|
-
pairDiffs.set(k, differ(a, b));
|
|
10862
|
-
}
|
|
10863
|
-
const d = pairDiffs.get(k);
|
|
10864
|
-
if (d !== undefined)
|
|
10865
|
-
diffs.set(c.frame, d);
|
|
10866
|
-
return d;
|
|
10867
|
-
});
|
|
10868
|
-
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
10869
|
-
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
10870
|
-
cols.forEach((c, i) => {
|
|
10871
|
-
const x = geo.x + c.x + GAP / 2;
|
|
10872
|
-
const cwid = Math.max(1, c.w - GAP);
|
|
10873
|
-
if (x + cwid < 0 || x > w)
|
|
10874
|
-
return;
|
|
10875
|
-
for (const row of [0, 1]) {
|
|
10876
|
-
const y = row * (rh + GAP);
|
|
10877
|
-
const pic = picOf(sides[row], c);
|
|
10878
|
-
ctx.save();
|
|
10879
|
-
ctx.beginPath();
|
|
10880
|
-
ctx.roundRect(x, y, cwid, rh, radius);
|
|
10881
|
-
ctx.fillStyle = colors.surface;
|
|
10882
|
-
ctx.fill();
|
|
10883
|
-
if (pic) {
|
|
10884
|
-
ctx.clip();
|
|
10885
|
-
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
10886
|
-
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
10887
|
-
}
|
|
10888
|
-
ctx.restore();
|
|
10889
|
-
}
|
|
10890
|
-
if (hover === i) {
|
|
10891
|
-
ctx.fillStyle = colors.wash;
|
|
10892
|
-
ctx.beginPath();
|
|
10893
|
-
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
10894
|
-
ctx.fill();
|
|
10895
|
-
}
|
|
10896
|
-
const d = ds[i];
|
|
10897
|
-
if (d !== undefined && d > 0) {
|
|
10898
|
-
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
10899
|
-
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
10900
|
-
ctx.fillStyle = colors.diff;
|
|
10901
|
-
ctx.beginPath();
|
|
10902
|
-
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
10903
|
-
ctx.fill();
|
|
10904
|
-
ctx.globalAlpha = 1;
|
|
10905
|
-
}
|
|
10906
|
-
});
|
|
10907
|
-
const v = timeline.view;
|
|
10908
|
-
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
10909
|
-
if (px >= -1 && px <= w + 1) {
|
|
10910
|
-
ctx.fillStyle = colors.accent;
|
|
10911
|
-
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
10912
|
-
}
|
|
10913
|
-
if (more)
|
|
10914
|
-
schedule();
|
|
10915
|
-
};
|
|
10916
|
-
const timeAt = (clientX) => {
|
|
10917
|
-
const r = root.getBoundingClientRect();
|
|
10918
|
-
const v = timeline.view;
|
|
10919
|
-
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
10920
|
-
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
10921
|
-
};
|
|
10922
|
-
const columnAt = (clientX) => {
|
|
10923
|
-
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
10924
|
-
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
10925
|
-
return i < 0 ? null : i;
|
|
10926
|
-
};
|
|
10927
|
-
let dragging = false;
|
|
10928
|
-
const pick = (clientX) => {
|
|
10929
|
-
const t = timeAt(clientX);
|
|
10930
|
-
timeline.seek(t);
|
|
10931
|
-
options.onPick?.(t);
|
|
10932
|
-
};
|
|
10933
|
-
root.addEventListener("pointerdown", (e) => {
|
|
10934
|
-
if (e.button !== 0)
|
|
10935
|
-
return;
|
|
10936
|
-
dragging = true;
|
|
10937
|
-
root.setPointerCapture(e.pointerId);
|
|
10938
|
-
const host = root.closest(".knobs-compare");
|
|
10939
|
-
if (host)
|
|
10940
|
-
host.focus({ preventScroll: true });
|
|
10941
|
-
else
|
|
10942
|
-
timeline.focus();
|
|
10943
|
-
if (timeline.playing)
|
|
10944
|
-
timeline.pause();
|
|
10945
|
-
pick(e.clientX);
|
|
10946
|
-
e.preventDefault();
|
|
10947
|
-
});
|
|
10948
|
-
root.addEventListener("pointermove", (e) => {
|
|
10949
|
-
if (dragging)
|
|
10950
|
-
pick(e.clientX);
|
|
10951
|
-
const i = columnAt(e.clientX);
|
|
10952
|
-
if (i !== hover) {
|
|
10953
|
-
hover = i;
|
|
10954
|
-
schedule();
|
|
10955
|
-
}
|
|
10956
|
-
});
|
|
10957
|
-
const end = () => {
|
|
10958
|
-
dragging = false;
|
|
10959
|
-
};
|
|
10960
|
-
root.addEventListener("pointerup", end);
|
|
10961
|
-
root.addEventListener("pointercancel", end);
|
|
10962
|
-
root.addEventListener("lostpointercapture", end);
|
|
10963
|
-
root.addEventListener("pointerleave", () => {
|
|
10964
|
-
if (hover === null)
|
|
10965
|
-
return;
|
|
10966
|
-
hover = null;
|
|
10967
|
-
schedule();
|
|
10968
|
-
});
|
|
10969
|
-
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
10970
|
-
const ro = new win.ResizeObserver(measure);
|
|
10971
|
-
ro.observe(root);
|
|
10972
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
10973
|
-
if (track)
|
|
10974
|
-
ro.observe(track);
|
|
10975
|
-
root.addEventListener("knobs-restyle", measure);
|
|
10976
|
-
const setOnScreen = (on) => {
|
|
10977
|
-
onScreen = on;
|
|
10978
|
-
for (const s of sides) {
|
|
10979
|
-
if (!s.frames || s.seen === on)
|
|
10980
|
-
continue;
|
|
10981
|
-
s.seen = on;
|
|
10982
|
-
s.frames.visible(on);
|
|
10983
|
-
}
|
|
10984
|
-
};
|
|
10985
|
-
const setNear = (on) => {
|
|
10986
|
-
near = on;
|
|
10987
|
-
if (on) {
|
|
10988
|
-
for (const s of sides)
|
|
10989
|
-
load(s);
|
|
10990
|
-
schedule();
|
|
10991
|
-
} else
|
|
10992
|
-
for (const s of sides)
|
|
10993
|
-
unload(s);
|
|
10994
|
-
};
|
|
10995
|
-
const IO = win.IntersectionObserver;
|
|
10996
|
-
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
10997
|
-
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
10998
|
-
measure();
|
|
10999
|
-
if (nearIo && screenIo) {
|
|
11000
|
-
nearIo.observe(root);
|
|
11001
|
-
screenIo.observe(root);
|
|
11002
|
-
} else {
|
|
11003
|
-
setNear(true);
|
|
11004
|
-
setOnScreen(true);
|
|
11005
|
-
}
|
|
11006
|
-
return {
|
|
11007
|
-
element: root,
|
|
11008
|
-
diffs,
|
|
11009
|
-
update(next) {
|
|
11010
|
-
const was = sides;
|
|
11011
|
-
sides = [
|
|
11012
|
-
next.before ? makeSide(next.before) : was[0],
|
|
11013
|
-
next.after ? makeSide(next.after) : was[1]
|
|
11014
|
-
];
|
|
11015
|
-
for (const s of was)
|
|
11016
|
-
if (!sides.includes(s))
|
|
11017
|
-
unload(s);
|
|
11018
|
-
diffs.clear();
|
|
11019
|
-
pairDiffs.clear();
|
|
11020
|
-
asked = "";
|
|
11021
|
-
if (near)
|
|
11022
|
-
for (const s of sides)
|
|
11023
|
-
load(s);
|
|
11024
|
-
if (onScreen)
|
|
11025
|
-
setOnScreen(true);
|
|
11026
|
-
schedule();
|
|
11027
|
-
},
|
|
11028
|
-
dispose() {
|
|
11029
|
-
disposed = true;
|
|
11030
|
-
if (raf)
|
|
11031
|
-
win.cancelAnimationFrame(raf);
|
|
11032
|
-
unlisten();
|
|
11033
|
-
ro.disconnect();
|
|
11034
|
-
nearIo?.disconnect();
|
|
11035
|
-
screenIo?.disconnect();
|
|
11036
|
-
for (const s of sides)
|
|
11037
|
-
unload(s);
|
|
11038
|
-
root.remove();
|
|
11039
|
-
}
|
|
11040
|
-
};
|
|
11041
|
-
}
|
|
11042
|
-
var BAR = 7, GAP = 2, DIFF, DIFFS_PER_DRAW = 6;
|
|
11043
|
-
var init_compare_strip = __esm(() => {
|
|
11044
|
-
init_compare_frames();
|
|
11045
|
-
init_compare_model();
|
|
11046
|
-
init_look();
|
|
11047
|
-
init_rgba();
|
|
11048
|
-
init_timeline_model();
|
|
11049
|
-
DIFF = { w: 32, h: 18 };
|
|
11050
|
-
});
|
|
11051
|
-
|
|
11052
|
-
// src/ui/compare.ts
|
|
11053
|
-
function compare(container, options) {
|
|
11054
|
-
const doc = container.ownerDocument;
|
|
11055
|
-
const win = doc.defaultView;
|
|
11056
|
-
const look = {
|
|
11057
|
-
...options.preset ? { preset: options.preset } : {},
|
|
11058
|
-
...options.theme ? { theme: options.theme } : {},
|
|
11059
|
-
...options.appearance ? { appearance: options.appearance } : {}
|
|
11060
|
-
};
|
|
11061
|
-
const offered = compareModes(options.modes, options.mode);
|
|
11062
|
-
const modeList = offered.modes;
|
|
11063
|
-
let mode = offered.mode;
|
|
11064
|
-
let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
|
|
11065
|
-
let loupeOn = options.loupe ?? false;
|
|
11066
|
-
let threshold = options.threshold ?? 4 / 255;
|
|
11067
|
-
let fpsOpt = options.fps;
|
|
11068
|
-
const bound = typeof options.timeline === "object" ? options.timeline : null;
|
|
11069
|
-
const fps = () => fpsOpt ?? bound?.fps ?? 30;
|
|
11070
|
-
const auto = container.clientHeight < 1;
|
|
11071
|
-
const el = (tag, cls) => {
|
|
11072
|
-
const n = doc.createElement(tag);
|
|
11073
|
-
n.className = cls;
|
|
11074
|
-
return n;
|
|
11075
|
-
};
|
|
11076
|
-
const root = el("div", "knobs-compare");
|
|
11077
|
-
root.tabIndex = 0;
|
|
11078
|
-
root.setAttribute("role", "group");
|
|
11079
|
-
root.setAttribute("aria-roledescription", "compare");
|
|
11080
|
-
root.setAttribute("aria-label", options.label ?? "Compare");
|
|
11081
|
-
root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
|
|
11082
|
-
applyLook2(root, options);
|
|
11083
|
-
root.dataset.mode = mode;
|
|
11084
|
-
if (auto)
|
|
11085
|
-
root.dataset.auto = "";
|
|
11086
|
-
const bar = el("div", "knobs-cmp-bar");
|
|
11087
|
-
const modes = el("div", "knobs-cmp-modes");
|
|
11088
|
-
modes.setAttribute("role", "radiogroup");
|
|
11089
|
-
modes.setAttribute("aria-label", "Mode");
|
|
11090
|
-
modes.hidden = modeList.length < 2;
|
|
11091
|
-
const modeBtns = modeList.map((m, i) => {
|
|
11092
|
-
const b = el("button", "knobs-cmp-mode");
|
|
11093
|
-
b.type = "button";
|
|
11094
|
-
b.tabIndex = -1;
|
|
11095
|
-
b.textContent = MODE_LABEL[m].short;
|
|
11096
|
-
b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
|
|
11097
|
-
b.setAttribute("role", "radio");
|
|
11098
|
-
b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
|
|
11099
|
-
b.addEventListener("click", () => {
|
|
11100
|
-
setMode(m);
|
|
11101
|
-
root.focus({ preventScroll: true });
|
|
11102
|
-
});
|
|
11103
|
-
modes.append(b);
|
|
11104
|
-
return b;
|
|
11105
|
-
});
|
|
11106
|
-
const toggleBtn = (name, label, title) => {
|
|
11107
|
-
const b = el("button", "knobs-cmp-toggle");
|
|
11108
|
-
b.type = "button";
|
|
11109
|
-
b.tabIndex = -1;
|
|
11110
|
-
b.innerHTML = icon(name);
|
|
11111
|
-
b.setAttribute("aria-label", label);
|
|
11112
|
-
b.title = title;
|
|
11113
|
-
return b;
|
|
11114
|
-
};
|
|
11115
|
-
const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
|
|
11116
|
-
loupeBtn.addEventListener("click", () => {
|
|
11117
|
-
setLoupe(!loupeOn);
|
|
11118
|
-
root.focus({ preventScroll: true });
|
|
11119
|
-
});
|
|
11120
|
-
const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
|
|
11121
|
-
loopBtn.setAttribute("aria-keyshortcuts", "L");
|
|
11122
|
-
loopBtn.addEventListener("click", () => {
|
|
11123
|
-
api.setLoop(!loopNow());
|
|
11124
|
-
root.focus({ preventScroll: true });
|
|
11125
|
-
});
|
|
11126
|
-
bar.append(modes, el("span", "knobs-cmp-fill"), loopBtn, loupeBtn);
|
|
11127
|
-
const stage = el("div", "knobs-cmp-stage");
|
|
11128
|
-
const canvas = el("canvas", "knobs-cmp-canvas");
|
|
11129
|
-
canvas.setAttribute("aria-hidden", "true");
|
|
11130
|
-
const split = el("div", "knobs-cmp-split");
|
|
11131
|
-
split.setAttribute("aria-hidden", "true");
|
|
11132
|
-
const zoomPill = el("div", "knobs-cmp-zoom");
|
|
11133
|
-
const zoomPct = el("span", "knobs-cmp-zoom-pct");
|
|
11134
|
-
const zoomReset = el("button", "knobs-cmp-zoom-reset");
|
|
11135
|
-
zoomReset.type = "button";
|
|
11136
|
-
zoomReset.tabIndex = -1;
|
|
11137
|
-
zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
|
|
11138
|
-
zoomReset.setAttribute("aria-label", "Reset zoom");
|
|
11139
|
-
zoomReset.title = "Fit · 0 or double-click";
|
|
11140
|
-
zoomReset.addEventListener("click", () => {
|
|
11141
|
-
api.setZoom(null);
|
|
11142
|
-
root.focus({ preventScroll: true });
|
|
11143
|
-
});
|
|
11144
|
-
zoomPill.append(zoomPct, zoomReset);
|
|
11145
|
-
const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
|
|
11146
|
-
labels[0].dataset.side = "before";
|
|
11147
|
-
labels[1].dataset.side = "after";
|
|
11148
|
-
const setLabel = (index, text) => {
|
|
11149
|
-
const l = labels[index];
|
|
11150
|
-
if (!l.firstElementChild) {
|
|
11151
|
-
const mark = el("span", "knobs-cmp-mark");
|
|
11152
|
-
mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
|
|
11153
|
-
l.append(mark, el("span", "knobs-cmp-name"));
|
|
11154
|
-
}
|
|
11155
|
-
const name = l.lastElementChild;
|
|
11156
|
-
if (name.textContent !== text)
|
|
11157
|
-
name.textContent = text;
|
|
11158
|
-
};
|
|
11159
|
-
const seam = el("div", "knobs-cmp-seam");
|
|
11160
|
-
seam.setAttribute("aria-hidden", "true");
|
|
11161
|
-
const handle = el("div", "knobs-cmp-handle");
|
|
11162
|
-
handle.setAttribute("role", "slider");
|
|
11163
|
-
handle.setAttribute("aria-label", "Wipe");
|
|
11164
|
-
handle.setAttribute("aria-valuemin", "0");
|
|
11165
|
-
handle.setAttribute("aria-valuemax", "100");
|
|
11166
|
-
const grip = el("span", "knobs-cmp-grip");
|
|
11167
|
-
grip.setAttribute("aria-hidden", "true");
|
|
11168
|
-
handle.append(grip);
|
|
11169
|
-
const note = el("div", "knobs-cmp-note");
|
|
11170
|
-
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
11171
|
-
root.append(bar, stage);
|
|
11172
|
-
const wantsStrip = options.strip === true;
|
|
11173
|
-
let tl = bound;
|
|
11174
|
-
let ownTimeline = false;
|
|
11175
|
-
let stripHost = null;
|
|
11176
|
-
if (wantsStrip) {
|
|
11177
|
-
stripHost = el("div", "knobs-cmp-strip-host");
|
|
11178
|
-
root.append(stripHost);
|
|
11179
|
-
}
|
|
11180
|
-
if (!tl && (options.timeline === true || wantsStrip)) {
|
|
11181
|
-
const host = el("div", "knobs-cmp-timeline-host");
|
|
11182
|
-
root.append(host);
|
|
11183
|
-
tl = timeline(host, {
|
|
11184
|
-
...look,
|
|
11185
|
-
variant: "line",
|
|
11186
|
-
duration: 1,
|
|
11187
|
-
fps: fps(),
|
|
11188
|
-
arrows: { step: 0.5, shift: 2 },
|
|
11189
|
-
label: "Compare time",
|
|
11190
|
-
...options.hints === false ? { hints: false } : {}
|
|
11191
|
-
});
|
|
11192
|
-
ownTimeline = true;
|
|
11193
|
-
}
|
|
11194
|
-
container.append(root);
|
|
11195
|
-
const sides = [null, null];
|
|
11196
|
-
let duration = 0;
|
|
11197
|
-
let renderer = "dom";
|
|
11198
|
-
let pending = false;
|
|
11199
|
-
let gpu = null;
|
|
11200
|
-
const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
11201
|
-
const fade = new Spring(1, FADE3.omega, FADE3.zeta, 0.001);
|
|
11202
|
-
let fromMode = mode;
|
|
11203
|
-
let toMode = mode;
|
|
11204
|
-
const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
|
|
11205
|
-
let wipeCatching = false;
|
|
11206
|
-
const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
|
|
11207
|
-
let loupeHeld = false;
|
|
11208
|
-
let pointer = null;
|
|
11209
|
-
let drag = null;
|
|
11210
|
-
let tap = null;
|
|
11211
|
-
const touches = new Map;
|
|
11212
|
-
let pinch = null;
|
|
11213
|
-
const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11214
|
-
const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11215
|
-
const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
|
|
11216
|
-
let viewSent = "0.0000,0.0000,1.0000|";
|
|
11217
|
-
let ownLoop = null;
|
|
11218
|
-
let scrubbing = false;
|
|
11219
|
-
let stageW = 1;
|
|
11220
|
-
let stageH = 1;
|
|
11221
|
-
let stageRect = null;
|
|
11222
|
-
let accent = [0.49, 0.81, 0.61];
|
|
11223
|
-
let labelW = [0, 0];
|
|
11224
|
-
let frameRadius = 0;
|
|
11225
|
-
let dirty = true;
|
|
11226
|
-
let disposed = false;
|
|
11227
|
-
let near = options.lazy === false;
|
|
11228
|
-
let onScreen = options.lazy === false;
|
|
11229
|
-
let releaseTimer = 0;
|
|
11230
|
-
let ownTime = 0;
|
|
11231
|
-
let ownPlaying = false;
|
|
11232
|
-
const videoOf = (s) => s && s.kind === "video" && s.loaded ? s.el : null;
|
|
11233
|
-
const master = () => videoOf(sides[1]) ?? videoOf(sides[0]);
|
|
11234
|
-
const follower = () => {
|
|
11235
|
-
const m = master();
|
|
11236
|
-
const v = videoOf(sides[0]);
|
|
11237
|
-
return m && v && v !== m ? v : null;
|
|
11238
|
-
};
|
|
11239
|
-
const now = () => tl ? tl.time : ownTime;
|
|
11240
|
-
const isPlaying = () => tl ? tl.playing : ownPlaying;
|
|
11241
|
-
const labelOf = (index) => sides[index]?.source.label ?? (index === 0 ? options.before.label : options.after.label) ?? (index === 0 ? "Before" : "After");
|
|
11242
|
-
const sizeOf = (s) => {
|
|
11243
|
-
if (s.size.w > 0)
|
|
11244
|
-
return s.size;
|
|
11245
|
-
const other = sides[s.index === 0 ? 1 : 0];
|
|
11246
|
-
if (other && other.size.w > 0)
|
|
11247
|
-
return other.size;
|
|
11248
|
-
return { w: options.aspect ?? 16 / 9, h: 1 };
|
|
11249
|
-
};
|
|
11250
|
-
const wantOf = (s) => {
|
|
11251
|
-
const v = s.el;
|
|
11252
|
-
const f = frameOf(now(), fps());
|
|
11253
|
-
return Number.isFinite(v.duration) ? Math.min(f, lastFrame(v.duration, fps())) : f;
|
|
11254
|
-
};
|
|
11255
|
-
const emitFrame = (s, frame, source) => root.dispatchEvent(new CustomEvent("knobs-compare-frame", { detail: { side: s.index, frame, source } }));
|
|
11256
|
-
const setCached = (s, on) => {
|
|
11257
|
-
if (renderer === "dom")
|
|
11258
|
-
s.preview.style.display = on ? "" : "none";
|
|
11259
|
-
};
|
|
11260
|
-
const showVideo = (s) => {
|
|
11261
|
-
const v = s.el;
|
|
11262
|
-
if (renderer === "gpu") {
|
|
11263
|
-
if (!gpu)
|
|
11264
|
-
return;
|
|
11265
|
-
if (!gpu.upload(s.index, v, v.videoWidth, v.videoHeight))
|
|
11266
|
-
return miss(s);
|
|
11267
|
-
s.misses = 0;
|
|
11268
|
-
}
|
|
11269
|
-
const f = frameOf(v.currentTime, fps());
|
|
11270
|
-
s.shown = { frame: f, cached: false };
|
|
11271
|
-
setCached(s, false);
|
|
11272
|
-
emitFrame(s, f, "video");
|
|
11273
|
-
requestDraw();
|
|
11274
|
-
};
|
|
11275
|
-
const showCached = (s, c) => {
|
|
11276
|
-
if (s.shown?.cached && s.shown.frame === c.frame)
|
|
11277
|
-
return;
|
|
11278
|
-
if (renderer === "gpu") {
|
|
11279
|
-
if (!gpu || !gpu.upload(2 + s.index, c.bitmap, c.bitmap.width, c.bitmap.height))
|
|
11280
|
-
return;
|
|
11281
|
-
} else if (renderer === "dom" && !pending) {
|
|
11282
|
-
const p = s.preview;
|
|
11283
|
-
if (p.width !== c.bitmap.width || p.height !== c.bitmap.height) {
|
|
11284
|
-
p.width = c.bitmap.width;
|
|
11285
|
-
p.height = c.bitmap.height;
|
|
11286
|
-
}
|
|
11287
|
-
p.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
11288
|
-
} else
|
|
11289
|
-
return;
|
|
11290
|
-
s.shown = { frame: c.frame, cached: true };
|
|
11291
|
-
setCached(s, true);
|
|
11292
|
-
emitFrame(s, c.frame, "cache");
|
|
11293
|
-
requestDraw();
|
|
11294
|
-
};
|
|
11295
|
-
const miss = (s) => {
|
|
11296
|
-
if (s.failed || ++s.misses < 3)
|
|
11297
|
-
return;
|
|
11298
|
-
gpu?.dispose();
|
|
11299
|
-
gpu = null;
|
|
11300
|
-
setRenderer("dom");
|
|
11301
|
-
};
|
|
11302
|
-
const pushFrame = (s) => {
|
|
11303
|
-
if (!s.loaded || disposed)
|
|
11304
|
-
return;
|
|
11305
|
-
if (s.kind === "image") {
|
|
11306
|
-
const img = s.el;
|
|
11307
|
-
if (!(img.complete && img.naturalWidth))
|
|
11308
|
-
return;
|
|
11309
|
-
if (renderer === "gpu" && gpu) {
|
|
11310
|
-
if (!gpu.upload(s.index, img, img.naturalWidth, img.naturalHeight))
|
|
11311
|
-
return miss(s);
|
|
11312
|
-
}
|
|
11313
|
-
s.shown = { frame: 0, cached: false };
|
|
11314
|
-
return requestDraw();
|
|
11315
|
-
}
|
|
11316
|
-
const v = s.el;
|
|
11317
|
-
if (v.readyState < 2)
|
|
11318
|
-
return;
|
|
11319
|
-
if (isPlaying() || !s.frames)
|
|
11320
|
-
return showVideo(s);
|
|
11321
|
-
const want = wantOf(s);
|
|
11322
|
-
const at = frameOf(v.currentTime, fps());
|
|
11323
|
-
if (at === want) {
|
|
11324
|
-
showVideo(s);
|
|
11325
|
-
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
11326
|
-
return;
|
|
11327
|
-
}
|
|
11328
|
-
const c = s.frames.nearest(want);
|
|
11306
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
11329
11307
|
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
11330
11308
|
showCached(s, c);
|
|
11331
11309
|
else
|
|
@@ -11337,13 +11315,23 @@ function compare(container, options) {
|
|
|
11337
11315
|
const want = wantOf(s);
|
|
11338
11316
|
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
11339
11317
|
return;
|
|
11340
|
-
const c = s.frames.nearest(want);
|
|
11318
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
11341
11319
|
if (!c)
|
|
11342
11320
|
return;
|
|
11343
11321
|
const was = s.shown ? Math.abs(s.shown.frame - want) : Number.POSITIVE_INFINITY;
|
|
11344
11322
|
if (Math.abs(c.frame - want) < was)
|
|
11345
11323
|
showCached(s, c);
|
|
11346
11324
|
};
|
|
11325
|
+
const giveUp = (s) => {
|
|
11326
|
+
s.seek = SEEK_IDLE;
|
|
11327
|
+
if (!s.loaded || disposed)
|
|
11328
|
+
return;
|
|
11329
|
+
if (s.el.readyState >= 2)
|
|
11330
|
+
return showVideo(s);
|
|
11331
|
+
s.shown = null;
|
|
11332
|
+
setCached(s, false);
|
|
11333
|
+
requestDraw();
|
|
11334
|
+
};
|
|
11347
11335
|
const makeSide = (index, source) => {
|
|
11348
11336
|
const kind = mediaKind(source.src, source.kind);
|
|
11349
11337
|
const media = el(kind === "video" ? "video" : "img", "knobs-cmp-media");
|
|
@@ -11375,7 +11363,9 @@ function compare(container, options) {
|
|
|
11375
11363
|
frameAt: 0,
|
|
11376
11364
|
misses: 0,
|
|
11377
11365
|
failed: false,
|
|
11366
|
+
stall: stallGuard(win, () => giveUp(side)),
|
|
11378
11367
|
stop: () => {
|
|
11368
|
+
side.stall.clear();
|
|
11379
11369
|
for (const f of offs)
|
|
11380
11370
|
f();
|
|
11381
11371
|
}
|
|
@@ -11385,6 +11375,10 @@ function compare(container, options) {
|
|
|
11385
11375
|
return;
|
|
11386
11376
|
side.failed = true;
|
|
11387
11377
|
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
11378
|
+
side.stall.clear();
|
|
11379
|
+
side.seek = SEEK_IDLE;
|
|
11380
|
+
side.shown = null;
|
|
11381
|
+
setCached(side, false);
|
|
11388
11382
|
};
|
|
11389
11383
|
if (media instanceof HTMLVideoElement) {
|
|
11390
11384
|
const v = media;
|
|
@@ -11415,6 +11409,8 @@ function compare(container, options) {
|
|
|
11415
11409
|
side.seek = step.state;
|
|
11416
11410
|
if (step.apply !== null)
|
|
11417
11411
|
seekVideo(side, v, step.apply);
|
|
11412
|
+
else
|
|
11413
|
+
side.stall.clear();
|
|
11418
11414
|
pushFrame(side);
|
|
11419
11415
|
});
|
|
11420
11416
|
on("error", fail);
|
|
@@ -11447,7 +11443,7 @@ function compare(container, options) {
|
|
|
11447
11443
|
s.loaded = true;
|
|
11448
11444
|
s.failed = false;
|
|
11449
11445
|
s.el.src = s.source.src;
|
|
11450
|
-
if (s.kind === "video") {
|
|
11446
|
+
if (s.kind === "video" && caching) {
|
|
11451
11447
|
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
11452
11448
|
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
11453
11449
|
if (onScreen) {
|
|
@@ -11464,6 +11460,7 @@ function compare(container, options) {
|
|
|
11464
11460
|
if (s.el instanceof HTMLVideoElement)
|
|
11465
11461
|
s.el.load();
|
|
11466
11462
|
s.seek = SEEK_IDLE;
|
|
11463
|
+
s.stall.clear();
|
|
11467
11464
|
s.shown = null;
|
|
11468
11465
|
setCached(s, false);
|
|
11469
11466
|
s.unlisten?.();
|
|
@@ -11587,6 +11584,7 @@ function compare(container, options) {
|
|
|
11587
11584
|
v.fastSeek(t);
|
|
11588
11585
|
else
|
|
11589
11586
|
v.currentTime = t;
|
|
11587
|
+
s.stall.arm();
|
|
11590
11588
|
};
|
|
11591
11589
|
const showFrame = (t, used) => {
|
|
11592
11590
|
for (const s of sides) {
|
|
@@ -11598,14 +11596,21 @@ function compare(container, options) {
|
|
|
11598
11596
|
v.playbackRate = 1;
|
|
11599
11597
|
}
|
|
11600
11598
|
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
if (
|
|
11608
|
-
|
|
11599
|
+
if (used) {
|
|
11600
|
+
if (scrubbing)
|
|
11601
|
+
s.frames?.follow(want);
|
|
11602
|
+
else
|
|
11603
|
+
s.frames?.focus(want);
|
|
11604
|
+
}
|
|
11605
|
+
if (!(scrubbing && s.frames?.covers(want))) {
|
|
11606
|
+
const at = frameMidpoint(want, fps(), v.duration);
|
|
11607
|
+
const step = requestSeek(s.seek, at, v.currentTime, fps());
|
|
11608
|
+
s.seek = step.state;
|
|
11609
|
+
if (step.apply !== null)
|
|
11610
|
+
seekVideo(s, v, step.apply);
|
|
11611
|
+
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
11612
|
+
pushFrame(s);
|
|
11613
|
+
}
|
|
11609
11614
|
refreshCached(s);
|
|
11610
11615
|
}
|
|
11611
11616
|
requestDraw();
|
|
@@ -12236,7 +12241,7 @@ function compare(container, options) {
|
|
|
12236
12241
|
holdLoupe(true);
|
|
12237
12242
|
return;
|
|
12238
12243
|
case "l":
|
|
12239
|
-
if (e.altKey || e.shiftKey)
|
|
12244
|
+
if (e.altKey || e.shiftKey || loopBtn.hidden)
|
|
12240
12245
|
return;
|
|
12241
12246
|
claim();
|
|
12242
12247
|
return api.setLoop(!loopNow());
|
|
@@ -12259,6 +12264,9 @@ function compare(container, options) {
|
|
|
12259
12264
|
return glideZoom(ZOOM_FIT);
|
|
12260
12265
|
}
|
|
12261
12266
|
};
|
|
12267
|
+
const syncLoopButton = () => {
|
|
12268
|
+
loopBtn.hidden = !sides.some((s) => s.kind === "video");
|
|
12269
|
+
};
|
|
12262
12270
|
const replace = (index, source) => {
|
|
12263
12271
|
const old = sides[index];
|
|
12264
12272
|
old.stop();
|
|
@@ -12267,6 +12275,7 @@ function compare(container, options) {
|
|
|
12267
12275
|
old.el.replaceWith(s.el);
|
|
12268
12276
|
old.preview.replaceWith(s.preview);
|
|
12269
12277
|
sides[index] = s;
|
|
12278
|
+
syncLoopButton();
|
|
12270
12279
|
note.textContent = "";
|
|
12271
12280
|
setLabel(index, labelOf(index));
|
|
12272
12281
|
if (near)
|
|
@@ -12281,6 +12290,7 @@ function compare(container, options) {
|
|
|
12281
12290
|
stage.insertBefore(sides[0].preview, labels[0]);
|
|
12282
12291
|
setLabel(0, labelOf(0));
|
|
12283
12292
|
setLabel(1, labelOf(1));
|
|
12293
|
+
syncLoopButton();
|
|
12284
12294
|
loupeBtn.setAttribute("aria-pressed", String(loupeOn));
|
|
12285
12295
|
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
12286
12296
|
const modePill = modeList.length > 1 ? selectionPill(modes, {
|
|
@@ -12359,7 +12369,7 @@ function compare(container, options) {
|
|
|
12359
12369
|
label: MODE_LABEL[m].long,
|
|
12360
12370
|
activate: () => setMode(m)
|
|
12361
12371
|
})) : [],
|
|
12362
|
-
{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
|
|
12372
|
+
...loopBtn.hidden ? [] : [{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) }],
|
|
12363
12373
|
...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
|
|
12364
12374
|
...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
|
|
12365
12375
|
...mode === "wipe" ? [
|
|
@@ -12370,14 +12380,6 @@ function compare(container, options) {
|
|
|
12370
12380
|
}
|
|
12371
12381
|
] : []
|
|
12372
12382
|
], { listen: false, rank: 1 });
|
|
12373
|
-
const strip = wantsStrip && stripHost && tl ? compareStrip(stripHost, {
|
|
12374
|
-
...look,
|
|
12375
|
-
before: options.before,
|
|
12376
|
-
after: options.after,
|
|
12377
|
-
timeline: tl,
|
|
12378
|
-
...fpsOpt ? { fps: fpsOpt } : {},
|
|
12379
|
-
threshold
|
|
12380
|
-
}) : null;
|
|
12381
12383
|
const api = {
|
|
12382
12384
|
element: root,
|
|
12383
12385
|
get mode() {
|
|
@@ -12403,7 +12405,6 @@ function compare(container, options) {
|
|
|
12403
12405
|
return renderer;
|
|
12404
12406
|
},
|
|
12405
12407
|
timeline: tl,
|
|
12406
|
-
strip,
|
|
12407
12408
|
get zoom() {
|
|
12408
12409
|
return clampZoom(zoomNow());
|
|
12409
12410
|
},
|
|
@@ -12425,8 +12426,6 @@ function compare(container, options) {
|
|
|
12425
12426
|
replace(1, after);
|
|
12426
12427
|
else if (next.after)
|
|
12427
12428
|
setLabel(1, next.after.label ?? "After");
|
|
12428
|
-
if (before || after)
|
|
12429
|
-
strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
|
|
12430
12429
|
if (next.before || next.after)
|
|
12431
12430
|
readColors();
|
|
12432
12431
|
if (next.mode !== undefined)
|
|
@@ -12493,7 +12492,6 @@ function compare(container, options) {
|
|
|
12493
12492
|
if (ownTimeline)
|
|
12494
12493
|
tl.dispose();
|
|
12495
12494
|
}
|
|
12496
|
-
strip?.dispose();
|
|
12497
12495
|
for (const s of sides) {
|
|
12498
12496
|
s.stop();
|
|
12499
12497
|
unloadSide(s);
|
|
@@ -12517,7 +12515,6 @@ var init_compare = __esm(() => {
|
|
|
12517
12515
|
init_compare_frames();
|
|
12518
12516
|
init_compare_gpu();
|
|
12519
12517
|
init_compare_model();
|
|
12520
|
-
init_compare_strip();
|
|
12521
12518
|
init_icons();
|
|
12522
12519
|
init_look();
|
|
12523
12520
|
init_rgba();
|
|
@@ -12537,204 +12534,672 @@ var init_compare = __esm(() => {
|
|
|
12537
12534
|
wipe: { short: "Wipe", long: "Wipe" },
|
|
12538
12535
|
difference: { short: "Difference", long: "Difference" }
|
|
12539
12536
|
};
|
|
12540
|
-
});
|
|
12537
|
+
});
|
|
12538
|
+
|
|
12539
|
+
// src/ui/player.ts
|
|
12540
|
+
function player(container, options) {
|
|
12541
|
+
const doc = container.ownerDocument;
|
|
12542
|
+
const el = (tag, cls) => {
|
|
12543
|
+
const n = doc.createElement(tag);
|
|
12544
|
+
n.className = cls;
|
|
12545
|
+
return n;
|
|
12546
|
+
};
|
|
12547
|
+
const look = {
|
|
12548
|
+
...options.preset ? { preset: options.preset } : {},
|
|
12549
|
+
...options.theme ? { theme: options.theme } : {},
|
|
12550
|
+
...options.appearance ? { appearance: options.appearance } : {}
|
|
12551
|
+
};
|
|
12552
|
+
const root = el("div", "knobs-player");
|
|
12553
|
+
root.setAttribute("role", "group");
|
|
12554
|
+
root.setAttribute("aria-label", options.label ?? "Video");
|
|
12555
|
+
applyLook2(root, options);
|
|
12556
|
+
const frame = el("div", "knobs-player-frame");
|
|
12557
|
+
frame.style.setProperty("--kp-aspect", String(options.aspect ?? 16 / 9));
|
|
12558
|
+
const v = el("video", "knobs-player-media");
|
|
12559
|
+
v.muted = options.muted ?? true;
|
|
12560
|
+
v.defaultMuted = v.muted;
|
|
12561
|
+
v.playsInline = true;
|
|
12562
|
+
v.preload = "metadata";
|
|
12563
|
+
v.setAttribute("playsinline", "");
|
|
12564
|
+
if (options.poster)
|
|
12565
|
+
v.poster = options.poster;
|
|
12566
|
+
if (options.captions) {
|
|
12567
|
+
const track = doc.createElement("track");
|
|
12568
|
+
track.kind = "captions";
|
|
12569
|
+
track.label = "Captions";
|
|
12570
|
+
track.srclang = doc.documentElement.lang || "en";
|
|
12571
|
+
track.default = true;
|
|
12572
|
+
track.src = options.captions;
|
|
12573
|
+
v.append(track);
|
|
12574
|
+
}
|
|
12575
|
+
const preview = el("canvas", "knobs-player-preview");
|
|
12576
|
+
preview.setAttribute("aria-hidden", "true");
|
|
12577
|
+
preview.hidden = true;
|
|
12578
|
+
frame.append(v, preview);
|
|
12579
|
+
const host = el("div", "knobs-player-line");
|
|
12580
|
+
root.append(frame, host);
|
|
12581
|
+
container.append(root);
|
|
12582
|
+
const markers = (options.chapters ?? []).map((c, i) => ({
|
|
12583
|
+
id: c.id ?? `c${i}`,
|
|
12584
|
+
time: c.start,
|
|
12585
|
+
label: c.label
|
|
12586
|
+
}));
|
|
12587
|
+
const tl = timeline(host, {
|
|
12588
|
+
...look,
|
|
12589
|
+
variant: "line",
|
|
12590
|
+
duration: 1,
|
|
12591
|
+
fps: options.fps ?? 30,
|
|
12592
|
+
markers,
|
|
12593
|
+
repeat: options.loop ?? true,
|
|
12594
|
+
arrows: { step: 5, shift: 10 },
|
|
12595
|
+
label: options.label ? `${options.label} time` : "Video time",
|
|
12596
|
+
...options.hints === false ? { hints: false } : {}
|
|
12597
|
+
});
|
|
12598
|
+
const onFrame = () => root.setAttribute("data-ready", "");
|
|
12599
|
+
let pending = null;
|
|
12600
|
+
const seekVideo = (t) => {
|
|
12601
|
+
if (v.readyState < 1)
|
|
12602
|
+
return;
|
|
12603
|
+
if (v.seeking)
|
|
12604
|
+
pending = t;
|
|
12605
|
+
else
|
|
12606
|
+
v.currentTime = t;
|
|
12607
|
+
};
|
|
12608
|
+
const fps = options.fps ?? 30;
|
|
12609
|
+
let frames = null;
|
|
12610
|
+
let unlistenFrames = null;
|
|
12611
|
+
let scrubbing = false;
|
|
12612
|
+
let previewing = null;
|
|
12613
|
+
const want = () => frameOf(tl.time, fps);
|
|
12614
|
+
const emitFrame = (frame, source) => root.dispatchEvent(new CustomEvent("knobs-player-frame", { detail: { frame, source } }));
|
|
12615
|
+
const drawPreview = (c) => {
|
|
12616
|
+
if (preview.width !== c.bitmap.width || preview.height !== c.bitmap.height) {
|
|
12617
|
+
preview.width = c.bitmap.width;
|
|
12618
|
+
preview.height = c.bitmap.height;
|
|
12619
|
+
}
|
|
12620
|
+
preview.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
12621
|
+
preview.hidden = false;
|
|
12622
|
+
previewing = c.frame;
|
|
12623
|
+
emitFrame(c.frame, "cache");
|
|
12624
|
+
};
|
|
12625
|
+
const hidePreview = () => {
|
|
12626
|
+
if (previewing === null)
|
|
12627
|
+
return;
|
|
12628
|
+
previewing = null;
|
|
12629
|
+
preview.hidden = true;
|
|
12630
|
+
};
|
|
12631
|
+
const showNearest = () => {
|
|
12632
|
+
if (!frames || !scrubbing)
|
|
12633
|
+
return;
|
|
12634
|
+
const w = want();
|
|
12635
|
+
const c = frames.nearest(w, Number.POSITIVE_INFINITY, true);
|
|
12636
|
+
if (!c || c.frame === previewing)
|
|
12637
|
+
return;
|
|
12638
|
+
const shown = previewing ?? frameOf(v.currentTime, fps);
|
|
12639
|
+
if (Math.abs(c.frame - w) < Math.abs(shown - w))
|
|
12640
|
+
drawPreview(c);
|
|
12641
|
+
};
|
|
12642
|
+
const caching = scrubCaching(options.scrubCache, doc.defaultView);
|
|
12643
|
+
const startFrames = () => {
|
|
12644
|
+
if (frames || !caching)
|
|
12645
|
+
return;
|
|
12646
|
+
frames = acquireFrames(doc, options.src, fps);
|
|
12647
|
+
frames.visible(true);
|
|
12648
|
+
unlistenFrames = frames.listen(showNearest);
|
|
12649
|
+
};
|
|
12650
|
+
const stall = stallGuard(doc.defaultView, () => {
|
|
12651
|
+
pending = null;
|
|
12652
|
+
hidePreview();
|
|
12653
|
+
});
|
|
12654
|
+
const onError = () => {
|
|
12655
|
+
stall.clear();
|
|
12656
|
+
pending = null;
|
|
12657
|
+
hidePreview();
|
|
12658
|
+
};
|
|
12659
|
+
let reported = 0;
|
|
12660
|
+
const onSeeked = () => {
|
|
12661
|
+
if (pending !== null) {
|
|
12662
|
+
const t = pending;
|
|
12663
|
+
pending = null;
|
|
12664
|
+
v.currentTime = t;
|
|
12665
|
+
return;
|
|
12666
|
+
}
|
|
12667
|
+
stall.clear();
|
|
12668
|
+
const at = frameOf(v.currentTime, fps);
|
|
12669
|
+
emitFrame(at, "video");
|
|
12670
|
+
if (!scrubbing || previewing !== null && Math.abs(at - want()) <= Math.abs(previewing - want()))
|
|
12671
|
+
hidePreview();
|
|
12672
|
+
if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
|
|
12673
|
+
return;
|
|
12674
|
+
reported = v.currentTime;
|
|
12675
|
+
options.onSeek?.(reported);
|
|
12676
|
+
};
|
|
12677
|
+
const onMeta = () => {
|
|
12678
|
+
onFrame();
|
|
12679
|
+
if (v.videoWidth)
|
|
12680
|
+
frame.style.setProperty("--kp-aspect", String(v.videoWidth / v.videoHeight));
|
|
12681
|
+
if (Number.isFinite(v.duration))
|
|
12682
|
+
tl.update({ duration: v.duration });
|
|
12683
|
+
};
|
|
12684
|
+
v.addEventListener("loadedmetadata", onMeta);
|
|
12685
|
+
v.addEventListener("loadeddata", onFrame);
|
|
12686
|
+
v.addEventListener("seeked", onSeeked);
|
|
12687
|
+
v.addEventListener("error", onError);
|
|
12688
|
+
const unlisten = tl.listen({
|
|
12689
|
+
onSeek(t, source) {
|
|
12690
|
+
if (source === "play") {
|
|
12691
|
+
if (Math.abs(v.currentTime - t) > 0.25)
|
|
12692
|
+
v.currentTime = t;
|
|
12693
|
+
return;
|
|
12694
|
+
}
|
|
12695
|
+
if (scrubbing && frames) {
|
|
12696
|
+
frames.follow(want());
|
|
12697
|
+
showNearest();
|
|
12698
|
+
if (frames.covers(want()))
|
|
12699
|
+
return;
|
|
12700
|
+
}
|
|
12701
|
+
if (sameFrame(v.currentTime, t, fps)) {
|
|
12702
|
+
if (!scrubbing)
|
|
12703
|
+
hidePreview();
|
|
12704
|
+
return;
|
|
12705
|
+
}
|
|
12706
|
+
seekVideo(t);
|
|
12707
|
+
},
|
|
12708
|
+
onPlay(on) {
|
|
12709
|
+
if (on) {
|
|
12710
|
+
hidePreview();
|
|
12711
|
+
v.play().catch(() => tl.pause());
|
|
12712
|
+
} else {
|
|
12713
|
+
v.pause();
|
|
12714
|
+
reported = tl.time;
|
|
12715
|
+
}
|
|
12716
|
+
root.toggleAttribute("data-playing", on);
|
|
12717
|
+
options.onPlay?.(on, tl.time);
|
|
12718
|
+
},
|
|
12719
|
+
onScrub(on) {
|
|
12720
|
+
scrubbing = on;
|
|
12721
|
+
if (on) {
|
|
12722
|
+
v.pause();
|
|
12723
|
+
startFrames();
|
|
12724
|
+
return;
|
|
12725
|
+
}
|
|
12726
|
+
if (v.readyState < 1)
|
|
12727
|
+
return hidePreview();
|
|
12728
|
+
const at = releaseSeek(v.currentTime, tl.time, fps, v.duration);
|
|
12729
|
+
if (at === null) {
|
|
12730
|
+
pending = null;
|
|
12731
|
+
if (!v.seeking)
|
|
12732
|
+
return hidePreview();
|
|
12733
|
+
} else
|
|
12734
|
+
seekVideo(at);
|
|
12735
|
+
stall.arm();
|
|
12736
|
+
}
|
|
12737
|
+
});
|
|
12738
|
+
tl.drive(() => {
|
|
12739
|
+
if (v.readyState < 1)
|
|
12740
|
+
return Number.NaN;
|
|
12741
|
+
if (v.seeking)
|
|
12742
|
+
return tl.time;
|
|
12743
|
+
return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
|
|
12744
|
+
});
|
|
12745
|
+
const onClick = () => {
|
|
12746
|
+
tl.focus();
|
|
12747
|
+
tl.toggle();
|
|
12748
|
+
};
|
|
12749
|
+
frame.addEventListener("click", onClick);
|
|
12750
|
+
root.addEventListener("pointerenter", startFrames);
|
|
12751
|
+
v.src = options.src;
|
|
12752
|
+
const api = {
|
|
12753
|
+
element: root,
|
|
12754
|
+
video: v,
|
|
12755
|
+
timeline: tl,
|
|
12756
|
+
get time() {
|
|
12757
|
+
return tl.time;
|
|
12758
|
+
},
|
|
12759
|
+
get playing() {
|
|
12760
|
+
return tl.playing;
|
|
12761
|
+
},
|
|
12762
|
+
get duration() {
|
|
12763
|
+
return Number.isFinite(v.duration) ? v.duration : 0;
|
|
12764
|
+
},
|
|
12765
|
+
play: () => tl.play(),
|
|
12766
|
+
pause: () => tl.pause(),
|
|
12767
|
+
toggle: () => tl.toggle(),
|
|
12768
|
+
seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
|
|
12769
|
+
seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
|
|
12770
|
+
focus: () => tl.focus(),
|
|
12771
|
+
dispose() {
|
|
12772
|
+
unlisten();
|
|
12773
|
+
tl.drive(null);
|
|
12774
|
+
tl.dispose();
|
|
12775
|
+
v.pause();
|
|
12776
|
+
frame.removeEventListener("click", onClick);
|
|
12777
|
+
root.removeEventListener("pointerenter", startFrames);
|
|
12778
|
+
unlistenFrames?.();
|
|
12779
|
+
if (frames) {
|
|
12780
|
+
frames.visible(false);
|
|
12781
|
+
frames.release();
|
|
12782
|
+
frames = null;
|
|
12783
|
+
}
|
|
12784
|
+
v.removeEventListener("loadedmetadata", onMeta);
|
|
12785
|
+
v.removeEventListener("loadeddata", onFrame);
|
|
12786
|
+
v.removeEventListener("seeked", onSeeked);
|
|
12787
|
+
v.removeEventListener("error", onError);
|
|
12788
|
+
stall.clear();
|
|
12789
|
+
v.removeAttribute("src");
|
|
12790
|
+
v.load();
|
|
12791
|
+
root.remove();
|
|
12792
|
+
}
|
|
12793
|
+
};
|
|
12794
|
+
return api;
|
|
12795
|
+
}
|
|
12796
|
+
var init_player = __esm(() => {
|
|
12797
|
+
init_compare_frames();
|
|
12798
|
+
init_compare_model();
|
|
12799
|
+
init_look();
|
|
12800
|
+
init_timeline();
|
|
12801
|
+
});
|
|
12802
|
+
|
|
12803
|
+
// src/ui/react.tsx
|
|
12804
|
+
import {
|
|
12805
|
+
Children,
|
|
12806
|
+
useEffect,
|
|
12807
|
+
useLayoutEffect,
|
|
12808
|
+
useRef,
|
|
12809
|
+
useState
|
|
12810
|
+
} from "react";
|
|
12811
|
+
import { createPortal } from "react-dom";
|
|
12812
|
+
init_diff();
|
|
12813
|
+
|
|
12814
|
+
// src/ui/follow.ts
|
|
12815
|
+
function propChanges(initial, same = Object.is) {
|
|
12816
|
+
let last = { ...initial };
|
|
12817
|
+
return (next) => {
|
|
12818
|
+
const changed = {};
|
|
12819
|
+
for (const key of Object.keys(next))
|
|
12820
|
+
if (!same(next[key], last[key]))
|
|
12821
|
+
changed[key] = next[key];
|
|
12822
|
+
last = { ...next };
|
|
12823
|
+
return changed;
|
|
12824
|
+
};
|
|
12825
|
+
}
|
|
12826
|
+
var anyChanged = (changed) => Object.keys(changed).length > 0;
|
|
12541
12827
|
|
|
12542
|
-
// src/ui/
|
|
12543
|
-
|
|
12828
|
+
// src/ui/react.tsx
|
|
12829
|
+
init_compare();
|
|
12830
|
+
|
|
12831
|
+
// src/ui/compare-strip.ts
|
|
12832
|
+
init_compare_frames();
|
|
12833
|
+
init_compare_model();
|
|
12834
|
+
init_look();
|
|
12835
|
+
init_rgba();
|
|
12836
|
+
init_timeline_model();
|
|
12837
|
+
var BAR = 7;
|
|
12838
|
+
var GAP = 2;
|
|
12839
|
+
var DIFF = { w: 32, h: 18 };
|
|
12840
|
+
var DIFFS_PER_DRAW = 6;
|
|
12841
|
+
function compareStrip(container, options) {
|
|
12544
12842
|
const doc = container.ownerDocument;
|
|
12545
|
-
const
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
const
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
};
|
|
12555
|
-
const root = el("div", "knobs-player");
|
|
12556
|
-
root.setAttribute("role", "group");
|
|
12557
|
-
root.setAttribute("aria-label", options.label ?? "Video");
|
|
12843
|
+
const win = doc.defaultView;
|
|
12844
|
+
const timeline = options.timeline;
|
|
12845
|
+
const fps = () => options.fps ?? timeline.fps;
|
|
12846
|
+
const align = options.align ?? "track";
|
|
12847
|
+
const threshold = options.threshold ?? 4 / 255;
|
|
12848
|
+
const root = doc.createElement("div");
|
|
12849
|
+
root.className = "knobs-cmp-strip";
|
|
12850
|
+
root.setAttribute("role", "img");
|
|
12851
|
+
root.setAttribute("aria-label", "Frame strip: Before over After, with how much they differ");
|
|
12558
12852
|
applyLook2(root, options);
|
|
12559
|
-
const
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
v.muted = options.muted ?? true;
|
|
12563
|
-
v.defaultMuted = v.muted;
|
|
12564
|
-
v.playsInline = true;
|
|
12565
|
-
v.preload = "metadata";
|
|
12566
|
-
v.setAttribute("playsinline", "");
|
|
12567
|
-
if (options.poster)
|
|
12568
|
-
v.poster = options.poster;
|
|
12569
|
-
if (options.captions) {
|
|
12570
|
-
const track = doc.createElement("track");
|
|
12571
|
-
track.kind = "captions";
|
|
12572
|
-
track.label = "Captions";
|
|
12573
|
-
track.srclang = doc.documentElement.lang || "en";
|
|
12574
|
-
track.default = true;
|
|
12575
|
-
track.src = options.captions;
|
|
12576
|
-
v.append(track);
|
|
12577
|
-
}
|
|
12578
|
-
frame.append(v);
|
|
12579
|
-
const host = el("div", "knobs-player-line");
|
|
12580
|
-
root.append(frame, host);
|
|
12853
|
+
const canvas = doc.createElement("canvas");
|
|
12854
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
12855
|
+
root.append(canvas);
|
|
12581
12856
|
container.append(root);
|
|
12582
|
-
const
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
const
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
repeat: options.loop ?? true,
|
|
12594
|
-
arrows: { step: 5, shift: 10 },
|
|
12595
|
-
label: options.label ? `${options.label} time` : "Video time",
|
|
12596
|
-
...options.hints === false ? { hints: false } : {}
|
|
12857
|
+
const diffs = new Map;
|
|
12858
|
+
const pairDiffs = new Map;
|
|
12859
|
+
let disposed = false;
|
|
12860
|
+
let near = false;
|
|
12861
|
+
let onScreen = false;
|
|
12862
|
+
const makeSide = (source) => ({
|
|
12863
|
+
source,
|
|
12864
|
+
kind: mediaKind(source.src, source.kind),
|
|
12865
|
+
frames: null,
|
|
12866
|
+
still: null,
|
|
12867
|
+
seen: false
|
|
12597
12868
|
});
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
const
|
|
12601
|
-
if (
|
|
12869
|
+
let sides = [makeSide(options.before), makeSide(options.after)];
|
|
12870
|
+
const offs = new Map;
|
|
12871
|
+
const load = (s) => {
|
|
12872
|
+
if (s.frames || s.still)
|
|
12602
12873
|
return;
|
|
12603
|
-
if (
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12874
|
+
if (s.kind === "video") {
|
|
12875
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
12876
|
+
offs.set(s, s.frames.listen(schedule));
|
|
12877
|
+
if (onScreen) {
|
|
12878
|
+
s.frames.visible(true);
|
|
12879
|
+
s.seen = true;
|
|
12880
|
+
}
|
|
12881
|
+
} else {
|
|
12882
|
+
const img = doc.createElement("img");
|
|
12883
|
+
img.crossOrigin = "anonymous";
|
|
12884
|
+
img.decoding = "async";
|
|
12885
|
+
img.addEventListener("load", () => {
|
|
12886
|
+
pairDiffs.clear();
|
|
12887
|
+
schedule();
|
|
12888
|
+
});
|
|
12889
|
+
img.src = s.source.src;
|
|
12890
|
+
s.still = img;
|
|
12891
|
+
}
|
|
12607
12892
|
};
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12893
|
+
const unload = (s) => {
|
|
12894
|
+
offs.get(s)?.();
|
|
12895
|
+
offs.delete(s);
|
|
12896
|
+
if (s.frames) {
|
|
12897
|
+
if (s.seen)
|
|
12898
|
+
s.frames.visible(false);
|
|
12899
|
+
s.frames.release();
|
|
12615
12900
|
}
|
|
12616
|
-
|
|
12901
|
+
s.frames = null;
|
|
12902
|
+
s.seen = false;
|
|
12903
|
+
if (s.still)
|
|
12904
|
+
s.still.removeAttribute("src");
|
|
12905
|
+
s.still = null;
|
|
12906
|
+
};
|
|
12907
|
+
let w = 1;
|
|
12908
|
+
let h = 1;
|
|
12909
|
+
let geo = { x: 0, w: 1 };
|
|
12910
|
+
const rowH = () => Math.max(4, (h - BAR - GAP * 2) / 2);
|
|
12911
|
+
const aspectOf = (s) => {
|
|
12912
|
+
if (s.still?.naturalWidth)
|
|
12913
|
+
return s.still.naturalWidth / s.still.naturalHeight;
|
|
12914
|
+
const c = s.frames?.nearest(0);
|
|
12915
|
+
return c ? c.bitmap.width / c.bitmap.height : 0;
|
|
12916
|
+
};
|
|
12917
|
+
const columns = () => stripColumns(timeline.view, geo.w, {
|
|
12918
|
+
duration: timeline.duration,
|
|
12919
|
+
fps: fps(),
|
|
12920
|
+
minWidth: rowH() * Math.max(16 / 9, ...sides.map(aspectOf)) + GAP
|
|
12921
|
+
});
|
|
12922
|
+
const measure = () => {
|
|
12923
|
+
const r = root.getBoundingClientRect();
|
|
12924
|
+
w = Math.max(1, r.width);
|
|
12925
|
+
h = Math.max(1, r.height);
|
|
12926
|
+
geo = { x: 0, w };
|
|
12927
|
+
if (align === "track") {
|
|
12928
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
12929
|
+
const t = track?.getBoundingClientRect();
|
|
12930
|
+
if (t && t.width > 0)
|
|
12931
|
+
geo = { x: t.left - r.left, w: t.width };
|
|
12932
|
+
}
|
|
12933
|
+
readColors();
|
|
12934
|
+
schedule();
|
|
12935
|
+
};
|
|
12936
|
+
const ask = (cols) => {
|
|
12937
|
+
const points = cols.map((c) => ({ frame: c.frame, tolerance: Math.floor((c.to - c.from) / 2) }));
|
|
12938
|
+
for (const s of sides)
|
|
12939
|
+
s.frames?.want(points);
|
|
12940
|
+
};
|
|
12941
|
+
const picOf = (s, c) => {
|
|
12942
|
+
if (s.still?.naturalWidth)
|
|
12943
|
+
return { img: s.still, w: s.still.naturalWidth, h: s.still.naturalHeight, key: "still" };
|
|
12944
|
+
const f = s.frames?.nearest(c.frame, Math.max(1, c.to - c.from));
|
|
12945
|
+
return f ? { img: f.bitmap, w: f.bitmap.width, h: f.bitmap.height, key: String(f.frame) } : null;
|
|
12946
|
+
};
|
|
12947
|
+
const probe = doc.createElement("canvas");
|
|
12948
|
+
probe.width = DIFF.w;
|
|
12949
|
+
probe.height = DIFF.h;
|
|
12950
|
+
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
12951
|
+
const differ = (a, b) => {
|
|
12952
|
+
if (!probeCtx)
|
|
12617
12953
|
return;
|
|
12618
|
-
|
|
12619
|
-
|
|
12954
|
+
try {
|
|
12955
|
+
probeCtx.drawImage(a.img, 0, 0, DIFF.w, DIFF.h);
|
|
12956
|
+
const pa = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
12957
|
+
probeCtx.drawImage(b.img, 0, 0, DIFF.w, DIFF.h);
|
|
12958
|
+
const pb = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
12959
|
+
return meanAbsDiff(pa, pb);
|
|
12960
|
+
} catch {
|
|
12961
|
+
return;
|
|
12962
|
+
}
|
|
12620
12963
|
};
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
if (Number.isFinite(v.duration))
|
|
12626
|
-
tl.update({ duration: v.duration });
|
|
12964
|
+
let colors = { surface: "", accent: "", diff: "", wash: "" };
|
|
12965
|
+
const rgba = (raw, fallback) => {
|
|
12966
|
+
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
12967
|
+
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
12627
12968
|
};
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12969
|
+
const readColors = () => {
|
|
12970
|
+
const cs = win.getComputedStyle(root);
|
|
12971
|
+
colors = {
|
|
12972
|
+
surface: rgba(tokenCss(cs, "--kv-fill", ""), [1, 1, 1, 0.055]),
|
|
12973
|
+
accent: rgba(tokenCss(cs, "--kv-accent", ""), [1, 1, 1, 1]),
|
|
12974
|
+
diff: rgba(tokenCss(cs, "--_ku-cmp-diff", ""), [0.49, 0.81, 0.61, 1]),
|
|
12975
|
+
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
12976
|
+
};
|
|
12977
|
+
};
|
|
12978
|
+
let hover = null;
|
|
12979
|
+
let raf = 0;
|
|
12980
|
+
let asked = "";
|
|
12981
|
+
function schedule() {
|
|
12982
|
+
if (!raf && !disposed)
|
|
12983
|
+
raf = win.requestAnimationFrame(draw);
|
|
12984
|
+
}
|
|
12985
|
+
const draw = () => {
|
|
12986
|
+
raf = 0;
|
|
12987
|
+
if (!near)
|
|
12988
|
+
return;
|
|
12989
|
+
const dpr = Math.min(3, Math.max(1, win.devicePixelRatio || 1));
|
|
12990
|
+
const cw = Math.round(w * dpr);
|
|
12991
|
+
const ch = Math.round(h * dpr);
|
|
12992
|
+
if (canvas.width !== cw || canvas.height !== ch) {
|
|
12993
|
+
canvas.width = cw;
|
|
12994
|
+
canvas.height = ch;
|
|
12995
|
+
}
|
|
12996
|
+
const ctx = canvas.getContext("2d");
|
|
12997
|
+
if (!ctx)
|
|
12998
|
+
return;
|
|
12999
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
13000
|
+
ctx.clearRect(0, 0, w, h);
|
|
13001
|
+
const cols = columns();
|
|
13002
|
+
const key = cols.map((c) => c.frame).join(",");
|
|
13003
|
+
if (key !== asked) {
|
|
13004
|
+
asked = key;
|
|
13005
|
+
ask(cols);
|
|
13006
|
+
}
|
|
13007
|
+
const rh = rowH();
|
|
13008
|
+
const radius = 3;
|
|
13009
|
+
let budget = DIFFS_PER_DRAW;
|
|
13010
|
+
let more = false;
|
|
13011
|
+
const ds = cols.map((c) => {
|
|
13012
|
+
const a = picOf(sides[0], c);
|
|
13013
|
+
const b = picOf(sides[1], c);
|
|
13014
|
+
if (!a || !b)
|
|
12636
13015
|
return;
|
|
13016
|
+
const k = `${a.key}|${b.key}`;
|
|
13017
|
+
if (!pairDiffs.has(k)) {
|
|
13018
|
+
if (budget-- <= 0) {
|
|
13019
|
+
more = true;
|
|
13020
|
+
return;
|
|
13021
|
+
}
|
|
13022
|
+
pairDiffs.set(k, differ(a, b));
|
|
12637
13023
|
}
|
|
12638
|
-
|
|
13024
|
+
const d = pairDiffs.get(k);
|
|
13025
|
+
if (d !== undefined)
|
|
13026
|
+
diffs.set(c.frame, d);
|
|
13027
|
+
return d;
|
|
13028
|
+
});
|
|
13029
|
+
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
13030
|
+
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
13031
|
+
cols.forEach((c, i) => {
|
|
13032
|
+
const x = geo.x + c.x + GAP / 2;
|
|
13033
|
+
const cwid = Math.max(1, c.w - GAP);
|
|
13034
|
+
if (x + cwid < 0 || x > w)
|
|
12639
13035
|
return;
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
13036
|
+
for (const row of [0, 1]) {
|
|
13037
|
+
const y = row * (rh + GAP);
|
|
13038
|
+
const pic = picOf(sides[row], c);
|
|
13039
|
+
ctx.save();
|
|
13040
|
+
ctx.beginPath();
|
|
13041
|
+
ctx.roundRect(x, y, cwid, rh, radius);
|
|
13042
|
+
ctx.fillStyle = colors.surface;
|
|
13043
|
+
ctx.fill();
|
|
13044
|
+
if (pic) {
|
|
13045
|
+
ctx.clip();
|
|
13046
|
+
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
13047
|
+
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
13048
|
+
}
|
|
13049
|
+
ctx.restore();
|
|
12648
13050
|
}
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
13051
|
+
if (hover === i) {
|
|
13052
|
+
ctx.fillStyle = colors.wash;
|
|
13053
|
+
ctx.beginPath();
|
|
13054
|
+
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
13055
|
+
ctx.fill();
|
|
13056
|
+
}
|
|
13057
|
+
const d = ds[i];
|
|
13058
|
+
if (d !== undefined && d > 0) {
|
|
13059
|
+
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
13060
|
+
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
13061
|
+
ctx.fillStyle = colors.diff;
|
|
13062
|
+
ctx.beginPath();
|
|
13063
|
+
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
13064
|
+
ctx.fill();
|
|
13065
|
+
ctx.globalAlpha = 1;
|
|
13066
|
+
}
|
|
13067
|
+
});
|
|
13068
|
+
const v = timeline.view;
|
|
13069
|
+
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
13070
|
+
if (px >= -1 && px <= w + 1) {
|
|
13071
|
+
ctx.fillStyle = colors.accent;
|
|
13072
|
+
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
12655
13073
|
}
|
|
13074
|
+
if (more)
|
|
13075
|
+
schedule();
|
|
13076
|
+
};
|
|
13077
|
+
const timeAt = (clientX) => {
|
|
13078
|
+
const r = root.getBoundingClientRect();
|
|
13079
|
+
const v = timeline.view;
|
|
13080
|
+
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
13081
|
+
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
13082
|
+
};
|
|
13083
|
+
const columnAt = (clientX) => {
|
|
13084
|
+
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
13085
|
+
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
13086
|
+
return i < 0 ? null : i;
|
|
13087
|
+
};
|
|
13088
|
+
let dragging = false;
|
|
13089
|
+
const pick = (clientX) => {
|
|
13090
|
+
const t = timeAt(clientX);
|
|
13091
|
+
timeline.seek(t);
|
|
13092
|
+
options.onPick?.(t);
|
|
13093
|
+
};
|
|
13094
|
+
root.addEventListener("pointerdown", (e) => {
|
|
13095
|
+
if (e.button !== 0)
|
|
13096
|
+
return;
|
|
13097
|
+
dragging = true;
|
|
13098
|
+
root.setPointerCapture(e.pointerId);
|
|
13099
|
+
const host = root.closest(".knobs-compare");
|
|
13100
|
+
if (host)
|
|
13101
|
+
host.focus({ preventScroll: true });
|
|
13102
|
+
else
|
|
13103
|
+
timeline.focus();
|
|
13104
|
+
if (timeline.playing)
|
|
13105
|
+
timeline.pause();
|
|
13106
|
+
pick(e.clientX);
|
|
13107
|
+
e.preventDefault();
|
|
12656
13108
|
});
|
|
12657
|
-
|
|
12658
|
-
if (
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
13109
|
+
root.addEventListener("pointermove", (e) => {
|
|
13110
|
+
if (dragging)
|
|
13111
|
+
pick(e.clientX);
|
|
13112
|
+
const i = columnAt(e.clientX);
|
|
13113
|
+
if (i !== hover) {
|
|
13114
|
+
hover = i;
|
|
13115
|
+
schedule();
|
|
13116
|
+
}
|
|
12663
13117
|
});
|
|
12664
|
-
const
|
|
12665
|
-
|
|
12666
|
-
tl.toggle();
|
|
13118
|
+
const end = () => {
|
|
13119
|
+
dragging = false;
|
|
12667
13120
|
};
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
13121
|
+
root.addEventListener("pointerup", end);
|
|
13122
|
+
root.addEventListener("pointercancel", end);
|
|
13123
|
+
root.addEventListener("lostpointercapture", end);
|
|
13124
|
+
root.addEventListener("pointerleave", () => {
|
|
13125
|
+
if (hover === null)
|
|
13126
|
+
return;
|
|
13127
|
+
hover = null;
|
|
13128
|
+
schedule();
|
|
13129
|
+
});
|
|
13130
|
+
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
13131
|
+
const ro = new win.ResizeObserver(measure);
|
|
13132
|
+
ro.observe(root);
|
|
13133
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
13134
|
+
if (track)
|
|
13135
|
+
ro.observe(track);
|
|
13136
|
+
root.addEventListener("knobs-restyle", measure);
|
|
13137
|
+
const setOnScreen = (on) => {
|
|
13138
|
+
onScreen = on;
|
|
13139
|
+
for (const s of sides) {
|
|
13140
|
+
if (!s.frames || s.seen === on)
|
|
13141
|
+
continue;
|
|
13142
|
+
s.seen = on;
|
|
13143
|
+
s.frames.visible(on);
|
|
13144
|
+
}
|
|
13145
|
+
};
|
|
13146
|
+
const setNear = (on) => {
|
|
13147
|
+
near = on;
|
|
13148
|
+
if (on) {
|
|
13149
|
+
for (const s of sides)
|
|
13150
|
+
load(s);
|
|
13151
|
+
schedule();
|
|
13152
|
+
} else
|
|
13153
|
+
for (const s of sides)
|
|
13154
|
+
unload(s);
|
|
13155
|
+
};
|
|
13156
|
+
const IO = win.IntersectionObserver;
|
|
13157
|
+
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
13158
|
+
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
13159
|
+
measure();
|
|
13160
|
+
if (nearIo && screenIo) {
|
|
13161
|
+
nearIo.observe(root);
|
|
13162
|
+
screenIo.observe(root);
|
|
13163
|
+
} else {
|
|
13164
|
+
setNear(true);
|
|
13165
|
+
setOnScreen(true);
|
|
13166
|
+
}
|
|
13167
|
+
return {
|
|
12671
13168
|
element: root,
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
13169
|
+
diffs,
|
|
13170
|
+
update(next) {
|
|
13171
|
+
const was = sides;
|
|
13172
|
+
sides = [
|
|
13173
|
+
next.before ? makeSide(next.before) : was[0],
|
|
13174
|
+
next.after ? makeSide(next.after) : was[1]
|
|
13175
|
+
];
|
|
13176
|
+
for (const s of was)
|
|
13177
|
+
if (!sides.includes(s))
|
|
13178
|
+
unload(s);
|
|
13179
|
+
diffs.clear();
|
|
13180
|
+
pairDiffs.clear();
|
|
13181
|
+
asked = "";
|
|
13182
|
+
if (near)
|
|
13183
|
+
for (const s of sides)
|
|
13184
|
+
load(s);
|
|
13185
|
+
if (onScreen)
|
|
13186
|
+
setOnScreen(true);
|
|
13187
|
+
schedule();
|
|
12682
13188
|
},
|
|
12683
|
-
play: () => tl.play(),
|
|
12684
|
-
pause: () => tl.pause(),
|
|
12685
|
-
toggle: () => tl.toggle(),
|
|
12686
|
-
seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
|
|
12687
|
-
seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
|
|
12688
|
-
focus: () => tl.focus(),
|
|
12689
13189
|
dispose() {
|
|
13190
|
+
disposed = true;
|
|
13191
|
+
if (raf)
|
|
13192
|
+
win.cancelAnimationFrame(raf);
|
|
12690
13193
|
unlisten();
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
v.removeEventListener("loadeddata", onFrame);
|
|
12697
|
-
v.removeEventListener("seeked", onSeeked);
|
|
12698
|
-
v.removeAttribute("src");
|
|
12699
|
-
v.load();
|
|
13194
|
+
ro.disconnect();
|
|
13195
|
+
nearIo?.disconnect();
|
|
13196
|
+
screenIo?.disconnect();
|
|
13197
|
+
for (const s of sides)
|
|
13198
|
+
unload(s);
|
|
12700
13199
|
root.remove();
|
|
12701
13200
|
}
|
|
12702
13201
|
};
|
|
12703
|
-
return api;
|
|
12704
|
-
}
|
|
12705
|
-
var init_player = __esm(() => {
|
|
12706
|
-
init_look();
|
|
12707
|
-
init_timeline();
|
|
12708
|
-
});
|
|
12709
|
-
|
|
12710
|
-
// src/ui/react.tsx
|
|
12711
|
-
import {
|
|
12712
|
-
Children,
|
|
12713
|
-
useEffect,
|
|
12714
|
-
useLayoutEffect,
|
|
12715
|
-
useRef,
|
|
12716
|
-
useState
|
|
12717
|
-
} from "react";
|
|
12718
|
-
import { createPortal } from "react-dom";
|
|
12719
|
-
init_diff();
|
|
12720
|
-
|
|
12721
|
-
// src/ui/follow.ts
|
|
12722
|
-
function propChanges(initial, same = Object.is) {
|
|
12723
|
-
let last = { ...initial };
|
|
12724
|
-
return (next) => {
|
|
12725
|
-
const changed = {};
|
|
12726
|
-
for (const key of Object.keys(next))
|
|
12727
|
-
if (!same(next[key], last[key]))
|
|
12728
|
-
changed[key] = next[key];
|
|
12729
|
-
last = { ...next };
|
|
12730
|
-
return changed;
|
|
12731
|
-
};
|
|
12732
13202
|
}
|
|
12733
|
-
var anyChanged = (changed) => Object.keys(changed).length > 0;
|
|
12734
|
-
|
|
12735
|
-
// src/ui/react.tsx
|
|
12736
|
-
init_compare();
|
|
12737
|
-
init_compare_strip();
|
|
12738
13203
|
|
|
12739
13204
|
// src/ui/walkthrough.ts
|
|
12740
13205
|
init_motion();
|
|
@@ -16029,9 +16494,8 @@ var walkthroughCss = `${foundationCss}
|
|
|
16029
16494
|
.knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
|
|
16030
16495
|
/* A compare is as tall as its two pictures side by side at this width, in every mode (Wipe fuses
|
|
16031
16496
|
them at one side's size, so nothing grows or re-measures), plus its mode bar, the line and gaps
|
|
16032
|
-
(92 px)
|
|
16497
|
+
(92 px). */
|
|
16033
16498
|
.knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
|
|
16034
|
-
.knobs-wt-fit[data-strip]{--kw-chrome:158px}
|
|
16035
16499
|
.knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
|
|
16036
16500
|
.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)}
|
|
16037
16501
|
.knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
|
|
@@ -16044,16 +16508,15 @@ var walkthroughCss = `${foundationCss}
|
|
|
16044
16508
|
/* The diff inside it: its header row 28 high on the container's padding, a hairline under it, and
|
|
16045
16509
|
no line of its own under the code (the container's ring closes it). */
|
|
16046
16510
|
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-head:28px;--kd-x:0px}
|
|
16047
|
-
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"])::after{display:none}
|
|
16048
|
-
.knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--
|
|
16511
|
+
.knobs-wt-slot[data-kind="diff"] :is(.knobs-ui.knobs-diff:not([data-on="pane"]),.knobs-diff-review)::after{display:none}
|
|
16512
|
+
.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);
|
|
16049
16513
|
font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
|
|
16050
|
-
.knobs-surface
|
|
16514
|
+
.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)}
|
|
16051
16515
|
.knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
16052
16516
|
.knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
|
|
16053
16517
|
.knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
|
|
16054
16518
|
.knobs-wt-out{color:var(--_k-fg-2)}
|
|
16055
16519
|
.knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
|
|
16056
|
-
.knobs-wt-mlabel{color:var(--_k-fg-2)}
|
|
16057
16520
|
.knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-feature-settings:var(--_kr-data)}
|
|
16058
16521
|
.knobs-wt-mfrom{color:var(--_k-fg-3)}
|
|
16059
16522
|
.knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
|
|
@@ -16072,6 +16535,9 @@ var walkthroughCss = `${foundationCss}
|
|
|
16072
16535
|
.knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
|
|
16073
16536
|
.knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
|
|
16074
16537
|
.knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
|
|
16538
|
+
/* A diff holds its room with its own well's edge, no fill: the diff reads the colour behind it
|
|
16539
|
+
as it mounts, and a loading fill there would stay under its code. */
|
|
16540
|
+
.knobs-wt-slot[data-kind="diff"]>.knobs-wt-hold{background:transparent}
|
|
16075
16541
|
.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)}
|
|
16076
16542
|
|
|
16077
16543
|
/* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
|
|
@@ -16321,8 +16787,6 @@ var side = (s, fallback) => ({
|
|
|
16321
16787
|
async function mountCompare(host, m, look, alive, onEvent) {
|
|
16322
16788
|
const doc = host.ownerDocument;
|
|
16323
16789
|
const fit = div(doc, "knobs-wt-fit");
|
|
16324
|
-
if (m.strip)
|
|
16325
|
-
fit.dataset.strip = "";
|
|
16326
16790
|
const { before, after } = m;
|
|
16327
16791
|
const known = parseAspect(m.aspect);
|
|
16328
16792
|
fit.style.setProperty("--kw-aspect", String(known ?? DEFAULT_ASPECT));
|
|
@@ -16349,7 +16813,6 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16349
16813
|
timeline: mediaKind(before.src) === "video" || mediaKind(after.src) === "video",
|
|
16350
16814
|
lazy: false,
|
|
16351
16815
|
...aspect ? { aspect } : {},
|
|
16352
|
-
...m.strip ? { strip: true } : {},
|
|
16353
16816
|
onMode: (next) => {
|
|
16354
16817
|
onEvent?.({ what: "mode", detail: next });
|
|
16355
16818
|
const layer = marks?.element;
|
|
@@ -16411,11 +16874,11 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16411
16874
|
pills?.sync();
|
|
16412
16875
|
marking?.kick();
|
|
16413
16876
|
if (source !== "play")
|
|
16414
|
-
onEvent?.({ what: "seek", time:
|
|
16877
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
16415
16878
|
},
|
|
16416
16879
|
onPlay: (on) => {
|
|
16417
16880
|
marking?.kick();
|
|
16418
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
16881
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(c.time) });
|
|
16419
16882
|
}
|
|
16420
16883
|
});
|
|
16421
16884
|
requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
|
|
@@ -16433,13 +16896,13 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16433
16896
|
const zoom = zoomedIn(c.zoom) ? c.zoom : null;
|
|
16434
16897
|
return {
|
|
16435
16898
|
kind: "compare",
|
|
16436
|
-
time:
|
|
16437
|
-
duration:
|
|
16899
|
+
time: round3(c.time),
|
|
16900
|
+
duration: round3(c.duration),
|
|
16438
16901
|
playing: c.playing,
|
|
16439
16902
|
mode: c.mode,
|
|
16440
16903
|
...pills?.current ? { chapter: pills.current.label } : {},
|
|
16441
|
-
...loop ? { loop: { start:
|
|
16442
|
-
...zoom ? { zoom: { u:
|
|
16904
|
+
...loop ? { loop: { start: round3(loop.start), end: round3(loop.end) } } : {},
|
|
16905
|
+
...zoom ? { zoom: { u: round3(zoom.u), v: round3(zoom.v), w: round3(zoom.w), h: round3(zoom.h) } } : {}
|
|
16443
16906
|
};
|
|
16444
16907
|
},
|
|
16445
16908
|
dispose: () => {
|
|
@@ -16452,7 +16915,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16452
16915
|
}
|
|
16453
16916
|
};
|
|
16454
16917
|
}
|
|
16455
|
-
var
|
|
16918
|
+
var round3 = (v) => Math.round(v * 100) / 100;
|
|
16456
16919
|
var MARK_GLIDE = 450;
|
|
16457
16920
|
var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
|
|
16458
16921
|
async function mountVideo(host, m, look, alive, onEvent) {
|
|
@@ -16479,12 +16942,12 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
16479
16942
|
} : {},
|
|
16480
16943
|
onPlay: (on, time) => {
|
|
16481
16944
|
marking?.kick();
|
|
16482
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
16945
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(time) });
|
|
16483
16946
|
},
|
|
16484
16947
|
onSeek: (time) => {
|
|
16485
16948
|
pills?.sync();
|
|
16486
16949
|
marking?.kick();
|
|
16487
|
-
onEvent?.({ what: "seek", time:
|
|
16950
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
16488
16951
|
}
|
|
16489
16952
|
});
|
|
16490
16953
|
const v = p.video;
|
|
@@ -16517,8 +16980,8 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
16517
16980
|
seekBy: (d) => p.seekBy(d),
|
|
16518
16981
|
context: () => ({
|
|
16519
16982
|
kind: "video",
|
|
16520
|
-
time:
|
|
16521
|
-
...p.duration ? { duration:
|
|
16983
|
+
time: round3(p.time),
|
|
16984
|
+
...p.duration ? { duration: round3(p.duration) } : {},
|
|
16522
16985
|
playing: p.playing,
|
|
16523
16986
|
...pills?.current ? { chapter: pills.current.label } : {}
|
|
16524
16987
|
}),
|
|
@@ -16662,8 +17125,6 @@ var fmt = (v, unit = "") => `${v}${unit}`;
|
|
|
16662
17125
|
function mountMetric(host, m) {
|
|
16663
17126
|
const doc = host.ownerDocument;
|
|
16664
17127
|
const box = div(doc, "knobs-wt-metric");
|
|
16665
|
-
const label = div(doc, "knobs-wt-mlabel");
|
|
16666
|
-
label.textContent = m.label;
|
|
16667
17128
|
const value = div(doc, "knobs-wt-mvalue");
|
|
16668
17129
|
const span = (cls, text) => {
|
|
16669
17130
|
const s = doc.createElement("span");
|
|
@@ -16683,7 +17144,7 @@ function mountMetric(host, m) {
|
|
|
16683
17144
|
delta.dataset.tone = d > 0 === (better === "up") ? "good" : "bad";
|
|
16684
17145
|
value.append(delta);
|
|
16685
17146
|
}
|
|
16686
|
-
box.append(
|
|
17147
|
+
box.append(value);
|
|
16687
17148
|
const total = num(m.of);
|
|
16688
17149
|
if (Number.isFinite(total) && total > 0 && Number.isFinite(a) && Number.isFinite(b)) {
|
|
16689
17150
|
const track = div(doc, "knobs-wt-mtrack");
|
|
@@ -16737,7 +17198,7 @@ function mountDecision(host, m, slot) {
|
|
|
16737
17198
|
}
|
|
16738
17199
|
var HEAVY = new Set(["compare", "video", "diff", "mount"]);
|
|
16739
17200
|
var RELEASES = new Set(["compare", "video"]);
|
|
16740
|
-
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "
|
|
17201
|
+
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "history" ? m.label : undefined;
|
|
16741
17202
|
function mediaSlot(container, given) {
|
|
16742
17203
|
const doc = container.ownerDocument;
|
|
16743
17204
|
injectWalkthroughStyles(doc);
|
|
@@ -16751,7 +17212,7 @@ function mediaSlot(container, given) {
|
|
|
16751
17212
|
root.dataset.kind = m.kind;
|
|
16752
17213
|
applyLook2(root, options);
|
|
16753
17214
|
const label = labelOf2(m);
|
|
16754
|
-
if (label
|
|
17215
|
+
if (label) {
|
|
16755
17216
|
const l = doc.createElement("span");
|
|
16756
17217
|
l.className = "knobs-wt-slot-label";
|
|
16757
17218
|
l.textContent = label;
|
|
@@ -18449,7 +18910,7 @@ function Compare(props) {
|
|
|
18449
18910
|
c.dispose();
|
|
18450
18911
|
handle.current = null;
|
|
18451
18912
|
};
|
|
18452
|
-
}, [props.timeline, props.
|
|
18913
|
+
}, [props.timeline, props.renderer, props.scrubCache]);
|
|
18453
18914
|
useFollow({
|
|
18454
18915
|
before: props.before,
|
|
18455
18916
|
after: props.after,
|