Skill Tracer
Skills › Visual Explainer › visual-explainer › driver.js

driver.js

105 lines · plugins/visual-explainer/video/driver.js at 9d5f880

1// Injected before deck scripts by render.mjs.
2// 1. Virtual time: timers, requestAnimationFrame, performance.now, Date, and every CSS or Web
3// Animation advance only when the renderer moves the clock, so a slow frame never drops or skews time.
4// 2. Scene state: __ve.cue() sets the attributes and events that the deck's own CSS and JS react to.
5(() => {
6 let now = 0, nextId = 1, frames = new Map(), cur = -1, caption = null;
7 const timers = new Map();
8 const owned = new WeakMap(); // Animation the renderer runs -> virtual ms of its last sync
9 const epoch = Date.now();
10 performance.now = () => now;
11 Date.now = () => epoch + now;
12 // new Date() and Date() read the virtual clock too; explicit arguments behave natively.
13 window.Date = new Proxy(Date, {
14 construct: (D, args, nt) => Reflect.construct(D, args.length ? args : [epoch + now], nt),
15 apply: (D) => new D(epoch + now).toString(),
16 });
17 window.setTimeout = (fn, ms, ...args) => { const id = nextId++; timers.set(id, { at: now + Math.max(0, +ms || 0), fn, args }); return id; };
18 window.setInterval = (fn, ms, ...args) => { const every = Math.max(1, +ms || 0), id = nextId++; timers.set(id, { at: now + every, fn, args, every }); return id; };
19 window.clearTimeout = window.clearInterval = (id) => { timers.delete(id); };
20 window.requestAnimationFrame = (cb) => { const id = nextId++; frames.set(id, cb); return id; };
21 window.cancelAnimationFrame = (id) => { frames.delete(id); };
23 // The renderer owns only the passage of time. It holds a running animation paused (natively) and moves it
24 // itself; the page's own pause(), finish(), and cancel() hand it back to the page, and play() returns it.
25 const { pause, play } = Animation.prototype;
26 // Setting currentTime completes the pause now, so a rate change pending from reverse() applies before the next sync.
27 const own = (a) => { pause.call(a); a.currentTime = a.currentTime; owned.set(a, now); };
28 for (const m of ['pause', 'finish', 'cancel']) {
29 const native = Animation.prototype[m];
30 Animation.prototype[m] = function () { owned.delete(this); return native.call(this); };
31 }
32 Animation.prototype.play = function () { play.call(this); own(this); };
33 // Take each running animation from now. Runs after every callback that could start one, so an animation
34 // started by a timer at 100 ms counts from 100 ms, not from the next frame. Paused ones stay with the page.
35 const adopt = () => { for (const a of document.getAnimations()) if (a.playState === 'running') own(a); };
36 // Move each owned animation on by the virtual time since its last sync at its current rate, from where it
37 // is, so a seek or rate change by the page counts from when the page made it.
38 const sync = () => {
39 for (const a of document.getAnimations()) {
40 const t0 = owned.get(a);
41 if (t0 === undefined || a.playState !== 'paused') continue; // the page's, or replayed by it (adopt takes it)
42 const rate = a.playbackRate, t = a.currentTime + (now - t0) * rate;
43 owned.set(a, now);
44 // finish() fires finish and animationend events, which a paused animation never would.
45 if (rate > 0 ? t >= a.effect.getComputedTiming().endTime : rate < 0 && t <= 0) a.finish();
46 else a.currentTime = t;
47 }
48 };
49 // Run due timers in order, each at its own time and seeing animations at that time.
50 const advance = (target) => {
51 if (target < now) throw new Error(`clock cannot go back: ${target} < ${now}`);
52 for (;;) {
53 let id = null, due = null;
54 for (const [k, t] of timers) if (t.at <= target && (!due || t.at < due.at)) { id = k; due = t; }
55 if (!due) break;
56 now = due.at;
57 if (due.every) due.at += due.every; else timers.delete(id);
58 sync();
59 if (typeof due.fn === 'function') due.fn(...due.args); else (0, eval)(String(due.fn));
60 adopt();
61 }
62 now = target;
63 sync();
64 };
66 const scenes = () => [...document.querySelectorAll('[data-say]')];
67 const fire = (el, type, detail) => el.dispatchEvent(new CustomEvent(type, { bubbles: true, detail }));
69 window.__ve = {
70 scenes: () => scenes().map((s) => ({ lines: s.dataset.say.trim().split(/(?<=[.!?])\s+/).filter(Boolean), hold: +s.dataset.hold || 0 })),
72 // At `at` ms, scene i is on with sentences 1..beat started: data-beat="0 1 … beat", so CSS can match
73 // [data-beat~="2"]. The scene before it keeps data-was until the next change, for its exit animation.
74 cue(at, i, beat, text, captions) {
75 advance(at);
76 const list = scenes(), next = list[i];
77 if (i !== cur) {
78 for (const s of list) s.removeAttribute('data-was');
79 const prev = list[cur];
80 if (prev) { prev.removeAttribute('data-beat'); prev.setAttribute('data-was', ''); fire(prev, 've:leave', { scene: cur }); }
81 cur = i;
82 }
83 next.setAttribute('data-beat', Array.from({ length: beat + 1 }, (_, n) => n).join(' '));
84 fire(next, 've:beat', { scene: i, beat });
85 if (!caption) {
86 caption = document.body.appendChild(Object.assign(document.createElement('p'), { className: 've-caption' }));
87 // Defaults go first in <head>, so the deck's own .ve-caption rules win.
88 document.head.prepend(Object.assign(document.createElement('style'), { textContent:
89 '.ve-caption{position:fixed;left:50%;bottom:36px;transform:translateX(-50%);z-index:2147483647;max-width:1040px;margin:0;padding:8px 16px;border-radius:6px;background:rgb(0 0 0/.75);color:#fff;font:500 24px/1.35 sans-serif;text-align:center;text-wrap:balance}.ve-caption:empty{display:none}' }));
90 }
91 caption.textContent = captions ? text : '';
92 adopt();
93 },
95 // One video frame at `target` ms.
96 tick(target) {
97 advance(target);
98 const batch = frames;
99 frames = new Map();
100 adopt();
101 for (const cb of batch.values()) { cb(now); adopt(); }
102 sync();
103 },
104 };
105})();