retake-dev 0.4.0
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/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- package/types/index.d.ts +111 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// Live preview: show any earlier moment instantly, without replaying. The DOM
|
|
2
|
+
// changes (from a MutationObserver) and every animation's timing are logged as
|
|
3
|
+
// they happen; previewing t undoes the DOM changes made after t and puts each
|
|
4
|
+
// animation at its currentTime for t. A scope element limits all of it to one
|
|
5
|
+
// component. Leaving the preview redoes everything back to the live page.
|
|
6
|
+
|
|
7
|
+
let previewing = false
|
|
8
|
+
const domLog = [] // { t, kind, node, ... }
|
|
9
|
+
let domAt = 0 // how many domLog entries are applied to the page right now
|
|
10
|
+
const animLog = [] // { anim, target, keyframes, timing, rate, vStart, vEnd, clone }
|
|
11
|
+
const animEntry = new WeakMap()
|
|
12
|
+
let observer = null
|
|
13
|
+
|
|
14
|
+
function logAnim(a, s, created) {
|
|
15
|
+
if (previewing || animEntry.has(a) || !a.effect || !a.effect.target) return
|
|
16
|
+
const rate = rateOf(a) || 1
|
|
17
|
+
let keyframes = []
|
|
18
|
+
try {
|
|
19
|
+
keyframes = a.effect.getKeyframes()
|
|
20
|
+
} catch {}
|
|
21
|
+
const e = { anim: a, target: a.effect.target, keyframes, timing: a.effect.getTiming(), rate, vStart: clock.now - s.t / rate, vEnd: null, clone: null }
|
|
22
|
+
// From when it's on the page: made right now (element.animate()), or, for a
|
|
23
|
+
// CSS transition or animation (seen only at the frame boundary after its
|
|
24
|
+
// cause), from just after the boundary before (input there lands just after
|
|
25
|
+
// its own boundary; until it starts, it's in its delay/"before" phase).
|
|
26
|
+
e.after = created ? clock.now : frameBefore(clock.now)
|
|
27
|
+
e.strict = !created
|
|
28
|
+
animLog.push(e)
|
|
29
|
+
animEntry.set(a, e)
|
|
30
|
+
recordClip(e)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The last recorded frame boundary before x (-Infinity: none).
|
|
34
|
+
function frameBefore(x) {
|
|
35
|
+
const fr = (rec && rec.frames) || []
|
|
36
|
+
let lo = 0
|
|
37
|
+
let hi = fr.length
|
|
38
|
+
while (lo < hi) {
|
|
39
|
+
const m = (lo + hi) >> 1
|
|
40
|
+
if (fr[m] < x) lo = m + 1
|
|
41
|
+
else hi = m
|
|
42
|
+
}
|
|
43
|
+
return lo ? fr[lo - 1] : -Infinity
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Was this animation on the page at t?
|
|
47
|
+
const thereAt = (e, t) => t >= e.vStart || (e.strict ? t > e.after : t >= e.after)
|
|
48
|
+
|
|
49
|
+
function endAnim(a) {
|
|
50
|
+
const e = animEntry.get(a)
|
|
51
|
+
if (e && e.vEnd == null) {
|
|
52
|
+
e.vEnd = clock.now
|
|
53
|
+
endClip(e)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- DOM log --------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
// Input recorded at a frame boundary B happens just after B (a rebuild to
|
|
60
|
+
// exactly B stops short of it), so what it changes is logged just after B as
|
|
61
|
+
// well (inputAt, 30-recorder.js): a preview of exactly B shows the page before
|
|
62
|
+
// it, like the rebuild.
|
|
63
|
+
const INPUT_EPS = 0.001
|
|
64
|
+
const logTime = () => (clock.now === inputAt ? clock.now + INPUT_EPS : clock.now)
|
|
65
|
+
|
|
66
|
+
function logMutations(records) {
|
|
67
|
+
if (records.length) appRan = true // style may have changed: look for new animations
|
|
68
|
+
const t = logTime()
|
|
69
|
+
for (let i = 0; i < records.length; i++) {
|
|
70
|
+
const r = records[i]
|
|
71
|
+
const later = (pred) => records.slice(i + 1).find(pred)
|
|
72
|
+
if (r.type === "attributes" && r.attributeName === HOVER_ATTR) continue // our :hover stand-in
|
|
73
|
+
if (r.type === "attributes") {
|
|
74
|
+
const next = later((x) => x.type === "attributes" && x.target === r.target && x.attributeName === r.attributeName)
|
|
75
|
+
domLog.push({ t, kind: "attr", node: r.target, name: r.attributeName, old: r.oldValue, now: next ? next.oldValue : r.target.getAttribute(r.attributeName) })
|
|
76
|
+
} else if (r.type === "characterData") {
|
|
77
|
+
const next = later((x) => x.type === "characterData" && x.target === r.target)
|
|
78
|
+
domLog.push({ t, kind: "text", node: r.target, old: r.oldValue, now: next ? next.oldValue : r.target.data })
|
|
79
|
+
} else {
|
|
80
|
+
domLog.push({ t, kind: "list", node: r.target, added: [...r.addedNodes], removed: [...r.removedNodes], next: r.nextSibling })
|
|
81
|
+
for (const n of r.addedNodes) if (n.nodeType === 1) findSmil(n)
|
|
82
|
+
// A subtree taken off the page can still change: Astro's ClientRouter
|
|
83
|
+
// swaps the body, then React empties the old body's islands. Those
|
|
84
|
+
// changes are logged too, or a preview putting the old body back shows
|
|
85
|
+
// them empty. (One record per change even where registrations overlap.)
|
|
86
|
+
for (const n of r.removedNodes) {
|
|
87
|
+
if (n.nodeType !== 1 || !n.firstChild || n.isConnected) continue
|
|
88
|
+
offPage.add(n)
|
|
89
|
+
observer.observe(n, OBSERVE)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
domAt = domLog.length
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const OBSERVE = { subtree: true, childList: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true }
|
|
97
|
+
const offPage = new Set() // subtrees taken off the page, still watched (the log keeps them anyway)
|
|
98
|
+
function observe() {
|
|
99
|
+
observer = observer || new MutationObserver(logMutations)
|
|
100
|
+
observer.observe(document, OBSERVE)
|
|
101
|
+
// (A preview disconnects the observer; back on, the subtrees still off the page.)
|
|
102
|
+
for (const n of offPage) n.isConnected ? offPage.delete(n) : observer.observe(n, OBSERVE)
|
|
103
|
+
// Where the page was scrolled when this document's log starts.
|
|
104
|
+
const se = document.scrollingElement
|
|
105
|
+
if (se && !scrollLog.has("d")) logScroll("d", se, se.scrollTop, se.scrollLeft)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyEntry(e, forward) {
|
|
109
|
+
try {
|
|
110
|
+
if (e.kind === "attr") {
|
|
111
|
+
const v = forward ? e.now : e.old
|
|
112
|
+
v == null ? e.node.removeAttribute(e.name) : e.node.setAttribute(e.name, v)
|
|
113
|
+
} else if (e.kind === "text") {
|
|
114
|
+
e.node.data = forward ? e.now : e.old
|
|
115
|
+
} else if (e.kind === "value") {
|
|
116
|
+
setField(e.node, forward ? e.now : e.old)
|
|
117
|
+
} else {
|
|
118
|
+
const out = forward ? e.removed : e.added
|
|
119
|
+
const into = forward ? e.added : e.removed
|
|
120
|
+
for (const n of out) if (n.parentNode === e.node) e.node.removeChild(n)
|
|
121
|
+
const ref = e.next && e.next.parentNode === e.node ? e.next : null
|
|
122
|
+
for (const n of into) e.node.insertBefore(n, ref)
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const inScope = (node, scope) => !scope || scope === node || scope.contains(node)
|
|
128
|
+
|
|
129
|
+
function moveDom(t, scope) {
|
|
130
|
+
let k = domLog.length
|
|
131
|
+
while (k > 0 && domLog[k - 1].t > t) k--
|
|
132
|
+
while (domAt > k) {
|
|
133
|
+
const e = domLog[--domAt]
|
|
134
|
+
if (inScope(e.node, scope)) applyEntry(e, false)
|
|
135
|
+
}
|
|
136
|
+
while (domAt < k) {
|
|
137
|
+
const e = domLog[domAt++]
|
|
138
|
+
if (inScope(e.node, scope)) applyEntry(e, true)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- form fields ------------------------------------------------------------------
|
|
143
|
+
// A field's value and a box's checked state are properties, not markup: the
|
|
144
|
+
// MutationObserver never sees them. Typing (input/change events, live or
|
|
145
|
+
// replayed) and the app setting them are logged as "value" entries in the DOM
|
|
146
|
+
// log, so a preview before the user typed shows the field as it was then.
|
|
147
|
+
const fieldSeen = new WeakMap() // field -> { value, checked } as last logged
|
|
148
|
+
const fieldNative = new Map() // prototype -> { value, checked, ... }: the setters we wrapped
|
|
149
|
+
const isField = (el) => !!el && (el.tagName === "INPUT" ? el.type !== "file" : el.tagName === "TEXTAREA" || el.tagName === "SELECT")
|
|
150
|
+
const fieldOf = (el) => ({ value: el.value, checked: !!el.checked })
|
|
151
|
+
|
|
152
|
+
function setField(el, v) {
|
|
153
|
+
const own = fieldNative.get(Object.getPrototypeOf(el)) || {}
|
|
154
|
+
try {
|
|
155
|
+
if (el.value !== v.value) (own.value || nativeSetter(el, "value")).call(el, v.value)
|
|
156
|
+
if (el.type === "checkbox" || el.type === "radio") (own.checked || nativeSetter(el, "checked")).call(el, v.checked)
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fieldChanged(el, old) {
|
|
161
|
+
const now = fieldOf(el)
|
|
162
|
+
fieldSeen.set(el, now)
|
|
163
|
+
if (!observer || previewing || (old.value === now.value && old.checked === now.checked)) return
|
|
164
|
+
// DOM changes made before this one come first in the log.
|
|
165
|
+
logMutations(observer.takeRecords())
|
|
166
|
+
domLog.push({ t: logTime(), kind: "value", node: el, old, now })
|
|
167
|
+
domAt = domLog.length
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The app setting a field (React's controlled inputs included: React wraps
|
|
171
|
+
// whatever setter the prototype has when it first sees the field).
|
|
172
|
+
for (const [C, props] of [
|
|
173
|
+
[W.HTMLInputElement, ["value", "checked"]],
|
|
174
|
+
[W.HTMLTextAreaElement, ["value"]],
|
|
175
|
+
[W.HTMLSelectElement, ["value", "selectedIndex"]],
|
|
176
|
+
]) {
|
|
177
|
+
const proto = C && C.prototype
|
|
178
|
+
if (!proto) continue
|
|
179
|
+
const own = {}
|
|
180
|
+
for (const p of props) {
|
|
181
|
+
const d = Object.getOwnPropertyDescriptor(proto, p)
|
|
182
|
+
if (!d || !d.set || !d.configurable) continue
|
|
183
|
+
own[p] = d.set
|
|
184
|
+
Object.defineProperty(proto, p, {
|
|
185
|
+
...d,
|
|
186
|
+
set(v) {
|
|
187
|
+
if (!observer || previewing || !isField(this)) return d.set.call(this, v)
|
|
188
|
+
const old = fieldOf(this)
|
|
189
|
+
d.set.call(this, v)
|
|
190
|
+
fieldChanged(this, old)
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
fieldNative.set(proto, own)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The user (or a replay of them) changing one. Its value before: as seen when
|
|
198
|
+
// they got to it (focus, a press, a key); else what a click on a box changed.
|
|
199
|
+
const seeField = (e) => isField(e.target) && !fieldSeen.has(e.target) && fieldSeen.set(e.target, fieldOf(e.target))
|
|
200
|
+
for (const type of ["focusin", "pointerdown", "mousedown", "keydown", "beforeinput"]) W.addEventListener(type, seeField, true)
|
|
201
|
+
for (const type of ["input", "change"])
|
|
202
|
+
W.addEventListener(
|
|
203
|
+
type,
|
|
204
|
+
(e) => {
|
|
205
|
+
const el = e.target
|
|
206
|
+
if (!isField(el)) return
|
|
207
|
+
const box = el.type === "checkbox" || el.type === "radio"
|
|
208
|
+
fieldChanged(el, fieldSeen.get(el) || { value: box ? el.value : el.defaultValue, checked: box ? el.type === "checkbox" && !el.checked : !!el.checked })
|
|
209
|
+
// Checking a radio unchecks the one before it, with no event of its own.
|
|
210
|
+
if (el.type === "radio" && el.checked && el.name) {
|
|
211
|
+
for (const r of (el.form || el.getRootNode()).querySelectorAll(`input[type=radio]`)) {
|
|
212
|
+
const seen = r !== el && r.name === el.name && fieldSeen.get(r)
|
|
213
|
+
if (seen && seen.checked && !r.checked) fieldChanged(r, seen)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
true,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// ---- animations at t --------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function placeAnimations(t, scope) {
|
|
223
|
+
// Anything the undo/redo itself started (restarted CSS animations,
|
|
224
|
+
// transitions) is noise: the log already knows what was running.
|
|
225
|
+
for (const a of document.getAnimations()) {
|
|
226
|
+
if (!animEntry.has(a) && !isClone(a)) orig.cancel.call(a)
|
|
227
|
+
}
|
|
228
|
+
for (const e of animLog) {
|
|
229
|
+
if (!inScope(e.target, scope)) {
|
|
230
|
+
unpark(e.anim)
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
const time = (t - e.vStart) * e.rate
|
|
234
|
+
const there = thereAt(e, t)
|
|
235
|
+
const live = stateOf(e.anim) !== "idle"
|
|
236
|
+
if (live) {
|
|
237
|
+
// One that only started after t isn't there yet. Paused at a negative
|
|
238
|
+
// time it would still show its start value (CSS transitions fill
|
|
239
|
+
// backwards), over the one really running then (a later transition of
|
|
240
|
+
// the same property wins): its effect comes off the page meanwhile.
|
|
241
|
+
if (!there) {
|
|
242
|
+
park(e.anim)
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
unpark(e.anim)
|
|
246
|
+
orig.pause.call(e.anim)
|
|
247
|
+
orig.currentTime.set.call(e.anim, time)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const alive = there && (e.vEnd == null || t < e.vEnd)
|
|
251
|
+
if (alive && !e.clone && e.target.isConnected) {
|
|
252
|
+
e.clone = orig.animate.call(e.target, e.keyframes, { ...e.timing })
|
|
253
|
+
orig.pause.call(e.clone)
|
|
254
|
+
}
|
|
255
|
+
if (e.clone && !alive) {
|
|
256
|
+
orig.cancel.call(e.clone)
|
|
257
|
+
e.clone = null
|
|
258
|
+
}
|
|
259
|
+
if (e.clone) orig.currentTime.set.call(e.clone, time)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const isClone = (a) => animLog.some((e) => e.clone === a)
|
|
264
|
+
|
|
265
|
+
// Animations whose effect is off the page for the preview, with the element
|
|
266
|
+
// it goes back on (the target of their effect).
|
|
267
|
+
const parked = new Map()
|
|
268
|
+
function park(a) {
|
|
269
|
+
const fx = a.effect
|
|
270
|
+
if (parked.has(a) || !fx || !fx.target) return
|
|
271
|
+
parked.set(a, fx.target)
|
|
272
|
+
try {
|
|
273
|
+
fx.target = null
|
|
274
|
+
} catch {
|
|
275
|
+
parked.delete(a)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function unpark(a) {
|
|
279
|
+
if (!parked.has(a)) return
|
|
280
|
+
const target = parked.get(a)
|
|
281
|
+
parked.delete(a)
|
|
282
|
+
try {
|
|
283
|
+
a.effect.target = target
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
286
|
+
function unparkAll() {
|
|
287
|
+
for (const a of [...parked.keys()]) unpark(a)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---- entering and leaving -----------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
// ---- scroll at t ------------------------------------------------------------------------
|
|
293
|
+
// Each scroller the recording moved goes where it was at t, except the ones
|
|
294
|
+
// the user has scrolled to look while paused (their view stays). Leaving the
|
|
295
|
+
// preview puts the rest back.
|
|
296
|
+
const liveScroll = new Map() // key -> { top, left } before the preview moved it
|
|
297
|
+
const shownScroll = new Map() // key -> { top, left } the preview last put it at
|
|
298
|
+
|
|
299
|
+
function placeScroll(t, scope) {
|
|
300
|
+
for (const [key, L] of scrollLog) {
|
|
301
|
+
if (viewScrolled.has(key)) continue
|
|
302
|
+
const el = scrollerOf(L)
|
|
303
|
+
if (!el || (scope && !scope.contains(el))) continue
|
|
304
|
+
const v = scrollAt(L, t)
|
|
305
|
+
const last = shownScroll.get(key)
|
|
306
|
+
if (last && last.top === v.top && last.left === v.left) continue
|
|
307
|
+
if (!liveScroll.has(key)) liveScroll.set(key, { top: el.scrollTop, left: el.scrollLeft, L })
|
|
308
|
+
shownScroll.set(key, v)
|
|
309
|
+
setOwnScroll(key, el, v.top, v.left)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function restoreLiveScroll() {
|
|
314
|
+
for (const [key, v] of liveScroll) {
|
|
315
|
+
if (viewScrolled.has(key)) continue // scrolled by the user meanwhile: theirs now
|
|
316
|
+
const el = scrollerOf(v.L)
|
|
317
|
+
if (el) setOwnScroll(key, el, v.top, v.left)
|
|
318
|
+
}
|
|
319
|
+
liveScroll.clear()
|
|
320
|
+
shownScroll.clear()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- entering and leaving -----------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
let previewScope = null
|
|
326
|
+
const liveTimes = new Map() // each live animation's state before the preview
|
|
327
|
+
|
|
328
|
+
function preview(t, scope) {
|
|
329
|
+
// Only this document's past can be shown (earlier pages need a rebuild).
|
|
330
|
+
t = Math.max(t, segmentAt(clock.now).t)
|
|
331
|
+
// An in-place seek is running (or about to): stop it where it is and show
|
|
332
|
+
// the moment once it has (previewing mid-seek would lose DOM changes the
|
|
333
|
+
// replay is making).
|
|
334
|
+
if (clock.seeking || seekTarget != null) {
|
|
335
|
+
PT.cancelSeek()
|
|
336
|
+
pendingPreview = { t, scope: scope || null }
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
if (!previewing) {
|
|
340
|
+
reclaimNative()
|
|
341
|
+
logMutations(observer.takeRecords())
|
|
342
|
+
observer.disconnect()
|
|
343
|
+
previewing = true
|
|
344
|
+
pause()
|
|
345
|
+
liveTimes.clear()
|
|
346
|
+
for (const [a, st] of managed) liveTimes.set(a, { t: orig.currentTime.get.call(a), done: st.done })
|
|
347
|
+
}
|
|
348
|
+
if (scope !== previewScope) {
|
|
349
|
+
// A new scope starts from the live page.
|
|
350
|
+
moveDom(Infinity, previewScope)
|
|
351
|
+
restoreLiveScroll()
|
|
352
|
+
previewScope = scope || null
|
|
353
|
+
}
|
|
354
|
+
moveDom(t, previewScope)
|
|
355
|
+
setHover(hoverAt(t))
|
|
356
|
+
alignMedia(t)
|
|
357
|
+
placeAnimations(t, previewScope)
|
|
358
|
+
syncSmil(t, previewScope)
|
|
359
|
+
placeScroll(t, previewScope)
|
|
360
|
+
previewAt = t
|
|
361
|
+
PT.emit()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let previewAt = null
|
|
365
|
+
|
|
366
|
+
function endPreview() {
|
|
367
|
+
pendingPreview = null
|
|
368
|
+
if (!previewing) return
|
|
369
|
+
unparkAll()
|
|
370
|
+
moveDom(Infinity, previewScope)
|
|
371
|
+
restoreLiveScroll()
|
|
372
|
+
for (const e of animLog) {
|
|
373
|
+
if (e.clone) orig.cancel.call(e.clone)
|
|
374
|
+
e.clone = null
|
|
375
|
+
}
|
|
376
|
+
for (const [a, was] of liveTimes) {
|
|
377
|
+
if (stateOf(a) === "idle") continue
|
|
378
|
+
if (was.done) orig.finish.call(a)
|
|
379
|
+
else orig.currentTime.set.call(a, was.t)
|
|
380
|
+
}
|
|
381
|
+
// Undo/redo restarted some CSS animations as new objects; carry on the
|
|
382
|
+
// original timing in them. Anything else it started is dropped.
|
|
383
|
+
for (const a of document.getAnimations()) {
|
|
384
|
+
if (animEntry.has(a)) continue
|
|
385
|
+
const lost =
|
|
386
|
+
typeof CSSAnimation !== "undefined" &&
|
|
387
|
+
a instanceof CSSAnimation &&
|
|
388
|
+
animLog.find((e) => e.vEnd == null && e.target === /** @type {KeyframeEffect} */ (a.effect).target && stateOf(e.anim) === "idle" && e.anim.animationName === a.animationName)
|
|
389
|
+
if (!lost) {
|
|
390
|
+
orig.cancel.call(a)
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
managed.set(a, { t: (clock.now - lost.vStart) * lost.rate, v: clock.now, userPaused: false, done: false })
|
|
394
|
+
orig.pause.call(a)
|
|
395
|
+
lost.anim = a
|
|
396
|
+
animEntry.set(a, lost)
|
|
397
|
+
}
|
|
398
|
+
previewing = false
|
|
399
|
+
previewScope = null
|
|
400
|
+
previewAt = null
|
|
401
|
+
if (hasFuture()) setHover(hoverAt())
|
|
402
|
+
else clearHover()
|
|
403
|
+
alignMedia()
|
|
404
|
+
observe()
|
|
405
|
+
syncAnimations()
|
|
406
|
+
PT.emit()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- activity, for the timeline's marks ------------------------------------------
|
|
410
|
+
|
|
411
|
+
// When input happened (clicks and keys) and when animations were running.
|
|
412
|
+
let activityCache = null
|
|
413
|
+
function activity() {
|
|
414
|
+
const key = rec.events.length + ":" + animLog.length + ":" + Math.round(clock.now / 250)
|
|
415
|
+
if (activityCache && activityCache.key === key) return activityCache.value
|
|
416
|
+
const inputs = []
|
|
417
|
+
for (const ev of rec.events) if (ev.type === "pointerdown" || ev.type === "keydown") inputs.push(ev.t)
|
|
418
|
+
const spans = []
|
|
419
|
+
for (const e of animLog) {
|
|
420
|
+
let end = e.vEnd
|
|
421
|
+
if (end == null) {
|
|
422
|
+
const total = (e.timing.delay || 0) + (Number(e.timing.duration) || 0) * (e.timing.iterations || 1)
|
|
423
|
+
end = Number.isFinite(total) ? e.vStart + total / (e.rate || 1) : clock.now
|
|
424
|
+
}
|
|
425
|
+
spans.push([e.vStart, Math.min(end, clock.now)])
|
|
426
|
+
}
|
|
427
|
+
// Merge overlapping runs into bands.
|
|
428
|
+
spans.sort((a, b) => a[0] - b[0])
|
|
429
|
+
const bands = []
|
|
430
|
+
for (const [a, b] of spans) {
|
|
431
|
+
const last = bands[bands.length - 1]
|
|
432
|
+
if (last && a <= last[1] + 30) last[1] = Math.max(last[1], b)
|
|
433
|
+
else bands.push([a, b])
|
|
434
|
+
}
|
|
435
|
+
activityCache = { key, value: { inputs, bands } }
|
|
436
|
+
return activityCache.value
|
|
437
|
+
}
|