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