@kitlangton/knobs 0.4.0-next.16 → 0.4.0-next.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-9y93net8.js → chunk-3vag4n3q.js} +39 -9
- package/dist/dev/{chunk-e5hbs8fe.js → chunk-5cv3g7ca.js} +205 -64
- package/dist/dev/{chunk-exaya50m.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +292 -83
- package/dist/review-react.js +1341 -880
- package/dist/review-solid.js +1341 -880
- package/dist/review.js +1711 -1248
- package/dist/themes.js +8 -0
- package/dist/types/core/schema.d.ts +7 -1
- package/dist/types/panel/description-ids.d.ts +6 -0
- package/dist/types/panel/folds.d.ts +7 -4
- package/dist/types/panel/gpu/color.d.ts +2 -0
- package/dist/types/panel/page-hints.d.ts +3 -2
- package/dist/types/panel/position.d.ts +23 -0
- package/dist/types/tokens/schema.d.ts +9 -0
- package/dist/types/ui/compare-css.d.ts +1 -1
- package/dist/types/ui/compare-frames.d.ts +64 -2
- package/dist/types/ui/compare-model.d.ts +23 -0
- package/dist/types/ui/compare.d.ts +6 -4
- package/dist/types/ui/player-css.d.ts +1 -1
- package/dist/types/ui/player.d.ts +6 -0
- package/dist/types/ui/react.d.ts +1 -1
- package/dist/types/ui/solid.d.ts +1 -1
- package/dist/types/ui/tooltip-model.d.ts +35 -0
- package/dist/types/ui/tooltip.d.ts +1 -1
- package/dist/types/ui/walkthrough-model.d.ts +4 -2
- package/dist/ui-react.js +2015 -1507
- package/dist/ui-solid.js +2014 -1506
- package/dist/ui.js +2013 -1505
- package/package.json +1 -1
package/dist/review.js
CHANGED
|
@@ -629,6 +629,14 @@ var init_schema = __esm(() => {
|
|
|
629
629
|
doc: "Panel and card radius, concentric with controls.",
|
|
630
630
|
deprecated: ["--k-panel-radius", "--ku-panel-radius"]
|
|
631
631
|
},
|
|
632
|
+
{
|
|
633
|
+
name: "radius-frame",
|
|
634
|
+
tier: "semantic",
|
|
635
|
+
type: "length",
|
|
636
|
+
group: "Shape",
|
|
637
|
+
source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
|
|
638
|
+
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."
|
|
639
|
+
},
|
|
632
640
|
{
|
|
633
641
|
name: "round",
|
|
634
642
|
tier: "semantic",
|
|
@@ -2253,7 +2261,6 @@ var compareCss = `
|
|
|
2253
2261
|
: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)}
|
|
2254
2262
|
.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
|
|
2255
2263
|
.knobs-cmp-note:empty{display:none}
|
|
2256
|
-
.knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
|
|
2257
2264
|
.knobs-cmp-timeline-host{flex:none;min-height:0}
|
|
2258
2265
|
/* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
|
|
2259
2266
|
.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;
|
|
@@ -2273,6 +2280,9 @@ var playerCss = `
|
|
|
2273
2280
|
.knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
|
|
2274
2281
|
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)}
|
|
2275
2282
|
.knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
|
|
2283
|
+
/* Mid-scrub, the cached frame under the hand, over the video until it lands. */
|
|
2284
|
+
.knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
|
|
2285
|
+
.knobs-player-preview[hidden]{display:none}
|
|
2276
2286
|
.knobs-player-line{flex:none;min-height:0}
|
|
2277
2287
|
/* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
|
|
2278
2288
|
about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
|
|
@@ -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(",");
|
|
@@ -3343,12 +3362,24 @@ function createRegistry(doc) {
|
|
|
3343
3362
|
return;
|
|
3344
3363
|
if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
|
|
3345
3364
|
return;
|
|
3346
|
-
if (isTyping(e.target) || !wide())
|
|
3365
|
+
if (isTyping(e.target) || !wide(e.target))
|
|
3347
3366
|
return;
|
|
3348
3367
|
if (registry.start())
|
|
3349
3368
|
e.preventDefault();
|
|
3350
3369
|
};
|
|
3351
|
-
const
|
|
3370
|
+
const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
|
|
3371
|
+
const wide = (target) => {
|
|
3372
|
+
if (!listening)
|
|
3373
|
+
return false;
|
|
3374
|
+
if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
|
|
3375
|
+
return true;
|
|
3376
|
+
if (sources.length < 2)
|
|
3377
|
+
return false;
|
|
3378
|
+
if (target === undefined)
|
|
3379
|
+
return true;
|
|
3380
|
+
const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
|
|
3381
|
+
return Boolean(piece?.classList.contains("knobs-ui"));
|
|
3382
|
+
};
|
|
3352
3383
|
const relisten = () => {
|
|
3353
3384
|
const next = sources.some((s) => s.listen);
|
|
3354
3385
|
if (next === listening)
|
|
@@ -4227,6 +4258,31 @@ function seeked(s, current, fps) {
|
|
|
4227
4258
|
return { state: { seeking: true, pending: null }, apply: s.pending, settled: false };
|
|
4228
4259
|
return { state: SEEK_IDLE, apply: null, settled: true };
|
|
4229
4260
|
}
|
|
4261
|
+
function releaseSeek(current, t, fps, duration) {
|
|
4262
|
+
const at = seekTime(t, fps, duration);
|
|
4263
|
+
return sameFrame(current, at, fps) ? null : at;
|
|
4264
|
+
}
|
|
4265
|
+
function stallGuard(timers, onStall, ms = SEEK_STALL_MS) {
|
|
4266
|
+
let id = 0;
|
|
4267
|
+
const clear = () => {
|
|
4268
|
+
if (id)
|
|
4269
|
+
timers.clearTimeout(id);
|
|
4270
|
+
id = 0;
|
|
4271
|
+
};
|
|
4272
|
+
return {
|
|
4273
|
+
arm() {
|
|
4274
|
+
clear();
|
|
4275
|
+
id = timers.setTimeout(() => {
|
|
4276
|
+
id = 0;
|
|
4277
|
+
onStall();
|
|
4278
|
+
}, ms);
|
|
4279
|
+
},
|
|
4280
|
+
clear,
|
|
4281
|
+
get armed() {
|
|
4282
|
+
return id !== 0;
|
|
4283
|
+
}
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4230
4286
|
function letterbox(media, box) {
|
|
4231
4287
|
if (media.w <= 0 || media.h <= 0 || box.w <= 0 || box.h <= 0)
|
|
4232
4288
|
return box;
|
|
@@ -4347,7 +4403,7 @@ function stripPeaks(diffs, o = {}) {
|
|
|
4347
4403
|
});
|
|
4348
4404
|
return out;
|
|
4349
4405
|
}
|
|
4350
|
-
var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO2, 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) => {
|
|
4406
|
+
var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO2, 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) => {
|
|
4351
4407
|
const inner = r * Math.min(1, Math.max(0, k));
|
|
4352
4408
|
return { before: { left: r, right: inner }, after: { left: inner, right: r } };
|
|
4353
4409
|
}, labelFold = (fold, mark, blur = 3) => {
|
|
@@ -4594,6 +4650,25 @@ var init_compare_mp4 = __esm(() => {
|
|
|
4594
4650
|
});
|
|
4595
4651
|
|
|
4596
4652
|
// src/ui/compare-frames.ts
|
|
4653
|
+
function scrubCaching(option, win) {
|
|
4654
|
+
if (option === false)
|
|
4655
|
+
return false;
|
|
4656
|
+
if (option === true)
|
|
4657
|
+
return true;
|
|
4658
|
+
const net = win?.navigator?.connection;
|
|
4659
|
+
if (net?.saveData)
|
|
4660
|
+
return false;
|
|
4661
|
+
return !win?.matchMedia?.("(prefers-reduced-data: reduce)").matches;
|
|
4662
|
+
}
|
|
4663
|
+
function proxyStride(bytes, count, total, budget) {
|
|
4664
|
+
if (count <= 0 || total <= 0)
|
|
4665
|
+
return 1;
|
|
4666
|
+
const each = bytes / count;
|
|
4667
|
+
let stride = 1;
|
|
4668
|
+
while (total / stride * each > budget && stride < total)
|
|
4669
|
+
stride *= 2;
|
|
4670
|
+
return stride;
|
|
4671
|
+
}
|
|
4597
4672
|
function gridPoints(total, count = GRID) {
|
|
4598
4673
|
if (total <= 0)
|
|
4599
4674
|
return [];
|
|
@@ -4624,6 +4699,24 @@ function nearestKey(keys, frame, within = Infinity) {
|
|
|
4624
4699
|
}
|
|
4625
4700
|
return best !== null && bestD <= within ? best : null;
|
|
4626
4701
|
}
|
|
4702
|
+
function claim(core) {
|
|
4703
|
+
if (!round) {
|
|
4704
|
+
const r = new Set;
|
|
4705
|
+
round = r;
|
|
4706
|
+
queueMicrotask(() => {
|
|
4707
|
+
for (const c of actives)
|
|
4708
|
+
if (!r.has(c)) {
|
|
4709
|
+
c.active = false;
|
|
4710
|
+
c.dropFull();
|
|
4711
|
+
}
|
|
4712
|
+
actives = r;
|
|
4713
|
+
round = null;
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
round.add(core);
|
|
4717
|
+
actives.add(core);
|
|
4718
|
+
core.active = true;
|
|
4719
|
+
}
|
|
4627
4720
|
function pump(win) {
|
|
4628
4721
|
if (running)
|
|
4629
4722
|
return;
|
|
@@ -4631,12 +4724,14 @@ function pump(win) {
|
|
|
4631
4724
|
(async () => {
|
|
4632
4725
|
try {
|
|
4633
4726
|
for (;; ) {
|
|
4634
|
-
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active));
|
|
4727
|
+
const order = [...cores.values()].filter((c) => c.shown > 0 || c.active).sort((a, b) => Number(b.active) - Number(a.active) || a.ran - b.ran);
|
|
4635
4728
|
let unit = null;
|
|
4636
4729
|
for (const c of order) {
|
|
4637
4730
|
unit = c.work();
|
|
4638
|
-
if (unit)
|
|
4731
|
+
if (unit) {
|
|
4732
|
+
c.ran = ++turns;
|
|
4639
4733
|
break;
|
|
4734
|
+
}
|
|
4640
4735
|
}
|
|
4641
4736
|
if (!unit)
|
|
4642
4737
|
break;
|
|
@@ -4666,12 +4761,99 @@ async function toBitmap(doc, source, w, h, cap) {
|
|
|
4666
4761
|
return createImageBitmap(c);
|
|
4667
4762
|
}
|
|
4668
4763
|
}
|
|
4764
|
+
function holdEncoders() {
|
|
4765
|
+
encoderUsers++;
|
|
4766
|
+
}
|
|
4767
|
+
function dropEncoders() {
|
|
4768
|
+
encoderUsers = Math.max(0, encoderUsers - 1);
|
|
4769
|
+
if (encoderUsers > 0)
|
|
4770
|
+
return;
|
|
4771
|
+
endEncoders("released");
|
|
4772
|
+
encoders = undefined;
|
|
4773
|
+
}
|
|
4774
|
+
function encodersOf(host) {
|
|
4775
|
+
if (encoders !== undefined)
|
|
4776
|
+
return encoders;
|
|
4777
|
+
encoders = null;
|
|
4778
|
+
const W = host.Worker;
|
|
4779
|
+
if (!W || !host.OffscreenCanvas)
|
|
4780
|
+
return null;
|
|
4781
|
+
const workers = [];
|
|
4782
|
+
let url = null;
|
|
4783
|
+
try {
|
|
4784
|
+
url = host.URL.createObjectURL(new host.Blob([ENCODER], { type: "text/javascript" }));
|
|
4785
|
+
for (let i = 0;i < ENCODERS; i++)
|
|
4786
|
+
workers.push(new W(url));
|
|
4787
|
+
} catch {
|
|
4788
|
+
for (const w of workers)
|
|
4789
|
+
w.terminate();
|
|
4790
|
+
return null;
|
|
4791
|
+
} finally {
|
|
4792
|
+
if (url)
|
|
4793
|
+
host.URL.revokeObjectURL(url);
|
|
4794
|
+
}
|
|
4795
|
+
const pool = { workers, waiting: new Map, next: 0 };
|
|
4796
|
+
for (const w of workers) {
|
|
4797
|
+
w.onmessage = (e) => {
|
|
4798
|
+
const job = pool.waiting.get(e.data.id);
|
|
4799
|
+
pool.waiting.delete(e.data.id);
|
|
4800
|
+
if (e.data.blob)
|
|
4801
|
+
job?.done(e.data.blob);
|
|
4802
|
+
else
|
|
4803
|
+
job?.fail(new Error(e.data.error));
|
|
4804
|
+
};
|
|
4805
|
+
w.onerror = () => {
|
|
4806
|
+
if (encoders !== pool)
|
|
4807
|
+
return;
|
|
4808
|
+
endEncoders("encoder");
|
|
4809
|
+
encoders = null;
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
encoders = pool;
|
|
4813
|
+
return pool;
|
|
4814
|
+
}
|
|
4815
|
+
function encodeOn(pool, bitmap, quality) {
|
|
4816
|
+
const id = pool.next++;
|
|
4817
|
+
const worker = pool.workers[id % pool.workers.length];
|
|
4818
|
+
return new Promise((done, fail) => {
|
|
4819
|
+
pool.waiting.set(id, { done, fail });
|
|
4820
|
+
try {
|
|
4821
|
+
worker.postMessage({ id, bitmap, quality }, [bitmap]);
|
|
4822
|
+
} catch (err) {
|
|
4823
|
+
pool.waiting.delete(id);
|
|
4824
|
+
bitmap.close();
|
|
4825
|
+
fail(err);
|
|
4826
|
+
}
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
async function toProxy(doc, source, w, h) {
|
|
4830
|
+
const bm = await toBitmap(doc, source, w, h, PROXY.size);
|
|
4831
|
+
const pool = encodersOf(doc.defaultView);
|
|
4832
|
+
if (pool)
|
|
4833
|
+
return encodeOn(pool, bm, PROXY.quality);
|
|
4834
|
+
try {
|
|
4835
|
+
const Off = doc.defaultView?.OffscreenCanvas;
|
|
4836
|
+
if (Off) {
|
|
4837
|
+
const c = new Off(bm.width, bm.height);
|
|
4838
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
4839
|
+
return await c.convertToBlob({ type: "image/jpeg", quality: PROXY.quality });
|
|
4840
|
+
}
|
|
4841
|
+
const c = doc.createElement("canvas");
|
|
4842
|
+
c.width = bm.width;
|
|
4843
|
+
c.height = bm.height;
|
|
4844
|
+
c.getContext("2d").drawImage(bm, 0, 0);
|
|
4845
|
+
return await new Promise((done, fail) => c.toBlob((b) => b ? done(b) : fail(new Error("toBlob")), "image/jpeg", PROXY.quality));
|
|
4846
|
+
} finally {
|
|
4847
|
+
bm.close();
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4669
4850
|
function acquireFrames(doc, src, fps) {
|
|
4670
4851
|
const key = `${fps}|${src}`;
|
|
4671
4852
|
let core = cores.get(key);
|
|
4672
4853
|
if (!core) {
|
|
4673
4854
|
core = makeCore(doc, key, src, fps);
|
|
4674
4855
|
cores.set(key, core);
|
|
4856
|
+
holdEncoders();
|
|
4675
4857
|
}
|
|
4676
4858
|
core.refs++;
|
|
4677
4859
|
return core.cache;
|
|
@@ -4692,6 +4874,71 @@ function makeCore(doc, key, src, fps) {
|
|
|
4692
4874
|
let generation = 0;
|
|
4693
4875
|
let tick = 0;
|
|
4694
4876
|
let notifyRaf = 0;
|
|
4877
|
+
const proxies = new Map;
|
|
4878
|
+
let proxyBytes = 0;
|
|
4879
|
+
let stride = 1;
|
|
4880
|
+
const decoded = new Map;
|
|
4881
|
+
const decoding = new Set;
|
|
4882
|
+
let aim = null;
|
|
4883
|
+
let lastAsked = null;
|
|
4884
|
+
const storeProxy = (frame, blob) => {
|
|
4885
|
+
if (disposed || frame % stride || proxies.has(frame))
|
|
4886
|
+
return;
|
|
4887
|
+
proxies.set(frame, blob);
|
|
4888
|
+
proxyBytes += blob.size;
|
|
4889
|
+
const next = proxyStride(proxyBytes, proxies.size, total, PROXY.budget);
|
|
4890
|
+
if (next <= stride)
|
|
4891
|
+
return;
|
|
4892
|
+
stride = next;
|
|
4893
|
+
for (const [f, b] of proxies)
|
|
4894
|
+
if (f % stride) {
|
|
4895
|
+
proxies.delete(f);
|
|
4896
|
+
proxyBytes -= b.size;
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
const proxyNear = (frame) => {
|
|
4900
|
+
const at = Math.round(frame / stride) * stride;
|
|
4901
|
+
let best = null;
|
|
4902
|
+
for (const f of [at, at - stride, at + stride])
|
|
4903
|
+
if (proxies.has(f) && (best === null || Math.abs(f - frame) < Math.abs(best - frame)))
|
|
4904
|
+
best = f;
|
|
4905
|
+
return best;
|
|
4906
|
+
};
|
|
4907
|
+
const proxyTodo = () => {
|
|
4908
|
+
const out = [];
|
|
4909
|
+
for (let f = 0;f < total; f += stride)
|
|
4910
|
+
if (!proxies.has(f))
|
|
4911
|
+
out.push(f);
|
|
4912
|
+
return out;
|
|
4913
|
+
};
|
|
4914
|
+
const decodeProxy = (frame) => {
|
|
4915
|
+
if (frame === null || decoded.has(frame) || decoding.has(frame) || disposed)
|
|
4916
|
+
return;
|
|
4917
|
+
const blob = proxies.get(frame);
|
|
4918
|
+
if (!blob)
|
|
4919
|
+
return;
|
|
4920
|
+
decoding.add(frame);
|
|
4921
|
+
createImageBitmap(blob).then((bitmap) => {
|
|
4922
|
+
decoding.delete(frame);
|
|
4923
|
+
if (disposed)
|
|
4924
|
+
return bitmap.close();
|
|
4925
|
+
decoded.set(frame, { bitmap, used: ++tick });
|
|
4926
|
+
if (decoded.size > PROXY.decoded) {
|
|
4927
|
+
const [oldest] = [...decoded].sort((a, b) => a[1].used - b[1].used);
|
|
4928
|
+
if (oldest && oldest[0] !== frame) {
|
|
4929
|
+
oldest[1].bitmap.close();
|
|
4930
|
+
decoded.delete(oldest[0]);
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
for (const f of listeners)
|
|
4934
|
+
f();
|
|
4935
|
+
}, () => decoding.delete(frame));
|
|
4936
|
+
};
|
|
4937
|
+
const dropDecoded = () => {
|
|
4938
|
+
for (const d of decoded.values())
|
|
4939
|
+
d.bitmap.close();
|
|
4940
|
+
decoded.clear();
|
|
4941
|
+
};
|
|
4695
4942
|
const notify = () => {
|
|
4696
4943
|
if (notifyRaf)
|
|
4697
4944
|
return;
|
|
@@ -4822,18 +5069,50 @@ function makeCore(doc, key, src, fps) {
|
|
|
4822
5069
|
return;
|
|
4823
5070
|
const forFocus = todo[0].kind === "focus";
|
|
4824
5071
|
const gen = generation;
|
|
4825
|
-
|
|
5072
|
+
await decodeRun(t, data, run, forFocus ? () => gen !== generation : () => false, (vf, f) => {
|
|
5073
|
+
const want = plan.get(f);
|
|
5074
|
+
if (!want)
|
|
5075
|
+
return null;
|
|
5076
|
+
plan.delete(f);
|
|
5077
|
+
return toBitmap(doc, vf, vf.displayWidth, vf.displayHeight, want.full ? FULL : PREVIEW).then((bm) => store(f, bm, want.full, want.kind));
|
|
5078
|
+
});
|
|
5079
|
+
};
|
|
5080
|
+
const proxyUnit = async () => {
|
|
5081
|
+
const todo = proxyTodo();
|
|
5082
|
+
if (!todo.length || !track || !buf)
|
|
5083
|
+
return;
|
|
5084
|
+
const runs = decodeRuns(track.samples, new Set(todo), (pts) => frameOf(pts, fps));
|
|
5085
|
+
const at = aim ?? focusAt ?? 0;
|
|
5086
|
+
let run = runs[0];
|
|
5087
|
+
let best = Number.POSITIVE_INFINITY;
|
|
5088
|
+
for (const r of runs) {
|
|
5089
|
+
const d = Math.min(...r.frames.map((f) => Math.abs(f - at)));
|
|
5090
|
+
if (d < best) {
|
|
5091
|
+
best = d;
|
|
5092
|
+
run = r;
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
if (!run)
|
|
5096
|
+
return;
|
|
5097
|
+
const gen = generation;
|
|
5098
|
+
let made = 0;
|
|
5099
|
+
await decodeRun(track, buf, run, () => gen !== generation || made >= PROXY.turn, (vf, f) => {
|
|
5100
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
5101
|
+
return null;
|
|
5102
|
+
made++;
|
|
5103
|
+
return toProxy(doc, vf, vf.displayWidth, vf.displayHeight).then((b) => storeProxy(f, b));
|
|
5104
|
+
});
|
|
5105
|
+
};
|
|
5106
|
+
const decodeRun = async (t, data, run, stale, take) => {
|
|
4826
5107
|
let pending = 0;
|
|
4827
5108
|
let failed = false;
|
|
4828
5109
|
const decoder = new VideoDecoder({
|
|
4829
5110
|
output: (vf) => {
|
|
4830
|
-
const
|
|
4831
|
-
|
|
4832
|
-
if (!want || stale() || disposed)
|
|
5111
|
+
const work = stale() || disposed ? null : take(vf, frameOf(vf.timestamp / 1e6, fps));
|
|
5112
|
+
if (!work)
|
|
4833
5113
|
return vf.close();
|
|
4834
|
-
plan.delete(f);
|
|
4835
5114
|
pending++;
|
|
4836
|
-
|
|
5115
|
+
work.catch(() => {}).finally(() => {
|
|
4837
5116
|
vf.close();
|
|
4838
5117
|
pending--;
|
|
4839
5118
|
});
|
|
@@ -4947,6 +5226,25 @@ function makeCore(doc, key, src, fps) {
|
|
|
4947
5226
|
return points.some((p) => p.frame + p.tolerance >= f && !covered(p));
|
|
4948
5227
|
}, 6000, false);
|
|
4949
5228
|
};
|
|
5229
|
+
const videoProxyUnit = async () => {
|
|
5230
|
+
const v = sampler;
|
|
5231
|
+
const todo = proxyTodo();
|
|
5232
|
+
if (!v || !todo.length || !v.videoWidth)
|
|
5233
|
+
return;
|
|
5234
|
+
const at = aim ?? focusAt ?? 0;
|
|
5235
|
+
const first = todo.find((f) => f >= at) ?? todo[0];
|
|
5236
|
+
const grab = async (f) => {
|
|
5237
|
+
if (f % stride || proxies.has(f) || f >= total)
|
|
5238
|
+
return;
|
|
5239
|
+
storeProxy(f, await toProxy(doc, v, v.videoWidth, v.videoHeight));
|
|
5240
|
+
};
|
|
5241
|
+
await seekTo(v, (first + 0.5) / fps);
|
|
5242
|
+
await grab(frameOf(v.currentTime, fps));
|
|
5243
|
+
await playThrough(v, 1, async (f) => {
|
|
5244
|
+
await grab(f);
|
|
5245
|
+
return proxyTodo().length > 0;
|
|
5246
|
+
}, 4000, true);
|
|
5247
|
+
};
|
|
4950
5248
|
const cache = {
|
|
4951
5249
|
src,
|
|
4952
5250
|
get backend() {
|
|
@@ -4955,29 +5253,56 @@ function makeCore(doc, key, src, fps) {
|
|
|
4955
5253
|
get size() {
|
|
4956
5254
|
return entries.size;
|
|
4957
5255
|
},
|
|
4958
|
-
|
|
5256
|
+
get proxies() {
|
|
5257
|
+
return { count: proxies.size, stride, bytes: proxyBytes };
|
|
5258
|
+
},
|
|
5259
|
+
nearest(frame, within = Infinity, decode = false) {
|
|
4959
5260
|
const k = nearestKey(entries.keys(), frame, within);
|
|
5261
|
+
const kd = nearestKey(decoded.keys(), frame, within);
|
|
5262
|
+
const dk = k === null ? Number.POSITIVE_INFINITY : Math.abs(k - frame);
|
|
5263
|
+
const dd = kd === null ? Number.POSITIVE_INFINITY : Math.abs(kd - frame);
|
|
5264
|
+
if (decode) {
|
|
5265
|
+
const p = proxyNear(frame);
|
|
5266
|
+
if (p !== null && Math.abs(p - frame) < Math.min(dk, dd))
|
|
5267
|
+
decodeProxy(p);
|
|
5268
|
+
const ahead = aheadOf(frame, lastAsked, total);
|
|
5269
|
+
if (ahead !== null)
|
|
5270
|
+
decodeProxy(proxyNear(ahead));
|
|
5271
|
+
lastAsked = frame;
|
|
5272
|
+
}
|
|
5273
|
+
if (kd !== null && (dd < dk || dd === dk && !entries.get(k)?.full)) {
|
|
5274
|
+
const d = decoded.get(kd);
|
|
5275
|
+
d.used = ++tick;
|
|
5276
|
+
return { frame: kd, bitmap: d.bitmap, full: false };
|
|
5277
|
+
}
|
|
4960
5278
|
if (k === null)
|
|
4961
5279
|
return null;
|
|
4962
5280
|
const e = entries.get(k);
|
|
4963
5281
|
e.used = ++tick;
|
|
4964
5282
|
return { frame: k, bitmap: e.bitmap, full: e.full };
|
|
4965
5283
|
},
|
|
5284
|
+
covers(frame) {
|
|
5285
|
+
if (entries.has(frame) || decoded.has(frame))
|
|
5286
|
+
return true;
|
|
5287
|
+
const p = proxyNear(frame);
|
|
5288
|
+
return p !== null && Math.abs(p - frame) <= stride / 2;
|
|
5289
|
+
},
|
|
4966
5290
|
focus(frame) {
|
|
4967
|
-
|
|
4968
|
-
active?.dropFull();
|
|
4969
|
-
if (active)
|
|
4970
|
-
active.active = false;
|
|
4971
|
-
active = core;
|
|
4972
|
-
core.active = true;
|
|
4973
|
-
}
|
|
5291
|
+
claim(core);
|
|
4974
5292
|
const moved = focusAt === null || Math.abs(frame - focusAt) > FOCUS_RADIUS;
|
|
4975
5293
|
focusAt = frame;
|
|
5294
|
+
aim = null;
|
|
4976
5295
|
if (moved)
|
|
4977
5296
|
generation++;
|
|
4978
5297
|
start();
|
|
4979
5298
|
pump(win);
|
|
4980
5299
|
},
|
|
5300
|
+
follow(frame) {
|
|
5301
|
+
claim(core);
|
|
5302
|
+
aim = frame;
|
|
5303
|
+
start();
|
|
5304
|
+
pump(win);
|
|
5305
|
+
},
|
|
4981
5306
|
want(points) {
|
|
4982
5307
|
wants = points;
|
|
4983
5308
|
if (core.shown > 0)
|
|
@@ -5015,12 +5340,15 @@ function makeCore(doc, key, src, fps) {
|
|
|
5015
5340
|
refs: 0,
|
|
5016
5341
|
shown: 0,
|
|
5017
5342
|
active: false,
|
|
5343
|
+
ran: 0,
|
|
5018
5344
|
work() {
|
|
5019
5345
|
if (disposed || backend === "pending")
|
|
5020
5346
|
return null;
|
|
5021
|
-
if (
|
|
5022
|
-
return
|
|
5023
|
-
|
|
5347
|
+
if (missing().length)
|
|
5348
|
+
return backend === "webcodecs" ? decodeUnit() : videoUnit();
|
|
5349
|
+
if (proxyTodo().length)
|
|
5350
|
+
return backend === "webcodecs" ? proxyUnit() : videoProxyUnit();
|
|
5351
|
+
return null;
|
|
5024
5352
|
},
|
|
5025
5353
|
dropFull() {
|
|
5026
5354
|
for (const [f, e] of entries)
|
|
@@ -5029,8 +5357,13 @@ function makeCore(doc, key, src, fps) {
|
|
|
5029
5357
|
entries.delete(f);
|
|
5030
5358
|
}
|
|
5031
5359
|
focusAt = null;
|
|
5360
|
+
aim = null;
|
|
5361
|
+
lastAsked = null;
|
|
5362
|
+
dropDecoded();
|
|
5032
5363
|
},
|
|
5033
5364
|
dispose() {
|
|
5365
|
+
if (disposed)
|
|
5366
|
+
return;
|
|
5034
5367
|
disposed = true;
|
|
5035
5368
|
generation++;
|
|
5036
5369
|
if (notifyRaf)
|
|
@@ -5038,6 +5371,8 @@ function makeCore(doc, key, src, fps) {
|
|
|
5038
5371
|
for (const e of entries.values())
|
|
5039
5372
|
e.bitmap.close();
|
|
5040
5373
|
entries.clear();
|
|
5374
|
+
dropDecoded();
|
|
5375
|
+
proxies.clear();
|
|
5041
5376
|
listeners.clear();
|
|
5042
5377
|
if (sampler) {
|
|
5043
5378
|
sampler.removeAttribute("src");
|
|
@@ -5045,24 +5380,45 @@ function makeCore(doc, key, src, fps) {
|
|
|
5045
5380
|
}
|
|
5046
5381
|
buf = null;
|
|
5047
5382
|
track = null;
|
|
5048
|
-
|
|
5049
|
-
|
|
5383
|
+
actives.delete(core);
|
|
5384
|
+
round?.delete(core);
|
|
5050
5385
|
cores.delete(key);
|
|
5386
|
+
dropEncoders();
|
|
5051
5387
|
}
|
|
5052
5388
|
};
|
|
5053
5389
|
return core;
|
|
5054
5390
|
}
|
|
5055
|
-
var FOCUS_RADIUS = 6, GRID = 48, SEEN = 12, WANTED = 48, PREVIEW = 384, FULL = 1280, cores,
|
|
5391
|
+
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, round = null, running = false, turns = 0, idle = (win) => new Promise((done) => {
|
|
5056
5392
|
const ric = win.requestIdleCallback;
|
|
5057
5393
|
if (ric)
|
|
5058
5394
|
ric(() => done(), { timeout: 60 });
|
|
5059
5395
|
else
|
|
5060
5396
|
win.setTimeout(done, 4);
|
|
5061
|
-
}), sleep = (win, ms) => new Promise((done) => win.setTimeout(done, ms));
|
|
5397
|
+
}), 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) => {
|
|
5398
|
+
const pool = encoders;
|
|
5399
|
+
if (!pool)
|
|
5400
|
+
return;
|
|
5401
|
+
for (const w of pool.workers) {
|
|
5402
|
+
w.onmessage = null;
|
|
5403
|
+
w.onerror = null;
|
|
5404
|
+
w.terminate();
|
|
5405
|
+
}
|
|
5406
|
+
for (const j of pool.waiting.values())
|
|
5407
|
+
j.fail(new Error(why));
|
|
5408
|
+
pool.waiting.clear();
|
|
5409
|
+
};
|
|
5062
5410
|
var init_compare_frames = __esm(() => {
|
|
5063
5411
|
init_compare_model();
|
|
5064
5412
|
init_compare_mp4();
|
|
5413
|
+
PROXY = {
|
|
5414
|
+
size: 960,
|
|
5415
|
+
quality: 0.8,
|
|
5416
|
+
budget: 48 * 2 ** 20,
|
|
5417
|
+
decoded: 8,
|
|
5418
|
+
turn: 90
|
|
5419
|
+
};
|
|
5065
5420
|
cores = new Map;
|
|
5421
|
+
actives = new Set;
|
|
5066
5422
|
});
|
|
5067
5423
|
|
|
5068
5424
|
// src/ui/compare-gpu.ts
|
|
@@ -5425,589 +5781,65 @@ var init_rgba = __esm(() => {
|
|
|
5425
5781
|
cache = new Map;
|
|
5426
5782
|
});
|
|
5427
5783
|
|
|
5428
|
-
// src/ui/
|
|
5429
|
-
function
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
const
|
|
5433
|
-
const
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5784
|
+
// src/ui/overlay-model.ts
|
|
5785
|
+
function place(o) {
|
|
5786
|
+
const { anchor: a, size, view: v } = o;
|
|
5787
|
+
const offset = o.offset ?? 6;
|
|
5788
|
+
const margin = o.margin ?? 8;
|
|
5789
|
+
const [want, align = "center"] = (o.placement ?? "bottom").split("-");
|
|
5790
|
+
const room = (s) => s === "top" ? a.y - v.y : s === "bottom" ? v.y + v.h - (a.y + a.h) : s === "left" ? a.x - v.x : v.x + v.w - (a.x + a.w);
|
|
5791
|
+
const need = (s) => (s === "top" || s === "bottom" ? size.h : size.w) + offset + margin;
|
|
5792
|
+
let side = want;
|
|
5793
|
+
if (room(want) < need(want) && room(opposite[want]) > room(want))
|
|
5794
|
+
side = opposite[want];
|
|
5795
|
+
const vertical = side === "top" || side === "bottom";
|
|
5796
|
+
let x;
|
|
5797
|
+
let y;
|
|
5798
|
+
if (vertical) {
|
|
5799
|
+
y = side === "bottom" ? a.y + a.h + offset : a.y - offset - size.h;
|
|
5800
|
+
x = align === "start" ? a.x : align === "end" ? a.x + a.w - size.w : a.x + (a.w - size.w) / 2;
|
|
5801
|
+
x = clamp(x, v.x + margin, v.x + v.w - margin - size.w);
|
|
5802
|
+
} else {
|
|
5803
|
+
x = side === "right" ? a.x + a.w + offset : a.x - offset - size.w;
|
|
5804
|
+
y = align === "start" ? a.y : align === "end" ? a.y + a.h - size.h : a.y + (a.h - size.h) / 2;
|
|
5805
|
+
y = clamp(y, v.y + margin, v.y + v.h - margin - size.h);
|
|
5440
5806
|
}
|
|
5441
|
-
const
|
|
5442
|
-
return
|
|
5807
|
+
const origin = vertical ? { x: clamp(a.x + a.w / 2 - x, 0, size.w), y: side === "bottom" ? 0 : size.h } : { x: side === "right" ? 0 : size.w, y: clamp(a.y + a.h / 2 - y, 0, size.h) };
|
|
5808
|
+
return {
|
|
5809
|
+
x: Math.round(x),
|
|
5810
|
+
y: Math.round(y),
|
|
5811
|
+
placement: align === "center" ? side : `${side}-${align}`,
|
|
5812
|
+
origin
|
|
5813
|
+
};
|
|
5443
5814
|
}
|
|
5444
|
-
function
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
const steps = units === "frames" ? FRAME_STEPS.map((f) => f / fps) : SECOND_STEPS;
|
|
5450
|
-
const step = steps.find((s) => s * pxPerSec >= minGap) ?? steps[steps.length - 1];
|
|
5451
|
-
const frame = 1 / fps;
|
|
5452
|
-
let minor = 0;
|
|
5453
|
-
for (const div of [10, 5, 4, 2]) {
|
|
5454
|
-
const m = step / div;
|
|
5455
|
-
if (m * pxPerSec < 7)
|
|
5456
|
-
continue;
|
|
5457
|
-
if (units === "frames" && Math.abs(m * fps - Math.round(m * fps)) > 0.000001)
|
|
5458
|
-
continue;
|
|
5459
|
-
minor = m;
|
|
5460
|
-
break;
|
|
5461
|
-
}
|
|
5462
|
-
if (frame * pxPerSec >= (o.frameGap ?? 6))
|
|
5463
|
-
minor = Math.min(frame, step);
|
|
5464
|
-
const end = view.start + view.span;
|
|
5465
|
-
const major = [];
|
|
5466
|
-
const first = Math.ceil(view.start / step - 0.000000001);
|
|
5467
|
-
for (let i = first;i * step <= end + 0.000000001; i++) {
|
|
5468
|
-
const t = round(i * step);
|
|
5469
|
-
major.push({ t, label: timeLabel(t, units, fps, step) });
|
|
5470
|
-
}
|
|
5471
|
-
const minors = [];
|
|
5472
|
-
if (minor > 0) {
|
|
5473
|
-
const firstMinor = Math.ceil(view.start / minor - 0.000000001);
|
|
5474
|
-
for (let i = firstMinor;i * minor <= end + 0.000000001; i++) {
|
|
5475
|
-
const t = i * minor;
|
|
5476
|
-
if (Math.abs(t / step - Math.round(t / step)) > 0.000001)
|
|
5477
|
-
minors.push(round(t));
|
|
5478
|
-
}
|
|
5815
|
+
function stepItem(count, from, by, ok) {
|
|
5816
|
+
for (let k = 1;k <= count; k++) {
|
|
5817
|
+
const i = (((from < 0 ? by > 0 ? -1 : 0 : from) + by * k) % count + count) % count;
|
|
5818
|
+
if (ok(i))
|
|
5819
|
+
return i;
|
|
5479
5820
|
}
|
|
5480
|
-
return
|
|
5481
|
-
}
|
|
5482
|
-
function clampView(view, duration, o = {}) {
|
|
5483
|
-
const pad = o.pad ?? 0.04;
|
|
5484
|
-
const maxSpan = duration * (1 + pad * 2);
|
|
5485
|
-
const span = Math.min(maxSpan, Math.max(o.minSpan ?? 0.05, view.span));
|
|
5486
|
-
const lo = -duration * pad;
|
|
5487
|
-
const hi = duration * (1 + pad) - span;
|
|
5488
|
-
const start = hi < lo ? (lo + hi) / 2 : Math.min(hi, Math.max(lo, view.start));
|
|
5489
|
-
return { start, span };
|
|
5490
|
-
}
|
|
5491
|
-
function zoomView(view, factor, at) {
|
|
5492
|
-
const span = view.span / factor;
|
|
5493
|
-
const k = (at - view.start) / view.span;
|
|
5494
|
-
return { start: at - k * span, span };
|
|
5495
|
-
}
|
|
5496
|
-
function followView(view, t, margin = 0.08) {
|
|
5497
|
-
const m = view.span * margin;
|
|
5498
|
-
if (t < view.start + m)
|
|
5499
|
-
return { ...view, start: t - m };
|
|
5500
|
-
if (t > view.start + view.span - m)
|
|
5501
|
-
return { ...view, start: t - view.span + m };
|
|
5502
|
-
return view;
|
|
5821
|
+
return from;
|
|
5503
5822
|
}
|
|
5504
|
-
function
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5823
|
+
function matchItem(labels, from, typed) {
|
|
5824
|
+
const q = typed.toLowerCase();
|
|
5825
|
+
if (!q || !labels.length)
|
|
5826
|
+
return from;
|
|
5827
|
+
const same = q.length > 1 && [...q].every((c) => c === q[0]);
|
|
5828
|
+
const word = same ? q[0] : q;
|
|
5829
|
+
const start = q.length > 1 && !same ? Math.max(0, from) : from + 1;
|
|
5830
|
+
for (let k = 0;k < labels.length; k++) {
|
|
5831
|
+
const i = ((start + k) % labels.length + labels.length) % labels.length;
|
|
5832
|
+
if (labels[i]?.toLowerCase().startsWith(word))
|
|
5833
|
+
return i;
|
|
5513
5834
|
}
|
|
5514
|
-
|
|
5515
|
-
return { t: raw, stop: null };
|
|
5516
|
-
const u = bestPx / radius;
|
|
5517
|
-
const pulled = best + Math.sign(raw - best) * (u * u * radius) / pxPerSec;
|
|
5518
|
-
return { t: pulled, stop: u < stick ? best : null };
|
|
5835
|
+
return from;
|
|
5519
5836
|
}
|
|
5520
|
-
function
|
|
5521
|
-
const
|
|
5522
|
-
const
|
|
5523
|
-
const
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
function adjacentMarker(markers, t, dir, fps) {
|
|
5527
|
-
const eps = 0.5 / fps;
|
|
5528
|
-
const sorted = [...markers].sort((a, b) => a.time - b.time);
|
|
5529
|
-
if (dir > 0)
|
|
5530
|
-
return sorted.find((m) => m.time > t + eps) ?? null;
|
|
5531
|
-
return [...sorted].reverse().find((m) => m.time < t - eps) ?? null;
|
|
5532
|
-
}
|
|
5533
|
-
function advance(t, dt, o) {
|
|
5534
|
-
const lo = o.loop ? o.loop.start : 0;
|
|
5535
|
-
const hi = o.loop ? o.loop.end : o.duration;
|
|
5536
|
-
let next = t + dt;
|
|
5537
|
-
if (hi - lo <= 0.000001)
|
|
5538
|
-
return { t: lo, ended: true, wrapped: false };
|
|
5539
|
-
if (next < hi)
|
|
5540
|
-
return { t: next, ended: false, wrapped: false };
|
|
5541
|
-
if (o.loop || o.repeat) {
|
|
5542
|
-
next = lo + (next - lo) % (hi - lo);
|
|
5543
|
-
return { t: next, ended: false, wrapped: true };
|
|
5544
|
-
}
|
|
5545
|
-
return { t: hi, ended: true, wrapped: false };
|
|
5546
|
-
}
|
|
5547
|
-
function moveHead(head, t, motion, catching, minGap = 0) {
|
|
5548
|
-
if (motion === "jump" && Math.abs(t - head.x) > minGap)
|
|
5549
|
-
return { x: head.x, target: t, catching: true };
|
|
5550
|
-
if (catching)
|
|
5551
|
-
return { x: head.x + (t - head.target), target: t, catching: true };
|
|
5552
|
-
return { x: t, target: t, catching: false };
|
|
5553
|
-
}
|
|
5554
|
-
function currentMarker(markers, t, fps) {
|
|
5555
|
-
let best = null;
|
|
5556
|
-
for (const m of markers)
|
|
5557
|
-
if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
|
|
5558
|
-
best = m;
|
|
5559
|
-
return best;
|
|
5560
|
-
}
|
|
5561
|
-
function minutes(t) {
|
|
5562
|
-
const s = Math.max(0, Math.floor(t + 0.000001));
|
|
5563
|
-
const h = Math.floor(s / 3600);
|
|
5564
|
-
const m = Math.floor(s % 3600 / 60);
|
|
5565
|
-
const ss = String(s % 60).padStart(2, "0");
|
|
5566
|
-
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
5567
|
-
}
|
|
5568
|
-
function chapterBounds(times, duration) {
|
|
5569
|
-
const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
|
|
5570
|
-
return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
|
|
5571
|
-
}
|
|
5572
|
-
var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
|
|
5573
|
-
var init_timeline_model = __esm(() => {
|
|
5574
|
-
SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
|
|
5575
|
-
FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
|
|
5576
|
-
CATCH_UP = { duration: 0.15, bounce: 0 };
|
|
5577
|
-
});
|
|
5578
|
-
|
|
5579
|
-
// src/ui/compare-strip.ts
|
|
5580
|
-
function compareStrip(container, options) {
|
|
5581
|
-
const doc = container.ownerDocument;
|
|
5582
|
-
const win = doc.defaultView;
|
|
5583
|
-
const timeline = options.timeline;
|
|
5584
|
-
const fps = () => options.fps ?? timeline.fps;
|
|
5585
|
-
const align = options.align ?? "track";
|
|
5586
|
-
const threshold = options.threshold ?? 4 / 255;
|
|
5587
|
-
const root = doc.createElement("div");
|
|
5588
|
-
root.className = "knobs-cmp-strip";
|
|
5589
|
-
root.setAttribute("role", "img");
|
|
5590
|
-
root.setAttribute("aria-label", "Frame strip: Before over After, with how much they differ");
|
|
5591
|
-
applyLook2(root, options);
|
|
5592
|
-
const canvas = doc.createElement("canvas");
|
|
5593
|
-
canvas.setAttribute("aria-hidden", "true");
|
|
5594
|
-
root.append(canvas);
|
|
5595
|
-
container.append(root);
|
|
5596
|
-
const diffs = new Map;
|
|
5597
|
-
const pairDiffs = new Map;
|
|
5598
|
-
let disposed = false;
|
|
5599
|
-
let near = false;
|
|
5600
|
-
let onScreen = false;
|
|
5601
|
-
const makeSide = (source) => ({
|
|
5602
|
-
source,
|
|
5603
|
-
kind: mediaKind(source.src, source.kind),
|
|
5604
|
-
frames: null,
|
|
5605
|
-
still: null,
|
|
5606
|
-
seen: false
|
|
5607
|
-
});
|
|
5608
|
-
let sides = [makeSide(options.before), makeSide(options.after)];
|
|
5609
|
-
const offs = new Map;
|
|
5610
|
-
const load = (s) => {
|
|
5611
|
-
if (s.frames || s.still)
|
|
5612
|
-
return;
|
|
5613
|
-
if (s.kind === "video") {
|
|
5614
|
-
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
5615
|
-
offs.set(s, s.frames.listen(schedule));
|
|
5616
|
-
if (onScreen) {
|
|
5617
|
-
s.frames.visible(true);
|
|
5618
|
-
s.seen = true;
|
|
5619
|
-
}
|
|
5620
|
-
} else {
|
|
5621
|
-
const img = doc.createElement("img");
|
|
5622
|
-
img.crossOrigin = "anonymous";
|
|
5623
|
-
img.decoding = "async";
|
|
5624
|
-
img.addEventListener("load", () => {
|
|
5625
|
-
pairDiffs.clear();
|
|
5626
|
-
schedule();
|
|
5627
|
-
});
|
|
5628
|
-
img.src = s.source.src;
|
|
5629
|
-
s.still = img;
|
|
5630
|
-
}
|
|
5631
|
-
};
|
|
5632
|
-
const unload = (s) => {
|
|
5633
|
-
offs.get(s)?.();
|
|
5634
|
-
offs.delete(s);
|
|
5635
|
-
if (s.frames) {
|
|
5636
|
-
if (s.seen)
|
|
5637
|
-
s.frames.visible(false);
|
|
5638
|
-
s.frames.release();
|
|
5639
|
-
}
|
|
5640
|
-
s.frames = null;
|
|
5641
|
-
s.seen = false;
|
|
5642
|
-
if (s.still)
|
|
5643
|
-
s.still.removeAttribute("src");
|
|
5644
|
-
s.still = null;
|
|
5645
|
-
};
|
|
5646
|
-
let w = 1;
|
|
5647
|
-
let h = 1;
|
|
5648
|
-
let geo = { x: 0, w: 1 };
|
|
5649
|
-
const rowH = () => Math.max(4, (h - BAR - GAP * 2) / 2);
|
|
5650
|
-
const aspectOf = (s) => {
|
|
5651
|
-
if (s.still?.naturalWidth)
|
|
5652
|
-
return s.still.naturalWidth / s.still.naturalHeight;
|
|
5653
|
-
const c = s.frames?.nearest(0);
|
|
5654
|
-
return c ? c.bitmap.width / c.bitmap.height : 0;
|
|
5655
|
-
};
|
|
5656
|
-
const columns = () => stripColumns(timeline.view, geo.w, {
|
|
5657
|
-
duration: timeline.duration,
|
|
5658
|
-
fps: fps(),
|
|
5659
|
-
minWidth: rowH() * Math.max(16 / 9, ...sides.map(aspectOf)) + GAP
|
|
5660
|
-
});
|
|
5661
|
-
const measure = () => {
|
|
5662
|
-
const r = root.getBoundingClientRect();
|
|
5663
|
-
w = Math.max(1, r.width);
|
|
5664
|
-
h = Math.max(1, r.height);
|
|
5665
|
-
geo = { x: 0, w };
|
|
5666
|
-
if (align === "track") {
|
|
5667
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
5668
|
-
const t = track?.getBoundingClientRect();
|
|
5669
|
-
if (t && t.width > 0)
|
|
5670
|
-
geo = { x: t.left - r.left, w: t.width };
|
|
5671
|
-
}
|
|
5672
|
-
readColors();
|
|
5673
|
-
schedule();
|
|
5674
|
-
};
|
|
5675
|
-
const ask = (cols) => {
|
|
5676
|
-
const points = cols.map((c) => ({ frame: c.frame, tolerance: Math.floor((c.to - c.from) / 2) }));
|
|
5677
|
-
for (const s of sides)
|
|
5678
|
-
s.frames?.want(points);
|
|
5679
|
-
};
|
|
5680
|
-
const picOf = (s, c) => {
|
|
5681
|
-
if (s.still?.naturalWidth)
|
|
5682
|
-
return { img: s.still, w: s.still.naturalWidth, h: s.still.naturalHeight, key: "still" };
|
|
5683
|
-
const f = s.frames?.nearest(c.frame, Math.max(1, c.to - c.from));
|
|
5684
|
-
return f ? { img: f.bitmap, w: f.bitmap.width, h: f.bitmap.height, key: String(f.frame) } : null;
|
|
5685
|
-
};
|
|
5686
|
-
const probe = doc.createElement("canvas");
|
|
5687
|
-
probe.width = DIFF.w;
|
|
5688
|
-
probe.height = DIFF.h;
|
|
5689
|
-
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
5690
|
-
const differ = (a, b) => {
|
|
5691
|
-
if (!probeCtx)
|
|
5692
|
-
return;
|
|
5693
|
-
try {
|
|
5694
|
-
probeCtx.drawImage(a.img, 0, 0, DIFF.w, DIFF.h);
|
|
5695
|
-
const pa = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
5696
|
-
probeCtx.drawImage(b.img, 0, 0, DIFF.w, DIFF.h);
|
|
5697
|
-
const pb = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
5698
|
-
return meanAbsDiff(pa, pb);
|
|
5699
|
-
} catch {
|
|
5700
|
-
return;
|
|
5701
|
-
}
|
|
5702
|
-
};
|
|
5703
|
-
let colors = { surface: "", accent: "", diff: "", wash: "" };
|
|
5704
|
-
const rgba = (raw, fallback) => {
|
|
5705
|
-
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
5706
|
-
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
5707
|
-
};
|
|
5708
|
-
const readColors = () => {
|
|
5709
|
-
const cs = win.getComputedStyle(root);
|
|
5710
|
-
colors = {
|
|
5711
|
-
surface: rgba(tokenCss(cs, "--kv-fill", ""), [1, 1, 1, 0.055]),
|
|
5712
|
-
accent: rgba(tokenCss(cs, "--kv-accent", ""), [1, 1, 1, 1]),
|
|
5713
|
-
diff: rgba(tokenCss(cs, "--_ku-cmp-diff", ""), [0.49, 0.81, 0.61, 1]),
|
|
5714
|
-
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
5715
|
-
};
|
|
5716
|
-
};
|
|
5717
|
-
let hover = null;
|
|
5718
|
-
let raf = 0;
|
|
5719
|
-
let asked = "";
|
|
5720
|
-
function schedule() {
|
|
5721
|
-
if (!raf && !disposed)
|
|
5722
|
-
raf = win.requestAnimationFrame(draw);
|
|
5723
|
-
}
|
|
5724
|
-
const draw = () => {
|
|
5725
|
-
raf = 0;
|
|
5726
|
-
if (!near)
|
|
5727
|
-
return;
|
|
5728
|
-
const dpr = Math.min(3, Math.max(1, win.devicePixelRatio || 1));
|
|
5729
|
-
const cw = Math.round(w * dpr);
|
|
5730
|
-
const ch = Math.round(h * dpr);
|
|
5731
|
-
if (canvas.width !== cw || canvas.height !== ch) {
|
|
5732
|
-
canvas.width = cw;
|
|
5733
|
-
canvas.height = ch;
|
|
5734
|
-
}
|
|
5735
|
-
const ctx = canvas.getContext("2d");
|
|
5736
|
-
if (!ctx)
|
|
5737
|
-
return;
|
|
5738
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
5739
|
-
ctx.clearRect(0, 0, w, h);
|
|
5740
|
-
const cols = columns();
|
|
5741
|
-
const key = cols.map((c) => c.frame).join(",");
|
|
5742
|
-
if (key !== asked) {
|
|
5743
|
-
asked = key;
|
|
5744
|
-
ask(cols);
|
|
5745
|
-
}
|
|
5746
|
-
const rh = rowH();
|
|
5747
|
-
const radius = 3;
|
|
5748
|
-
let budget = DIFFS_PER_DRAW;
|
|
5749
|
-
let more = false;
|
|
5750
|
-
const ds = cols.map((c) => {
|
|
5751
|
-
const a = picOf(sides[0], c);
|
|
5752
|
-
const b = picOf(sides[1], c);
|
|
5753
|
-
if (!a || !b)
|
|
5754
|
-
return;
|
|
5755
|
-
const k = `${a.key}|${b.key}`;
|
|
5756
|
-
if (!pairDiffs.has(k)) {
|
|
5757
|
-
if (budget-- <= 0) {
|
|
5758
|
-
more = true;
|
|
5759
|
-
return;
|
|
5760
|
-
}
|
|
5761
|
-
pairDiffs.set(k, differ(a, b));
|
|
5762
|
-
}
|
|
5763
|
-
const d = pairDiffs.get(k);
|
|
5764
|
-
if (d !== undefined)
|
|
5765
|
-
diffs.set(c.frame, d);
|
|
5766
|
-
return d;
|
|
5767
|
-
});
|
|
5768
|
-
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
5769
|
-
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
5770
|
-
cols.forEach((c, i) => {
|
|
5771
|
-
const x = geo.x + c.x + GAP / 2;
|
|
5772
|
-
const cwid = Math.max(1, c.w - GAP);
|
|
5773
|
-
if (x + cwid < 0 || x > w)
|
|
5774
|
-
return;
|
|
5775
|
-
for (const row of [0, 1]) {
|
|
5776
|
-
const y = row * (rh + GAP);
|
|
5777
|
-
const pic = picOf(sides[row], c);
|
|
5778
|
-
ctx.save();
|
|
5779
|
-
ctx.beginPath();
|
|
5780
|
-
ctx.roundRect(x, y, cwid, rh, radius);
|
|
5781
|
-
ctx.fillStyle = colors.surface;
|
|
5782
|
-
ctx.fill();
|
|
5783
|
-
if (pic) {
|
|
5784
|
-
ctx.clip();
|
|
5785
|
-
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
5786
|
-
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
5787
|
-
}
|
|
5788
|
-
ctx.restore();
|
|
5789
|
-
}
|
|
5790
|
-
if (hover === i) {
|
|
5791
|
-
ctx.fillStyle = colors.wash;
|
|
5792
|
-
ctx.beginPath();
|
|
5793
|
-
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
5794
|
-
ctx.fill();
|
|
5795
|
-
}
|
|
5796
|
-
const d = ds[i];
|
|
5797
|
-
if (d !== undefined && d > 0) {
|
|
5798
|
-
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
5799
|
-
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
5800
|
-
ctx.fillStyle = colors.diff;
|
|
5801
|
-
ctx.beginPath();
|
|
5802
|
-
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
5803
|
-
ctx.fill();
|
|
5804
|
-
ctx.globalAlpha = 1;
|
|
5805
|
-
}
|
|
5806
|
-
});
|
|
5807
|
-
const v = timeline.view;
|
|
5808
|
-
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
5809
|
-
if (px >= -1 && px <= w + 1) {
|
|
5810
|
-
ctx.fillStyle = colors.accent;
|
|
5811
|
-
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
5812
|
-
}
|
|
5813
|
-
if (more)
|
|
5814
|
-
schedule();
|
|
5815
|
-
};
|
|
5816
|
-
const timeAt = (clientX) => {
|
|
5817
|
-
const r = root.getBoundingClientRect();
|
|
5818
|
-
const v = timeline.view;
|
|
5819
|
-
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
5820
|
-
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
5821
|
-
};
|
|
5822
|
-
const columnAt = (clientX) => {
|
|
5823
|
-
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
5824
|
-
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
5825
|
-
return i < 0 ? null : i;
|
|
5826
|
-
};
|
|
5827
|
-
let dragging = false;
|
|
5828
|
-
const pick = (clientX) => {
|
|
5829
|
-
const t = timeAt(clientX);
|
|
5830
|
-
timeline.seek(t);
|
|
5831
|
-
options.onPick?.(t);
|
|
5832
|
-
};
|
|
5833
|
-
root.addEventListener("pointerdown", (e) => {
|
|
5834
|
-
if (e.button !== 0)
|
|
5835
|
-
return;
|
|
5836
|
-
dragging = true;
|
|
5837
|
-
root.setPointerCapture(e.pointerId);
|
|
5838
|
-
const host = root.closest(".knobs-compare");
|
|
5839
|
-
if (host)
|
|
5840
|
-
host.focus({ preventScroll: true });
|
|
5841
|
-
else
|
|
5842
|
-
timeline.focus();
|
|
5843
|
-
if (timeline.playing)
|
|
5844
|
-
timeline.pause();
|
|
5845
|
-
pick(e.clientX);
|
|
5846
|
-
e.preventDefault();
|
|
5847
|
-
});
|
|
5848
|
-
root.addEventListener("pointermove", (e) => {
|
|
5849
|
-
if (dragging)
|
|
5850
|
-
pick(e.clientX);
|
|
5851
|
-
const i = columnAt(e.clientX);
|
|
5852
|
-
if (i !== hover) {
|
|
5853
|
-
hover = i;
|
|
5854
|
-
schedule();
|
|
5855
|
-
}
|
|
5856
|
-
});
|
|
5857
|
-
const end = () => {
|
|
5858
|
-
dragging = false;
|
|
5859
|
-
};
|
|
5860
|
-
root.addEventListener("pointerup", end);
|
|
5861
|
-
root.addEventListener("pointercancel", end);
|
|
5862
|
-
root.addEventListener("lostpointercapture", end);
|
|
5863
|
-
root.addEventListener("pointerleave", () => {
|
|
5864
|
-
if (hover === null)
|
|
5865
|
-
return;
|
|
5866
|
-
hover = null;
|
|
5867
|
-
schedule();
|
|
5868
|
-
});
|
|
5869
|
-
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
5870
|
-
const ro = new win.ResizeObserver(measure);
|
|
5871
|
-
ro.observe(root);
|
|
5872
|
-
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
5873
|
-
if (track)
|
|
5874
|
-
ro.observe(track);
|
|
5875
|
-
root.addEventListener("knobs-restyle", measure);
|
|
5876
|
-
const setOnScreen = (on) => {
|
|
5877
|
-
onScreen = on;
|
|
5878
|
-
for (const s of sides) {
|
|
5879
|
-
if (!s.frames || s.seen === on)
|
|
5880
|
-
continue;
|
|
5881
|
-
s.seen = on;
|
|
5882
|
-
s.frames.visible(on);
|
|
5883
|
-
}
|
|
5884
|
-
};
|
|
5885
|
-
const setNear = (on) => {
|
|
5886
|
-
near = on;
|
|
5887
|
-
if (on) {
|
|
5888
|
-
for (const s of sides)
|
|
5889
|
-
load(s);
|
|
5890
|
-
schedule();
|
|
5891
|
-
} else
|
|
5892
|
-
for (const s of sides)
|
|
5893
|
-
unload(s);
|
|
5894
|
-
};
|
|
5895
|
-
const IO = win.IntersectionObserver;
|
|
5896
|
-
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
5897
|
-
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
5898
|
-
measure();
|
|
5899
|
-
if (nearIo && screenIo) {
|
|
5900
|
-
nearIo.observe(root);
|
|
5901
|
-
screenIo.observe(root);
|
|
5902
|
-
} else {
|
|
5903
|
-
setNear(true);
|
|
5904
|
-
setOnScreen(true);
|
|
5905
|
-
}
|
|
5906
|
-
return {
|
|
5907
|
-
element: root,
|
|
5908
|
-
diffs,
|
|
5909
|
-
update(next) {
|
|
5910
|
-
const was = sides;
|
|
5911
|
-
sides = [
|
|
5912
|
-
next.before ? makeSide(next.before) : was[0],
|
|
5913
|
-
next.after ? makeSide(next.after) : was[1]
|
|
5914
|
-
];
|
|
5915
|
-
for (const s of was)
|
|
5916
|
-
if (!sides.includes(s))
|
|
5917
|
-
unload(s);
|
|
5918
|
-
diffs.clear();
|
|
5919
|
-
pairDiffs.clear();
|
|
5920
|
-
asked = "";
|
|
5921
|
-
if (near)
|
|
5922
|
-
for (const s of sides)
|
|
5923
|
-
load(s);
|
|
5924
|
-
if (onScreen)
|
|
5925
|
-
setOnScreen(true);
|
|
5926
|
-
schedule();
|
|
5927
|
-
},
|
|
5928
|
-
dispose() {
|
|
5929
|
-
disposed = true;
|
|
5930
|
-
if (raf)
|
|
5931
|
-
win.cancelAnimationFrame(raf);
|
|
5932
|
-
unlisten();
|
|
5933
|
-
ro.disconnect();
|
|
5934
|
-
nearIo?.disconnect();
|
|
5935
|
-
screenIo?.disconnect();
|
|
5936
|
-
for (const s of sides)
|
|
5937
|
-
unload(s);
|
|
5938
|
-
root.remove();
|
|
5939
|
-
}
|
|
5940
|
-
};
|
|
5941
|
-
}
|
|
5942
|
-
var BAR = 7, GAP = 2, DIFF, DIFFS_PER_DRAW = 6;
|
|
5943
|
-
var init_compare_strip = __esm(() => {
|
|
5944
|
-
init_compare_frames();
|
|
5945
|
-
init_compare_model();
|
|
5946
|
-
init_look();
|
|
5947
|
-
init_rgba();
|
|
5948
|
-
init_timeline_model();
|
|
5949
|
-
DIFF = { w: 32, h: 18 };
|
|
5950
|
-
});
|
|
5951
|
-
|
|
5952
|
-
// src/ui/overlay-model.ts
|
|
5953
|
-
function place(o) {
|
|
5954
|
-
const { anchor: a, size, view: v } = o;
|
|
5955
|
-
const offset = o.offset ?? 6;
|
|
5956
|
-
const margin = o.margin ?? 8;
|
|
5957
|
-
const [want, align = "center"] = (o.placement ?? "bottom").split("-");
|
|
5958
|
-
const room = (s) => s === "top" ? a.y - v.y : s === "bottom" ? v.y + v.h - (a.y + a.h) : s === "left" ? a.x - v.x : v.x + v.w - (a.x + a.w);
|
|
5959
|
-
const need = (s) => (s === "top" || s === "bottom" ? size.h : size.w) + offset + margin;
|
|
5960
|
-
let side = want;
|
|
5961
|
-
if (room(want) < need(want) && room(opposite[want]) > room(want))
|
|
5962
|
-
side = opposite[want];
|
|
5963
|
-
const vertical = side === "top" || side === "bottom";
|
|
5964
|
-
let x;
|
|
5965
|
-
let y;
|
|
5966
|
-
if (vertical) {
|
|
5967
|
-
y = side === "bottom" ? a.y + a.h + offset : a.y - offset - size.h;
|
|
5968
|
-
x = align === "start" ? a.x : align === "end" ? a.x + a.w - size.w : a.x + (a.w - size.w) / 2;
|
|
5969
|
-
x = clamp(x, v.x + margin, v.x + v.w - margin - size.w);
|
|
5970
|
-
} else {
|
|
5971
|
-
x = side === "right" ? a.x + a.w + offset : a.x - offset - size.w;
|
|
5972
|
-
y = align === "start" ? a.y : align === "end" ? a.y + a.h - size.h : a.y + (a.h - size.h) / 2;
|
|
5973
|
-
y = clamp(y, v.y + margin, v.y + v.h - margin - size.h);
|
|
5974
|
-
}
|
|
5975
|
-
const origin = vertical ? { x: clamp(a.x + a.w / 2 - x, 0, size.w), y: side === "bottom" ? 0 : size.h } : { x: side === "right" ? 0 : size.w, y: clamp(a.y + a.h / 2 - y, 0, size.h) };
|
|
5976
|
-
return {
|
|
5977
|
-
x: Math.round(x),
|
|
5978
|
-
y: Math.round(y),
|
|
5979
|
-
placement: align === "center" ? side : `${side}-${align}`,
|
|
5980
|
-
origin
|
|
5981
|
-
};
|
|
5982
|
-
}
|
|
5983
|
-
function stepItem(count, from, by, ok) {
|
|
5984
|
-
for (let k = 1;k <= count; k++) {
|
|
5985
|
-
const i = (((from < 0 ? by > 0 ? -1 : 0 : from) + by * k) % count + count) % count;
|
|
5986
|
-
if (ok(i))
|
|
5987
|
-
return i;
|
|
5988
|
-
}
|
|
5989
|
-
return from;
|
|
5990
|
-
}
|
|
5991
|
-
function matchItem(labels, from, typed) {
|
|
5992
|
-
const q = typed.toLowerCase();
|
|
5993
|
-
if (!q || !labels.length)
|
|
5994
|
-
return from;
|
|
5995
|
-
const same = q.length > 1 && [...q].every((c) => c === q[0]);
|
|
5996
|
-
const word = same ? q[0] : q;
|
|
5997
|
-
const start = q.length > 1 && !same ? Math.max(0, from) : from + 1;
|
|
5998
|
-
for (let k = 0;k < labels.length; k++) {
|
|
5999
|
-
const i = ((start + k) % labels.length + labels.length) % labels.length;
|
|
6000
|
-
if (labels[i]?.toLowerCase().startsWith(word))
|
|
6001
|
-
return i;
|
|
6002
|
-
}
|
|
6003
|
-
return from;
|
|
6004
|
-
}
|
|
6005
|
-
function inTriangle(p, a, b, c) {
|
|
6006
|
-
const s = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
|
|
6007
|
-
const d1 = s(p, a, b);
|
|
6008
|
-
const d2 = s(p, b, c);
|
|
6009
|
-
const d3 = s(p, c, a);
|
|
6010
|
-
return !((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0));
|
|
5837
|
+
function inTriangle(p, a, b, c) {
|
|
5838
|
+
const s = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
|
|
5839
|
+
const d1 = s(p, a, b);
|
|
5840
|
+
const d2 = s(p, b, c);
|
|
5841
|
+
const d3 = s(p, c, a);
|
|
5842
|
+
return !((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0));
|
|
6011
5843
|
}
|
|
6012
5844
|
function safeTriangle(from, sub) {
|
|
6013
5845
|
const right = sub.x >= from.x;
|
|
@@ -6440,6 +6272,157 @@ function softClamp(x, min, max, knee = 12) {
|
|
|
6440
6272
|
}
|
|
6441
6273
|
var init_split_model = () => {};
|
|
6442
6274
|
|
|
6275
|
+
// src/ui/timeline-model.ts
|
|
6276
|
+
function timeLabel(t, units, fps, step = 1) {
|
|
6277
|
+
if (units === "frames")
|
|
6278
|
+
return String(toFrame(t, fps));
|
|
6279
|
+
const sign = t < 0 ? "-" : "";
|
|
6280
|
+
const a = Math.abs(t);
|
|
6281
|
+
if (a >= 60 || step >= 60 && a > 0) {
|
|
6282
|
+
const s = Math.round(a);
|
|
6283
|
+
const h = Math.floor(s / 3600);
|
|
6284
|
+
const m = Math.floor(s % 3600 / 60);
|
|
6285
|
+
const sec = String(s % 60).padStart(2, "0");
|
|
6286
|
+
return `${sign}${h ? `${h}:${String(m).padStart(2, "0")}` : m}:${sec}`;
|
|
6287
|
+
}
|
|
6288
|
+
const decimals = step < 0.1 ? 2 : step < 1 ? 1 : 0;
|
|
6289
|
+
return `${sign}${Number(a.toFixed(decimals))}s`;
|
|
6290
|
+
}
|
|
6291
|
+
function rulerTicks(view, width, o) {
|
|
6292
|
+
const units = o.units ?? "seconds";
|
|
6293
|
+
const fps = o.fps;
|
|
6294
|
+
const pxPerSec = width / Math.max(0.000000001, view.span);
|
|
6295
|
+
const minGap = o.minGap ?? 72;
|
|
6296
|
+
const steps = units === "frames" ? FRAME_STEPS.map((f) => f / fps) : SECOND_STEPS;
|
|
6297
|
+
const step = steps.find((s) => s * pxPerSec >= minGap) ?? steps[steps.length - 1];
|
|
6298
|
+
const frame = 1 / fps;
|
|
6299
|
+
let minor = 0;
|
|
6300
|
+
for (const div of [10, 5, 4, 2]) {
|
|
6301
|
+
const m = step / div;
|
|
6302
|
+
if (m * pxPerSec < 7)
|
|
6303
|
+
continue;
|
|
6304
|
+
if (units === "frames" && Math.abs(m * fps - Math.round(m * fps)) > 0.000001)
|
|
6305
|
+
continue;
|
|
6306
|
+
minor = m;
|
|
6307
|
+
break;
|
|
6308
|
+
}
|
|
6309
|
+
if (frame * pxPerSec >= (o.frameGap ?? 6))
|
|
6310
|
+
minor = Math.min(frame, step);
|
|
6311
|
+
const end = view.start + view.span;
|
|
6312
|
+
const major = [];
|
|
6313
|
+
const first = Math.ceil(view.start / step - 0.000000001);
|
|
6314
|
+
for (let i = first;i * step <= end + 0.000000001; i++) {
|
|
6315
|
+
const t = round2(i * step);
|
|
6316
|
+
major.push({ t, label: timeLabel(t, units, fps, step) });
|
|
6317
|
+
}
|
|
6318
|
+
const minors = [];
|
|
6319
|
+
if (minor > 0) {
|
|
6320
|
+
const firstMinor = Math.ceil(view.start / minor - 0.000000001);
|
|
6321
|
+
for (let i = firstMinor;i * minor <= end + 0.000000001; i++) {
|
|
6322
|
+
const t = i * minor;
|
|
6323
|
+
if (Math.abs(t / step - Math.round(t / step)) > 0.000001)
|
|
6324
|
+
minors.push(round2(t));
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
return { major, minor: minors, step };
|
|
6328
|
+
}
|
|
6329
|
+
function clampView(view, duration, o = {}) {
|
|
6330
|
+
const pad = o.pad ?? 0.04;
|
|
6331
|
+
const maxSpan = duration * (1 + pad * 2);
|
|
6332
|
+
const span = Math.min(maxSpan, Math.max(o.minSpan ?? 0.05, view.span));
|
|
6333
|
+
const lo = -duration * pad;
|
|
6334
|
+
const hi = duration * (1 + pad) - span;
|
|
6335
|
+
const start = hi < lo ? (lo + hi) / 2 : Math.min(hi, Math.max(lo, view.start));
|
|
6336
|
+
return { start, span };
|
|
6337
|
+
}
|
|
6338
|
+
function zoomView(view, factor, at) {
|
|
6339
|
+
const span = view.span / factor;
|
|
6340
|
+
const k = (at - view.start) / view.span;
|
|
6341
|
+
return { start: at - k * span, span };
|
|
6342
|
+
}
|
|
6343
|
+
function followView(view, t, margin = 0.08) {
|
|
6344
|
+
const m = view.span * margin;
|
|
6345
|
+
if (t < view.start + m)
|
|
6346
|
+
return { ...view, start: t - m };
|
|
6347
|
+
if (t > view.start + view.span - m)
|
|
6348
|
+
return { ...view, start: t - view.span + m };
|
|
6349
|
+
return view;
|
|
6350
|
+
}
|
|
6351
|
+
function magnet(raw, stops, pxPerSec, radius = 10, stick = 0.3) {
|
|
6352
|
+
let best = null;
|
|
6353
|
+
let bestPx = Number.POSITIVE_INFINITY;
|
|
6354
|
+
for (const s of stops) {
|
|
6355
|
+
const px = Math.abs(raw - s) * pxPerSec;
|
|
6356
|
+
if (px < bestPx) {
|
|
6357
|
+
bestPx = px;
|
|
6358
|
+
best = s;
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
if (best === null || bestPx >= radius)
|
|
6362
|
+
return { t: raw, stop: null };
|
|
6363
|
+
const u = bestPx / radius;
|
|
6364
|
+
const pulled = best + Math.sign(raw - best) * (u * u * radius) / pxPerSec;
|
|
6365
|
+
return { t: pulled, stop: u < stick ? best : null };
|
|
6366
|
+
}
|
|
6367
|
+
function scrubTime(raw, o) {
|
|
6368
|
+
const clamped = Math.min(o.duration, Math.max(0, raw));
|
|
6369
|
+
const m = o.stops?.length ? magnet(clamped, o.stops, o.pxPerSec, o.radius) : { t: clamped, stop: null };
|
|
6370
|
+
const t = m.stop ?? (o.frames === false ? m.t : frameTime(m.t, o.fps));
|
|
6371
|
+
return { t: Math.min(o.duration, Math.max(0, t)), shown: m.stop ?? m.t, stop: m.stop };
|
|
6372
|
+
}
|
|
6373
|
+
function adjacentMarker(markers, t, dir, fps) {
|
|
6374
|
+
const eps = 0.5 / fps;
|
|
6375
|
+
const sorted = [...markers].sort((a, b) => a.time - b.time);
|
|
6376
|
+
if (dir > 0)
|
|
6377
|
+
return sorted.find((m) => m.time > t + eps) ?? null;
|
|
6378
|
+
return [...sorted].reverse().find((m) => m.time < t - eps) ?? null;
|
|
6379
|
+
}
|
|
6380
|
+
function advance(t, dt, o) {
|
|
6381
|
+
const lo = o.loop ? o.loop.start : 0;
|
|
6382
|
+
const hi = o.loop ? o.loop.end : o.duration;
|
|
6383
|
+
let next = t + dt;
|
|
6384
|
+
if (hi - lo <= 0.000001)
|
|
6385
|
+
return { t: lo, ended: true, wrapped: false };
|
|
6386
|
+
if (next < hi)
|
|
6387
|
+
return { t: next, ended: false, wrapped: false };
|
|
6388
|
+
if (o.loop || o.repeat) {
|
|
6389
|
+
next = lo + (next - lo) % (hi - lo);
|
|
6390
|
+
return { t: next, ended: false, wrapped: true };
|
|
6391
|
+
}
|
|
6392
|
+
return { t: hi, ended: true, wrapped: false };
|
|
6393
|
+
}
|
|
6394
|
+
function moveHead(head, t, motion, catching, minGap = 0) {
|
|
6395
|
+
if (motion === "jump" && Math.abs(t - head.x) > minGap)
|
|
6396
|
+
return { x: head.x, target: t, catching: true };
|
|
6397
|
+
if (catching)
|
|
6398
|
+
return { x: head.x + (t - head.target), target: t, catching: true };
|
|
6399
|
+
return { x: t, target: t, catching: false };
|
|
6400
|
+
}
|
|
6401
|
+
function currentMarker(markers, t, fps) {
|
|
6402
|
+
let best = null;
|
|
6403
|
+
for (const m of markers)
|
|
6404
|
+
if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
|
|
6405
|
+
best = m;
|
|
6406
|
+
return best;
|
|
6407
|
+
}
|
|
6408
|
+
function minutes(t) {
|
|
6409
|
+
const s = Math.max(0, Math.floor(t + 0.000001));
|
|
6410
|
+
const h = Math.floor(s / 3600);
|
|
6411
|
+
const m = Math.floor(s % 3600 / 60);
|
|
6412
|
+
const ss = String(s % 60).padStart(2, "0");
|
|
6413
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
6414
|
+
}
|
|
6415
|
+
function chapterBounds(times, duration) {
|
|
6416
|
+
const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
|
|
6417
|
+
return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
|
|
6418
|
+
}
|
|
6419
|
+
var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round2 = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
|
|
6420
|
+
var init_timeline_model = __esm(() => {
|
|
6421
|
+
SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
|
|
6422
|
+
FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
|
|
6423
|
+
CATCH_UP = { duration: 0.15, bounce: 0 };
|
|
6424
|
+
});
|
|
6425
|
+
|
|
6443
6426
|
// src/ui/timeline.ts
|
|
6444
6427
|
function timeline(container, options) {
|
|
6445
6428
|
const doc = container.ownerDocument;
|
|
@@ -7552,6 +7535,7 @@ var init_timeline = __esm(() => {
|
|
|
7552
7535
|
function compare(container, options) {
|
|
7553
7536
|
const doc = container.ownerDocument;
|
|
7554
7537
|
const win = doc.defaultView;
|
|
7538
|
+
const caching = scrubCaching(options.scrubCache, win);
|
|
7555
7539
|
const look = {
|
|
7556
7540
|
...options.preset ? { preset: options.preset } : {},
|
|
7557
7541
|
...options.theme ? { theme: options.theme } : {},
|
|
@@ -7668,15 +7652,9 @@ function compare(container, options) {
|
|
|
7668
7652
|
const note = el("div", "knobs-cmp-note");
|
|
7669
7653
|
stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
|
|
7670
7654
|
root.append(bar, stage);
|
|
7671
|
-
const wantsStrip = options.strip === true;
|
|
7672
7655
|
let tl = bound;
|
|
7673
7656
|
let ownTimeline = false;
|
|
7674
|
-
|
|
7675
|
-
if (wantsStrip) {
|
|
7676
|
-
stripHost = el("div", "knobs-cmp-strip-host");
|
|
7677
|
-
root.append(stripHost);
|
|
7678
|
-
}
|
|
7679
|
-
if (!tl && (options.timeline === true || wantsStrip)) {
|
|
7657
|
+
if (!tl && options.timeline === true) {
|
|
7680
7658
|
const host = el("div", "knobs-cmp-timeline-host");
|
|
7681
7659
|
root.append(host);
|
|
7682
7660
|
tl = timeline(host, {
|
|
@@ -7824,7 +7802,7 @@ function compare(container, options) {
|
|
|
7824
7802
|
s.frames.keep(at, v, v.videoWidth, v.videoHeight);
|
|
7825
7803
|
return;
|
|
7826
7804
|
}
|
|
7827
|
-
const c = s.frames.nearest(want);
|
|
7805
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
7828
7806
|
if (c && Math.abs(c.frame - want) < Math.abs(at - want))
|
|
7829
7807
|
showCached(s, c);
|
|
7830
7808
|
else
|
|
@@ -7836,13 +7814,23 @@ function compare(container, options) {
|
|
|
7836
7814
|
const want = wantOf(s);
|
|
7837
7815
|
if (s.shown && !s.shown.cached && s.shown.frame === want)
|
|
7838
7816
|
return;
|
|
7839
|
-
const c = s.frames.nearest(want);
|
|
7817
|
+
const c = s.frames.nearest(want, Number.POSITIVE_INFINITY, true);
|
|
7840
7818
|
if (!c)
|
|
7841
7819
|
return;
|
|
7842
7820
|
const was = s.shown ? Math.abs(s.shown.frame - want) : Number.POSITIVE_INFINITY;
|
|
7843
7821
|
if (Math.abs(c.frame - want) < was)
|
|
7844
7822
|
showCached(s, c);
|
|
7845
7823
|
};
|
|
7824
|
+
const giveUp = (s) => {
|
|
7825
|
+
s.seek = SEEK_IDLE;
|
|
7826
|
+
if (!s.loaded || disposed)
|
|
7827
|
+
return;
|
|
7828
|
+
if (s.el.readyState >= 2)
|
|
7829
|
+
return showVideo(s);
|
|
7830
|
+
s.shown = null;
|
|
7831
|
+
setCached(s, false);
|
|
7832
|
+
requestDraw();
|
|
7833
|
+
};
|
|
7846
7834
|
const makeSide = (index, source) => {
|
|
7847
7835
|
const kind = mediaKind(source.src, source.kind);
|
|
7848
7836
|
const media = el(kind === "video" ? "video" : "img", "knobs-cmp-media");
|
|
@@ -7874,7 +7862,9 @@ function compare(container, options) {
|
|
|
7874
7862
|
frameAt: 0,
|
|
7875
7863
|
misses: 0,
|
|
7876
7864
|
failed: false,
|
|
7865
|
+
stall: stallGuard(win, () => giveUp(side)),
|
|
7877
7866
|
stop: () => {
|
|
7867
|
+
side.stall.clear();
|
|
7878
7868
|
for (const f of offs)
|
|
7879
7869
|
f();
|
|
7880
7870
|
}
|
|
@@ -7884,6 +7874,10 @@ function compare(container, options) {
|
|
|
7884
7874
|
return;
|
|
7885
7875
|
side.failed = true;
|
|
7886
7876
|
note.textContent = `Couldn't load ${labelOf(index)}`;
|
|
7877
|
+
side.stall.clear();
|
|
7878
|
+
side.seek = SEEK_IDLE;
|
|
7879
|
+
side.shown = null;
|
|
7880
|
+
setCached(side, false);
|
|
7887
7881
|
};
|
|
7888
7882
|
if (media instanceof HTMLVideoElement) {
|
|
7889
7883
|
const v = media;
|
|
@@ -7914,6 +7908,8 @@ function compare(container, options) {
|
|
|
7914
7908
|
side.seek = step.state;
|
|
7915
7909
|
if (step.apply !== null)
|
|
7916
7910
|
seekVideo(side, v, step.apply);
|
|
7911
|
+
else
|
|
7912
|
+
side.stall.clear();
|
|
7917
7913
|
pushFrame(side);
|
|
7918
7914
|
});
|
|
7919
7915
|
on("error", fail);
|
|
@@ -7946,7 +7942,7 @@ function compare(container, options) {
|
|
|
7946
7942
|
s.loaded = true;
|
|
7947
7943
|
s.failed = false;
|
|
7948
7944
|
s.el.src = s.source.src;
|
|
7949
|
-
if (s.kind === "video") {
|
|
7945
|
+
if (s.kind === "video" && caching) {
|
|
7950
7946
|
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
7951
7947
|
s.unlisten = s.frames.listen(() => refreshCached(s));
|
|
7952
7948
|
if (onScreen) {
|
|
@@ -7963,6 +7959,7 @@ function compare(container, options) {
|
|
|
7963
7959
|
if (s.el instanceof HTMLVideoElement)
|
|
7964
7960
|
s.el.load();
|
|
7965
7961
|
s.seek = SEEK_IDLE;
|
|
7962
|
+
s.stall.clear();
|
|
7966
7963
|
s.shown = null;
|
|
7967
7964
|
setCached(s, false);
|
|
7968
7965
|
s.unlisten?.();
|
|
@@ -8086,6 +8083,7 @@ function compare(container, options) {
|
|
|
8086
8083
|
v.fastSeek(t);
|
|
8087
8084
|
else
|
|
8088
8085
|
v.currentTime = t;
|
|
8086
|
+
s.stall.arm();
|
|
8089
8087
|
};
|
|
8090
8088
|
const showFrame = (t, used) => {
|
|
8091
8089
|
for (const s of sides) {
|
|
@@ -8097,14 +8095,21 @@ function compare(container, options) {
|
|
|
8097
8095
|
v.playbackRate = 1;
|
|
8098
8096
|
}
|
|
8099
8097
|
const want = Math.min(frameOf(t, fps()), lastFrame(v.duration, fps()));
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
if (
|
|
8107
|
-
|
|
8098
|
+
if (used) {
|
|
8099
|
+
if (scrubbing)
|
|
8100
|
+
s.frames?.follow(want);
|
|
8101
|
+
else
|
|
8102
|
+
s.frames?.focus(want);
|
|
8103
|
+
}
|
|
8104
|
+
if (!(scrubbing && s.frames?.covers(want))) {
|
|
8105
|
+
const at = frameMidpoint(want, fps(), v.duration);
|
|
8106
|
+
const step = requestSeek(s.seek, at, v.currentTime, fps());
|
|
8107
|
+
s.seek = step.state;
|
|
8108
|
+
if (step.apply !== null)
|
|
8109
|
+
seekVideo(s, v, step.apply);
|
|
8110
|
+
else if (step.settled && (s.shown?.cached || s.shown?.frame !== want))
|
|
8111
|
+
pushFrame(s);
|
|
8112
|
+
}
|
|
8108
8113
|
refreshCached(s);
|
|
8109
8114
|
}
|
|
8110
8115
|
requestDraw();
|
|
@@ -8735,7 +8740,7 @@ function compare(container, options) {
|
|
|
8735
8740
|
holdLoupe(true);
|
|
8736
8741
|
return;
|
|
8737
8742
|
case "l":
|
|
8738
|
-
if (e.altKey || e.shiftKey)
|
|
8743
|
+
if (e.altKey || e.shiftKey || loopBtn.hidden)
|
|
8739
8744
|
return;
|
|
8740
8745
|
claim();
|
|
8741
8746
|
return api.setLoop(!loopNow());
|
|
@@ -8758,6 +8763,9 @@ function compare(container, options) {
|
|
|
8758
8763
|
return glideZoom(ZOOM_FIT);
|
|
8759
8764
|
}
|
|
8760
8765
|
};
|
|
8766
|
+
const syncLoopButton = () => {
|
|
8767
|
+
loopBtn.hidden = !sides.some((s) => s.kind === "video");
|
|
8768
|
+
};
|
|
8761
8769
|
const replace = (index, source) => {
|
|
8762
8770
|
const old = sides[index];
|
|
8763
8771
|
old.stop();
|
|
@@ -8766,6 +8774,7 @@ function compare(container, options) {
|
|
|
8766
8774
|
old.el.replaceWith(s.el);
|
|
8767
8775
|
old.preview.replaceWith(s.preview);
|
|
8768
8776
|
sides[index] = s;
|
|
8777
|
+
syncLoopButton();
|
|
8769
8778
|
note.textContent = "";
|
|
8770
8779
|
setLabel(index, labelOf(index));
|
|
8771
8780
|
if (near)
|
|
@@ -8780,6 +8789,7 @@ function compare(container, options) {
|
|
|
8780
8789
|
stage.insertBefore(sides[0].preview, labels[0]);
|
|
8781
8790
|
setLabel(0, labelOf(0));
|
|
8782
8791
|
setLabel(1, labelOf(1));
|
|
8792
|
+
syncLoopButton();
|
|
8783
8793
|
loupeBtn.setAttribute("aria-pressed", String(loupeOn));
|
|
8784
8794
|
loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
|
|
8785
8795
|
const modePill = modeList.length > 1 ? selectionPill(modes, {
|
|
@@ -8858,7 +8868,7 @@ function compare(container, options) {
|
|
|
8858
8868
|
label: MODE_LABEL[m].long,
|
|
8859
8869
|
activate: () => setMode(m)
|
|
8860
8870
|
})) : [],
|
|
8861
|
-
{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
|
|
8871
|
+
...loopBtn.hidden ? [] : [{ el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) }],
|
|
8862
8872
|
...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
|
|
8863
8873
|
...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
|
|
8864
8874
|
...mode === "wipe" ? [
|
|
@@ -8869,14 +8879,6 @@ function compare(container, options) {
|
|
|
8869
8879
|
}
|
|
8870
8880
|
] : []
|
|
8871
8881
|
], { listen: false, rank: 1 });
|
|
8872
|
-
const strip = wantsStrip && stripHost && tl ? compareStrip(stripHost, {
|
|
8873
|
-
...look,
|
|
8874
|
-
before: options.before,
|
|
8875
|
-
after: options.after,
|
|
8876
|
-
timeline: tl,
|
|
8877
|
-
...fpsOpt ? { fps: fpsOpt } : {},
|
|
8878
|
-
threshold
|
|
8879
|
-
}) : null;
|
|
8880
8882
|
const api = {
|
|
8881
8883
|
element: root,
|
|
8882
8884
|
get mode() {
|
|
@@ -8902,7 +8904,6 @@ function compare(container, options) {
|
|
|
8902
8904
|
return renderer;
|
|
8903
8905
|
},
|
|
8904
8906
|
timeline: tl,
|
|
8905
|
-
strip,
|
|
8906
8907
|
get zoom() {
|
|
8907
8908
|
return clampZoom(zoomNow());
|
|
8908
8909
|
},
|
|
@@ -8924,8 +8925,6 @@ function compare(container, options) {
|
|
|
8924
8925
|
replace(1, after);
|
|
8925
8926
|
else if (next.after)
|
|
8926
8927
|
setLabel(1, next.after.label ?? "After");
|
|
8927
|
-
if (before || after)
|
|
8928
|
-
strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
|
|
8929
8928
|
if (next.before || next.after)
|
|
8930
8929
|
readColors();
|
|
8931
8930
|
if (next.mode !== undefined)
|
|
@@ -8992,7 +8991,6 @@ function compare(container, options) {
|
|
|
8992
8991
|
if (ownTimeline)
|
|
8993
8992
|
tl.dispose();
|
|
8994
8993
|
}
|
|
8995
|
-
strip?.dispose();
|
|
8996
8994
|
for (const s of sides) {
|
|
8997
8995
|
s.stop();
|
|
8998
8996
|
unloadSide(s);
|
|
@@ -9016,7 +9014,6 @@ var init_compare = __esm(() => {
|
|
|
9016
9014
|
init_compare_frames();
|
|
9017
9015
|
init_compare_gpu();
|
|
9018
9016
|
init_compare_model();
|
|
9019
|
-
init_compare_strip();
|
|
9020
9017
|
init_icons();
|
|
9021
9018
|
init_look();
|
|
9022
9019
|
init_rgba();
|
|
@@ -9074,7 +9071,10 @@ function player(container, options) {
|
|
|
9074
9071
|
track.src = options.captions;
|
|
9075
9072
|
v.append(track);
|
|
9076
9073
|
}
|
|
9077
|
-
|
|
9074
|
+
const preview = el("canvas", "knobs-player-preview");
|
|
9075
|
+
preview.setAttribute("aria-hidden", "true");
|
|
9076
|
+
preview.hidden = true;
|
|
9077
|
+
frame.append(v, preview);
|
|
9078
9078
|
const host = el("div", "knobs-player-line");
|
|
9079
9079
|
root.append(frame, host);
|
|
9080
9080
|
container.append(root);
|
|
@@ -9104,6 +9104,57 @@ function player(container, options) {
|
|
|
9104
9104
|
else
|
|
9105
9105
|
v.currentTime = t;
|
|
9106
9106
|
};
|
|
9107
|
+
const fps = options.fps ?? 30;
|
|
9108
|
+
let frames = null;
|
|
9109
|
+
let unlistenFrames = null;
|
|
9110
|
+
let scrubbing = false;
|
|
9111
|
+
let previewing = null;
|
|
9112
|
+
const want = () => frameOf(tl.time, fps);
|
|
9113
|
+
const emitFrame = (frame, source) => root.dispatchEvent(new CustomEvent("knobs-player-frame", { detail: { frame, source } }));
|
|
9114
|
+
const drawPreview = (c) => {
|
|
9115
|
+
if (preview.width !== c.bitmap.width || preview.height !== c.bitmap.height) {
|
|
9116
|
+
preview.width = c.bitmap.width;
|
|
9117
|
+
preview.height = c.bitmap.height;
|
|
9118
|
+
}
|
|
9119
|
+
preview.getContext("2d")?.drawImage(c.bitmap, 0, 0);
|
|
9120
|
+
preview.hidden = false;
|
|
9121
|
+
previewing = c.frame;
|
|
9122
|
+
emitFrame(c.frame, "cache");
|
|
9123
|
+
};
|
|
9124
|
+
const hidePreview = () => {
|
|
9125
|
+
if (previewing === null)
|
|
9126
|
+
return;
|
|
9127
|
+
previewing = null;
|
|
9128
|
+
preview.hidden = true;
|
|
9129
|
+
};
|
|
9130
|
+
const showNearest = () => {
|
|
9131
|
+
if (!frames || !scrubbing)
|
|
9132
|
+
return;
|
|
9133
|
+
const w = want();
|
|
9134
|
+
const c = frames.nearest(w, Number.POSITIVE_INFINITY, true);
|
|
9135
|
+
if (!c || c.frame === previewing)
|
|
9136
|
+
return;
|
|
9137
|
+
const shown = previewing ?? frameOf(v.currentTime, fps);
|
|
9138
|
+
if (Math.abs(c.frame - w) < Math.abs(shown - w))
|
|
9139
|
+
drawPreview(c);
|
|
9140
|
+
};
|
|
9141
|
+
const caching = scrubCaching(options.scrubCache, doc.defaultView);
|
|
9142
|
+
const startFrames = () => {
|
|
9143
|
+
if (frames || !caching)
|
|
9144
|
+
return;
|
|
9145
|
+
frames = acquireFrames(doc, options.src, fps);
|
|
9146
|
+
frames.visible(true);
|
|
9147
|
+
unlistenFrames = frames.listen(showNearest);
|
|
9148
|
+
};
|
|
9149
|
+
const stall = stallGuard(doc.defaultView, () => {
|
|
9150
|
+
pending = null;
|
|
9151
|
+
hidePreview();
|
|
9152
|
+
});
|
|
9153
|
+
const onError = () => {
|
|
9154
|
+
stall.clear();
|
|
9155
|
+
pending = null;
|
|
9156
|
+
hidePreview();
|
|
9157
|
+
};
|
|
9107
9158
|
let reported = 0;
|
|
9108
9159
|
const onSeeked = () => {
|
|
9109
9160
|
if (pending !== null) {
|
|
@@ -9112,6 +9163,11 @@ function player(container, options) {
|
|
|
9112
9163
|
v.currentTime = t;
|
|
9113
9164
|
return;
|
|
9114
9165
|
}
|
|
9166
|
+
stall.clear();
|
|
9167
|
+
const at = frameOf(v.currentTime, fps);
|
|
9168
|
+
emitFrame(at, "video");
|
|
9169
|
+
if (!scrubbing || previewing !== null && Math.abs(at - want()) <= Math.abs(previewing - want()))
|
|
9170
|
+
hidePreview();
|
|
9115
9171
|
if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
|
|
9116
9172
|
return;
|
|
9117
9173
|
reported = v.currentTime;
|
|
@@ -9127,6 +9183,7 @@ function player(container, options) {
|
|
|
9127
9183
|
v.addEventListener("loadedmetadata", onMeta);
|
|
9128
9184
|
v.addEventListener("loadeddata", onFrame);
|
|
9129
9185
|
v.addEventListener("seeked", onSeeked);
|
|
9186
|
+
v.addEventListener("error", onError);
|
|
9130
9187
|
const unlisten = tl.listen({
|
|
9131
9188
|
onSeek(t, source) {
|
|
9132
9189
|
if (source === "play") {
|
|
@@ -9134,14 +9191,24 @@ function player(container, options) {
|
|
|
9134
9191
|
v.currentTime = t;
|
|
9135
9192
|
return;
|
|
9136
9193
|
}
|
|
9137
|
-
if (
|
|
9194
|
+
if (scrubbing && frames) {
|
|
9195
|
+
frames.follow(want());
|
|
9196
|
+
showNearest();
|
|
9197
|
+
if (frames.covers(want()))
|
|
9198
|
+
return;
|
|
9199
|
+
}
|
|
9200
|
+
if (sameFrame(v.currentTime, t, fps)) {
|
|
9201
|
+
if (!scrubbing)
|
|
9202
|
+
hidePreview();
|
|
9138
9203
|
return;
|
|
9204
|
+
}
|
|
9139
9205
|
seekVideo(t);
|
|
9140
9206
|
},
|
|
9141
9207
|
onPlay(on) {
|
|
9142
|
-
if (on)
|
|
9208
|
+
if (on) {
|
|
9209
|
+
hidePreview();
|
|
9143
9210
|
v.play().catch(() => tl.pause());
|
|
9144
|
-
else {
|
|
9211
|
+
} else {
|
|
9145
9212
|
v.pause();
|
|
9146
9213
|
reported = tl.time;
|
|
9147
9214
|
}
|
|
@@ -9149,8 +9216,22 @@ function player(container, options) {
|
|
|
9149
9216
|
options.onPlay?.(on, tl.time);
|
|
9150
9217
|
},
|
|
9151
9218
|
onScrub(on) {
|
|
9152
|
-
|
|
9219
|
+
scrubbing = on;
|
|
9220
|
+
if (on) {
|
|
9153
9221
|
v.pause();
|
|
9222
|
+
startFrames();
|
|
9223
|
+
return;
|
|
9224
|
+
}
|
|
9225
|
+
if (v.readyState < 1)
|
|
9226
|
+
return hidePreview();
|
|
9227
|
+
const at = releaseSeek(v.currentTime, tl.time, fps, v.duration);
|
|
9228
|
+
if (at === null) {
|
|
9229
|
+
pending = null;
|
|
9230
|
+
if (!v.seeking)
|
|
9231
|
+
return hidePreview();
|
|
9232
|
+
} else
|
|
9233
|
+
seekVideo(at);
|
|
9234
|
+
stall.arm();
|
|
9154
9235
|
}
|
|
9155
9236
|
});
|
|
9156
9237
|
tl.drive(() => {
|
|
@@ -9165,6 +9246,7 @@ function player(container, options) {
|
|
|
9165
9246
|
tl.toggle();
|
|
9166
9247
|
};
|
|
9167
9248
|
frame.addEventListener("click", onClick);
|
|
9249
|
+
root.addEventListener("pointerenter", startFrames);
|
|
9168
9250
|
v.src = options.src;
|
|
9169
9251
|
const api = {
|
|
9170
9252
|
element: root,
|
|
@@ -9191,9 +9273,18 @@ function player(container, options) {
|
|
|
9191
9273
|
tl.dispose();
|
|
9192
9274
|
v.pause();
|
|
9193
9275
|
frame.removeEventListener("click", onClick);
|
|
9276
|
+
root.removeEventListener("pointerenter", startFrames);
|
|
9277
|
+
unlistenFrames?.();
|
|
9278
|
+
if (frames) {
|
|
9279
|
+
frames.visible(false);
|
|
9280
|
+
frames.release();
|
|
9281
|
+
frames = null;
|
|
9282
|
+
}
|
|
9194
9283
|
v.removeEventListener("loadedmetadata", onMeta);
|
|
9195
9284
|
v.removeEventListener("loadeddata", onFrame);
|
|
9196
9285
|
v.removeEventListener("seeked", onSeeked);
|
|
9286
|
+
v.removeEventListener("error", onError);
|
|
9287
|
+
stall.clear();
|
|
9197
9288
|
v.removeAttribute("src");
|
|
9198
9289
|
v.load();
|
|
9199
9290
|
root.remove();
|
|
@@ -9202,6 +9293,8 @@ function player(container, options) {
|
|
|
9202
9293
|
return api;
|
|
9203
9294
|
}
|
|
9204
9295
|
var init_player = __esm(() => {
|
|
9296
|
+
init_compare_frames();
|
|
9297
|
+
init_compare_model();
|
|
9205
9298
|
init_look();
|
|
9206
9299
|
init_timeline();
|
|
9207
9300
|
});
|
|
@@ -16121,9 +16214,8 @@ var walkthroughCss = `${foundationCss}
|
|
|
16121
16214
|
.knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
|
|
16122
16215
|
/* A compare is as tall as its two pictures side by side at this width, in every mode (Wipe fuses
|
|
16123
16216
|
them at one side's size, so nothing grows or re-measures), plus its mode bar, the line and gaps
|
|
16124
|
-
(92 px)
|
|
16217
|
+
(92 px). */
|
|
16125
16218
|
.knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
|
|
16126
|
-
.knobs-wt-fit[data-strip]{--kw-chrome:158px}
|
|
16127
16219
|
.knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
|
|
16128
16220
|
.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)}
|
|
16129
16221
|
.knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
|
|
@@ -16136,16 +16228,15 @@ var walkthroughCss = `${foundationCss}
|
|
|
16136
16228
|
/* The diff inside it: its header row 28 high on the container's padding, a hairline under it, and
|
|
16137
16229
|
no line of its own under the code (the container's ring closes it). */
|
|
16138
16230
|
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-head:28px;--kd-x:0px}
|
|
16139
|
-
.knobs-wt-slot[data-kind="diff"] .knobs-ui.knobs-diff:not([data-on="pane"])::after{display:none}
|
|
16140
|
-
.knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--
|
|
16231
|
+
.knobs-wt-slot[data-kind="diff"] :is(.knobs-ui.knobs-diff:not([data-on="pane"]),.knobs-diff-review)::after{display:none}
|
|
16232
|
+
.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);
|
|
16141
16233
|
font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
|
|
16142
|
-
.knobs-surface
|
|
16234
|
+
.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)}
|
|
16143
16235
|
.knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
16144
16236
|
.knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
|
|
16145
16237
|
.knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
|
|
16146
16238
|
.knobs-wt-out{color:var(--_k-fg-2)}
|
|
16147
16239
|
.knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
|
|
16148
|
-
.knobs-wt-mlabel{color:var(--_k-fg-2)}
|
|
16149
16240
|
.knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-feature-settings:var(--_kr-data)}
|
|
16150
16241
|
.knobs-wt-mfrom{color:var(--_k-fg-3)}
|
|
16151
16242
|
.knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
|
|
@@ -16164,6 +16255,9 @@ var walkthroughCss = `${foundationCss}
|
|
|
16164
16255
|
.knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
|
|
16165
16256
|
.knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
|
|
16166
16257
|
.knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
|
|
16258
|
+
/* A diff holds its room with its own well's edge, no fill: the diff reads the colour behind it
|
|
16259
|
+
as it mounts, and a loading fill there would stay under its code. */
|
|
16260
|
+
.knobs-wt-slot[data-kind="diff"]>.knobs-wt-hold{background:transparent}
|
|
16167
16261
|
.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)}
|
|
16168
16262
|
|
|
16169
16263
|
/* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
|
|
@@ -16413,8 +16507,6 @@ var side = (s, fallback) => ({
|
|
|
16413
16507
|
async function mountCompare(host, m, look, alive, onEvent) {
|
|
16414
16508
|
const doc = host.ownerDocument;
|
|
16415
16509
|
const fit = div(doc, "knobs-wt-fit");
|
|
16416
|
-
if (m.strip)
|
|
16417
|
-
fit.dataset.strip = "";
|
|
16418
16510
|
const { before, after } = m;
|
|
16419
16511
|
const known = parseAspect(m.aspect);
|
|
16420
16512
|
fit.style.setProperty("--kw-aspect", String(known ?? DEFAULT_ASPECT));
|
|
@@ -16441,7 +16533,6 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16441
16533
|
timeline: mediaKind(before.src) === "video" || mediaKind(after.src) === "video",
|
|
16442
16534
|
lazy: false,
|
|
16443
16535
|
...aspect ? { aspect } : {},
|
|
16444
|
-
...m.strip ? { strip: true } : {},
|
|
16445
16536
|
onMode: (next) => {
|
|
16446
16537
|
onEvent?.({ what: "mode", detail: next });
|
|
16447
16538
|
const layer = marks?.element;
|
|
@@ -16503,11 +16594,11 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16503
16594
|
pills?.sync();
|
|
16504
16595
|
marking?.kick();
|
|
16505
16596
|
if (source !== "play")
|
|
16506
|
-
onEvent?.({ what: "seek", time:
|
|
16597
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
16507
16598
|
},
|
|
16508
16599
|
onPlay: (on) => {
|
|
16509
16600
|
marking?.kick();
|
|
16510
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
16601
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(c.time) });
|
|
16511
16602
|
}
|
|
16512
16603
|
});
|
|
16513
16604
|
requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
|
|
@@ -16525,13 +16616,13 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16525
16616
|
const zoom = zoomedIn(c.zoom) ? c.zoom : null;
|
|
16526
16617
|
return {
|
|
16527
16618
|
kind: "compare",
|
|
16528
|
-
time:
|
|
16529
|
-
duration:
|
|
16619
|
+
time: round3(c.time),
|
|
16620
|
+
duration: round3(c.duration),
|
|
16530
16621
|
playing: c.playing,
|
|
16531
16622
|
mode: c.mode,
|
|
16532
16623
|
...pills?.current ? { chapter: pills.current.label } : {},
|
|
16533
|
-
...loop ? { loop: { start:
|
|
16534
|
-
...zoom ? { zoom: { u:
|
|
16624
|
+
...loop ? { loop: { start: round3(loop.start), end: round3(loop.end) } } : {},
|
|
16625
|
+
...zoom ? { zoom: { u: round3(zoom.u), v: round3(zoom.v), w: round3(zoom.w), h: round3(zoom.h) } } : {}
|
|
16535
16626
|
};
|
|
16536
16627
|
},
|
|
16537
16628
|
dispose: () => {
|
|
@@ -16544,7 +16635,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
|
|
|
16544
16635
|
}
|
|
16545
16636
|
};
|
|
16546
16637
|
}
|
|
16547
|
-
var
|
|
16638
|
+
var round3 = (v) => Math.round(v * 100) / 100;
|
|
16548
16639
|
var MARK_GLIDE = 450;
|
|
16549
16640
|
var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
|
|
16550
16641
|
async function mountVideo(host, m, look, alive, onEvent) {
|
|
@@ -16571,12 +16662,12 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
16571
16662
|
} : {},
|
|
16572
16663
|
onPlay: (on, time) => {
|
|
16573
16664
|
marking?.kick();
|
|
16574
|
-
onEvent?.({ what: on ? "play" : "pause", time:
|
|
16665
|
+
onEvent?.({ what: on ? "play" : "pause", time: round3(time) });
|
|
16575
16666
|
},
|
|
16576
16667
|
onSeek: (time) => {
|
|
16577
16668
|
pills?.sync();
|
|
16578
16669
|
marking?.kick();
|
|
16579
|
-
onEvent?.({ what: "seek", time:
|
|
16670
|
+
onEvent?.({ what: "seek", time: round3(time) });
|
|
16580
16671
|
}
|
|
16581
16672
|
});
|
|
16582
16673
|
const v = p.video;
|
|
@@ -16609,8 +16700,8 @@ async function mountVideo(host, m, look, alive, onEvent) {
|
|
|
16609
16700
|
seekBy: (d) => p.seekBy(d),
|
|
16610
16701
|
context: () => ({
|
|
16611
16702
|
kind: "video",
|
|
16612
|
-
time:
|
|
16613
|
-
...p.duration ? { duration:
|
|
16703
|
+
time: round3(p.time),
|
|
16704
|
+
...p.duration ? { duration: round3(p.duration) } : {},
|
|
16614
16705
|
playing: p.playing,
|
|
16615
16706
|
...pills?.current ? { chapter: pills.current.label } : {}
|
|
16616
16707
|
}),
|
|
@@ -16754,8 +16845,6 @@ var fmt = (v, unit = "") => `${v}${unit}`;
|
|
|
16754
16845
|
function mountMetric(host, m) {
|
|
16755
16846
|
const doc = host.ownerDocument;
|
|
16756
16847
|
const box = div(doc, "knobs-wt-metric");
|
|
16757
|
-
const label = div(doc, "knobs-wt-mlabel");
|
|
16758
|
-
label.textContent = m.label;
|
|
16759
16848
|
const value = div(doc, "knobs-wt-mvalue");
|
|
16760
16849
|
const span = (cls, text) => {
|
|
16761
16850
|
const s = doc.createElement("span");
|
|
@@ -16775,7 +16864,7 @@ function mountMetric(host, m) {
|
|
|
16775
16864
|
delta.dataset.tone = d > 0 === (better === "up") ? "good" : "bad";
|
|
16776
16865
|
value.append(delta);
|
|
16777
16866
|
}
|
|
16778
|
-
box.append(
|
|
16867
|
+
box.append(value);
|
|
16779
16868
|
const total = num(m.of);
|
|
16780
16869
|
if (Number.isFinite(total) && total > 0 && Number.isFinite(a) && Number.isFinite(b)) {
|
|
16781
16870
|
const track = div(doc, "knobs-wt-mtrack");
|
|
@@ -16829,7 +16918,7 @@ function mountDecision(host, m, slot) {
|
|
|
16829
16918
|
}
|
|
16830
16919
|
var HEAVY = new Set(["compare", "video", "diff", "mount"]);
|
|
16831
16920
|
var RELEASES = new Set(["compare", "video"]);
|
|
16832
|
-
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "
|
|
16921
|
+
var labelOf2 = (m) => m.kind === "facts" ? m.title : ("label" in m) && m.kind !== "history" ? m.label : undefined;
|
|
16833
16922
|
function mediaSlot(container, given) {
|
|
16834
16923
|
const doc = container.ownerDocument;
|
|
16835
16924
|
injectWalkthroughStyles(doc);
|
|
@@ -16843,7 +16932,7 @@ function mediaSlot(container, given) {
|
|
|
16843
16932
|
root.dataset.kind = m.kind;
|
|
16844
16933
|
applyLook2(root, options);
|
|
16845
16934
|
const label = labelOf2(m);
|
|
16846
|
-
if (label
|
|
16935
|
+
if (label) {
|
|
16847
16936
|
const l = doc.createElement("span");
|
|
16848
16937
|
l.className = "knobs-wt-slot-label";
|
|
16849
16938
|
l.textContent = label;
|
|
@@ -17820,641 +17909,1015 @@ function walkthrough(container, given) {
|
|
|
17820
17909
|
bar.input.focus({ preventScroll: true });
|
|
17821
17910
|
}
|
|
17822
17911
|
}
|
|
17823
|
-
};
|
|
17824
|
-
const renderNote = (v, n, entering = false) => {
|
|
17825
|
-
const row = el4(doc, "div", "knobs-wt-n");
|
|
17826
|
-
row.dataset.id = n.id;
|
|
17827
|
-
const meta = el4(doc, "span", "knobs-wt-n-meta");
|
|
17828
|
-
const m = n.context.media;
|
|
17829
|
-
meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
|
|
17830
|
-
if (m?.time !== undefined) {
|
|
17831
|
-
meta.title = "Go to this moment";
|
|
17832
|
-
meta.dataset.seek = "";
|
|
17833
|
-
meta.addEventListener("click", () => {
|
|
17834
|
-
const now = mediaContext(v)?.time;
|
|
17835
|
-
if (now !== undefined) {
|
|
17836
|
-
for (const s of slotsOf(v))
|
|
17837
|
-
if (s.seekBy(m.time - now))
|
|
17838
|
-
break;
|
|
17912
|
+
};
|
|
17913
|
+
const renderNote = (v, n, entering = false) => {
|
|
17914
|
+
const row = el4(doc, "div", "knobs-wt-n");
|
|
17915
|
+
row.dataset.id = n.id;
|
|
17916
|
+
const meta = el4(doc, "span", "knobs-wt-n-meta");
|
|
17917
|
+
const m = n.context.media;
|
|
17918
|
+
meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
|
|
17919
|
+
if (m?.time !== undefined) {
|
|
17920
|
+
meta.title = "Go to this moment";
|
|
17921
|
+
meta.dataset.seek = "";
|
|
17922
|
+
meta.addEventListener("click", () => {
|
|
17923
|
+
const now = mediaContext(v)?.time;
|
|
17924
|
+
if (now !== undefined) {
|
|
17925
|
+
for (const s of slotsOf(v))
|
|
17926
|
+
if (s.seekBy(m.time - now))
|
|
17927
|
+
break;
|
|
17928
|
+
}
|
|
17929
|
+
});
|
|
17930
|
+
}
|
|
17931
|
+
row.append(meta);
|
|
17932
|
+
const body = el4(doc, "div", "knobs-wt-n-body");
|
|
17933
|
+
if (n.text)
|
|
17934
|
+
body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
|
|
17935
|
+
if (n.voice) {
|
|
17936
|
+
const a = el4(doc, "audio", "knobs-wt-n-voice");
|
|
17937
|
+
a.controls = true;
|
|
17938
|
+
a.preload = "none";
|
|
17939
|
+
a.src = n.voice.url;
|
|
17940
|
+
body.append(a);
|
|
17941
|
+
}
|
|
17942
|
+
if (n.ink?.paths.length) {
|
|
17943
|
+
const thumb = doc.createElementNS(SVG, "svg");
|
|
17944
|
+
thumb.classList.add("knobs-wt-n-ink");
|
|
17945
|
+
thumb.setAttribute("viewBox", "0 0 1 1");
|
|
17946
|
+
thumb.setAttribute("preserveAspectRatio", "none");
|
|
17947
|
+
thumb.style.aspectRatio = String(n.ink.aspect);
|
|
17948
|
+
for (const d of n.ink.paths) {
|
|
17949
|
+
const p = doc.createElementNS(SVG, "path");
|
|
17950
|
+
p.setAttribute("d", d);
|
|
17951
|
+
thumb.append(p);
|
|
17952
|
+
}
|
|
17953
|
+
body.append(thumb);
|
|
17954
|
+
}
|
|
17955
|
+
row.append(body);
|
|
17956
|
+
const x = el4(doc, "button", "knobs-wt-x");
|
|
17957
|
+
x.type = "button";
|
|
17958
|
+
x.innerHTML = ICON.close;
|
|
17959
|
+
x.setAttribute("aria-label", "Delete this note");
|
|
17960
|
+
x.addEventListener("click", () => {
|
|
17961
|
+
state = removeNote(state, n.id);
|
|
17962
|
+
if (n.voice)
|
|
17963
|
+
URL.revokeObjectURL(n.voice.url);
|
|
17964
|
+
leave(row);
|
|
17965
|
+
syncGlyph(v);
|
|
17966
|
+
syncProgress();
|
|
17967
|
+
emit();
|
|
17968
|
+
});
|
|
17969
|
+
row.append(x);
|
|
17970
|
+
v.notes.append(row);
|
|
17971
|
+
if (entering)
|
|
17972
|
+
enter(row);
|
|
17973
|
+
};
|
|
17974
|
+
const enter = (row) => {
|
|
17975
|
+
if (reduced())
|
|
17976
|
+
return;
|
|
17977
|
+
const h = row.offsetHeight;
|
|
17978
|
+
row.style.height = "0px";
|
|
17979
|
+
row.dataset.entering = "";
|
|
17980
|
+
row.offsetHeight;
|
|
17981
|
+
row.style.height = `${h}px`;
|
|
17982
|
+
delete row.dataset.entering;
|
|
17983
|
+
row.addEventListener("transitionend", () => row.style.height = "", { once: true });
|
|
17984
|
+
};
|
|
17985
|
+
const leave = (row) => {
|
|
17986
|
+
if (reduced())
|
|
17987
|
+
return row.remove();
|
|
17988
|
+
row.style.height = `${row.offsetHeight}px`;
|
|
17989
|
+
row.offsetHeight;
|
|
17990
|
+
row.dataset.leaving = "";
|
|
17991
|
+
row.style.height = "0px";
|
|
17992
|
+
const done = () => row.remove();
|
|
17993
|
+
row.addEventListener("transitionend", done, { once: true });
|
|
17994
|
+
win.setTimeout(done, 600);
|
|
17995
|
+
};
|
|
17996
|
+
const metaLine = (cls) => el4(doc, "div", `knobs-wt-metaline ${cls}`);
|
|
17997
|
+
const metaText = (line, text, cls) => {
|
|
17998
|
+
const s = el4(doc, "span", cls);
|
|
17999
|
+
s.textContent = text;
|
|
18000
|
+
line.append(s);
|
|
18001
|
+
return s;
|
|
18002
|
+
};
|
|
18003
|
+
const findPRIn = (n, kind = "pr") => findPR(page.prs, n, page.repo, kind);
|
|
18004
|
+
const metaLink = (line, text, href) => {
|
|
18005
|
+
const n = prNumber(href);
|
|
18006
|
+
const issue = n ? null : issueNumber(href);
|
|
18007
|
+
if (n || issue) {
|
|
18008
|
+
const chip = n ? prChip(doc, n, { ...findPRIn(n), href }) : prChip(doc, issue, { ...findPRIn(issue, "issue"), href });
|
|
18009
|
+
line.append(chip);
|
|
18010
|
+
return chip;
|
|
18011
|
+
}
|
|
18012
|
+
const a = el4(doc, "a");
|
|
18013
|
+
a.href = href;
|
|
18014
|
+
a.target = "_blank";
|
|
18015
|
+
a.rel = "noreferrer";
|
|
18016
|
+
a.textContent = text;
|
|
18017
|
+
a.insertAdjacentHTML("beforeend", ICON.out);
|
|
18018
|
+
line.append(a);
|
|
18019
|
+
return a;
|
|
18020
|
+
};
|
|
18021
|
+
const feedItem = (f, now) => {
|
|
18022
|
+
const li = el4(doc, "li", "knobs-wt-fi");
|
|
18023
|
+
li.dataset.kind = f.kind;
|
|
18024
|
+
const q = el4(doc, "div", "knobs-wt-fi-q");
|
|
18025
|
+
const meta = metaLine("knobs-wt-fi-meta");
|
|
18026
|
+
if (f.at !== null) {
|
|
18027
|
+
const when = metaText(meta, ago(f.at, now));
|
|
18028
|
+
when.title = new Date(f.at).toLocaleString();
|
|
18029
|
+
}
|
|
18030
|
+
const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
|
|
18031
|
+
if (step) {
|
|
18032
|
+
const section = page.sections.find((s) => s.id === step.section);
|
|
18033
|
+
const b = el4(doc, "button", "knobs-wt-fi-step");
|
|
18034
|
+
b.type = "button";
|
|
18035
|
+
b.textContent = section?.link?.label ?? plainMarkdown(step.title);
|
|
18036
|
+
b.title = `Go to “${plainMarkdown(step.title)}”`;
|
|
18037
|
+
b.addEventListener("click", () => go(step.key));
|
|
18038
|
+
meta.append(b);
|
|
18039
|
+
}
|
|
18040
|
+
if (f.status && review) {
|
|
18041
|
+
const id = f.status.toLowerCase();
|
|
18042
|
+
const shipped = /^(ship|shipped|merged|done)$/.test(id);
|
|
18043
|
+
const changes = /^(changes?|changes queued)$/.test(id);
|
|
18044
|
+
const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
|
|
18045
|
+
s.dataset.status = shipped ? "on" : changes ? "warn" : "";
|
|
18046
|
+
}
|
|
18047
|
+
if (meta.childNodes.length)
|
|
18048
|
+
q.append(meta);
|
|
18049
|
+
if (f.q)
|
|
18050
|
+
q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
|
|
18051
|
+
else
|
|
18052
|
+
q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
|
|
18053
|
+
li.append(q);
|
|
18054
|
+
if (f.q || f.trail.length) {
|
|
18055
|
+
const a = el4(doc, "div", "knobs-wt-fi-a");
|
|
18056
|
+
if (f.q)
|
|
18057
|
+
a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
|
|
18058
|
+
if (f.trail.length) {
|
|
18059
|
+
const list = el4(doc, "ol", "knobs-wt-trail");
|
|
18060
|
+
for (const line of f.trail) {
|
|
18061
|
+
const i = line.indexOf(" · ");
|
|
18062
|
+
const row = el4(doc, "li");
|
|
18063
|
+
const words = el4(doc, "span", "knobs-wt-trail-text");
|
|
18064
|
+
words.textContent = i < 0 ? line : line.slice(i + 3);
|
|
18065
|
+
row.append(words);
|
|
18066
|
+
if (i >= 0) {
|
|
18067
|
+
const at = el4(doc, "span", "knobs-wt-trail-at");
|
|
18068
|
+
at.textContent = line.slice(0, i);
|
|
18069
|
+
row.append(at);
|
|
18070
|
+
}
|
|
18071
|
+
list.append(row);
|
|
18072
|
+
}
|
|
18073
|
+
discs.push(disclosure(a, { label: "Context", content: list }));
|
|
18074
|
+
}
|
|
18075
|
+
li.append(a);
|
|
18076
|
+
}
|
|
18077
|
+
return li;
|
|
18078
|
+
};
|
|
18079
|
+
const build = () => {
|
|
18080
|
+
for (const v of views) {
|
|
18081
|
+
v.slots.forEach((s) => s.dispose());
|
|
18082
|
+
v.taste?.dispose();
|
|
18083
|
+
}
|
|
18084
|
+
for (const d of discs)
|
|
18085
|
+
d.dispose();
|
|
18086
|
+
discs = [];
|
|
18087
|
+
pill?.dispose();
|
|
18088
|
+
pill = null;
|
|
18089
|
+
tocRows = [];
|
|
18090
|
+
if (full) {
|
|
18091
|
+
full.p.snap(0);
|
|
18092
|
+
closeFull();
|
|
18093
|
+
}
|
|
18094
|
+
clearInk();
|
|
18095
|
+
views = [];
|
|
18096
|
+
root.textContent = "";
|
|
18097
|
+
root.removeAttribute("data-settled");
|
|
18098
|
+
root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
|
|
18099
|
+
root.toggleAttribute("data-cards", options.cards === true);
|
|
18100
|
+
root.toggleAttribute("data-review", review);
|
|
18101
|
+
const layout = el4(doc, "div", "knobs-wt-layout");
|
|
18102
|
+
const main = el4(doc, "div", "knobs-wt-main");
|
|
18103
|
+
const flow = el4(doc, "div", "knobs-wt-flow");
|
|
18104
|
+
const head = el4(doc, "header", "knobs-wt-head");
|
|
18105
|
+
const h1 = el4(doc, "h1", "knobs-wt-title");
|
|
18106
|
+
h1.textContent = page.title;
|
|
18107
|
+
head.append(h1);
|
|
18108
|
+
if (page.summary)
|
|
18109
|
+
head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
|
|
18110
|
+
if (page.more)
|
|
18111
|
+
head.append(more(page.more));
|
|
18112
|
+
if (page.meta.length) {
|
|
18113
|
+
const meta = el4(doc, "div", "knobs-wt-meta");
|
|
18114
|
+
for (const m of page.meta) {
|
|
18115
|
+
const s = el4(doc, "span");
|
|
18116
|
+
s.append(m.label);
|
|
18117
|
+
const b = el4(doc, "b");
|
|
18118
|
+
if (m.href) {
|
|
18119
|
+
const a = el4(doc, "a");
|
|
18120
|
+
a.href = m.href;
|
|
18121
|
+
a.target = "_blank";
|
|
18122
|
+
a.rel = "noreferrer";
|
|
18123
|
+
a.textContent = m.value;
|
|
18124
|
+
b.append(a);
|
|
18125
|
+
} else
|
|
18126
|
+
b.textContent = m.value;
|
|
18127
|
+
s.append(b);
|
|
18128
|
+
meta.append(s);
|
|
18129
|
+
}
|
|
18130
|
+
head.append(meta);
|
|
18131
|
+
}
|
|
18132
|
+
flow.append(head);
|
|
18133
|
+
let feedEl = null;
|
|
18134
|
+
if (page.feed.length) {
|
|
18135
|
+
feedEl = el4(doc, "section", "knobs-wt-feed");
|
|
18136
|
+
feedEl.id = "wt-latest";
|
|
18137
|
+
feedEl.setAttribute("aria-label", "Updates and answers");
|
|
18138
|
+
const fh = el4(doc, "header", "knobs-wt-feed-head");
|
|
18139
|
+
const h = el4(doc, "h2", "knobs-wt-feed-title");
|
|
18140
|
+
h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
|
|
18141
|
+
fh.append(h);
|
|
18142
|
+
const list = el4(doc, "ol", "knobs-wt-feed-list");
|
|
18143
|
+
const now = Date.now();
|
|
18144
|
+
for (const f of page.feed)
|
|
18145
|
+
list.append(feedItem(f, now));
|
|
18146
|
+
feedEl.append(fh, list);
|
|
18147
|
+
flow.append(feedEl);
|
|
18148
|
+
}
|
|
18149
|
+
let rail = null;
|
|
18150
|
+
count = fill = tocEl = null;
|
|
18151
|
+
if (options.rail !== false) {
|
|
18152
|
+
rail = el4(doc, "aside", "knobs-wt-rail");
|
|
18153
|
+
rail.setAttribute("aria-label", "Contents");
|
|
18154
|
+
tocEl = el4(doc, "nav", "knobs-wt-toc");
|
|
18155
|
+
tocEl.setAttribute("aria-label", "Contents");
|
|
18156
|
+
if (feedEl) {
|
|
18157
|
+
const group = el4(doc, "div", "knobs-wt-toc-group");
|
|
18158
|
+
const target = feedEl;
|
|
18159
|
+
group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
|
|
18160
|
+
tocEl.append(group);
|
|
18161
|
+
}
|
|
18162
|
+
if (review) {
|
|
18163
|
+
const progress = el4(doc, "div", "knobs-wt-progress");
|
|
18164
|
+
count = el4(doc, "div", "knobs-wt-count");
|
|
18165
|
+
const meter = el4(doc, "div", "knobs-wt-bar");
|
|
18166
|
+
fill = el4(doc, "div", "knobs-wt-fill");
|
|
18167
|
+
meter.append(fill);
|
|
18168
|
+
progress.append(count, meter);
|
|
18169
|
+
rail.append(progress);
|
|
18170
|
+
}
|
|
18171
|
+
rail.append(tocEl);
|
|
18172
|
+
if (review)
|
|
18173
|
+
rail.append(actions(true));
|
|
18174
|
+
if (review && options.keys !== false) {
|
|
18175
|
+
const keys = el4(doc, "div", "knobs-wt-keys");
|
|
18176
|
+
keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
|
|
18177
|
+
rail.append(keys);
|
|
18178
|
+
}
|
|
18179
|
+
}
|
|
18180
|
+
for (const section of page.sections) {
|
|
18181
|
+
const sec = el4(doc, "section", "knobs-wt-section");
|
|
18182
|
+
sec.id = `wt-${section.id}`;
|
|
18183
|
+
const sh = el4(doc, "header", "knobs-wt-shead");
|
|
18184
|
+
const status = review ? section.status : undefined;
|
|
18185
|
+
if (section.kicker || section.link || status) {
|
|
18186
|
+
const meta = metaLine("knobs-wt-kicker");
|
|
18187
|
+
if (section.kicker)
|
|
18188
|
+
metaText(meta, section.kicker, "knobs-wt-kicker-lead");
|
|
18189
|
+
if (section.link)
|
|
18190
|
+
metaLink(meta, section.link.label, section.link.href);
|
|
18191
|
+
if (status) {
|
|
18192
|
+
const s = statusLook(status);
|
|
18193
|
+
metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
|
|
17839
18194
|
}
|
|
17840
|
-
|
|
17841
|
-
}
|
|
17842
|
-
row.append(meta);
|
|
17843
|
-
const body = el4(doc, "div", "knobs-wt-n-body");
|
|
17844
|
-
if (n.text)
|
|
17845
|
-
body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
|
|
17846
|
-
if (n.voice) {
|
|
17847
|
-
const a = el4(doc, "audio", "knobs-wt-n-voice");
|
|
17848
|
-
a.controls = true;
|
|
17849
|
-
a.preload = "none";
|
|
17850
|
-
a.src = n.voice.url;
|
|
17851
|
-
body.append(a);
|
|
17852
|
-
}
|
|
17853
|
-
if (n.ink?.paths.length) {
|
|
17854
|
-
const thumb = doc.createElementNS(SVG, "svg");
|
|
17855
|
-
thumb.classList.add("knobs-wt-n-ink");
|
|
17856
|
-
thumb.setAttribute("viewBox", "0 0 1 1");
|
|
17857
|
-
thumb.setAttribute("preserveAspectRatio", "none");
|
|
17858
|
-
thumb.style.aspectRatio = String(n.ink.aspect);
|
|
17859
|
-
for (const d of n.ink.paths) {
|
|
17860
|
-
const p = doc.createElementNS(SVG, "path");
|
|
17861
|
-
p.setAttribute("d", d);
|
|
17862
|
-
thumb.append(p);
|
|
18195
|
+
sh.append(meta);
|
|
17863
18196
|
}
|
|
17864
|
-
|
|
18197
|
+
const h2 = el4(doc, "h2", "knobs-wt-heading");
|
|
18198
|
+
h2.innerHTML = inlineMarkdown(section.title);
|
|
18199
|
+
sh.append(h2);
|
|
18200
|
+
const intro = el4(doc, "div", "knobs-wt-sintro");
|
|
18201
|
+
intro.append(sh);
|
|
18202
|
+
if (section.summary)
|
|
18203
|
+
intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
|
|
18204
|
+
if (section.more)
|
|
18205
|
+
intro.append(more(section.more));
|
|
18206
|
+
sec.append(intro);
|
|
18207
|
+
let group = null;
|
|
18208
|
+
if (tocEl) {
|
|
18209
|
+
group = el4(doc, "div", "knobs-wt-toc-group");
|
|
18210
|
+
const first = section.steps[0]?.key;
|
|
18211
|
+
group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
|
|
18212
|
+
tocEl.append(group);
|
|
18213
|
+
}
|
|
18214
|
+
for (const step of section.steps) {
|
|
18215
|
+
const v = stepView(step, group);
|
|
18216
|
+
sec.append(v.card);
|
|
18217
|
+
views.push(v);
|
|
18218
|
+
}
|
|
18219
|
+
flow.append(sec);
|
|
17865
18220
|
}
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
leave(row);
|
|
18221
|
+
linkPRs(flow, findPRIn);
|
|
18222
|
+
main.append(flow);
|
|
18223
|
+
if (noteMode !== "hidden")
|
|
18224
|
+
main.append(bar.dock);
|
|
18225
|
+
if (rail)
|
|
18226
|
+
layout.append(rail);
|
|
18227
|
+
layout.append(main);
|
|
18228
|
+
root.append(layout, scrim);
|
|
18229
|
+
for (const v of views) {
|
|
17876
18230
|
syncGlyph(v);
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
}
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
|
|
18231
|
+
for (const n of notesOn(state, v.step.key))
|
|
18232
|
+
renderNote(v, n);
|
|
18233
|
+
}
|
|
18234
|
+
if (tocEl)
|
|
18235
|
+
pill = selectionPill(tocEl, {
|
|
18236
|
+
selected: () => viewOf(current)?.toc ?? null,
|
|
18237
|
+
rows: () => tocRows,
|
|
18238
|
+
spring: SPRINGS.snappy
|
|
18239
|
+
});
|
|
18240
|
+
syncProgress();
|
|
18241
|
+
current = null;
|
|
18242
|
+
fromScroll();
|
|
18243
|
+
placeMark();
|
|
18244
|
+
bar.sync();
|
|
18245
|
+
syncReserve();
|
|
18246
|
+
win.requestAnimationFrame(() => win.requestAnimationFrame(() => {
|
|
18247
|
+
if (!disposed && !reduced())
|
|
18248
|
+
root.dataset.settled = "";
|
|
18249
|
+
}));
|
|
17895
18250
|
};
|
|
17896
|
-
const
|
|
17897
|
-
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
row.addEventListener("transitionend", done, { once: true });
|
|
17905
|
-
win.setTimeout(done, 600);
|
|
18251
|
+
const more = (text) => {
|
|
18252
|
+
const box = el4(doc, "div", "knobs-wt-more");
|
|
18253
|
+
const body = el4(doc, "div", "knobs-wt-more-text");
|
|
18254
|
+
for (const para of text.split(/\n\s*\n/))
|
|
18255
|
+
if (para.trim())
|
|
18256
|
+
body.append(prose2(doc, "p", "", para.trim()));
|
|
18257
|
+
discs.push(disclosure(box, { label: "More", content: body }));
|
|
18258
|
+
return box;
|
|
17906
18259
|
};
|
|
17907
|
-
const
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
18260
|
+
const tocRow = (label, sub, run, section) => {
|
|
18261
|
+
const b = el4(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
|
|
18262
|
+
b.type = "button";
|
|
18263
|
+
if (section)
|
|
18264
|
+
b.dataset.section = section;
|
|
18265
|
+
const t = el4(doc, "span", "knobs-wt-toc-label");
|
|
18266
|
+
t.textContent = label;
|
|
18267
|
+
b.title = label;
|
|
18268
|
+
b.append(t);
|
|
18269
|
+
if (review)
|
|
18270
|
+
b.append(el4(doc, "span", "knobs-wt-toc-end"));
|
|
18271
|
+
b.addEventListener("mousedown", (ev) => ev.preventDefault());
|
|
18272
|
+
b.addEventListener("click", run);
|
|
18273
|
+
tocRows.push(b);
|
|
18274
|
+
return b;
|
|
17913
18275
|
};
|
|
17914
|
-
const
|
|
17915
|
-
|
|
17916
|
-
const
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
18276
|
+
const actions = (rail) => {
|
|
18277
|
+
const row = el4(doc, "div", "knobs-wt-actions");
|
|
18278
|
+
const btn = (text, run, primary = false) => {
|
|
18279
|
+
const b = el4(doc, "button", "knobs-wt-btn");
|
|
18280
|
+
b.type = "button";
|
|
18281
|
+
b.textContent = text;
|
|
18282
|
+
if (primary)
|
|
18283
|
+
b.dataset.primary = "";
|
|
18284
|
+
b.addEventListener("click", run);
|
|
18285
|
+
row.append(b);
|
|
18286
|
+
return b;
|
|
18287
|
+
};
|
|
18288
|
+
btn("Copy feedback", () => copy("markdown"), true);
|
|
18289
|
+
btn("JSON", () => copy("json"));
|
|
18290
|
+
if (rail)
|
|
18291
|
+
btn("Reset", async () => {
|
|
18292
|
+
const ok = await confirm({
|
|
18293
|
+
title: "Forget every answer on this page?",
|
|
18294
|
+
confirm: "Reset",
|
|
18295
|
+
destructive: true,
|
|
18296
|
+
look,
|
|
18297
|
+
doc
|
|
18298
|
+
});
|
|
18299
|
+
if (ok)
|
|
18300
|
+
api.reset();
|
|
18301
|
+
});
|
|
18302
|
+
return row;
|
|
17931
18303
|
};
|
|
17932
|
-
const
|
|
17933
|
-
const
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
18304
|
+
const copy = async (format) => {
|
|
18305
|
+
const text = format === "json" ? JSON.stringify(feedback(), null, 2) : markdown();
|
|
18306
|
+
try {
|
|
18307
|
+
await win.navigator.clipboard.writeText(text);
|
|
18308
|
+
toast(format === "json" ? "Feedback copied as JSON" : "Feedback copied as Markdown", {
|
|
18309
|
+
look,
|
|
18310
|
+
doc
|
|
18311
|
+
});
|
|
18312
|
+
} catch {
|
|
18313
|
+
toast("Couldn't reach the clipboard", { tone: "warn", look, doc });
|
|
17940
18314
|
}
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
18315
|
+
};
|
|
18316
|
+
const stepView = (step, group) => {
|
|
18317
|
+
const card = el4(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
|
|
18318
|
+
card.id = `wt-${step.key.replace("/", "--")}`;
|
|
18319
|
+
card.tabIndex = -1;
|
|
18320
|
+
card.dataset.key = step.key;
|
|
18321
|
+
card.setAttribute("aria-label", plainMarkdown(step.title));
|
|
18322
|
+
const head = el4(doc, "header", "knobs-wt-step-head");
|
|
18323
|
+
const h3 = el4(doc, "h3", "knobs-wt-stepTitle");
|
|
18324
|
+
h3.innerHTML = inlineMarkdown(step.title);
|
|
18325
|
+
head.append(h3);
|
|
18326
|
+
if (step.caption)
|
|
18327
|
+
head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
|
|
18328
|
+
if (step.more)
|
|
18329
|
+
head.append(more(step.more));
|
|
18330
|
+
card.append(head);
|
|
18331
|
+
const slots = [];
|
|
18332
|
+
const onEvent = (e) => record({ ...e, step: step.key });
|
|
18333
|
+
let media = null;
|
|
18334
|
+
if (step.media.length) {
|
|
18335
|
+
media = el4(doc, "div", "knobs-wt-media");
|
|
18336
|
+
media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
|
|
18337
|
+
for (const m of step.media)
|
|
18338
|
+
slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks, findPR: findPRIn }));
|
|
18339
|
+
card.append(media);
|
|
17950
18340
|
}
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
18341
|
+
let taste = null;
|
|
18342
|
+
if (step.question && review)
|
|
18343
|
+
card.append(prose2(doc, "p", "knobs-wt-question", step.question));
|
|
18344
|
+
if (step.ask === "pick") {
|
|
18345
|
+
taste = tasteTest(card, {
|
|
18346
|
+
...look,
|
|
18347
|
+
pickable: review,
|
|
18348
|
+
options: step.options,
|
|
18349
|
+
picked: state.steps[step.key]?.pick ?? null,
|
|
18350
|
+
onPick: (id) => pick(step.key, id),
|
|
18351
|
+
steps: stepLinks
|
|
18352
|
+
});
|
|
18353
|
+
media ??= taste.element;
|
|
17957
18354
|
}
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
const words = el4(doc, "span", "knobs-wt-trail-text");
|
|
17975
|
-
words.textContent = i < 0 ? line : line.slice(i + 3);
|
|
17976
|
-
row.append(words);
|
|
17977
|
-
if (i >= 0) {
|
|
17978
|
-
const at = el4(doc, "span", "knobs-wt-trail-at");
|
|
17979
|
-
at.textContent = line.slice(0, i);
|
|
17980
|
-
row.append(at);
|
|
17981
|
-
}
|
|
17982
|
-
list.append(row);
|
|
17983
|
-
}
|
|
17984
|
-
discs.push(disclosure(a, { label: "Context", content: list }));
|
|
18355
|
+
const notes = el4(doc, "div", "knobs-wt-notes");
|
|
18356
|
+
if (review)
|
|
18357
|
+
card.append(notes);
|
|
18358
|
+
card.addEventListener("focusin", () => {
|
|
18359
|
+
if (scroll.to)
|
|
18360
|
+
return;
|
|
18361
|
+
pinUntil = performance.now() + 250;
|
|
18362
|
+
setCurrent(step.key);
|
|
18363
|
+
});
|
|
18364
|
+
let toc = null;
|
|
18365
|
+
let glyph = null;
|
|
18366
|
+
if (group) {
|
|
18367
|
+
toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
|
|
18368
|
+
if (review) {
|
|
18369
|
+
glyph = el4(doc, "span", "knobs-wt-toc-glyph");
|
|
18370
|
+
toc.querySelector(".knobs-wt-toc-end").append(glyph);
|
|
17985
18371
|
}
|
|
17986
|
-
|
|
18372
|
+
group.append(toc);
|
|
17987
18373
|
}
|
|
17988
|
-
return
|
|
18374
|
+
return { step, card, media, notes, toc, glyph, taste, slots };
|
|
17989
18375
|
};
|
|
17990
|
-
const
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
18376
|
+
const pick = (key, id) => {
|
|
18377
|
+
state = setFeedback(state, key, { pick: id });
|
|
18378
|
+
const v = viewOf(key);
|
|
18379
|
+
if (v) {
|
|
18380
|
+
v.taste?.pick(id);
|
|
18381
|
+
syncGlyph(v);
|
|
17994
18382
|
}
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
if (
|
|
18002
|
-
|
|
18003
|
-
|
|
18383
|
+
record({ what: "pick", step: key, detail: id ?? "cleared" });
|
|
18384
|
+
syncProgress();
|
|
18385
|
+
emit();
|
|
18386
|
+
};
|
|
18387
|
+
const LEAVE = "[role=tree],[role=listbox],[role=grid],[role=slider],[role=menu],.knobs-timeline";
|
|
18388
|
+
const onKey = (e) => {
|
|
18389
|
+
if (disposed || e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey)
|
|
18390
|
+
return;
|
|
18391
|
+
if (isTypingTarget(e.target))
|
|
18392
|
+
return;
|
|
18393
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
18394
|
+
if (target && target !== doc.body && target !== doc.documentElement && !root.contains(target))
|
|
18395
|
+
return;
|
|
18396
|
+
const own = target?.closest(LEAVE);
|
|
18397
|
+
const claim = () => {
|
|
18398
|
+
e.preventDefault();
|
|
18399
|
+
e.stopPropagation();
|
|
18400
|
+
};
|
|
18401
|
+
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
18402
|
+
if (key === "Escape") {
|
|
18403
|
+
if (ink.on)
|
|
18404
|
+
return claim(), setInk(false);
|
|
18405
|
+
if (slotsOf(viewOf(current)).some((s) => s.unzoom()))
|
|
18406
|
+
return claim();
|
|
18407
|
+
if (full)
|
|
18408
|
+
return claim(), setFull(false);
|
|
18409
|
+
return;
|
|
18004
18410
|
}
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
root.toggleAttribute("data-cards", options.cards === true);
|
|
18011
|
-
root.toggleAttribute("data-review", review);
|
|
18012
|
-
const layout = el4(doc, "div", "knobs-wt-layout");
|
|
18013
|
-
const main = el4(doc, "div", "knobs-wt-main");
|
|
18014
|
-
const flow = el4(doc, "div", "knobs-wt-flow");
|
|
18015
|
-
const head = el4(doc, "header", "knobs-wt-head");
|
|
18016
|
-
const h1 = el4(doc, "h1", "knobs-wt-title");
|
|
18017
|
-
h1.textContent = page.title;
|
|
18018
|
-
head.append(h1);
|
|
18019
|
-
if (page.summary)
|
|
18020
|
-
head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
|
|
18021
|
-
if (page.more)
|
|
18022
|
-
head.append(more(page.more));
|
|
18023
|
-
if (page.meta.length) {
|
|
18024
|
-
const meta = el4(doc, "div", "knobs-wt-meta");
|
|
18025
|
-
for (const m of page.meta) {
|
|
18026
|
-
const s = el4(doc, "span");
|
|
18027
|
-
s.append(m.label);
|
|
18028
|
-
const b = el4(doc, "b");
|
|
18029
|
-
if (m.href) {
|
|
18030
|
-
const a = el4(doc, "a");
|
|
18031
|
-
a.href = m.href;
|
|
18032
|
-
a.target = "_blank";
|
|
18033
|
-
a.rel = "noreferrer";
|
|
18034
|
-
a.textContent = m.value;
|
|
18035
|
-
b.append(a);
|
|
18036
|
-
} else
|
|
18037
|
-
b.textContent = m.value;
|
|
18038
|
-
s.append(b);
|
|
18039
|
-
meta.append(s);
|
|
18040
|
-
}
|
|
18041
|
-
head.append(meta);
|
|
18411
|
+
if (key === "f" && !e.shiftKey) {
|
|
18412
|
+
if (!viewOf(current)?.media)
|
|
18413
|
+
return;
|
|
18414
|
+
claim();
|
|
18415
|
+
return setFull(!full);
|
|
18042
18416
|
}
|
|
18043
|
-
|
|
18044
|
-
|
|
18045
|
-
if (
|
|
18046
|
-
|
|
18047
|
-
|
|
18048
|
-
|
|
18049
|
-
const
|
|
18050
|
-
|
|
18051
|
-
|
|
18052
|
-
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
list.append(feedItem(f, now));
|
|
18057
|
-
feedEl.append(fh, list);
|
|
18058
|
-
flow.append(feedEl);
|
|
18417
|
+
if (own)
|
|
18418
|
+
return;
|
|
18419
|
+
if (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp") {
|
|
18420
|
+
if (e.shiftKey && key.startsWith("Arrow"))
|
|
18421
|
+
return;
|
|
18422
|
+
const dir = key === "j" || key === "ArrowDown" ? 1 : -1;
|
|
18423
|
+
const next = adjacentStep(views.map((v) => v.step.key), current, dir);
|
|
18424
|
+
if (!next)
|
|
18425
|
+
return;
|
|
18426
|
+
claim();
|
|
18427
|
+
if (full)
|
|
18428
|
+
setFull(false);
|
|
18429
|
+
return go(next);
|
|
18059
18430
|
}
|
|
18060
|
-
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
tocEl.append(group);
|
|
18072
|
-
}
|
|
18073
|
-
if (review) {
|
|
18074
|
-
const progress = el4(doc, "div", "knobs-wt-progress");
|
|
18075
|
-
count = el4(doc, "div", "knobs-wt-count");
|
|
18076
|
-
const meter = el4(doc, "div", "knobs-wt-bar");
|
|
18077
|
-
fill = el4(doc, "div", "knobs-wt-fill");
|
|
18078
|
-
meter.append(fill);
|
|
18079
|
-
progress.append(count, meter);
|
|
18080
|
-
rail.append(progress);
|
|
18081
|
-
}
|
|
18082
|
-
rail.append(tocEl);
|
|
18083
|
-
if (review)
|
|
18084
|
-
rail.append(actions(true));
|
|
18085
|
-
if (review && options.keys !== false) {
|
|
18086
|
-
const keys = el4(doc, "div", "knobs-wt-keys");
|
|
18087
|
-
keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
|
|
18088
|
-
rail.append(keys);
|
|
18089
|
-
}
|
|
18431
|
+
if ((key === "Home" || key === "End") && views.length) {
|
|
18432
|
+
claim();
|
|
18433
|
+
if (full)
|
|
18434
|
+
setFull(false);
|
|
18435
|
+
return go(views[key === "Home" ? 0 : views.length - 1].step.key);
|
|
18436
|
+
}
|
|
18437
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
18438
|
+
const d = (key === "ArrowLeft" ? -1 : 1) * (e.shiftKey ? 2 : 0.5);
|
|
18439
|
+
if (seekBy(d))
|
|
18440
|
+
claim();
|
|
18441
|
+
return;
|
|
18090
18442
|
}
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18443
|
+
if (key === "a" && !e.shiftKey && root.querySelector(".knobs-wt-anno")) {
|
|
18444
|
+
claim();
|
|
18445
|
+
return setAnnotations(root, !annotationsOn(root));
|
|
18446
|
+
}
|
|
18447
|
+
if (key === "n" && noteMode !== "hidden") {
|
|
18448
|
+
claim();
|
|
18449
|
+
bar.setOpen(true, true);
|
|
18450
|
+
return;
|
|
18451
|
+
}
|
|
18452
|
+
const v = viewOf(current);
|
|
18453
|
+
if (!v || e.shiftKey || !review)
|
|
18454
|
+
return;
|
|
18455
|
+
const choice = digitChoice(v.step, state.steps[v.step.key], key);
|
|
18456
|
+
if (choice === undefined)
|
|
18457
|
+
return;
|
|
18458
|
+
claim();
|
|
18459
|
+
pick(v.step.key, choice);
|
|
18460
|
+
};
|
|
18461
|
+
if (options.keys !== false)
|
|
18462
|
+
doc.addEventListener("keydown", onKey);
|
|
18463
|
+
const unregisterHints = hints.register(root, () => {
|
|
18464
|
+
const v = viewOf(current);
|
|
18465
|
+
return [
|
|
18466
|
+
...v ? v.card.querySelectorAll(".knobs-wt-pick") : [],
|
|
18467
|
+
...root.querySelectorAll(".knobs-wt-tool:not([hidden]),.knobs-wt-rail .knobs-wt-btn")
|
|
18468
|
+
];
|
|
18469
|
+
}, { listen: false, rank: 1 });
|
|
18470
|
+
build();
|
|
18471
|
+
const api = {
|
|
18472
|
+
element: root,
|
|
18473
|
+
get page() {
|
|
18474
|
+
return page;
|
|
18475
|
+
},
|
|
18476
|
+
get state() {
|
|
18477
|
+
return state;
|
|
18478
|
+
},
|
|
18479
|
+
get feedback() {
|
|
18480
|
+
return feedback();
|
|
18481
|
+
},
|
|
18482
|
+
get current() {
|
|
18483
|
+
return current;
|
|
18484
|
+
},
|
|
18485
|
+
markdown,
|
|
18486
|
+
go: (key) => go(key),
|
|
18487
|
+
full: (on) => setFull(on ?? !full),
|
|
18488
|
+
send: (text) => send(text),
|
|
18489
|
+
set(next) {
|
|
18490
|
+
const keep = current;
|
|
18491
|
+
const sameId = next.id === page.id;
|
|
18492
|
+
page = normalizeWalkthrough(next);
|
|
18493
|
+
if (!sameId)
|
|
18494
|
+
state = load();
|
|
18495
|
+
build();
|
|
18496
|
+
if (keep && viewOf(keep))
|
|
18497
|
+
go(keep);
|
|
18498
|
+
},
|
|
18499
|
+
reset() {
|
|
18500
|
+
state = EMPTY_STATE;
|
|
18501
|
+
for (const v of views) {
|
|
18502
|
+
v.taste?.pick(null);
|
|
18503
|
+
v.notes.textContent = "";
|
|
18504
|
+
syncGlyph(v);
|
|
18107
18505
|
}
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
|
|
18111
|
-
|
|
18112
|
-
|
|
18113
|
-
|
|
18114
|
-
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18506
|
+
syncProgress();
|
|
18507
|
+
emit();
|
|
18508
|
+
},
|
|
18509
|
+
dispose() {
|
|
18510
|
+
if (disposed)
|
|
18511
|
+
return;
|
|
18512
|
+
disposed = true;
|
|
18513
|
+
win.clearTimeout(emitTimer);
|
|
18514
|
+
win.clearTimeout(backTimer);
|
|
18515
|
+
win.clearInterval(ticker);
|
|
18516
|
+
win.clearInterval(voice.timer);
|
|
18517
|
+
voice.recorder?.stop();
|
|
18518
|
+
motion.stop();
|
|
18519
|
+
if (scrollRaf)
|
|
18520
|
+
win.cancelAnimationFrame(scrollRaf);
|
|
18521
|
+
win.removeEventListener("scroll", onScroll, { capture: true });
|
|
18522
|
+
win.removeEventListener("resize", onScroll);
|
|
18523
|
+
win.removeEventListener("resize", onFullResize);
|
|
18524
|
+
for (const type of ["wheel", "touchstart"])
|
|
18525
|
+
win.removeEventListener(type, stopScroll);
|
|
18526
|
+
doc.removeEventListener("keydown", onKey);
|
|
18527
|
+
unregisterHints();
|
|
18528
|
+
bar.unobserve();
|
|
18529
|
+
pill?.dispose();
|
|
18530
|
+
for (const d of discs)
|
|
18531
|
+
d.dispose();
|
|
18532
|
+
if (padded)
|
|
18533
|
+
padded.el.style.scrollPaddingBottom = padded.was;
|
|
18534
|
+
for (const v of views) {
|
|
18535
|
+
v.slots.forEach((s) => s.dispose());
|
|
18536
|
+
v.taste?.dispose();
|
|
18124
18537
|
}
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18538
|
+
root.remove();
|
|
18539
|
+
}
|
|
18540
|
+
};
|
|
18541
|
+
return api;
|
|
18542
|
+
}
|
|
18543
|
+
|
|
18544
|
+
// src/review/index.ts
|
|
18545
|
+
init_compare();
|
|
18546
|
+
|
|
18547
|
+
// src/ui/compare-strip.ts
|
|
18548
|
+
init_compare_frames();
|
|
18549
|
+
init_compare_model();
|
|
18550
|
+
init_look();
|
|
18551
|
+
init_rgba();
|
|
18552
|
+
init_timeline_model();
|
|
18553
|
+
var BAR = 7;
|
|
18554
|
+
var GAP = 2;
|
|
18555
|
+
var DIFF = { w: 32, h: 18 };
|
|
18556
|
+
var DIFFS_PER_DRAW = 6;
|
|
18557
|
+
function compareStrip(container, options) {
|
|
18558
|
+
const doc = container.ownerDocument;
|
|
18559
|
+
const win = doc.defaultView;
|
|
18560
|
+
const timeline = options.timeline;
|
|
18561
|
+
const fps = () => options.fps ?? timeline.fps;
|
|
18562
|
+
const align = options.align ?? "track";
|
|
18563
|
+
const threshold = options.threshold ?? 4 / 255;
|
|
18564
|
+
const root = doc.createElement("div");
|
|
18565
|
+
root.className = "knobs-cmp-strip";
|
|
18566
|
+
root.setAttribute("role", "img");
|
|
18567
|
+
root.setAttribute("aria-label", "Frame strip: Before over After, with how much they differ");
|
|
18568
|
+
applyLook2(root, options);
|
|
18569
|
+
const canvas = doc.createElement("canvas");
|
|
18570
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
18571
|
+
root.append(canvas);
|
|
18572
|
+
container.append(root);
|
|
18573
|
+
const diffs = new Map;
|
|
18574
|
+
const pairDiffs = new Map;
|
|
18575
|
+
let disposed = false;
|
|
18576
|
+
let near = false;
|
|
18577
|
+
let onScreen = false;
|
|
18578
|
+
const makeSide = (source) => ({
|
|
18579
|
+
source,
|
|
18580
|
+
kind: mediaKind(source.src, source.kind),
|
|
18581
|
+
frames: null,
|
|
18582
|
+
still: null,
|
|
18583
|
+
seen: false
|
|
18584
|
+
});
|
|
18585
|
+
let sides = [makeSide(options.before), makeSide(options.after)];
|
|
18586
|
+
const offs = new Map;
|
|
18587
|
+
const load = (s) => {
|
|
18588
|
+
if (s.frames || s.still)
|
|
18589
|
+
return;
|
|
18590
|
+
if (s.kind === "video") {
|
|
18591
|
+
s.frames = acquireFrames(doc, s.source.src, fps());
|
|
18592
|
+
offs.set(s, s.frames.listen(schedule));
|
|
18593
|
+
if (onScreen) {
|
|
18594
|
+
s.frames.visible(true);
|
|
18595
|
+
s.seen = true;
|
|
18129
18596
|
}
|
|
18130
|
-
|
|
18597
|
+
} else {
|
|
18598
|
+
const img = doc.createElement("img");
|
|
18599
|
+
img.crossOrigin = "anonymous";
|
|
18600
|
+
img.decoding = "async";
|
|
18601
|
+
img.addEventListener("load", () => {
|
|
18602
|
+
pairDiffs.clear();
|
|
18603
|
+
schedule();
|
|
18604
|
+
});
|
|
18605
|
+
img.src = s.source.src;
|
|
18606
|
+
s.still = img;
|
|
18131
18607
|
}
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
if (
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
for (const v of views) {
|
|
18141
|
-
syncGlyph(v);
|
|
18142
|
-
for (const n of notesOn(state, v.step.key))
|
|
18143
|
-
renderNote(v, n);
|
|
18608
|
+
};
|
|
18609
|
+
const unload = (s) => {
|
|
18610
|
+
offs.get(s)?.();
|
|
18611
|
+
offs.delete(s);
|
|
18612
|
+
if (s.frames) {
|
|
18613
|
+
if (s.seen)
|
|
18614
|
+
s.frames.visible(false);
|
|
18615
|
+
s.frames.release();
|
|
18144
18616
|
}
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
});
|
|
18151
|
-
syncProgress();
|
|
18152
|
-
current = null;
|
|
18153
|
-
fromScroll();
|
|
18154
|
-
placeMark();
|
|
18155
|
-
bar.sync();
|
|
18156
|
-
syncReserve();
|
|
18157
|
-
win.requestAnimationFrame(() => win.requestAnimationFrame(() => {
|
|
18158
|
-
if (!disposed && !reduced())
|
|
18159
|
-
root.dataset.settled = "";
|
|
18160
|
-
}));
|
|
18617
|
+
s.frames = null;
|
|
18618
|
+
s.seen = false;
|
|
18619
|
+
if (s.still)
|
|
18620
|
+
s.still.removeAttribute("src");
|
|
18621
|
+
s.still = null;
|
|
18161
18622
|
};
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18623
|
+
let w = 1;
|
|
18624
|
+
let h = 1;
|
|
18625
|
+
let geo = { x: 0, w: 1 };
|
|
18626
|
+
const rowH = () => Math.max(4, (h - BAR - GAP * 2) / 2);
|
|
18627
|
+
const aspectOf = (s) => {
|
|
18628
|
+
if (s.still?.naturalWidth)
|
|
18629
|
+
return s.still.naturalWidth / s.still.naturalHeight;
|
|
18630
|
+
const c = s.frames?.nearest(0);
|
|
18631
|
+
return c ? c.bitmap.width / c.bitmap.height : 0;
|
|
18632
|
+
};
|
|
18633
|
+
const columns = () => stripColumns(timeline.view, geo.w, {
|
|
18634
|
+
duration: timeline.duration,
|
|
18635
|
+
fps: fps(),
|
|
18636
|
+
minWidth: rowH() * Math.max(16 / 9, ...sides.map(aspectOf)) + GAP
|
|
18637
|
+
});
|
|
18638
|
+
const measure = () => {
|
|
18639
|
+
const r = root.getBoundingClientRect();
|
|
18640
|
+
w = Math.max(1, r.width);
|
|
18641
|
+
h = Math.max(1, r.height);
|
|
18642
|
+
geo = { x: 0, w };
|
|
18643
|
+
if (align === "track") {
|
|
18644
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
18645
|
+
const t = track?.getBoundingClientRect();
|
|
18646
|
+
if (t && t.width > 0)
|
|
18647
|
+
geo = { x: t.left - r.left, w: t.width };
|
|
18648
|
+
}
|
|
18649
|
+
readColors();
|
|
18650
|
+
schedule();
|
|
18170
18651
|
};
|
|
18171
|
-
const
|
|
18172
|
-
const
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
b.dataset.section = section;
|
|
18176
|
-
const t = el4(doc, "span", "knobs-wt-toc-label");
|
|
18177
|
-
t.textContent = label;
|
|
18178
|
-
b.title = label;
|
|
18179
|
-
b.append(t);
|
|
18180
|
-
if (review)
|
|
18181
|
-
b.append(el4(doc, "span", "knobs-wt-toc-end"));
|
|
18182
|
-
b.addEventListener("mousedown", (ev) => ev.preventDefault());
|
|
18183
|
-
b.addEventListener("click", run);
|
|
18184
|
-
tocRows.push(b);
|
|
18185
|
-
return b;
|
|
18652
|
+
const ask = (cols) => {
|
|
18653
|
+
const points = cols.map((c) => ({ frame: c.frame, tolerance: Math.floor((c.to - c.from) / 2) }));
|
|
18654
|
+
for (const s of sides)
|
|
18655
|
+
s.frames?.want(points);
|
|
18186
18656
|
};
|
|
18187
|
-
const
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
b.textContent = text;
|
|
18193
|
-
if (primary)
|
|
18194
|
-
b.dataset.primary = "";
|
|
18195
|
-
b.addEventListener("click", run);
|
|
18196
|
-
row.append(b);
|
|
18197
|
-
return b;
|
|
18198
|
-
};
|
|
18199
|
-
btn("Copy feedback", () => copy("markdown"), true);
|
|
18200
|
-
btn("JSON", () => copy("json"));
|
|
18201
|
-
if (rail)
|
|
18202
|
-
btn("Reset", async () => {
|
|
18203
|
-
const ok = await confirm({
|
|
18204
|
-
title: "Forget every answer on this page?",
|
|
18205
|
-
confirm: "Reset",
|
|
18206
|
-
destructive: true,
|
|
18207
|
-
look,
|
|
18208
|
-
doc
|
|
18209
|
-
});
|
|
18210
|
-
if (ok)
|
|
18211
|
-
api.reset();
|
|
18212
|
-
});
|
|
18213
|
-
return row;
|
|
18657
|
+
const picOf = (s, c) => {
|
|
18658
|
+
if (s.still?.naturalWidth)
|
|
18659
|
+
return { img: s.still, w: s.still.naturalWidth, h: s.still.naturalHeight, key: "still" };
|
|
18660
|
+
const f = s.frames?.nearest(c.frame, Math.max(1, c.to - c.from));
|
|
18661
|
+
return f ? { img: f.bitmap, w: f.bitmap.width, h: f.bitmap.height, key: String(f.frame) } : null;
|
|
18214
18662
|
};
|
|
18215
|
-
const
|
|
18216
|
-
|
|
18663
|
+
const probe = doc.createElement("canvas");
|
|
18664
|
+
probe.width = DIFF.w;
|
|
18665
|
+
probe.height = DIFF.h;
|
|
18666
|
+
const probeCtx = probe.getContext("2d", { willReadFrequently: true });
|
|
18667
|
+
const differ = (a, b) => {
|
|
18668
|
+
if (!probeCtx)
|
|
18669
|
+
return;
|
|
18217
18670
|
try {
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18671
|
+
probeCtx.drawImage(a.img, 0, 0, DIFF.w, DIFF.h);
|
|
18672
|
+
const pa = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
18673
|
+
probeCtx.drawImage(b.img, 0, 0, DIFF.w, DIFF.h);
|
|
18674
|
+
const pb = probeCtx.getImageData(0, 0, DIFF.w, DIFF.h).data;
|
|
18675
|
+
return meanAbsDiff(pa, pb);
|
|
18223
18676
|
} catch {
|
|
18224
|
-
|
|
18677
|
+
return;
|
|
18225
18678
|
}
|
|
18226
18679
|
};
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
const
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
if (
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18680
|
+
let colors = { surface: "", accent: "", diff: "", wash: "" };
|
|
18681
|
+
const rgba = (raw, fallback) => {
|
|
18682
|
+
const [r, g, b, a] = parseCssColor(raw, fallback);
|
|
18683
|
+
return `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
|
|
18684
|
+
};
|
|
18685
|
+
const readColors = () => {
|
|
18686
|
+
const cs = win.getComputedStyle(root);
|
|
18687
|
+
colors = {
|
|
18688
|
+
surface: rgba(tokenCss(cs, "--kv-fill", ""), [1, 1, 1, 0.055]),
|
|
18689
|
+
accent: rgba(tokenCss(cs, "--kv-accent", ""), [1, 1, 1, 1]),
|
|
18690
|
+
diff: rgba(tokenCss(cs, "--_ku-cmp-diff", ""), [0.49, 0.81, 0.61, 1]),
|
|
18691
|
+
wash: rgba(tokenCss(cs, "--kv-fill-selected", ""), [1, 1, 1, 0.11])
|
|
18692
|
+
};
|
|
18693
|
+
};
|
|
18694
|
+
let hover = null;
|
|
18695
|
+
let raf = 0;
|
|
18696
|
+
let asked = "";
|
|
18697
|
+
function schedule() {
|
|
18698
|
+
if (!raf && !disposed)
|
|
18699
|
+
raf = win.requestAnimationFrame(draw);
|
|
18700
|
+
}
|
|
18701
|
+
const draw = () => {
|
|
18702
|
+
raf = 0;
|
|
18703
|
+
if (!near)
|
|
18704
|
+
return;
|
|
18705
|
+
const dpr = Math.min(3, Math.max(1, win.devicePixelRatio || 1));
|
|
18706
|
+
const cw = Math.round(w * dpr);
|
|
18707
|
+
const ch = Math.round(h * dpr);
|
|
18708
|
+
if (canvas.width !== cw || canvas.height !== ch) {
|
|
18709
|
+
canvas.width = cw;
|
|
18710
|
+
canvas.height = ch;
|
|
18251
18711
|
}
|
|
18252
|
-
|
|
18253
|
-
if (
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
steps: stepLinks
|
|
18263
|
-
});
|
|
18264
|
-
media ??= taste.element;
|
|
18712
|
+
const ctx = canvas.getContext("2d");
|
|
18713
|
+
if (!ctx)
|
|
18714
|
+
return;
|
|
18715
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
18716
|
+
ctx.clearRect(0, 0, w, h);
|
|
18717
|
+
const cols = columns();
|
|
18718
|
+
const key = cols.map((c) => c.frame).join(",");
|
|
18719
|
+
if (key !== asked) {
|
|
18720
|
+
asked = key;
|
|
18721
|
+
ask(cols);
|
|
18265
18722
|
}
|
|
18266
|
-
const
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18723
|
+
const rh = rowH();
|
|
18724
|
+
const radius = 3;
|
|
18725
|
+
let budget = DIFFS_PER_DRAW;
|
|
18726
|
+
let more = false;
|
|
18727
|
+
const ds = cols.map((c) => {
|
|
18728
|
+
const a = picOf(sides[0], c);
|
|
18729
|
+
const b = picOf(sides[1], c);
|
|
18730
|
+
if (!a || !b)
|
|
18271
18731
|
return;
|
|
18272
|
-
|
|
18273
|
-
|
|
18732
|
+
const k = `${a.key}|${b.key}`;
|
|
18733
|
+
if (!pairDiffs.has(k)) {
|
|
18734
|
+
if (budget-- <= 0) {
|
|
18735
|
+
more = true;
|
|
18736
|
+
return;
|
|
18737
|
+
}
|
|
18738
|
+
pairDiffs.set(k, differ(a, b));
|
|
18739
|
+
}
|
|
18740
|
+
const d = pairDiffs.get(k);
|
|
18741
|
+
if (d !== undefined)
|
|
18742
|
+
diffs.set(c.frame, d);
|
|
18743
|
+
return d;
|
|
18274
18744
|
});
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18745
|
+
const max = Math.max(0.02, ...ds.map((d) => d ?? 0));
|
|
18746
|
+
const peaks = new Set(stripPeaks(ds, { threshold }));
|
|
18747
|
+
cols.forEach((c, i) => {
|
|
18748
|
+
const x = geo.x + c.x + GAP / 2;
|
|
18749
|
+
const cwid = Math.max(1, c.w - GAP);
|
|
18750
|
+
if (x + cwid < 0 || x > w)
|
|
18751
|
+
return;
|
|
18752
|
+
for (const row of [0, 1]) {
|
|
18753
|
+
const y = row * (rh + GAP);
|
|
18754
|
+
const pic = picOf(sides[row], c);
|
|
18755
|
+
ctx.save();
|
|
18756
|
+
ctx.beginPath();
|
|
18757
|
+
ctx.roundRect(x, y, cwid, rh, radius);
|
|
18758
|
+
ctx.fillStyle = colors.surface;
|
|
18759
|
+
ctx.fill();
|
|
18760
|
+
if (pic) {
|
|
18761
|
+
ctx.clip();
|
|
18762
|
+
const r = letterbox({ w: pic.w, h: pic.h }, { x, y, w: cwid, h: rh });
|
|
18763
|
+
ctx.drawImage(pic.img, r.x, r.y, r.w, r.h);
|
|
18764
|
+
}
|
|
18765
|
+
ctx.restore();
|
|
18282
18766
|
}
|
|
18283
|
-
|
|
18767
|
+
if (hover === i) {
|
|
18768
|
+
ctx.fillStyle = colors.wash;
|
|
18769
|
+
ctx.beginPath();
|
|
18770
|
+
ctx.roundRect(x, 0, cwid, rh * 2 + GAP, radius);
|
|
18771
|
+
ctx.fill();
|
|
18772
|
+
}
|
|
18773
|
+
const d = ds[i];
|
|
18774
|
+
if (d !== undefined && d > 0) {
|
|
18775
|
+
const bh = Math.max(1, Math.min(1, d / max) * BAR);
|
|
18776
|
+
ctx.globalAlpha = peaks.has(i) ? 1 : 0.42;
|
|
18777
|
+
ctx.fillStyle = colors.diff;
|
|
18778
|
+
ctx.beginPath();
|
|
18779
|
+
ctx.roundRect(x, h - bh, cwid, bh, Math.min(1.5, bh / 2));
|
|
18780
|
+
ctx.fill();
|
|
18781
|
+
ctx.globalAlpha = 1;
|
|
18782
|
+
}
|
|
18783
|
+
});
|
|
18784
|
+
const v = timeline.view;
|
|
18785
|
+
const px = geo.x + (timeline.time - v.start) / v.span * geo.w;
|
|
18786
|
+
if (px >= -1 && px <= w + 1) {
|
|
18787
|
+
ctx.fillStyle = colors.accent;
|
|
18788
|
+
ctx.fillRect(Math.round(px * dpr) / dpr - 0.75, 0, 1.5, h);
|
|
18284
18789
|
}
|
|
18285
|
-
|
|
18790
|
+
if (more)
|
|
18791
|
+
schedule();
|
|
18792
|
+
};
|
|
18793
|
+
const timeAt = (clientX) => {
|
|
18794
|
+
const r = root.getBoundingClientRect();
|
|
18795
|
+
const v = timeline.view;
|
|
18796
|
+
const t = v.start + (clientX - r.left - geo.x) / geo.w * v.span;
|
|
18797
|
+
return Math.min(timeline.duration, Math.max(0, frameTime(t, fps())));
|
|
18798
|
+
};
|
|
18799
|
+
const columnAt = (clientX) => {
|
|
18800
|
+
const x = clientX - root.getBoundingClientRect().left - geo.x;
|
|
18801
|
+
const i = columns().findIndex((c) => x >= c.x && x < c.x + c.w);
|
|
18802
|
+
return i < 0 ? null : i;
|
|
18286
18803
|
};
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
const
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
syncGlyph(v);
|
|
18293
|
-
}
|
|
18294
|
-
record({ what: "pick", step: key, detail: id ?? "cleared" });
|
|
18295
|
-
syncProgress();
|
|
18296
|
-
emit();
|
|
18804
|
+
let dragging = false;
|
|
18805
|
+
const pick = (clientX) => {
|
|
18806
|
+
const t = timeAt(clientX);
|
|
18807
|
+
timeline.seek(t);
|
|
18808
|
+
options.onPick?.(t);
|
|
18297
18809
|
};
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
if (disposed || e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey)
|
|
18301
|
-
return;
|
|
18302
|
-
if (isTypingTarget(e.target))
|
|
18303
|
-
return;
|
|
18304
|
-
const target = e.target instanceof Element ? e.target : null;
|
|
18305
|
-
if (target && target !== doc.body && target !== doc.documentElement && !root.contains(target))
|
|
18306
|
-
return;
|
|
18307
|
-
const own = target?.closest(LEAVE);
|
|
18308
|
-
const claim = () => {
|
|
18309
|
-
e.preventDefault();
|
|
18310
|
-
e.stopPropagation();
|
|
18311
|
-
};
|
|
18312
|
-
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
18313
|
-
if (key === "Escape") {
|
|
18314
|
-
if (ink.on)
|
|
18315
|
-
return claim(), setInk(false);
|
|
18316
|
-
if (slotsOf(viewOf(current)).some((s) => s.unzoom()))
|
|
18317
|
-
return claim();
|
|
18318
|
-
if (full)
|
|
18319
|
-
return claim(), setFull(false);
|
|
18320
|
-
return;
|
|
18321
|
-
}
|
|
18322
|
-
if (key === "f" && !e.shiftKey) {
|
|
18323
|
-
if (!viewOf(current)?.media)
|
|
18324
|
-
return;
|
|
18325
|
-
claim();
|
|
18326
|
-
return setFull(!full);
|
|
18327
|
-
}
|
|
18328
|
-
if (own)
|
|
18329
|
-
return;
|
|
18330
|
-
if (key === "j" || key === "k" || key === "ArrowDown" || key === "ArrowUp") {
|
|
18331
|
-
if (e.shiftKey && key.startsWith("Arrow"))
|
|
18332
|
-
return;
|
|
18333
|
-
const dir = key === "j" || key === "ArrowDown" ? 1 : -1;
|
|
18334
|
-
const next = adjacentStep(views.map((v) => v.step.key), current, dir);
|
|
18335
|
-
if (!next)
|
|
18336
|
-
return;
|
|
18337
|
-
claim();
|
|
18338
|
-
if (full)
|
|
18339
|
-
setFull(false);
|
|
18340
|
-
return go(next);
|
|
18341
|
-
}
|
|
18342
|
-
if ((key === "Home" || key === "End") && views.length) {
|
|
18343
|
-
claim();
|
|
18344
|
-
if (full)
|
|
18345
|
-
setFull(false);
|
|
18346
|
-
return go(views[key === "Home" ? 0 : views.length - 1].step.key);
|
|
18347
|
-
}
|
|
18348
|
-
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
18349
|
-
const d = (key === "ArrowLeft" ? -1 : 1) * (e.shiftKey ? 2 : 0.5);
|
|
18350
|
-
if (seekBy(d))
|
|
18351
|
-
claim();
|
|
18810
|
+
root.addEventListener("pointerdown", (e) => {
|
|
18811
|
+
if (e.button !== 0)
|
|
18352
18812
|
return;
|
|
18813
|
+
dragging = true;
|
|
18814
|
+
root.setPointerCapture(e.pointerId);
|
|
18815
|
+
const host = root.closest(".knobs-compare");
|
|
18816
|
+
if (host)
|
|
18817
|
+
host.focus({ preventScroll: true });
|
|
18818
|
+
else
|
|
18819
|
+
timeline.focus();
|
|
18820
|
+
if (timeline.playing)
|
|
18821
|
+
timeline.pause();
|
|
18822
|
+
pick(e.clientX);
|
|
18823
|
+
e.preventDefault();
|
|
18824
|
+
});
|
|
18825
|
+
root.addEventListener("pointermove", (e) => {
|
|
18826
|
+
if (dragging)
|
|
18827
|
+
pick(e.clientX);
|
|
18828
|
+
const i = columnAt(e.clientX);
|
|
18829
|
+
if (i !== hover) {
|
|
18830
|
+
hover = i;
|
|
18831
|
+
schedule();
|
|
18353
18832
|
}
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18833
|
+
});
|
|
18834
|
+
const end = () => {
|
|
18835
|
+
dragging = false;
|
|
18836
|
+
};
|
|
18837
|
+
root.addEventListener("pointerup", end);
|
|
18838
|
+
root.addEventListener("pointercancel", end);
|
|
18839
|
+
root.addEventListener("lostpointercapture", end);
|
|
18840
|
+
root.addEventListener("pointerleave", () => {
|
|
18841
|
+
if (hover === null)
|
|
18361
18842
|
return;
|
|
18843
|
+
hover = null;
|
|
18844
|
+
schedule();
|
|
18845
|
+
});
|
|
18846
|
+
const unlisten = timeline.listen({ onSeek: schedule, onView: schedule });
|
|
18847
|
+
const ro = new win.ResizeObserver(measure);
|
|
18848
|
+
ro.observe(root);
|
|
18849
|
+
const track = timeline.element.querySelector(".knobs-tl-track");
|
|
18850
|
+
if (track)
|
|
18851
|
+
ro.observe(track);
|
|
18852
|
+
root.addEventListener("knobs-restyle", measure);
|
|
18853
|
+
const setOnScreen = (on) => {
|
|
18854
|
+
onScreen = on;
|
|
18855
|
+
for (const s of sides) {
|
|
18856
|
+
if (!s.frames || s.seen === on)
|
|
18857
|
+
continue;
|
|
18858
|
+
s.seen = on;
|
|
18859
|
+
s.frames.visible(on);
|
|
18362
18860
|
}
|
|
18363
|
-
const v = viewOf(current);
|
|
18364
|
-
if (!v || e.shiftKey || !review)
|
|
18365
|
-
return;
|
|
18366
|
-
const choice = digitChoice(v.step, state.steps[v.step.key], key);
|
|
18367
|
-
if (choice === undefined)
|
|
18368
|
-
return;
|
|
18369
|
-
claim();
|
|
18370
|
-
pick(v.step.key, choice);
|
|
18371
18861
|
};
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
const
|
|
18862
|
+
const setNear = (on) => {
|
|
18863
|
+
near = on;
|
|
18864
|
+
if (on) {
|
|
18865
|
+
for (const s of sides)
|
|
18866
|
+
load(s);
|
|
18867
|
+
schedule();
|
|
18868
|
+
} else
|
|
18869
|
+
for (const s of sides)
|
|
18870
|
+
unload(s);
|
|
18871
|
+
};
|
|
18872
|
+
const IO = win.IntersectionObserver;
|
|
18873
|
+
const nearIo = IO ? new IO((e) => setNear(e.at(-1).isIntersecting), { rootMargin: "100% 0px" }) : null;
|
|
18874
|
+
const screenIo = IO ? new IO((e) => setOnScreen(e.at(-1).isIntersecting)) : null;
|
|
18875
|
+
measure();
|
|
18876
|
+
if (nearIo && screenIo) {
|
|
18877
|
+
nearIo.observe(root);
|
|
18878
|
+
screenIo.observe(root);
|
|
18879
|
+
} else {
|
|
18880
|
+
setNear(true);
|
|
18881
|
+
setOnScreen(true);
|
|
18882
|
+
}
|
|
18883
|
+
return {
|
|
18383
18884
|
element: root,
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18396
|
-
|
|
18397
|
-
|
|
18398
|
-
|
|
18399
|
-
|
|
18400
|
-
|
|
18401
|
-
|
|
18402
|
-
|
|
18403
|
-
page = normalizeWalkthrough(next);
|
|
18404
|
-
if (!sameId)
|
|
18405
|
-
state = load();
|
|
18406
|
-
build();
|
|
18407
|
-
if (keep && viewOf(keep))
|
|
18408
|
-
go(keep);
|
|
18409
|
-
},
|
|
18410
|
-
reset() {
|
|
18411
|
-
state = EMPTY_STATE;
|
|
18412
|
-
for (const v of views) {
|
|
18413
|
-
v.taste?.pick(null);
|
|
18414
|
-
v.notes.textContent = "";
|
|
18415
|
-
syncGlyph(v);
|
|
18416
|
-
}
|
|
18417
|
-
syncProgress();
|
|
18418
|
-
emit();
|
|
18885
|
+
diffs,
|
|
18886
|
+
update(next) {
|
|
18887
|
+
const was = sides;
|
|
18888
|
+
sides = [
|
|
18889
|
+
next.before ? makeSide(next.before) : was[0],
|
|
18890
|
+
next.after ? makeSide(next.after) : was[1]
|
|
18891
|
+
];
|
|
18892
|
+
for (const s of was)
|
|
18893
|
+
if (!sides.includes(s))
|
|
18894
|
+
unload(s);
|
|
18895
|
+
diffs.clear();
|
|
18896
|
+
pairDiffs.clear();
|
|
18897
|
+
asked = "";
|
|
18898
|
+
if (near)
|
|
18899
|
+
for (const s of sides)
|
|
18900
|
+
load(s);
|
|
18901
|
+
if (onScreen)
|
|
18902
|
+
setOnScreen(true);
|
|
18903
|
+
schedule();
|
|
18419
18904
|
},
|
|
18420
18905
|
dispose() {
|
|
18421
|
-
if (disposed)
|
|
18422
|
-
return;
|
|
18423
18906
|
disposed = true;
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
win.removeEventListener("scroll", onScroll, { capture: true });
|
|
18433
|
-
win.removeEventListener("resize", onScroll);
|
|
18434
|
-
win.removeEventListener("resize", onFullResize);
|
|
18435
|
-
for (const type of ["wheel", "touchstart"])
|
|
18436
|
-
win.removeEventListener(type, stopScroll);
|
|
18437
|
-
doc.removeEventListener("keydown", onKey);
|
|
18438
|
-
unregisterHints();
|
|
18439
|
-
bar.unobserve();
|
|
18440
|
-
pill?.dispose();
|
|
18441
|
-
for (const d of discs)
|
|
18442
|
-
d.dispose();
|
|
18443
|
-
if (padded)
|
|
18444
|
-
padded.el.style.scrollPaddingBottom = padded.was;
|
|
18445
|
-
for (const v of views) {
|
|
18446
|
-
v.slots.forEach((s) => s.dispose());
|
|
18447
|
-
v.taste?.dispose();
|
|
18448
|
-
}
|
|
18907
|
+
if (raf)
|
|
18908
|
+
win.cancelAnimationFrame(raf);
|
|
18909
|
+
unlisten();
|
|
18910
|
+
ro.disconnect();
|
|
18911
|
+
nearIo?.disconnect();
|
|
18912
|
+
screenIo?.disconnect();
|
|
18913
|
+
for (const s of sides)
|
|
18914
|
+
unload(s);
|
|
18449
18915
|
root.remove();
|
|
18450
18916
|
}
|
|
18451
18917
|
};
|
|
18452
|
-
return api;
|
|
18453
18918
|
}
|
|
18454
18919
|
|
|
18455
18920
|
// src/review/index.ts
|
|
18456
|
-
init_compare();
|
|
18457
|
-
init_compare_strip();
|
|
18458
18921
|
init_player();
|
|
18459
18922
|
init_diff();
|
|
18460
18923
|
export {
|