saccade 0.0.1
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 +135 -0
- package/dist/core.cjs +1736 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +276 -0
- package/dist/core.d.ts +276 -0
- package/dist/core.mjs +1703 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.cjs +2822 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.mjs +2791 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core/index.ts
|
|
21
|
+
var core_exports = {};
|
|
22
|
+
__export(core_exports, {
|
|
23
|
+
LapseEngine: () => LapseEngine,
|
|
24
|
+
TimelineRecorder: () => TimelineRecorder,
|
|
25
|
+
TimelineScrubber: () => TimelineScrubber,
|
|
26
|
+
TimingController: () => TimingController,
|
|
27
|
+
formatExportForLLM: () => formatExportForLLM,
|
|
28
|
+
generateExport: () => generateExport,
|
|
29
|
+
getFrameAtTime: () => getFrameAtTime
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(core_exports);
|
|
32
|
+
|
|
33
|
+
// src/core/timing.ts
|
|
34
|
+
var TimingController = class {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.speed = 1;
|
|
37
|
+
this.virtualBaseline = 0;
|
|
38
|
+
this.intervalMap = /* @__PURE__ */ new Map();
|
|
39
|
+
this.nextIntervalId = 1e6;
|
|
40
|
+
this.mediaObserver = null;
|
|
41
|
+
this.animObserver = null;
|
|
42
|
+
this._origAnimate = null;
|
|
43
|
+
this.installed = false;
|
|
44
|
+
this._raf = requestAnimationFrame.bind(window);
|
|
45
|
+
this._caf = cancelAnimationFrame.bind(window);
|
|
46
|
+
this._setTimeout = setTimeout.bind(window);
|
|
47
|
+
this._clearTimeout = clearTimeout.bind(window);
|
|
48
|
+
this._setInterval = setInterval.bind(window);
|
|
49
|
+
this._clearInterval = clearInterval.bind(window);
|
|
50
|
+
this._perfNow = performance.now.bind(performance);
|
|
51
|
+
this._dateNow = Date.now;
|
|
52
|
+
this.realBaseline = this._perfNow();
|
|
53
|
+
}
|
|
54
|
+
getVirtualTime() {
|
|
55
|
+
const realElapsed = this._perfNow() - this.realBaseline;
|
|
56
|
+
return this.virtualBaseline + realElapsed * this.speed;
|
|
57
|
+
}
|
|
58
|
+
reanchor() {
|
|
59
|
+
const virtualNow = this.getVirtualTime();
|
|
60
|
+
this.realBaseline = this._perfNow();
|
|
61
|
+
this.virtualBaseline = virtualNow;
|
|
62
|
+
}
|
|
63
|
+
/** Install timing patches. Safe to call multiple times. */
|
|
64
|
+
install() {
|
|
65
|
+
if (this.installed) return;
|
|
66
|
+
this.installed = true;
|
|
67
|
+
const self = this;
|
|
68
|
+
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
69
|
+
const origAnimate = Element.prototype.animate;
|
|
70
|
+
this._origAnimate = origAnimate;
|
|
71
|
+
Element.prototype.animate = function(...args) {
|
|
72
|
+
const anim = origAnimate.apply(this, args);
|
|
73
|
+
if (self.speed !== 1) {
|
|
74
|
+
anim.playbackRate = self.speed || 1e-3;
|
|
75
|
+
}
|
|
76
|
+
return anim;
|
|
77
|
+
};
|
|
78
|
+
performance.now = () => self.getVirtualTime();
|
|
79
|
+
const dateBaseline = this._dateNow();
|
|
80
|
+
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
81
|
+
window.requestAnimationFrame = (callback) => {
|
|
82
|
+
return self._raf(() => {
|
|
83
|
+
callback(self.getVirtualTime());
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
window.cancelAnimationFrame = this._caf;
|
|
87
|
+
window.setTimeout = ((handler, delay, ...args) => {
|
|
88
|
+
const scaledDelay = (delay ?? 0) / (self.speed || 1);
|
|
89
|
+
return self._setTimeout(handler, scaledDelay, ...args);
|
|
90
|
+
});
|
|
91
|
+
window.clearTimeout = this._clearTimeout;
|
|
92
|
+
window.setInterval = ((handler, delay, ...args) => {
|
|
93
|
+
const id = self.nextIntervalId++;
|
|
94
|
+
const baseDelay = delay ?? 0;
|
|
95
|
+
function tick() {
|
|
96
|
+
const scaledDelay = baseDelay / (self.speed || 1);
|
|
97
|
+
const realId = self._setTimeout(() => {
|
|
98
|
+
if (typeof handler === "function") {
|
|
99
|
+
;
|
|
100
|
+
handler(...args);
|
|
101
|
+
}
|
|
102
|
+
if (self.intervalMap.has(id)) tick();
|
|
103
|
+
}, scaledDelay);
|
|
104
|
+
const entry = self.intervalMap.get(id);
|
|
105
|
+
if (entry) entry.realId = realId;
|
|
106
|
+
}
|
|
107
|
+
self.intervalMap.set(id, { handler, delay: baseDelay, realId: 0 });
|
|
108
|
+
tick();
|
|
109
|
+
return id;
|
|
110
|
+
});
|
|
111
|
+
window.clearInterval = ((id) => {
|
|
112
|
+
if (id == null) return;
|
|
113
|
+
const entry = self.intervalMap.get(id);
|
|
114
|
+
if (entry) {
|
|
115
|
+
self._clearTimeout(entry.realId);
|
|
116
|
+
self.intervalMap.delete(id);
|
|
117
|
+
} else {
|
|
118
|
+
self._clearInterval(id);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
this.mediaObserver = new MutationObserver((mutations) => {
|
|
122
|
+
for (const mutation of mutations) {
|
|
123
|
+
for (const node of mutation.addedNodes) {
|
|
124
|
+
if (node instanceof HTMLVideoElement || node instanceof HTMLAudioElement) {
|
|
125
|
+
node.playbackRate = self.speed;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (document.body) {
|
|
131
|
+
this.mediaObserver.observe(document.body, { childList: true, subtree: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Set playback speed. Requires install() first. */
|
|
135
|
+
setSpeed(newSpeed) {
|
|
136
|
+
if (!this.installed) this.install();
|
|
137
|
+
this.reanchor();
|
|
138
|
+
this.speed = newSpeed;
|
|
139
|
+
document.querySelectorAll("video, audio").forEach((el) => {
|
|
140
|
+
;
|
|
141
|
+
el.playbackRate = newSpeed;
|
|
142
|
+
});
|
|
143
|
+
this.patchAnimations();
|
|
144
|
+
}
|
|
145
|
+
/** Patch playbackRate on all active CSS transitions/animations via WAAPI. */
|
|
146
|
+
patchAnimations() {
|
|
147
|
+
try {
|
|
148
|
+
const anims = document.getAnimations();
|
|
149
|
+
for (const anim of anims) {
|
|
150
|
+
const target = anim.effect?.target;
|
|
151
|
+
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
152
|
+
anim.playbackRate = this.speed || 1e-3;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
if (!this.animObserver) {
|
|
157
|
+
this.animObserver = this._setInterval(() => {
|
|
158
|
+
if (!this.installed) return;
|
|
159
|
+
try {
|
|
160
|
+
const anims = document.getAnimations();
|
|
161
|
+
for (const anim of anims) {
|
|
162
|
+
const target = anim.effect?.target;
|
|
163
|
+
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
164
|
+
if (anim.playbackRate !== this.speed) {
|
|
165
|
+
anim.playbackRate = this.speed || 1e-3;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}, 100);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
getSpeed() {
|
|
174
|
+
return this.speed;
|
|
175
|
+
}
|
|
176
|
+
/** Restore all patched APIs to originals. */
|
|
177
|
+
destroy() {
|
|
178
|
+
if (!this.installed) return;
|
|
179
|
+
performance.now = this._perfNow;
|
|
180
|
+
Date.now = this._dateNow;
|
|
181
|
+
window.requestAnimationFrame = this._raf;
|
|
182
|
+
window.cancelAnimationFrame = this._caf;
|
|
183
|
+
window.setTimeout = this._setTimeout;
|
|
184
|
+
window.clearTimeout = this._clearTimeout;
|
|
185
|
+
window.setInterval = this._setInterval;
|
|
186
|
+
window.clearInterval = this._clearInterval;
|
|
187
|
+
for (const [, entry] of this.intervalMap) {
|
|
188
|
+
this._clearTimeout(entry.realId);
|
|
189
|
+
}
|
|
190
|
+
this.intervalMap.clear();
|
|
191
|
+
if (this._origAnimate) {
|
|
192
|
+
Element.prototype.animate = this._origAnimate;
|
|
193
|
+
this._origAnimate = null;
|
|
194
|
+
}
|
|
195
|
+
this.mediaObserver?.disconnect();
|
|
196
|
+
this.mediaObserver = null;
|
|
197
|
+
if (this.animObserver != null) {
|
|
198
|
+
this._clearInterval(this.animObserver);
|
|
199
|
+
this.animObserver = null;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
for (const anim of document.getAnimations()) {
|
|
203
|
+
anim.playbackRate = 1;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
208
|
+
this.installed = false;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/core/recorder.ts
|
|
213
|
+
var SAFE_PROPS = [
|
|
214
|
+
"opacity",
|
|
215
|
+
"transform",
|
|
216
|
+
"background-color",
|
|
217
|
+
"color",
|
|
218
|
+
"border-color",
|
|
219
|
+
"box-shadow",
|
|
220
|
+
"border-radius",
|
|
221
|
+
"filter",
|
|
222
|
+
"clip-path",
|
|
223
|
+
"scale",
|
|
224
|
+
"rotate",
|
|
225
|
+
"translate",
|
|
226
|
+
"outline-color",
|
|
227
|
+
"outline-width",
|
|
228
|
+
"outline-offset",
|
|
229
|
+
"outline-style",
|
|
230
|
+
"text-decoration-color",
|
|
231
|
+
"fill",
|
|
232
|
+
"stroke",
|
|
233
|
+
"stroke-dasharray",
|
|
234
|
+
"stroke-dashoffset",
|
|
235
|
+
"visibility",
|
|
236
|
+
"pointer-events"
|
|
237
|
+
];
|
|
238
|
+
var LAYOUT_PROPS = [
|
|
239
|
+
"width",
|
|
240
|
+
"height",
|
|
241
|
+
"top",
|
|
242
|
+
"left",
|
|
243
|
+
"right",
|
|
244
|
+
"bottom",
|
|
245
|
+
"margin-top",
|
|
246
|
+
"margin-right",
|
|
247
|
+
"margin-bottom",
|
|
248
|
+
"margin-left",
|
|
249
|
+
"padding-top",
|
|
250
|
+
"padding-right",
|
|
251
|
+
"padding-bottom",
|
|
252
|
+
"padding-left",
|
|
253
|
+
"max-height",
|
|
254
|
+
"max-width",
|
|
255
|
+
"min-height",
|
|
256
|
+
"min-width",
|
|
257
|
+
"display"
|
|
258
|
+
];
|
|
259
|
+
var SNAPSHOT_PROPS = [...SAFE_PROPS, ...LAYOUT_PROPS];
|
|
260
|
+
var SAFE_PROPS_SET = new Set(SAFE_PROPS);
|
|
261
|
+
var SNAPSHOT_ATTRS = [
|
|
262
|
+
"data-state",
|
|
263
|
+
"data-checked",
|
|
264
|
+
"data-disabled",
|
|
265
|
+
"data-highlighted",
|
|
266
|
+
"data-orientation",
|
|
267
|
+
"data-active",
|
|
268
|
+
"data-selected",
|
|
269
|
+
"data-open",
|
|
270
|
+
"data-closed",
|
|
271
|
+
"data-side",
|
|
272
|
+
"data-align",
|
|
273
|
+
"data-focus",
|
|
274
|
+
"data-hover",
|
|
275
|
+
"data-at-boundary",
|
|
276
|
+
"data-scrubbing",
|
|
277
|
+
"aria-checked",
|
|
278
|
+
"aria-selected",
|
|
279
|
+
"aria-expanded",
|
|
280
|
+
"aria-pressed",
|
|
281
|
+
"aria-hidden",
|
|
282
|
+
"aria-disabled",
|
|
283
|
+
"aria-valuenow",
|
|
284
|
+
"aria-valuemin",
|
|
285
|
+
"aria-valuemax",
|
|
286
|
+
"aria-invalid",
|
|
287
|
+
"checked",
|
|
288
|
+
"disabled",
|
|
289
|
+
"hidden",
|
|
290
|
+
"value",
|
|
291
|
+
"class",
|
|
292
|
+
"style"
|
|
293
|
+
];
|
|
294
|
+
var STATE_SELECTORS = [
|
|
295
|
+
"[data-state]",
|
|
296
|
+
"[aria-checked]",
|
|
297
|
+
"[aria-selected]",
|
|
298
|
+
"[aria-expanded]",
|
|
299
|
+
"[aria-pressed]",
|
|
300
|
+
'[role="radio"]',
|
|
301
|
+
'[role="checkbox"]',
|
|
302
|
+
'[role="switch"]',
|
|
303
|
+
'[role="tab"]',
|
|
304
|
+
'[role="tabpanel"]',
|
|
305
|
+
'[role="option"]',
|
|
306
|
+
'[role="slider"]',
|
|
307
|
+
'[role="menuitem"]',
|
|
308
|
+
'[role="menuitemcheckbox"]',
|
|
309
|
+
'[role="menuitemradio"]',
|
|
310
|
+
'[type="radio"]',
|
|
311
|
+
'[type="checkbox"]',
|
|
312
|
+
'[type="range"]',
|
|
313
|
+
"input",
|
|
314
|
+
"button",
|
|
315
|
+
"select",
|
|
316
|
+
"textarea",
|
|
317
|
+
"[data-radix-collection-item]",
|
|
318
|
+
'[data-slot="slider-thumb"]',
|
|
319
|
+
'[data-slot="slider-track"]'
|
|
320
|
+
].join(",");
|
|
321
|
+
function getSelector(el) {
|
|
322
|
+
if (!el || !el.tagName) return null;
|
|
323
|
+
const parts = [];
|
|
324
|
+
let current = el;
|
|
325
|
+
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
326
|
+
const tag = current.tagName.toLowerCase();
|
|
327
|
+
const parent = current.parentElement;
|
|
328
|
+
if (parent) {
|
|
329
|
+
const siblings = Array.from(parent.children);
|
|
330
|
+
const idx = siblings.indexOf(current) + 1;
|
|
331
|
+
parts.unshift(tag + ":nth-child(" + idx + ")");
|
|
332
|
+
} else {
|
|
333
|
+
parts.unshift(tag);
|
|
334
|
+
}
|
|
335
|
+
current = parent;
|
|
336
|
+
}
|
|
337
|
+
return parts.join(" > ");
|
|
338
|
+
}
|
|
339
|
+
function getReadableSelector(el) {
|
|
340
|
+
if (!el || !el.tagName) return "unknown";
|
|
341
|
+
if (el.id) return "#" + el.id;
|
|
342
|
+
const tag = el.tagName.toLowerCase();
|
|
343
|
+
const classes = Array.from(el.classList || []).slice(0, 3).map((c) => "." + c).join("");
|
|
344
|
+
return tag + classes;
|
|
345
|
+
}
|
|
346
|
+
function getElementLabel(el) {
|
|
347
|
+
if (!el) return "unknown";
|
|
348
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
349
|
+
if (ariaLabel) return ariaLabel;
|
|
350
|
+
const text = (el.textContent || "").trim();
|
|
351
|
+
if (text && text.length < 40 && text.length > 0) return text;
|
|
352
|
+
const label = el.closest("label") || el.closest("[aria-label]");
|
|
353
|
+
if (label) {
|
|
354
|
+
const lt = label.getAttribute("aria-label") || (label.textContent || "").trim();
|
|
355
|
+
if (lt && lt.length < 40) return lt;
|
|
356
|
+
}
|
|
357
|
+
const state = el.getAttribute("data-state");
|
|
358
|
+
const role = el.getAttribute("role");
|
|
359
|
+
if (role && state) return role + " (" + state + ")";
|
|
360
|
+
if (role) return role;
|
|
361
|
+
return getReadableSelector(el);
|
|
362
|
+
}
|
|
363
|
+
function cssSplit(str) {
|
|
364
|
+
const parts = [];
|
|
365
|
+
let depth = 0;
|
|
366
|
+
let start = 0;
|
|
367
|
+
for (let i = 0; i < str.length; i++) {
|
|
368
|
+
if (str[i] === "(") depth++;
|
|
369
|
+
else if (str[i] === ")") depth--;
|
|
370
|
+
else if (str[i] === "," && depth === 0) {
|
|
371
|
+
parts.push(str.slice(start, i).trim());
|
|
372
|
+
start = i + 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
parts.push(str.slice(start).trim());
|
|
376
|
+
return parts;
|
|
377
|
+
}
|
|
378
|
+
var _TimelineRecorder = class _TimelineRecorder {
|
|
379
|
+
constructor() {
|
|
380
|
+
// ---- Recording state ----------------------------------------------------
|
|
381
|
+
this.recording = false;
|
|
382
|
+
this.startTime = 0;
|
|
383
|
+
this.boundingBox = null;
|
|
384
|
+
/** Structural-selector -> live DOM element. */
|
|
385
|
+
this.elements = /* @__PURE__ */ new Map();
|
|
386
|
+
/** Animation id -> info. */
|
|
387
|
+
this.animations = /* @__PURE__ */ new Map();
|
|
388
|
+
/** Original inline style per element (captured on first encounter). */
|
|
389
|
+
this.originalStyles = /* @__PURE__ */ new Map();
|
|
390
|
+
/** Captured frames. */
|
|
391
|
+
this.frames = [];
|
|
392
|
+
// ---- Portal management --------------------------------------------------
|
|
393
|
+
this.activePortals = /* @__PURE__ */ new Map();
|
|
394
|
+
this.portalIdCounter = 0;
|
|
395
|
+
this.currentPortalIds = /* @__PURE__ */ new Set();
|
|
396
|
+
this.capturedPortals = /* @__PURE__ */ new Set();
|
|
397
|
+
// ---- JS animation detection (Phase 2) -----------------------------------
|
|
398
|
+
this.prevInlineStyles = /* @__PURE__ */ new Map();
|
|
399
|
+
this.jsAnimStartTimes = /* @__PURE__ */ new Map();
|
|
400
|
+
this.jsAnimLastSeen = /* @__PURE__ */ new Map();
|
|
401
|
+
this.jsAnimFromValues = /* @__PURE__ */ new Map();
|
|
402
|
+
this.jsAnimChangeCount = /* @__PURE__ */ new Map();
|
|
403
|
+
// ---- Interaction state --------------------------------------------------
|
|
404
|
+
this.currentHoverEls = /* @__PURE__ */ new Set();
|
|
405
|
+
this.currentFocusSel = null;
|
|
406
|
+
this.currentPointer = { x: 0, y: 0, buttons: 0 };
|
|
407
|
+
// ---- Observers & listeners ----------------------------------------------
|
|
408
|
+
this.attrObserver = null;
|
|
409
|
+
this.portalObserver = null;
|
|
410
|
+
this.exitObserver = null;
|
|
411
|
+
this.onMouseOver = null;
|
|
412
|
+
this.onMouseOut = null;
|
|
413
|
+
this.onFocusIn = null;
|
|
414
|
+
this.onFocusOut = null;
|
|
415
|
+
this.onPointerMove = null;
|
|
416
|
+
this.onPointerDown = null;
|
|
417
|
+
this.onPointerUp = null;
|
|
418
|
+
// ---- WAAPI interception --------------------------------------------------
|
|
419
|
+
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
420
|
+
this.interceptedAnimations = [];
|
|
421
|
+
this.hiddenSince = null;
|
|
422
|
+
this.onVisibilityChange = null;
|
|
423
|
+
/** Set to true when the capture loop self-terminates due to limits. */
|
|
424
|
+
this.autoStopped = false;
|
|
425
|
+
/** Called when recording auto-stops. Set by the engine. */
|
|
426
|
+
this.onAutoStop = null;
|
|
427
|
+
// ---- Saved originals (for restoration) ----------------------------------
|
|
428
|
+
this._removeChild = null;
|
|
429
|
+
this._remove = null;
|
|
430
|
+
this._elementAnimate = null;
|
|
431
|
+
// ---- DOM elements injected during stop ----------------------------------
|
|
432
|
+
/** `<style>` that disables all transitions/animations after recording. */
|
|
433
|
+
this.noTransitionsEl = null;
|
|
434
|
+
/** Full-screen overlay blocking interaction during scrub. */
|
|
435
|
+
this.blockerEl = null;
|
|
436
|
+
/** `<style>` with cloned :hover / :focus rules rewritten as `[data-lapse-*]`. */
|
|
437
|
+
this.lapseStyleEl = null;
|
|
438
|
+
// =========================================================================
|
|
439
|
+
// Public API — helpers exposed for the scrubber
|
|
440
|
+
// =========================================================================
|
|
441
|
+
this.getSelector = getSelector;
|
|
442
|
+
}
|
|
443
|
+
// ---- Unpatched rAF reference --------------------------------------------
|
|
444
|
+
get _raf() {
|
|
445
|
+
return window.__LAPSE_ORIGINAL_RAF__ || requestAnimationFrame;
|
|
446
|
+
}
|
|
447
|
+
get SAFE_PROPS_SET() {
|
|
448
|
+
return SAFE_PROPS_SET;
|
|
449
|
+
}
|
|
450
|
+
get capturedPortalIds() {
|
|
451
|
+
return this.capturedPortals;
|
|
452
|
+
}
|
|
453
|
+
// =========================================================================
|
|
454
|
+
// startRecording
|
|
455
|
+
// =========================================================================
|
|
456
|
+
startRecording(boundingBox) {
|
|
457
|
+
if (this.recording) return;
|
|
458
|
+
this.recording = true;
|
|
459
|
+
this.autoStopped = false;
|
|
460
|
+
this.hiddenSince = null;
|
|
461
|
+
this.startTime = performance.now();
|
|
462
|
+
this.frames = [];
|
|
463
|
+
this.animations.clear();
|
|
464
|
+
this.elements.clear();
|
|
465
|
+
this.originalStyles.clear();
|
|
466
|
+
this.boundingBox = boundingBox || null;
|
|
467
|
+
this.activePortals.clear();
|
|
468
|
+
this.portalIdCounter = 0;
|
|
469
|
+
this.currentPortalIds.clear();
|
|
470
|
+
this.capturedPortals.clear();
|
|
471
|
+
this.prevInlineStyles.clear();
|
|
472
|
+
this.jsAnimStartTimes.clear();
|
|
473
|
+
this.jsAnimLastSeen.clear();
|
|
474
|
+
this.jsAnimFromValues.clear();
|
|
475
|
+
this.jsAnimChangeCount.clear();
|
|
476
|
+
this.currentHoverEls.clear();
|
|
477
|
+
this.currentFocusSel = null;
|
|
478
|
+
this.currentPointer = { x: 0, y: 0, buttons: 0 };
|
|
479
|
+
this.onVisibilityChange = () => {
|
|
480
|
+
if (!this.recording) return;
|
|
481
|
+
if (document.hidden) {
|
|
482
|
+
this.hiddenSince = performance.now();
|
|
483
|
+
} else {
|
|
484
|
+
this.hiddenSince = null;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
488
|
+
const trackElement = (el) => {
|
|
489
|
+
if (!el || !el.tagName) return;
|
|
490
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
491
|
+
const sel = getSelector(el);
|
|
492
|
+
if (!sel) return;
|
|
493
|
+
const isNew = !this.elements.has(sel);
|
|
494
|
+
this.elements.set(sel, el);
|
|
495
|
+
if (isNew) {
|
|
496
|
+
this.originalStyles.set(sel, el.getAttribute("style") || "");
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const trackTree = (root) => {
|
|
500
|
+
if (!root || !root.querySelectorAll) return;
|
|
501
|
+
trackElement(root);
|
|
502
|
+
for (const child of root.querySelectorAll("*")) {
|
|
503
|
+
trackElement(child);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const isInBounds = (el) => {
|
|
507
|
+
if (el.closest?.("[data-lapse-panel]")) return false;
|
|
508
|
+
if (!this.boundingBox) return true;
|
|
509
|
+
const r = el.getBoundingClientRect();
|
|
510
|
+
const bb = this.boundingBox;
|
|
511
|
+
return r.x < bb.x + bb.width && r.x + r.width > bb.x && r.y < bb.y + bb.height && r.y + r.height > bb.y;
|
|
512
|
+
};
|
|
513
|
+
try {
|
|
514
|
+
const stateEls = document.querySelectorAll(STATE_SELECTORS);
|
|
515
|
+
for (const el of stateEls) {
|
|
516
|
+
if (!isInBounds(el)) continue;
|
|
517
|
+
trackElement(el);
|
|
518
|
+
for (const child of el.querySelectorAll("*")) {
|
|
519
|
+
trackElement(child);
|
|
520
|
+
}
|
|
521
|
+
const parent = el.parentElement;
|
|
522
|
+
if (parent) {
|
|
523
|
+
trackElement(parent);
|
|
524
|
+
for (const sibling of parent.children) {
|
|
525
|
+
trackElement(sibling);
|
|
526
|
+
for (const child of sibling.querySelectorAll("*")) {
|
|
527
|
+
trackElement(child);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch (_) {
|
|
533
|
+
}
|
|
534
|
+
this.attrObserver = new MutationObserver((mutations) => {
|
|
535
|
+
for (const m of mutations) {
|
|
536
|
+
if (m.target instanceof HTMLElement) {
|
|
537
|
+
trackElement(m.target);
|
|
538
|
+
const parent = m.target.parentElement;
|
|
539
|
+
if (parent) {
|
|
540
|
+
for (const sibling of parent.children) {
|
|
541
|
+
trackElement(sibling);
|
|
542
|
+
for (const child of sibling.querySelectorAll("*")) {
|
|
543
|
+
trackElement(child);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
this.attrObserver.observe(document.body, {
|
|
551
|
+
attributes: true,
|
|
552
|
+
attributeFilter: [
|
|
553
|
+
"data-state",
|
|
554
|
+
"data-highlighted",
|
|
555
|
+
"data-hover",
|
|
556
|
+
"data-focus",
|
|
557
|
+
"data-active",
|
|
558
|
+
"data-selected",
|
|
559
|
+
"data-open",
|
|
560
|
+
"data-closed",
|
|
561
|
+
"data-at-boundary",
|
|
562
|
+
"data-scrubbing",
|
|
563
|
+
"aria-checked",
|
|
564
|
+
"aria-selected",
|
|
565
|
+
"aria-expanded",
|
|
566
|
+
"aria-valuenow",
|
|
567
|
+
"aria-invalid",
|
|
568
|
+
"class",
|
|
569
|
+
"style"
|
|
570
|
+
],
|
|
571
|
+
subtree: true
|
|
572
|
+
});
|
|
573
|
+
this._removeChild = Node.prototype.removeChild;
|
|
574
|
+
const savedRemoveChild = this._removeChild;
|
|
575
|
+
const self = this;
|
|
576
|
+
Node.prototype.removeChild = function(child) {
|
|
577
|
+
if (self.recording && child instanceof HTMLElement && child.hasAttribute("data-lapse-portal-id")) {
|
|
578
|
+
child.style.display = "none";
|
|
579
|
+
child.setAttribute("data-lapse-portal-hidden", "");
|
|
580
|
+
const id = child.getAttribute("data-lapse-portal-id");
|
|
581
|
+
if (id) self.currentPortalIds.delete(id);
|
|
582
|
+
return child;
|
|
583
|
+
}
|
|
584
|
+
return savedRemoveChild.call(this, child);
|
|
585
|
+
};
|
|
586
|
+
this._remove = Element.prototype.remove;
|
|
587
|
+
const savedRemove = this._remove;
|
|
588
|
+
Element.prototype.remove = function() {
|
|
589
|
+
if (self.recording && this instanceof HTMLElement && this.hasAttribute("data-lapse-portal-id")) {
|
|
590
|
+
this.style.display = "none";
|
|
591
|
+
this.setAttribute("data-lapse-portal-hidden", "");
|
|
592
|
+
const id = this.getAttribute("data-lapse-portal-id");
|
|
593
|
+
if (id) self.currentPortalIds.delete(id);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
return savedRemove.call(this);
|
|
597
|
+
};
|
|
598
|
+
this._elementAnimate = Element.prototype.animate;
|
|
599
|
+
const savedAnimate = this._elementAnimate;
|
|
600
|
+
Element.prototype.animate = function(keyframes, options) {
|
|
601
|
+
const anim = savedAnimate.call(this, keyframes, options);
|
|
602
|
+
if (self.recording && this instanceof HTMLElement && isInBounds(this)) {
|
|
603
|
+
trackElement(this);
|
|
604
|
+
self.interceptedAnimations.push({ animation: anim, target: this });
|
|
605
|
+
}
|
|
606
|
+
return anim;
|
|
607
|
+
};
|
|
608
|
+
this.portalObserver = new MutationObserver((mutations) => {
|
|
609
|
+
for (const m of mutations) {
|
|
610
|
+
for (const node of m.addedNodes) {
|
|
611
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
612
|
+
if (node.parentElement !== document.body) continue;
|
|
613
|
+
if (node.hasAttribute("data-lapse-portal-id")) continue;
|
|
614
|
+
const isPortal = node.hasAttribute("data-radix-popper-content-wrapper") || node.hasAttribute("data-radix-portal") || node.getAttribute("role") === "dialog" || node.getAttribute("role") === "alertdialog" || node.querySelector(
|
|
615
|
+
'[role="menu"], [role="dialog"], [role="alertdialog"], [role="listbox"], [role="tooltip"], [data-radix-popper-content-wrapper]'
|
|
616
|
+
) || node.hasAttribute("data-state") || node.style && (node.style.position === "fixed" || node.style.position === "absolute") || getComputedStyle(node).position === "fixed";
|
|
617
|
+
if (!isPortal) continue;
|
|
618
|
+
const id = "__lapse_portal_" + this.portalIdCounter++;
|
|
619
|
+
node.setAttribute("data-lapse-portal-id", id);
|
|
620
|
+
this.activePortals.set(id, { element: node });
|
|
621
|
+
this.currentPortalIds.add(id);
|
|
622
|
+
this.capturedPortals.add(id);
|
|
623
|
+
trackTree(node);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
this.portalObserver.observe(document.body, { childList: true });
|
|
628
|
+
this.exitObserver = new MutationObserver((mutations) => {
|
|
629
|
+
if (!this.recording) return;
|
|
630
|
+
for (const m of mutations) {
|
|
631
|
+
for (const node of m.removedNodes) {
|
|
632
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
633
|
+
const sel = getSelector(node);
|
|
634
|
+
if (sel && this.elements.has(sel)) {
|
|
635
|
+
if (!node.isConnected) {
|
|
636
|
+
node.style.display = "none";
|
|
637
|
+
node.setAttribute("data-lapse-exit-captured", "");
|
|
638
|
+
(m.target || document.body).appendChild(node);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
this.exitObserver.observe(document.body, { childList: true, subtree: true });
|
|
645
|
+
this.onMouseOver = (e) => {
|
|
646
|
+
if (!this.recording) return;
|
|
647
|
+
this.currentHoverEls.clear();
|
|
648
|
+
let el = e.target;
|
|
649
|
+
while (el && el !== document.body) {
|
|
650
|
+
const sel = getSelector(el);
|
|
651
|
+
if (sel) {
|
|
652
|
+
this.currentHoverEls.add(sel);
|
|
653
|
+
trackElement(el);
|
|
654
|
+
}
|
|
655
|
+
el = el.parentElement;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
this.onMouseOut = (e) => {
|
|
659
|
+
if (!this.recording) return;
|
|
660
|
+
if (!e.relatedTarget || !document.body.contains(e.relatedTarget)) {
|
|
661
|
+
this.currentHoverEls.clear();
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
document.addEventListener("mouseover", this.onMouseOver, true);
|
|
665
|
+
document.addEventListener("mouseout", this.onMouseOut, true);
|
|
666
|
+
this.onFocusIn = (e) => {
|
|
667
|
+
if (!this.recording) return;
|
|
668
|
+
const el = e.target;
|
|
669
|
+
if (el && el !== document.body) {
|
|
670
|
+
const sel = getSelector(el);
|
|
671
|
+
this.currentFocusSel = sel;
|
|
672
|
+
trackElement(el);
|
|
673
|
+
let parent = el.parentElement;
|
|
674
|
+
while (parent && parent !== document.body) {
|
|
675
|
+
trackElement(parent);
|
|
676
|
+
parent = parent.parentElement;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
this.onFocusOut = () => {
|
|
681
|
+
if (!this.recording) return;
|
|
682
|
+
this.currentFocusSel = null;
|
|
683
|
+
};
|
|
684
|
+
document.addEventListener("focusin", this.onFocusIn, true);
|
|
685
|
+
document.addEventListener("focusout", this.onFocusOut, true);
|
|
686
|
+
this.onPointerMove = (e) => {
|
|
687
|
+
if (!this.recording) return;
|
|
688
|
+
this.currentPointer = { x: e.clientX, y: e.clientY, buttons: e.buttons };
|
|
689
|
+
};
|
|
690
|
+
this.onPointerDown = (e) => {
|
|
691
|
+
if (!this.recording) return;
|
|
692
|
+
this.currentPointer = { x: e.clientX, y: e.clientY, buttons: e.buttons };
|
|
693
|
+
};
|
|
694
|
+
this.onPointerUp = (e) => {
|
|
695
|
+
if (!this.recording) return;
|
|
696
|
+
this.currentPointer = { x: e.clientX, y: e.clientY, buttons: 0 };
|
|
697
|
+
};
|
|
698
|
+
document.addEventListener("pointermove", this.onPointerMove, true);
|
|
699
|
+
document.addEventListener("pointerdown", this.onPointerDown, true);
|
|
700
|
+
document.addEventListener("pointerup", this.onPointerUp, true);
|
|
701
|
+
const captureFrame = () => {
|
|
702
|
+
if (!this.recording) return;
|
|
703
|
+
const time = performance.now() - this.startTime;
|
|
704
|
+
if (time >= _TimelineRecorder.MAX_DURATION_MS || this.frames.length >= _TimelineRecorder.MAX_FRAMES || this.hiddenSince && performance.now() - this.hiddenSince > 5e3) {
|
|
705
|
+
this.recording = false;
|
|
706
|
+
this.autoStopped = true;
|
|
707
|
+
this.onAutoStop?.();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const anims = document.getAnimations();
|
|
711
|
+
const frameAnims = [];
|
|
712
|
+
for (const a of anims) {
|
|
713
|
+
const target = a.effect?.target;
|
|
714
|
+
if (!target) continue;
|
|
715
|
+
if (!isInBounds(target)) continue;
|
|
716
|
+
const uniqueSelector = getSelector(target);
|
|
717
|
+
if (!uniqueSelector) continue;
|
|
718
|
+
const readableSelector = getReadableSelector(target);
|
|
719
|
+
trackElement(target);
|
|
720
|
+
const timing = a.effect?.getTiming?.() || {};
|
|
721
|
+
const duration = typeof timing.duration === "number" ? timing.duration : 0;
|
|
722
|
+
const delay = timing.delay || 0;
|
|
723
|
+
const easing = timing.easing || "linear";
|
|
724
|
+
let name = "";
|
|
725
|
+
let type = "WebAnimation";
|
|
726
|
+
if (a.constructor.name === "CSSTransition" || a.transitionProperty) {
|
|
727
|
+
name = a.transitionProperty || "";
|
|
728
|
+
type = "CSSTransition";
|
|
729
|
+
} else if (a.constructor.name === "CSSAnimation" || a.animationName) {
|
|
730
|
+
name = a.animationName || "";
|
|
731
|
+
type = "CSSAnimation";
|
|
732
|
+
}
|
|
733
|
+
const id = type + ":" + name + ":" + uniqueSelector;
|
|
734
|
+
if (!this.animations.has(id)) {
|
|
735
|
+
const keyframes2 = a.effect?.getKeyframes?.() || [];
|
|
736
|
+
let source = null;
|
|
737
|
+
if (type === "CSSAnimation" && keyframes2.length > 0) {
|
|
738
|
+
const kfLines = keyframes2.map((kf) => {
|
|
739
|
+
const offset = Math.round((kf.offset ?? 0) * 100) + "%";
|
|
740
|
+
const props = Object.entries(kf).filter(
|
|
741
|
+
([k]) => !["offset", "easing", "composite", "computedOffset"].includes(k)
|
|
742
|
+
).map(
|
|
743
|
+
([k, v]) => k.replace(/([A-Z])/g, "-$1").toLowerCase() + ": " + v
|
|
744
|
+
).join("; ");
|
|
745
|
+
return " " + offset + " { " + props + " }";
|
|
746
|
+
}).join("\n");
|
|
747
|
+
source = "@keyframes " + (name || "anonymous") + " {\n" + kfLines + "\n}";
|
|
748
|
+
} else if (type === "CSSTransition") {
|
|
749
|
+
source = "transition: " + name + " " + duration + "ms " + easing;
|
|
750
|
+
}
|
|
751
|
+
const resolvedVars = {};
|
|
752
|
+
try {
|
|
753
|
+
const cs = getComputedStyle(target);
|
|
754
|
+
const style = target.getAttribute("style") || "";
|
|
755
|
+
const allRules = [];
|
|
756
|
+
for (const sheet of document.styleSheets) {
|
|
757
|
+
try {
|
|
758
|
+
for (const rule of sheet.cssRules) {
|
|
759
|
+
if (rule.selectorText && target.matches(rule.selectorText)) {
|
|
760
|
+
allRules.push(rule.cssText);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (_) {
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const allCssText = allRules.join(" ") + " " + style;
|
|
767
|
+
const varRegex = /var\(\s*(--[a-zA-Z0-9-]+)/g;
|
|
768
|
+
let varMatch;
|
|
769
|
+
while ((varMatch = varRegex.exec(allCssText)) !== null) {
|
|
770
|
+
const varName = varMatch[1];
|
|
771
|
+
const resolved = cs.getPropertyValue(varName).trim();
|
|
772
|
+
if (resolved) {
|
|
773
|
+
resolvedVars[varName] = resolved;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} catch (_) {
|
|
777
|
+
}
|
|
778
|
+
let conflicts = null;
|
|
779
|
+
if (type === "CSSTransition") {
|
|
780
|
+
try {
|
|
781
|
+
const cs = getComputedStyle(target);
|
|
782
|
+
const tProps = cssSplit(cs.transitionProperty);
|
|
783
|
+
const tDurations = cssSplit(cs.transitionDuration).map(
|
|
784
|
+
(s) => parseFloat(s) * 1e3
|
|
785
|
+
);
|
|
786
|
+
const tEasings = cssSplit(cs.transitionTimingFunction);
|
|
787
|
+
const tDelays = cssSplit(cs.transitionDelay).map(
|
|
788
|
+
(s) => parseFloat(s) * 1e3
|
|
789
|
+
);
|
|
790
|
+
let idx = tProps.indexOf(name);
|
|
791
|
+
if (idx === -1 && tProps.includes("all"))
|
|
792
|
+
idx = tProps.indexOf("all");
|
|
793
|
+
if (idx >= 0) {
|
|
794
|
+
const declaredDuration = tDurations[idx % tDurations.length] || 0;
|
|
795
|
+
const declaredEasing = tEasings[idx % tEasings.length] || "ease";
|
|
796
|
+
const declaredDelay = tDelays[idx % tDelays.length] || 0;
|
|
797
|
+
const diffs = [];
|
|
798
|
+
if (Math.abs(declaredDuration - duration) > 1) {
|
|
799
|
+
diffs.push(
|
|
800
|
+
"duration: declared " + declaredDuration + "ms, actual " + duration + "ms"
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (declaredEasing !== easing) {
|
|
804
|
+
const normDeclared = declaredEasing.replace(/\s/g, "");
|
|
805
|
+
const normActual = easing.replace(/\s/g, "");
|
|
806
|
+
if (normDeclared !== normActual) {
|
|
807
|
+
diffs.push(
|
|
808
|
+
"easing: declared " + declaredEasing + ", actual " + easing
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (Math.abs(declaredDelay - delay) > 1) {
|
|
813
|
+
diffs.push(
|
|
814
|
+
"delay: declared " + declaredDelay + "ms, actual " + delay + "ms"
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (diffs.length > 0) {
|
|
818
|
+
conflicts = diffs;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
} catch (_) {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
this.animations.set(id, {
|
|
825
|
+
id,
|
|
826
|
+
name,
|
|
827
|
+
selector: readableSelector,
|
|
828
|
+
elementLabel: getElementLabel(target),
|
|
829
|
+
duration,
|
|
830
|
+
delay,
|
|
831
|
+
easing,
|
|
832
|
+
type,
|
|
833
|
+
source,
|
|
834
|
+
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
835
|
+
conflicts
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
839
|
+
const animatedProps = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const kf of keyframes) {
|
|
841
|
+
for (const key of Object.keys(kf)) {
|
|
842
|
+
if (!["offset", "easing", "composite", "computedOffset"].includes(key)) {
|
|
843
|
+
animatedProps.add(key);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const computedStyle = getComputedStyle(target);
|
|
848
|
+
const properties = [];
|
|
849
|
+
for (const prop of animatedProps) {
|
|
850
|
+
const kebab = prop.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
851
|
+
const value = computedStyle.getPropertyValue(kebab) || "";
|
|
852
|
+
if (value) {
|
|
853
|
+
const from = keyframes[0]?.[prop] || null;
|
|
854
|
+
const to = keyframes[keyframes.length - 1]?.[prop] || null;
|
|
855
|
+
properties.push({ property: kebab, value, from, to });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const currentTime = a.currentTime ?? 0;
|
|
859
|
+
const progress = duration > 0 ? Math.max(0, Math.min(1, currentTime / duration)) : 0;
|
|
860
|
+
frameAnims.push({ animationId: id, currentTime, progress, properties });
|
|
861
|
+
}
|
|
862
|
+
const waapiAnimatedIds = /* @__PURE__ */ new Set();
|
|
863
|
+
for (const fa of frameAnims) {
|
|
864
|
+
const parts = fa.animationId.split(":");
|
|
865
|
+
waapiAnimatedIds.add(parts[parts.length - 1]);
|
|
866
|
+
}
|
|
867
|
+
const JS_TRACK_PROPS = ["transform", "opacity", "left", "top", "right", "bottom", "background"];
|
|
868
|
+
const waapiAnimatedProps = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const fa of frameAnims) {
|
|
870
|
+
const parts = fa.animationId.split(":");
|
|
871
|
+
const elKey = parts[parts.length - 1];
|
|
872
|
+
if (!waapiAnimatedProps.has(elKey)) waapiAnimatedProps.set(elKey, /* @__PURE__ */ new Set());
|
|
873
|
+
const propName = parts[1];
|
|
874
|
+
if (propName) waapiAnimatedProps.get(elKey).add(propName);
|
|
875
|
+
}
|
|
876
|
+
for (const [elId, el] of this.elements) {
|
|
877
|
+
if (!el.isConnected) continue;
|
|
878
|
+
const hasWaapi = waapiAnimatedIds.has(elId);
|
|
879
|
+
const waapiProps = waapiAnimatedProps.get(elId);
|
|
880
|
+
const currentInline = {};
|
|
881
|
+
let hasAnyInline = false;
|
|
882
|
+
for (const prop of JS_TRACK_PROPS) {
|
|
883
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
884
|
+
const val = el.style[camel];
|
|
885
|
+
if (val) {
|
|
886
|
+
currentInline[prop] = val;
|
|
887
|
+
hasAnyInline = true;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (!hasAnyInline) {
|
|
891
|
+
this.prevInlineStyles.set(elId, currentInline);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const prevInline = this.prevInlineStyles.get(elId) || {};
|
|
895
|
+
this.prevInlineStyles.set(elId, currentInline);
|
|
896
|
+
for (const p of JS_TRACK_PROPS) {
|
|
897
|
+
const cur = currentInline[p] || "";
|
|
898
|
+
const prev = prevInline[p] || "";
|
|
899
|
+
const propCoveredByWaapi = waapiProps?.has(p) || waapiProps?.has(p.replace(/-([a-z])/g, (_, c) => c.toUpperCase()));
|
|
900
|
+
if (cur && cur !== prev && !propCoveredByWaapi) {
|
|
901
|
+
const jsId = "JSAnimation:" + p + ":" + elId;
|
|
902
|
+
this.jsAnimChangeCount.set(jsId, (this.jsAnimChangeCount.get(jsId) || 0) + 1);
|
|
903
|
+
if (!this.jsAnimStartTimes.has(jsId)) {
|
|
904
|
+
this.jsAnimStartTimes.set(jsId, time);
|
|
905
|
+
this.jsAnimFromValues.set(jsId, prev || cur);
|
|
906
|
+
}
|
|
907
|
+
this.jsAnimLastSeen.set(jsId, time);
|
|
908
|
+
if (this.jsAnimChangeCount.get(jsId) < 3) continue;
|
|
909
|
+
if (!this.animations.has(jsId)) {
|
|
910
|
+
const readableSel = getReadableSelector(el);
|
|
911
|
+
this.animations.set(jsId, {
|
|
912
|
+
id: jsId,
|
|
913
|
+
name: p,
|
|
914
|
+
selector: readableSel,
|
|
915
|
+
elementLabel: getElementLabel(el),
|
|
916
|
+
duration: 0,
|
|
917
|
+
delay: 0,
|
|
918
|
+
easing: "JS-driven",
|
|
919
|
+
type: "JSAnimation",
|
|
920
|
+
source: "Detected: inline style animation (JS library or requestAnimationFrame)",
|
|
921
|
+
resolvedVars: null,
|
|
922
|
+
conflicts: null
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const startT = this.jsAnimStartTimes.get(jsId);
|
|
926
|
+
const anim = this.animations.get(jsId);
|
|
927
|
+
anim.duration = time - startT;
|
|
928
|
+
const fromVal = this.jsAnimFromValues.get(jsId) || "";
|
|
929
|
+
const elapsed = time - startT;
|
|
930
|
+
const estimatedDuration = Math.max(anim.duration, 300);
|
|
931
|
+
const prog = Math.min(1, elapsed / estimatedDuration);
|
|
932
|
+
frameAnims.push({
|
|
933
|
+
animationId: jsId,
|
|
934
|
+
currentTime: elapsed,
|
|
935
|
+
progress: prog,
|
|
936
|
+
properties: [{
|
|
937
|
+
property: p,
|
|
938
|
+
value: cur,
|
|
939
|
+
from: fromVal,
|
|
940
|
+
to: cur
|
|
941
|
+
}]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const elementSnapshots = {};
|
|
947
|
+
for (const [sel, el] of this.elements) {
|
|
948
|
+
if (!el.isConnected) continue;
|
|
949
|
+
const cs = getComputedStyle(el);
|
|
950
|
+
const snap = { __styles: {}, __attrs: {} };
|
|
951
|
+
for (const prop of SNAPSHOT_PROPS) {
|
|
952
|
+
snap.__styles[prop] = cs.getPropertyValue(prop);
|
|
953
|
+
}
|
|
954
|
+
for (const attr of SNAPSHOT_ATTRS) {
|
|
955
|
+
if (attr === "checked") {
|
|
956
|
+
snap.__attrs[attr] = el.checked ? "true" : null;
|
|
957
|
+
} else if (attr === "class") {
|
|
958
|
+
snap.__attrs[attr] = el.className;
|
|
959
|
+
} else if (attr === "style") {
|
|
960
|
+
snap.__attrs[attr] = el.getAttribute("style") || "";
|
|
961
|
+
} else if (attr === "value") {
|
|
962
|
+
snap.__attrs[attr] = el.value !== void 0 ? String(el.value) : null;
|
|
963
|
+
} else {
|
|
964
|
+
snap.__attrs[attr] = el.getAttribute(attr);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
snap.__afterOpacity = getComputedStyle(el, "::after").getPropertyValue(
|
|
969
|
+
"opacity"
|
|
970
|
+
);
|
|
971
|
+
snap.__beforeOpacity = getComputedStyle(
|
|
972
|
+
el,
|
|
973
|
+
"::before"
|
|
974
|
+
).getPropertyValue("opacity");
|
|
975
|
+
} catch (_) {
|
|
976
|
+
}
|
|
977
|
+
elementSnapshots[sel] = snap;
|
|
978
|
+
}
|
|
979
|
+
for (const [id, portal] of this.activePortals) {
|
|
980
|
+
if (portal.element?.isConnected && !portal.element.hasAttribute("data-lapse-portal-hidden")) {
|
|
981
|
+
this.currentPortalIds.add(id);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
this.frames.push({
|
|
985
|
+
time,
|
|
986
|
+
animations: frameAnims,
|
|
987
|
+
elementSnapshots,
|
|
988
|
+
activePortalIds: [...this.currentPortalIds],
|
|
989
|
+
hoveredSels: [...this.currentHoverEls],
|
|
990
|
+
focusSel: this.currentFocusSel,
|
|
991
|
+
pointer: { ...this.currentPointer },
|
|
992
|
+
scrollPositions: {}
|
|
993
|
+
});
|
|
994
|
+
this._raf(captureFrame);
|
|
995
|
+
};
|
|
996
|
+
this._raf(captureFrame);
|
|
997
|
+
}
|
|
998
|
+
// =========================================================================
|
|
999
|
+
// stopRecording
|
|
1000
|
+
// =========================================================================
|
|
1001
|
+
stopRecording() {
|
|
1002
|
+
if (!this.recording) {
|
|
1003
|
+
return {
|
|
1004
|
+
startTime: 0,
|
|
1005
|
+
endTime: 0,
|
|
1006
|
+
duration: 0,
|
|
1007
|
+
animations: [],
|
|
1008
|
+
frames: [],
|
|
1009
|
+
boundingBox: null
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
this.recording = false;
|
|
1013
|
+
if (this._removeChild) Node.prototype.removeChild = this._removeChild;
|
|
1014
|
+
if (this._remove) Element.prototype.remove = this._remove;
|
|
1015
|
+
if (this._elementAnimate) Element.prototype.animate = this._elementAnimate;
|
|
1016
|
+
this.attrObserver?.disconnect();
|
|
1017
|
+
this.portalObserver?.disconnect();
|
|
1018
|
+
this.exitObserver?.disconnect();
|
|
1019
|
+
if (this.onVisibilityChange) {
|
|
1020
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
1021
|
+
this.onVisibilityChange = null;
|
|
1022
|
+
}
|
|
1023
|
+
if (this.onMouseOver)
|
|
1024
|
+
document.removeEventListener("mouseover", this.onMouseOver, true);
|
|
1025
|
+
if (this.onMouseOut)
|
|
1026
|
+
document.removeEventListener("mouseout", this.onMouseOut, true);
|
|
1027
|
+
if (this.onFocusIn)
|
|
1028
|
+
document.removeEventListener("focusin", this.onFocusIn, true);
|
|
1029
|
+
if (this.onFocusOut)
|
|
1030
|
+
document.removeEventListener("focusout", this.onFocusOut, true);
|
|
1031
|
+
if (this.onPointerMove)
|
|
1032
|
+
document.removeEventListener("pointermove", this.onPointerMove, true);
|
|
1033
|
+
if (this.onPointerDown)
|
|
1034
|
+
document.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
1035
|
+
if (this.onPointerUp)
|
|
1036
|
+
document.removeEventListener("pointerup", this.onPointerUp, true);
|
|
1037
|
+
try {
|
|
1038
|
+
for (const anim of document.getAnimations()) {
|
|
1039
|
+
anim.cancel();
|
|
1040
|
+
}
|
|
1041
|
+
} catch (_) {
|
|
1042
|
+
}
|
|
1043
|
+
window.requestAnimationFrame = () => 0;
|
|
1044
|
+
const noTransitions = document.createElement("style");
|
|
1045
|
+
noTransitions.id = "__lapse-no-transitions";
|
|
1046
|
+
noTransitions.textContent = "*, *::before, *::after { transition: none !important; animation: none !important; }";
|
|
1047
|
+
document.head.appendChild(noTransitions);
|
|
1048
|
+
this.noTransitionsEl = noTransitions;
|
|
1049
|
+
const blocker = document.createElement("div");
|
|
1050
|
+
blocker.id = "__lapse-scrub-blocker";
|
|
1051
|
+
blocker.style.cssText = "position:fixed;inset:0;z-index:999999;cursor:not-allowed;background:transparent;";
|
|
1052
|
+
blocker.title = "Clear the timeline to interact with the page";
|
|
1053
|
+
document.body.appendChild(blocker);
|
|
1054
|
+
this.blockerEl = blocker;
|
|
1055
|
+
try {
|
|
1056
|
+
const lapseStyle = document.createElement("style");
|
|
1057
|
+
lapseStyle.id = "__lapse-state-rules";
|
|
1058
|
+
let allCss = "";
|
|
1059
|
+
for (const style of document.querySelectorAll("style")) {
|
|
1060
|
+
if (style.id === "__lapse-state-rules") continue;
|
|
1061
|
+
allCss += style.textContent + "\n";
|
|
1062
|
+
}
|
|
1063
|
+
for (const sheet of document.styleSheets) {
|
|
1064
|
+
try {
|
|
1065
|
+
let walk2 = function(rules) {
|
|
1066
|
+
for (const rule of rules) {
|
|
1067
|
+
if (rule.cssRules) {
|
|
1068
|
+
walk2(rule.cssRules);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const t = rule.cssText;
|
|
1072
|
+
if (t && (t.includes(":hover") || t.includes(":focus"))) {
|
|
1073
|
+
allCss += t + "\n";
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
var walk = walk2;
|
|
1078
|
+
walk2(sheet.cssRules);
|
|
1079
|
+
} catch (_) {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const stateRegex = /([^{}]*(?::hover|:focus-visible|:focus-within|:focus(?!-))[^{}]*)\{([^{}]*)\}/g;
|
|
1083
|
+
let match;
|
|
1084
|
+
while ((match = stateRegex.exec(allCss)) !== null) {
|
|
1085
|
+
const selector = match[1].trim();
|
|
1086
|
+
const body = match[2].trim();
|
|
1087
|
+
if (!body) continue;
|
|
1088
|
+
const newBody = body.replace(
|
|
1089
|
+
/([^;:]+):\s*([^;]+)(;|$)/g,
|
|
1090
|
+
(m, prop, val, end) => {
|
|
1091
|
+
if (val.includes("!important")) return m;
|
|
1092
|
+
return prop + ": " + val.trim() + " !important" + end;
|
|
1093
|
+
}
|
|
1094
|
+
);
|
|
1095
|
+
if (selector.includes(":hover")) {
|
|
1096
|
+
lapseStyle.textContent += selector.replace(/:hover/g, "[data-lapse-hover]") + " { " + newBody + " }\n";
|
|
1097
|
+
}
|
|
1098
|
+
if (selector.includes(":focus-visible")) {
|
|
1099
|
+
lapseStyle.textContent += selector.replace(/:focus-visible/g, "[data-lapse-focus]") + " { " + newBody + " }\n";
|
|
1100
|
+
} else if (selector.includes(":focus-within")) {
|
|
1101
|
+
lapseStyle.textContent += selector.replace(
|
|
1102
|
+
/:focus-within/g,
|
|
1103
|
+
":has([data-lapse-focus])"
|
|
1104
|
+
) + " { " + newBody + " }\n";
|
|
1105
|
+
} else if (selector.includes(":focus")) {
|
|
1106
|
+
lapseStyle.textContent += selector.replace(/:focus(?!-)/g, "[data-lapse-focus]") + " { " + newBody + " }\n";
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
document.head.appendChild(lapseStyle);
|
|
1110
|
+
this.lapseStyleEl = lapseStyle;
|
|
1111
|
+
} catch (_) {
|
|
1112
|
+
}
|
|
1113
|
+
if (this.frames.length > 0) {
|
|
1114
|
+
const frame0 = this.frames[0];
|
|
1115
|
+
if (frame0.elementSnapshots) {
|
|
1116
|
+
for (const [sel, snap] of Object.entries(frame0.elementSnapshots)) {
|
|
1117
|
+
const el = this.elements.get(sel);
|
|
1118
|
+
if (!el || !el.isConnected) continue;
|
|
1119
|
+
if (snap.__styles) {
|
|
1120
|
+
for (const [prop, value] of Object.entries(snap.__styles)) {
|
|
1121
|
+
if (SAFE_PROPS_SET.has(prop)) {
|
|
1122
|
+
el.style.setProperty(prop, value, "important");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (snap.__attrs) {
|
|
1127
|
+
for (const [attr, value] of Object.entries(snap.__attrs)) {
|
|
1128
|
+
if (attr === "checked") {
|
|
1129
|
+
;
|
|
1130
|
+
el.checked = value === "true";
|
|
1131
|
+
} else if (attr === "class" && value != null) {
|
|
1132
|
+
el.className = value;
|
|
1133
|
+
} else if (attr === "style") {
|
|
1134
|
+
} else if (attr === "value" && value != null) {
|
|
1135
|
+
;
|
|
1136
|
+
el.value = value;
|
|
1137
|
+
} else if (value == null) {
|
|
1138
|
+
el.removeAttribute(attr);
|
|
1139
|
+
} else {
|
|
1140
|
+
el.setAttribute(attr, value);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
1148
|
+
const capture = {
|
|
1149
|
+
startTime: this.startTime,
|
|
1150
|
+
endTime: performance.now(),
|
|
1151
|
+
duration,
|
|
1152
|
+
animations: Array.from(this.animations.values()),
|
|
1153
|
+
frames: this.frames,
|
|
1154
|
+
boundingBox: this.boundingBox
|
|
1155
|
+
};
|
|
1156
|
+
return capture;
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
// ---- Recording limits ---------------------------------------------------
|
|
1160
|
+
_TimelineRecorder.MAX_DURATION_MS = 6e4;
|
|
1161
|
+
_TimelineRecorder.MAX_FRAMES = 3600;
|
|
1162
|
+
var TimelineRecorder = _TimelineRecorder;
|
|
1163
|
+
|
|
1164
|
+
// src/core/scrubber.ts
|
|
1165
|
+
var TimelineScrubber = class {
|
|
1166
|
+
constructor(state) {
|
|
1167
|
+
/** Saved originals for restore on release */
|
|
1168
|
+
this._originalAnimate = null;
|
|
1169
|
+
this._originalRaf = null;
|
|
1170
|
+
this._originalRemoveChild = null;
|
|
1171
|
+
this._originalRemove = null;
|
|
1172
|
+
this.elements = state.elements;
|
|
1173
|
+
this.frames = state.frames;
|
|
1174
|
+
this.capturedPortals = state.capturedPortals;
|
|
1175
|
+
this.interceptedAnimations = state.interceptedAnimations;
|
|
1176
|
+
this.SAFE_PROPS_SET = state.SAFE_PROPS_SET;
|
|
1177
|
+
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1178
|
+
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1179
|
+
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1180
|
+
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1181
|
+
}
|
|
1182
|
+
// ---------------------------------------------------------------------------
|
|
1183
|
+
// Selector helper — mirrors the recorder's getSelector so we can look up
|
|
1184
|
+
// elements by the same key the recorder used in frame.animations[].animationId
|
|
1185
|
+
// ---------------------------------------------------------------------------
|
|
1186
|
+
getSelector(el) {
|
|
1187
|
+
if (!el || !el.tagName) return null;
|
|
1188
|
+
const parts = [];
|
|
1189
|
+
let current = el;
|
|
1190
|
+
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1191
|
+
const tag = current.tagName.toLowerCase();
|
|
1192
|
+
const parent = current.parentElement;
|
|
1193
|
+
if (parent) {
|
|
1194
|
+
const siblings = Array.from(parent.children);
|
|
1195
|
+
const idx = siblings.indexOf(current) + 1;
|
|
1196
|
+
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1197
|
+
} else {
|
|
1198
|
+
parts.unshift(tag);
|
|
1199
|
+
}
|
|
1200
|
+
current = parent;
|
|
1201
|
+
}
|
|
1202
|
+
return parts.join(" > ");
|
|
1203
|
+
}
|
|
1204
|
+
// ---------------------------------------------------------------------------
|
|
1205
|
+
// seekTo — scrub the DOM to match a specific timestamp
|
|
1206
|
+
// ---------------------------------------------------------------------------
|
|
1207
|
+
seekTo(timeMs) {
|
|
1208
|
+
if (!this.frames.length) return;
|
|
1209
|
+
let lo = 0;
|
|
1210
|
+
let hi = this.frames.length - 1;
|
|
1211
|
+
while (lo < hi) {
|
|
1212
|
+
const mid = lo + hi >> 1;
|
|
1213
|
+
if (this.frames[mid].time < timeMs) lo = mid + 1;
|
|
1214
|
+
else hi = mid;
|
|
1215
|
+
}
|
|
1216
|
+
const frame = this.frames[lo];
|
|
1217
|
+
if (!frame || !frame.elementSnapshots) return;
|
|
1218
|
+
document.querySelectorAll("[data-lapse-hover]").forEach((el) => {
|
|
1219
|
+
el.removeAttribute("data-lapse-hover");
|
|
1220
|
+
});
|
|
1221
|
+
document.querySelectorAll("[data-lapse-focus]").forEach((el) => {
|
|
1222
|
+
el.removeAttribute("data-lapse-focus");
|
|
1223
|
+
});
|
|
1224
|
+
const activeIds = new Set(frame.activePortalIds || []);
|
|
1225
|
+
for (const id of this.capturedPortals) {
|
|
1226
|
+
const portalEl = document.querySelector(
|
|
1227
|
+
`[data-lapse-portal-id="${id}"]`
|
|
1228
|
+
);
|
|
1229
|
+
if (!portalEl) continue;
|
|
1230
|
+
if (activeIds.has(id)) {
|
|
1231
|
+
portalEl.style.removeProperty("display");
|
|
1232
|
+
portalEl.removeAttribute("data-lapse-portal-hidden");
|
|
1233
|
+
} else {
|
|
1234
|
+
portalEl.style.setProperty("display", "none", "important");
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const hoveredSels = frame.hoveredSels || [];
|
|
1238
|
+
for (const sel of hoveredSels) {
|
|
1239
|
+
const el = this.elements.get(sel);
|
|
1240
|
+
if (el && el.isConnected) el.setAttribute("data-lapse-hover", "");
|
|
1241
|
+
}
|
|
1242
|
+
if (frame.focusSel) {
|
|
1243
|
+
const focusEl = this.elements.get(frame.focusSel);
|
|
1244
|
+
if (focusEl && focusEl.isConnected) {
|
|
1245
|
+
focusEl.setAttribute("data-lapse-focus", "");
|
|
1246
|
+
let parent = focusEl.parentElement;
|
|
1247
|
+
while (parent && parent !== document.body) {
|
|
1248
|
+
parent.setAttribute("data-lapse-focus", "");
|
|
1249
|
+
parent = parent.parentElement;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
for (const entry of this.interceptedAnimations) {
|
|
1254
|
+
try {
|
|
1255
|
+
const anim = entry.animation;
|
|
1256
|
+
if (anim.playState !== "finished") {
|
|
1257
|
+
anim.pause();
|
|
1258
|
+
}
|
|
1259
|
+
anim.currentTime = timeMs;
|
|
1260
|
+
} catch {
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const [sel, snap] of Object.entries(frame.elementSnapshots)) {
|
|
1264
|
+
const el = this.elements.get(sel);
|
|
1265
|
+
if (!el || !el.isConnected) continue;
|
|
1266
|
+
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1267
|
+
const hasAnimation = (frame.animations || []).some(
|
|
1268
|
+
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1269
|
+
);
|
|
1270
|
+
const snapTyped = snap;
|
|
1271
|
+
if (snapTyped.__styles) {
|
|
1272
|
+
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1273
|
+
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1274
|
+
el.style.setProperty(prop, value, "important");
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (snapTyped.__attrs) {
|
|
1279
|
+
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1280
|
+
if (attr === "checked") {
|
|
1281
|
+
;
|
|
1282
|
+
el.checked = value === "true";
|
|
1283
|
+
} else if (attr === "class") {
|
|
1284
|
+
if (value != null) el.className = value;
|
|
1285
|
+
} else if (attr === "style") {
|
|
1286
|
+
if (value) {
|
|
1287
|
+
el.setAttribute("style", value);
|
|
1288
|
+
el.style.transition = "none";
|
|
1289
|
+
if (snapTyped.__styles) {
|
|
1290
|
+
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1291
|
+
el.style.setProperty(prop, val, "important");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} else if (attr === "value") {
|
|
1296
|
+
if (value != null) el.value = value;
|
|
1297
|
+
} else if (value == null) {
|
|
1298
|
+
el.removeAttribute(attr);
|
|
1299
|
+
} else {
|
|
1300
|
+
el.setAttribute(attr, value);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const anim of frame.animations || []) {
|
|
1306
|
+
const firstColon = anim.animationId.indexOf(":");
|
|
1307
|
+
const secondColon = anim.animationId.indexOf(":", firstColon + 1);
|
|
1308
|
+
const animSel = secondColon >= 0 ? anim.animationId.substring(secondColon + 1) : "";
|
|
1309
|
+
const animEl = this.elements.get(animSel);
|
|
1310
|
+
if (!animEl || !animEl.isConnected) continue;
|
|
1311
|
+
for (const prop of anim.properties || []) {
|
|
1312
|
+
if (prop.value) {
|
|
1313
|
+
animEl.style.setProperty(prop.property, prop.value, "important");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const animatedSels = /* @__PURE__ */ new Set();
|
|
1318
|
+
for (const anim of frame.animations || []) {
|
|
1319
|
+
const fc = anim.animationId.indexOf(":");
|
|
1320
|
+
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1321
|
+
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1322
|
+
}
|
|
1323
|
+
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1324
|
+
const el = rawEl;
|
|
1325
|
+
const sel = this.getSelector(el);
|
|
1326
|
+
if (sel && !animatedSels.has(sel)) {
|
|
1327
|
+
el.style.removeProperty("opacity");
|
|
1328
|
+
el.style.removeProperty("transform");
|
|
1329
|
+
el.style.removeProperty("filter");
|
|
1330
|
+
el.style.removeProperty("stroke-dashoffset");
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
// ---------------------------------------------------------------------------
|
|
1335
|
+
// release — tear down all scrub state and restore the page to normal
|
|
1336
|
+
// ---------------------------------------------------------------------------
|
|
1337
|
+
release() {
|
|
1338
|
+
for (const entry of this.interceptedAnimations) {
|
|
1339
|
+
try {
|
|
1340
|
+
entry.animation.cancel();
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
this.interceptedAnimations.length = 0;
|
|
1345
|
+
if (this._originalAnimate) {
|
|
1346
|
+
Element.prototype.animate = this._originalAnimate;
|
|
1347
|
+
this._originalAnimate = null;
|
|
1348
|
+
delete window.__LAPSE_ORIGINAL_ANIMATE__;
|
|
1349
|
+
}
|
|
1350
|
+
if (this._originalRaf) {
|
|
1351
|
+
window.requestAnimationFrame = this._originalRaf;
|
|
1352
|
+
this._originalRaf = null;
|
|
1353
|
+
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
1354
|
+
}
|
|
1355
|
+
const tl = window.__LAPSE_TIMELINE__;
|
|
1356
|
+
if (tl?.noTransitionsEl) {
|
|
1357
|
+
tl.noTransitionsEl.remove();
|
|
1358
|
+
}
|
|
1359
|
+
const noTrans = document.getElementById("__lapse-no-transitions");
|
|
1360
|
+
if (noTrans) noTrans.remove();
|
|
1361
|
+
const blocker = document.getElementById("__lapse-scrub-blocker");
|
|
1362
|
+
if (blocker) blocker.remove();
|
|
1363
|
+
if (tl?.blockerEl) tl.blockerEl.remove();
|
|
1364
|
+
const stateRules = document.getElementById("__lapse-state-rules");
|
|
1365
|
+
if (stateRules) stateRules.remove();
|
|
1366
|
+
if (tl?.lapseStyleEl) tl.lapseStyleEl.remove();
|
|
1367
|
+
if (this._originalRemoveChild) {
|
|
1368
|
+
Node.prototype.removeChild = this._originalRemoveChild;
|
|
1369
|
+
this._originalRemoveChild = null;
|
|
1370
|
+
} else if (tl?._removeChild) {
|
|
1371
|
+
Node.prototype.removeChild = tl._removeChild;
|
|
1372
|
+
}
|
|
1373
|
+
if (this._originalRemove) {
|
|
1374
|
+
Element.prototype.remove = this._originalRemove;
|
|
1375
|
+
this._originalRemove = null;
|
|
1376
|
+
} else if (tl?._remove) {
|
|
1377
|
+
Element.prototype.remove = tl._remove;
|
|
1378
|
+
}
|
|
1379
|
+
document.querySelectorAll("[data-lapse-exit-captured]").forEach((el) => {
|
|
1380
|
+
el.remove();
|
|
1381
|
+
});
|
|
1382
|
+
document.querySelectorAll("[data-lapse-id]").forEach((el) => {
|
|
1383
|
+
el.removeAttribute("data-lapse-id");
|
|
1384
|
+
});
|
|
1385
|
+
document.querySelectorAll("[data-lapse-hover]").forEach((el) => {
|
|
1386
|
+
el.removeAttribute("data-lapse-hover");
|
|
1387
|
+
});
|
|
1388
|
+
document.querySelectorAll("[data-lapse-focus]").forEach((el) => {
|
|
1389
|
+
el.removeAttribute("data-lapse-focus");
|
|
1390
|
+
});
|
|
1391
|
+
document.querySelectorAll("[data-lapse-portal-id]").forEach((el) => {
|
|
1392
|
+
el.removeAttribute("data-lapse-portal-id");
|
|
1393
|
+
el.removeAttribute("data-lapse-portal-hidden");
|
|
1394
|
+
});
|
|
1395
|
+
delete window.__LAPSE_TIMELINE__;
|
|
1396
|
+
this.elements.clear();
|
|
1397
|
+
this.frames.length = 0;
|
|
1398
|
+
this.capturedPortals.clear();
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/core/export.ts
|
|
1403
|
+
function getFrameAtTime(frames, timeMs) {
|
|
1404
|
+
if (frames.length === 0) return null;
|
|
1405
|
+
let lo = 0;
|
|
1406
|
+
let hi = frames.length - 1;
|
|
1407
|
+
while (lo < hi) {
|
|
1408
|
+
const mid = lo + hi >> 1;
|
|
1409
|
+
if (frames[mid].time < timeMs) lo = mid + 1;
|
|
1410
|
+
else hi = mid;
|
|
1411
|
+
}
|
|
1412
|
+
return frames[lo];
|
|
1413
|
+
}
|
|
1414
|
+
function generateExport(animations, frames, timeMs, filter = "active") {
|
|
1415
|
+
const frame = getFrameAtTime(frames, timeMs);
|
|
1416
|
+
const duration = frames.at(-1)?.time ?? 0;
|
|
1417
|
+
const filteredAnims = animations.filter((anim) => {
|
|
1418
|
+
if (filter === "all-animations") return true;
|
|
1419
|
+
if (filter === "active") {
|
|
1420
|
+
const frameAnim = frame?.animations.find((a) => a.animationId === anim.id);
|
|
1421
|
+
if (!frameAnim || frameAnim.progress <= 0 || frameAnim.progress >= 1) return false;
|
|
1422
|
+
if (frameAnim.properties.length > 0) {
|
|
1423
|
+
const hasRealChange = frameAnim.properties.some(
|
|
1424
|
+
(p) => p.from && p.to && p.from !== p.to
|
|
1425
|
+
);
|
|
1426
|
+
if (!hasRealChange) return false;
|
|
1427
|
+
}
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
return true;
|
|
1431
|
+
});
|
|
1432
|
+
const animExports = filteredAnims.map((anim) => {
|
|
1433
|
+
let frameAnim = frame?.animations.find((a) => a.animationId === anim.id);
|
|
1434
|
+
let status = frameAnim ? "active" : "completed";
|
|
1435
|
+
if (!frameAnim) {
|
|
1436
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
1437
|
+
if (frames[i].time > timeMs) continue;
|
|
1438
|
+
const found = frames[i].animations.find((a) => a.animationId === anim.id);
|
|
1439
|
+
if (found) {
|
|
1440
|
+
frameAnim = found;
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (!frameAnim) {
|
|
1445
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1446
|
+
if (frames[i].time < timeMs) continue;
|
|
1447
|
+
const found = frames[i].animations.find((a) => a.animationId === anim.id);
|
|
1448
|
+
if (found) {
|
|
1449
|
+
frameAnim = found;
|
|
1450
|
+
status = "upcoming";
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const progressLabel = status === "active" ? `${Math.round((frameAnim?.progress ?? 0) * 100)}%` : status === "completed" ? "done" : "upcoming";
|
|
1457
|
+
return {
|
|
1458
|
+
element: anim.selector,
|
|
1459
|
+
elementLabel: anim.elementLabel || anim.selector,
|
|
1460
|
+
name: anim.name || anim.type,
|
|
1461
|
+
type: anim.type,
|
|
1462
|
+
timing: `${Math.round(anim.duration)}ms ${anim.easing}${anim.delay ? ` (delay: ${Math.round(anim.delay)}ms)` : ""}`,
|
|
1463
|
+
progress: progressLabel,
|
|
1464
|
+
properties: (frameAnim?.properties ?? []).map((p) => ({
|
|
1465
|
+
property: p.property,
|
|
1466
|
+
value: p.value,
|
|
1467
|
+
range: p.from && p.to ? `${p.from} \u2192 ${p.to}` : ""
|
|
1468
|
+
})),
|
|
1469
|
+
source: anim.source,
|
|
1470
|
+
resolvedVars: anim.resolvedVars,
|
|
1471
|
+
conflicts: anim.conflicts
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
1474
|
+
const hoveredElements = Array.isArray(frame?.hoveredSels) ? frame.hoveredSels.map(String) : [];
|
|
1475
|
+
const focusedElement = frame?.focusSel ? String(frame.focusSel) : null;
|
|
1476
|
+
return {
|
|
1477
|
+
timestamp: `${Math.round(timeMs)}ms into ${Math.round(duration)}ms recording`,
|
|
1478
|
+
duration: `${Math.round(duration)}ms`,
|
|
1479
|
+
scrubPosition: timeMs,
|
|
1480
|
+
hoveredElements,
|
|
1481
|
+
focusedElement,
|
|
1482
|
+
animations: animExports
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
function formatExportForLLM(exp, detail = "standard") {
|
|
1486
|
+
const lines = [];
|
|
1487
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1488
|
+
for (const anim of exp.animations) {
|
|
1489
|
+
const key = (anim.elementLabel || "") + "|||" + anim.element;
|
|
1490
|
+
if (!grouped.has(key)) {
|
|
1491
|
+
grouped.set(key, { label: anim.elementLabel || "", selector: anim.element, anims: [] });
|
|
1492
|
+
}
|
|
1493
|
+
grouped.get(key).anims.push(anim);
|
|
1494
|
+
}
|
|
1495
|
+
function isRealChange(prop) {
|
|
1496
|
+
if (!prop.range) return true;
|
|
1497
|
+
const [from, to] = prop.range.split(" \u2192 ");
|
|
1498
|
+
return !(from && to && from.trim() === to.trim());
|
|
1499
|
+
}
|
|
1500
|
+
if (detail === "compact") {
|
|
1501
|
+
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
for (const [, group] of grouped) {
|
|
1504
|
+
const label = group.label && group.label !== group.selector ? `**${group.label}** \`${group.selector}\`` : `\`${group.selector}\``;
|
|
1505
|
+
const cssAnims = group.anims.filter((a) => a.type !== "JSAnimation");
|
|
1506
|
+
const jsAnims = group.anims.filter((a) => a.type === "JSAnimation");
|
|
1507
|
+
if (cssAnims.length > 0) {
|
|
1508
|
+
const props = /* @__PURE__ */ new Set();
|
|
1509
|
+
for (const a of cssAnims) a.properties.filter(isRealChange).forEach((p) => props.add(p.property));
|
|
1510
|
+
const timing = cssAnims[0]?.timing || "";
|
|
1511
|
+
const progress = cssAnims[0]?.progress || "";
|
|
1512
|
+
const progressStr = progress && progress !== "unknown" ? ` @ ${progress}` : "";
|
|
1513
|
+
lines.push(`- ${label}: ${[...props].join(", ")} (${timing}${progressStr})`);
|
|
1514
|
+
}
|
|
1515
|
+
if (jsAnims.length > 0) {
|
|
1516
|
+
const props = /* @__PURE__ */ new Set();
|
|
1517
|
+
for (const a of jsAnims) a.properties.filter(isRealChange).forEach((p) => props.add(p.property));
|
|
1518
|
+
if (props.size > 0) lines.push(`- ${label}: ${[...props].join(", ")} (JS)`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return lines.join("\n");
|
|
1522
|
+
}
|
|
1523
|
+
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1524
|
+
lines.push("");
|
|
1525
|
+
if (detail === "forensic") {
|
|
1526
|
+
lines.push("**Environment:**");
|
|
1527
|
+
lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
|
|
1528
|
+
lines.push(`- URL: ${window.location.href}`);
|
|
1529
|
+
lines.push(`- User Agent: ${navigator.userAgent}`);
|
|
1530
|
+
lines.push(`- Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1531
|
+
lines.push(`- Device Pixel Ratio: ${window.devicePixelRatio}`);
|
|
1532
|
+
lines.push("");
|
|
1533
|
+
}
|
|
1534
|
+
if (exp.hoveredElements.length > 0 || exp.focusedElement) {
|
|
1535
|
+
lines.push("**Interaction state:**");
|
|
1536
|
+
if (exp.hoveredElements.length > 0) {
|
|
1537
|
+
const deepest = exp.hoveredElements[exp.hoveredElements.length - 1] || exp.hoveredElements[0];
|
|
1538
|
+
lines.push(`- Hovered: \`${deepest}\``);
|
|
1539
|
+
}
|
|
1540
|
+
if (exp.focusedElement) {
|
|
1541
|
+
lines.push(`- Focused: \`${exp.focusedElement}\``);
|
|
1542
|
+
}
|
|
1543
|
+
lines.push("");
|
|
1544
|
+
}
|
|
1545
|
+
if (exp.animations.length === 0 && exp.hoveredElements.length === 0 && !exp.focusedElement) {
|
|
1546
|
+
lines.push("No active animations or interactions at this position.");
|
|
1547
|
+
return lines.join("\n");
|
|
1548
|
+
}
|
|
1549
|
+
if (exp.animations.length === 0) {
|
|
1550
|
+
return lines.join("\n");
|
|
1551
|
+
}
|
|
1552
|
+
for (const [, group] of grouped) {
|
|
1553
|
+
const label = group.label && group.label !== group.selector ? `"${group.label}" ` : "";
|
|
1554
|
+
const cssAnims = group.anims.filter((a) => a.type !== "JSAnimation");
|
|
1555
|
+
const jsAnims = group.anims.filter((a) => a.type === "JSAnimation");
|
|
1556
|
+
const cssPropLines = [];
|
|
1557
|
+
if (cssAnims.length > 0) {
|
|
1558
|
+
const seenProps = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const anim of cssAnims) {
|
|
1560
|
+
const progressStr = anim.progress !== "unknown" ? ` @ ${anim.progress}` : "";
|
|
1561
|
+
for (const prop of anim.properties) {
|
|
1562
|
+
if (seenProps.has(prop.property)) continue;
|
|
1563
|
+
if (!isRealChange(prop)) continue;
|
|
1564
|
+
seenProps.add(prop.property);
|
|
1565
|
+
let line = `- \`${prop.property}\`: ${prop.value}`;
|
|
1566
|
+
if (prop.range) line += ` [${prop.range}]`;
|
|
1567
|
+
line += ` (${anim.timing}${progressStr})`;
|
|
1568
|
+
cssPropLines.push(line);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const jsPropLines = [];
|
|
1573
|
+
if (jsAnims.length > 0) {
|
|
1574
|
+
const seenProps = /* @__PURE__ */ new Set();
|
|
1575
|
+
for (const anim of jsAnims) {
|
|
1576
|
+
for (const prop of anim.properties) {
|
|
1577
|
+
if (seenProps.has(prop.property)) continue;
|
|
1578
|
+
if (!isRealChange(prop)) continue;
|
|
1579
|
+
seenProps.add(prop.property);
|
|
1580
|
+
jsPropLines.push(`- \`${prop.property}\`: ${prop.value}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (cssPropLines.length === 0 && jsPropLines.length === 0) continue;
|
|
1585
|
+
lines.push(`## ${label}\`${group.selector}\``);
|
|
1586
|
+
lines.push("");
|
|
1587
|
+
if (cssPropLines.length > 0) {
|
|
1588
|
+
const transitionSet = new Set(cssAnims.map((a) => `${a.name} ${a.timing}`));
|
|
1589
|
+
lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
|
|
1590
|
+
lines.push("");
|
|
1591
|
+
for (const line of cssPropLines) lines.push(line);
|
|
1592
|
+
if (detail === "detailed" || detail === "forensic") {
|
|
1593
|
+
const allVars = {};
|
|
1594
|
+
for (const anim of cssAnims) {
|
|
1595
|
+
if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
|
|
1596
|
+
}
|
|
1597
|
+
if (Object.keys(allVars).length > 0) {
|
|
1598
|
+
lines.push("");
|
|
1599
|
+
lines.push("CSS variables:");
|
|
1600
|
+
for (const [name, value] of Object.entries(allVars)) {
|
|
1601
|
+
lines.push(`- \`${name}\`: ${value}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
for (const anim of cssAnims) {
|
|
1605
|
+
if (anim.source) {
|
|
1606
|
+
lines.push("");
|
|
1607
|
+
lines.push("```css");
|
|
1608
|
+
lines.push(anim.source);
|
|
1609
|
+
lines.push("```");
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (jsPropLines.length > 0) {
|
|
1616
|
+
if (cssPropLines.length > 0) lines.push("");
|
|
1617
|
+
for (const line of jsPropLines) lines.push(line);
|
|
1618
|
+
}
|
|
1619
|
+
lines.push("");
|
|
1620
|
+
}
|
|
1621
|
+
return lines.join("\n");
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/core/engine.ts
|
|
1625
|
+
var LapseEngine = class {
|
|
1626
|
+
constructor() {
|
|
1627
|
+
this.timing = new TimingController();
|
|
1628
|
+
this.recorder = new TimelineRecorder();
|
|
1629
|
+
this.scrubber = null;
|
|
1630
|
+
this.capture = null;
|
|
1631
|
+
this._state = "idle";
|
|
1632
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1633
|
+
}
|
|
1634
|
+
get state() {
|
|
1635
|
+
return this._state;
|
|
1636
|
+
}
|
|
1637
|
+
getCapture() {
|
|
1638
|
+
return this.capture;
|
|
1639
|
+
}
|
|
1640
|
+
// -- Speed control --------------------------------------------------------
|
|
1641
|
+
setSpeed(speed) {
|
|
1642
|
+
this.timing.setSpeed(speed);
|
|
1643
|
+
}
|
|
1644
|
+
getSpeed() {
|
|
1645
|
+
return this.timing.getSpeed();
|
|
1646
|
+
}
|
|
1647
|
+
// -- Timeline recording ---------------------------------------------------
|
|
1648
|
+
startRecording(boundingBox) {
|
|
1649
|
+
if (this._state !== "idle") return;
|
|
1650
|
+
this.timing.install();
|
|
1651
|
+
this.recorder.onAutoStop = () => this.stopRecording();
|
|
1652
|
+
this.recorder.startRecording(boundingBox);
|
|
1653
|
+
this._state = "recording";
|
|
1654
|
+
this.notify();
|
|
1655
|
+
}
|
|
1656
|
+
stopRecording() {
|
|
1657
|
+
if (this._state !== "recording") {
|
|
1658
|
+
return {
|
|
1659
|
+
startTime: 0,
|
|
1660
|
+
endTime: 0,
|
|
1661
|
+
duration: 0,
|
|
1662
|
+
animations: [],
|
|
1663
|
+
frames: [],
|
|
1664
|
+
boundingBox: null
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const capture = this.recorder.stopRecording();
|
|
1668
|
+
this.capture = capture;
|
|
1669
|
+
const scrubberState = {
|
|
1670
|
+
elements: this.recorder.elements,
|
|
1671
|
+
frames: capture.frames,
|
|
1672
|
+
capturedPortals: this.recorder.capturedPortalIds,
|
|
1673
|
+
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1674
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1675
|
+
};
|
|
1676
|
+
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1677
|
+
this._state = "scrubbing";
|
|
1678
|
+
this.notify();
|
|
1679
|
+
return capture;
|
|
1680
|
+
}
|
|
1681
|
+
// -- Scrubbing ------------------------------------------------------------
|
|
1682
|
+
seekTo(timeMs) {
|
|
1683
|
+
this.scrubber?.seekTo(timeMs);
|
|
1684
|
+
}
|
|
1685
|
+
release() {
|
|
1686
|
+
this.scrubber?.release();
|
|
1687
|
+
this.scrubber = null;
|
|
1688
|
+
this.capture = null;
|
|
1689
|
+
this._state = "idle";
|
|
1690
|
+
this.notify();
|
|
1691
|
+
}
|
|
1692
|
+
// -- Export ---------------------------------------------------------------
|
|
1693
|
+
generateExport(timeMs, filter = "active") {
|
|
1694
|
+
if (!this.capture) return null;
|
|
1695
|
+
return generateExport(
|
|
1696
|
+
this.capture.animations,
|
|
1697
|
+
this.capture.frames,
|
|
1698
|
+
timeMs,
|
|
1699
|
+
filter
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
exportForLLM(timeMs, filter = "active", detail = "standard") {
|
|
1703
|
+
const exp = this.generateExport(timeMs, filter);
|
|
1704
|
+
if (!exp) return "";
|
|
1705
|
+
return formatExportForLLM(exp, detail);
|
|
1706
|
+
}
|
|
1707
|
+
// -- State subscription ---------------------------------------------------
|
|
1708
|
+
subscribe(listener) {
|
|
1709
|
+
this.listeners.add(listener);
|
|
1710
|
+
return () => this.listeners.delete(listener);
|
|
1711
|
+
}
|
|
1712
|
+
notify() {
|
|
1713
|
+
for (const listener of this.listeners) {
|
|
1714
|
+
listener();
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
// -- Cleanup --------------------------------------------------------------
|
|
1718
|
+
destroy() {
|
|
1719
|
+
this.scrubber?.release();
|
|
1720
|
+
this.scrubber = null;
|
|
1721
|
+
this.capture = null;
|
|
1722
|
+
this.timing.destroy();
|
|
1723
|
+
this._state = "idle";
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1727
|
+
0 && (module.exports = {
|
|
1728
|
+
LapseEngine,
|
|
1729
|
+
TimelineRecorder,
|
|
1730
|
+
TimelineScrubber,
|
|
1731
|
+
TimingController,
|
|
1732
|
+
formatExportForLLM,
|
|
1733
|
+
generateExport,
|
|
1734
|
+
getFrameAtTime
|
|
1735
|
+
});
|
|
1736
|
+
//# sourceMappingURL=core.cjs.map
|