saccade 0.0.3 → 0.2.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/README.md +65 -2
- package/dist/core.cjs +267 -157
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +63 -11
- package/dist/core.d.ts +63 -11
- package/dist/core.mjs +264 -156
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +299 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -11
- package/dist/index.d.ts +37 -11
- package/dist/index.mjs +297 -193
- package/dist/index.mjs.map +1 -1
- package/dist/install.cjs +1846 -0
- package/dist/install.cjs.map +1 -0
- package/dist/install.d.cts +129 -0
- package/dist/install.d.ts +129 -0
- package/dist/install.mjs +1819 -0
- package/dist/install.mjs.map +1 -0
- package/package.json +12 -1
package/dist/core.cjs
CHANGED
|
@@ -26,21 +26,29 @@ __export(core_exports, {
|
|
|
26
26
|
TimingController: () => TimingController,
|
|
27
27
|
formatExportForLLM: () => formatExportForLLM,
|
|
28
28
|
generateExport: () => generateExport,
|
|
29
|
-
getFrameAtTime: () => getFrameAtTime
|
|
29
|
+
getFrameAtTime: () => getFrameAtTime,
|
|
30
|
+
getSharedEngine: () => getSharedEngine,
|
|
31
|
+
resetSharedEngine: () => resetSharedEngine
|
|
30
32
|
});
|
|
31
33
|
module.exports = __toCommonJS(core_exports);
|
|
32
34
|
|
|
33
35
|
// src/core/timing.ts
|
|
36
|
+
var MEDIA_RATE_MIN = 0.0625;
|
|
37
|
+
var MEDIA_RATE_MAX = 16;
|
|
38
|
+
var ZERO_SPEED_DIVISOR = 1e-4;
|
|
34
39
|
var TimingController = class {
|
|
35
40
|
constructor() {
|
|
36
41
|
this.speed = 1;
|
|
37
42
|
this.virtualBaseline = 0;
|
|
38
43
|
this.intervalMap = /* @__PURE__ */ new Map();
|
|
39
44
|
this.nextIntervalId = 1e6;
|
|
40
|
-
this.mediaObserver = null;
|
|
41
|
-
this.animObserver = null;
|
|
42
45
|
this._origAnimate = null;
|
|
43
46
|
this.installed = false;
|
|
47
|
+
this.animPollId = 0;
|
|
48
|
+
this.gsapInstance = null;
|
|
49
|
+
// WeakMap tracking for animations and media
|
|
50
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
51
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
44
52
|
this._raf = requestAnimationFrame.bind(window);
|
|
45
53
|
this._caf = cancelAnimationFrame.bind(window);
|
|
46
54
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -50,19 +58,34 @@ var TimingController = class {
|
|
|
50
58
|
this._perfNow = performance.now.bind(performance);
|
|
51
59
|
this._dateNow = Date.now;
|
|
52
60
|
this.realBaseline = this._perfNow();
|
|
61
|
+
this.dateRealBaseline = this._dateNow();
|
|
62
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
53
63
|
}
|
|
54
64
|
getVirtualTime() {
|
|
55
65
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
56
66
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
57
67
|
}
|
|
68
|
+
getVirtualDateNow() {
|
|
69
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
70
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
71
|
+
}
|
|
58
72
|
reanchor() {
|
|
59
73
|
const virtualNow = this.getVirtualTime();
|
|
60
74
|
this.realBaseline = this._perfNow();
|
|
61
75
|
this.virtualBaseline = virtualNow;
|
|
76
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
77
|
+
this.dateRealBaseline = this._dateNow();
|
|
78
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
79
|
+
}
|
|
80
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
81
|
+
get speedDivisor() {
|
|
82
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
62
83
|
}
|
|
63
84
|
/** Install timing patches. Safe to call multiple times. */
|
|
64
85
|
install() {
|
|
65
86
|
if (this.installed) return;
|
|
87
|
+
if (window.__saccadeInstalled) return;
|
|
88
|
+
window.__saccadeInstalled = true;
|
|
66
89
|
this.installed = true;
|
|
67
90
|
const self = this;
|
|
68
91
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -71,21 +94,27 @@ var TimingController = class {
|
|
|
71
94
|
Element.prototype.animate = function(...args) {
|
|
72
95
|
const anim = origAnimate.apply(this, args);
|
|
73
96
|
if (self.speed !== 1) {
|
|
74
|
-
|
|
97
|
+
const originalRate = anim.playbackRate;
|
|
98
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
99
|
+
anim.playbackRate = applied;
|
|
100
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
75
101
|
}
|
|
76
102
|
return anim;
|
|
77
103
|
};
|
|
78
104
|
performance.now = () => self.getVirtualTime();
|
|
79
|
-
|
|
80
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
105
|
+
Date.now = () => self.getVirtualDateNow();
|
|
81
106
|
window.requestAnimationFrame = (callback) => {
|
|
82
107
|
return self._raf(() => {
|
|
108
|
+
if (self.speed === 0) {
|
|
109
|
+
window.requestAnimationFrame(callback);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
83
112
|
callback(self.getVirtualTime());
|
|
84
113
|
});
|
|
85
114
|
};
|
|
86
115
|
window.cancelAnimationFrame = this._caf;
|
|
87
116
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
88
|
-
const scaledDelay = (delay ?? 0) /
|
|
117
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
89
118
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
90
119
|
});
|
|
91
120
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -93,7 +122,7 @@ var TimingController = class {
|
|
|
93
122
|
const id = self.nextIntervalId++;
|
|
94
123
|
const baseDelay = delay ?? 0;
|
|
95
124
|
function tick() {
|
|
96
|
-
const scaledDelay = baseDelay /
|
|
125
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
97
126
|
const realId = self._setTimeout(() => {
|
|
98
127
|
if (typeof handler === "function") {
|
|
99
128
|
;
|
|
@@ -118,61 +147,99 @@ var TimingController = class {
|
|
|
118
147
|
self._clearInterval(id);
|
|
119
148
|
}
|
|
120
149
|
});
|
|
121
|
-
this.
|
|
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
|
-
}
|
|
150
|
+
this.startAnimationPoll();
|
|
133
151
|
}
|
|
134
152
|
/** Set playback speed. Requires install() first. */
|
|
135
153
|
setSpeed(newSpeed) {
|
|
136
154
|
if (!this.installed) this.install();
|
|
137
155
|
this.reanchor();
|
|
138
156
|
this.speed = newSpeed;
|
|
139
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
140
|
-
;
|
|
141
|
-
el.playbackRate = newSpeed;
|
|
142
|
-
});
|
|
143
157
|
this.patchAnimations();
|
|
158
|
+
this.patchMedia();
|
|
159
|
+
this.patchGSAP();
|
|
160
|
+
}
|
|
161
|
+
getSpeed() {
|
|
162
|
+
return this.speed;
|
|
144
163
|
}
|
|
145
|
-
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Animation polling — per-frame via original rAF
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
startAnimationPoll() {
|
|
168
|
+
const poll = () => {
|
|
169
|
+
if (!this.installed) return;
|
|
170
|
+
this.patchAnimations();
|
|
171
|
+
this.patchMedia();
|
|
172
|
+
this.animPollId = this._raf(poll);
|
|
173
|
+
};
|
|
174
|
+
this.animPollId = this._raf(poll);
|
|
175
|
+
}
|
|
176
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
146
177
|
patchAnimations() {
|
|
147
178
|
try {
|
|
148
179
|
const anims = document.getAnimations();
|
|
149
180
|
for (const anim of anims) {
|
|
150
181
|
const target = anim.effect?.target;
|
|
151
182
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
152
|
-
|
|
183
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
184
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
185
|
+
let tracked = this.trackedAnims.get(anim);
|
|
186
|
+
if (!tracked) {
|
|
187
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
188
|
+
this.trackedAnims.set(anim, tracked);
|
|
189
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
190
|
+
tracked.original = anim.playbackRate;
|
|
191
|
+
}
|
|
192
|
+
const desired = tracked.original * effectiveSpeed;
|
|
193
|
+
if (anim.playbackRate !== desired) {
|
|
194
|
+
anim.playbackRate = desired;
|
|
195
|
+
tracked.applied = desired;
|
|
196
|
+
}
|
|
153
197
|
}
|
|
154
198
|
} catch {
|
|
155
199
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
200
|
+
}
|
|
201
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
202
|
+
patchMedia() {
|
|
203
|
+
try {
|
|
204
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
205
|
+
const el = node;
|
|
206
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
207
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
208
|
+
let tracked = this.trackedMedia.get(el);
|
|
209
|
+
if (!tracked) {
|
|
210
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
211
|
+
this.trackedMedia.set(el, tracked);
|
|
212
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
213
|
+
tracked.original = el.playbackRate;
|
|
169
214
|
}
|
|
170
|
-
|
|
215
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
216
|
+
if (el.playbackRate !== desired) {
|
|
217
|
+
el.playbackRate = desired;
|
|
218
|
+
tracked.applied = desired;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
171
222
|
}
|
|
172
223
|
}
|
|
173
|
-
|
|
174
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Register a GSAP instance (for ES-module imports where window.gsap is
|
|
226
|
+
* undefined). Applies the current timeScale immediately if speed !== 1.
|
|
227
|
+
*/
|
|
228
|
+
registerGSAP(gsap) {
|
|
229
|
+
this.gsapInstance = gsap;
|
|
230
|
+
if (this.speed !== 1) this.patchGSAP();
|
|
231
|
+
}
|
|
232
|
+
/** Sync GSAP's global timeline if present. */
|
|
233
|
+
patchGSAP() {
|
|
234
|
+
try {
|
|
235
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
236
|
+
gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
175
239
|
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Cleanup
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
176
243
|
/** Restore all patched APIs to originals. */
|
|
177
244
|
destroy() {
|
|
178
245
|
if (!this.installed) return;
|
|
@@ -192,19 +259,33 @@ var TimingController = class {
|
|
|
192
259
|
Element.prototype.animate = this._origAnimate;
|
|
193
260
|
this._origAnimate = null;
|
|
194
261
|
}
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this._clearInterval(this.animObserver);
|
|
199
|
-
this.animObserver = null;
|
|
262
|
+
if (this.animPollId) {
|
|
263
|
+
this._caf(this.animPollId);
|
|
264
|
+
this.animPollId = 0;
|
|
200
265
|
}
|
|
201
266
|
try {
|
|
202
267
|
for (const anim of document.getAnimations()) {
|
|
203
|
-
|
|
268
|
+
const tracked = this.trackedAnims.get(anim);
|
|
269
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
204
270
|
}
|
|
205
271
|
} catch {
|
|
206
272
|
}
|
|
273
|
+
try {
|
|
274
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
275
|
+
const el = node;
|
|
276
|
+
const tracked = this.trackedMedia.get(el);
|
|
277
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
278
|
+
});
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
283
|
+
gsap?.globalTimeline?.timeScale(1);
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
this.gsapInstance = null;
|
|
207
287
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
288
|
+
delete window.__saccadeInstalled;
|
|
208
289
|
this.installed = false;
|
|
209
290
|
}
|
|
210
291
|
};
|
|
@@ -274,6 +355,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
274
355
|
"data-hover",
|
|
275
356
|
"data-at-boundary",
|
|
276
357
|
"data-scrubbing",
|
|
358
|
+
"data-starting-style",
|
|
359
|
+
"data-ending-style",
|
|
360
|
+
"data-panel-open",
|
|
361
|
+
"data-hidden",
|
|
277
362
|
"aria-checked",
|
|
278
363
|
"aria-selected",
|
|
279
364
|
"aria-expanded",
|
|
@@ -287,6 +372,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
287
372
|
"checked",
|
|
288
373
|
"disabled",
|
|
289
374
|
"hidden",
|
|
375
|
+
"inert",
|
|
290
376
|
"value",
|
|
291
377
|
"class",
|
|
292
378
|
"style"
|
|
@@ -418,6 +504,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
418
504
|
// ---- WAAPI interception --------------------------------------------------
|
|
419
505
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
420
506
|
this.interceptedAnimations = [];
|
|
507
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
508
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
421
509
|
this.hiddenSince = null;
|
|
422
510
|
this.onVisibilityChange = null;
|
|
423
511
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -468,6 +556,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
468
556
|
this.portalIdCounter = 0;
|
|
469
557
|
this.currentPortalIds.clear();
|
|
470
558
|
this.capturedPortals.clear();
|
|
559
|
+
this.seekableClones.clear();
|
|
471
560
|
this.prevInlineStyles.clear();
|
|
472
561
|
this.jsAnimStartTimes.clear();
|
|
473
562
|
this.jsAnimLastSeen.clear();
|
|
@@ -821,6 +910,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
821
910
|
} catch (_) {
|
|
822
911
|
}
|
|
823
912
|
}
|
|
913
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
914
|
+
const clean = {};
|
|
915
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
916
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
917
|
+
}
|
|
918
|
+
return clean;
|
|
919
|
+
});
|
|
824
920
|
this.animations.set(id, {
|
|
825
921
|
id,
|
|
826
922
|
name,
|
|
@@ -832,7 +928,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
832
928
|
type,
|
|
833
929
|
source,
|
|
834
930
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
835
|
-
conflicts
|
|
931
|
+
conflicts,
|
|
932
|
+
rawKeyframes: cleanedKeyframes,
|
|
933
|
+
rawTiming: { ...timing, fill: "both" }
|
|
836
934
|
});
|
|
837
935
|
}
|
|
838
936
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1108,38 +1206,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1108
1206
|
} catch (_) {
|
|
1109
1207
|
}
|
|
1110
1208
|
}, 0);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
;
|
|
1134
|
-
el.value = value;
|
|
1135
|
-
} else if (value == null) {
|
|
1136
|
-
el.removeAttribute(attr);
|
|
1137
|
-
} else {
|
|
1138
|
-
el.setAttribute(attr, value);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1209
|
+
this.seekableClones.clear();
|
|
1210
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1211
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1212
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1213
|
+
const firstColon = animId.indexOf(":");
|
|
1214
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1215
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1216
|
+
const el = this.elements.get(elSelector);
|
|
1217
|
+
if (!el?.isConnected) continue;
|
|
1218
|
+
try {
|
|
1219
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1220
|
+
...animInfo.rawTiming,
|
|
1221
|
+
fill: "both"
|
|
1222
|
+
});
|
|
1223
|
+
clone.pause();
|
|
1224
|
+
clone.currentTime = 0;
|
|
1225
|
+
this.seekableClones.set(animId, {
|
|
1226
|
+
animation: clone,
|
|
1227
|
+
element: el,
|
|
1228
|
+
effect: clone.effect
|
|
1229
|
+
});
|
|
1230
|
+
} catch (_) {
|
|
1143
1231
|
}
|
|
1144
1232
|
}
|
|
1145
1233
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1162,6 +1250,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1162
1250
|
// src/core/scrubber.ts
|
|
1163
1251
|
var TimelineScrubber = class {
|
|
1164
1252
|
constructor(state) {
|
|
1253
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1254
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1165
1255
|
/** Saved originals for restore on release */
|
|
1166
1256
|
this._originalAnimate = null;
|
|
1167
1257
|
this._originalRaf = null;
|
|
@@ -1171,36 +1261,24 @@ var TimelineScrubber = class {
|
|
|
1171
1261
|
this.frames = state.frames;
|
|
1172
1262
|
this.capturedPortals = state.capturedPortals;
|
|
1173
1263
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1174
|
-
this.
|
|
1264
|
+
this.seekableClones = state.seekableClones;
|
|
1175
1265
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1176
1266
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1177
1267
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1178
1268
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
let current = el;
|
|
1188
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1189
|
-
const tag = current.tagName.toLowerCase();
|
|
1190
|
-
const parent = current.parentElement;
|
|
1191
|
-
if (parent) {
|
|
1192
|
-
const siblings = Array.from(parent.children);
|
|
1193
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1194
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1195
|
-
} else {
|
|
1196
|
-
parts.unshift(tag);
|
|
1269
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1270
|
+
for (const fa of this.frames[i].animations) {
|
|
1271
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1272
|
+
if (!range) {
|
|
1273
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1274
|
+
} else {
|
|
1275
|
+
range.last = i;
|
|
1276
|
+
}
|
|
1197
1277
|
}
|
|
1198
|
-
current = parent;
|
|
1199
1278
|
}
|
|
1200
|
-
return parts.join(" > ");
|
|
1201
1279
|
}
|
|
1202
1280
|
// ---------------------------------------------------------------------------
|
|
1203
|
-
// seekTo — scrub
|
|
1281
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1204
1282
|
// ---------------------------------------------------------------------------
|
|
1205
1283
|
seekTo(timeMs) {
|
|
1206
1284
|
if (!this.frames.length) return;
|
|
@@ -1248,6 +1326,34 @@ var TimelineScrubber = class {
|
|
|
1248
1326
|
}
|
|
1249
1327
|
}
|
|
1250
1328
|
}
|
|
1329
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1330
|
+
for (const fa of frame.animations || []) {
|
|
1331
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1332
|
+
}
|
|
1333
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1334
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1335
|
+
try {
|
|
1336
|
+
if (frameAnim) {
|
|
1337
|
+
if (!clone.animation.effect) {
|
|
1338
|
+
clone.animation.effect = clone.effect;
|
|
1339
|
+
}
|
|
1340
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1341
|
+
} else {
|
|
1342
|
+
const range = this.animFrameRanges.get(animId);
|
|
1343
|
+
if (!range || lo < range.first) {
|
|
1344
|
+
clone.animation.effect = null;
|
|
1345
|
+
} else {
|
|
1346
|
+
if (!clone.animation.effect) {
|
|
1347
|
+
clone.animation.effect = clone.effect;
|
|
1348
|
+
}
|
|
1349
|
+
const timing = clone.effect.getTiming();
|
|
1350
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1351
|
+
clone.animation.currentTime = endTime;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1251
1357
|
for (const entry of this.interceptedAnimations) {
|
|
1252
1358
|
try {
|
|
1253
1359
|
const anim = entry.animation;
|
|
@@ -1262,34 +1368,13 @@ var TimelineScrubber = class {
|
|
|
1262
1368
|
const el = this.elements.get(sel);
|
|
1263
1369
|
if (!el || !el.isConnected) continue;
|
|
1264
1370
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1265
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1266
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1267
|
-
);
|
|
1268
1371
|
const snapTyped = snap;
|
|
1269
|
-
if (snapTyped.__styles) {
|
|
1270
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1271
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1272
|
-
el.style.setProperty(prop, value, "important");
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
1372
|
if (snapTyped.__attrs) {
|
|
1277
1373
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1374
|
+
if (attr === "class" || attr === "style") continue;
|
|
1278
1375
|
if (attr === "checked") {
|
|
1279
1376
|
;
|
|
1280
1377
|
el.checked = value === "true";
|
|
1281
|
-
} else if (attr === "class") {
|
|
1282
|
-
if (value != null) el.className = value;
|
|
1283
|
-
} else if (attr === "style") {
|
|
1284
|
-
if (value) {
|
|
1285
|
-
el.setAttribute("style", value);
|
|
1286
|
-
el.style.transition = "none";
|
|
1287
|
-
if (snapTyped.__styles) {
|
|
1288
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1289
|
-
el.style.setProperty(prop, val, "important");
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
1378
|
} else if (attr === "value") {
|
|
1294
1379
|
if (value != null) el.value = value;
|
|
1295
1380
|
} else if (value == null) {
|
|
@@ -1300,39 +1385,31 @@ var TimelineScrubber = class {
|
|
|
1300
1385
|
}
|
|
1301
1386
|
}
|
|
1302
1387
|
}
|
|
1303
|
-
for (const
|
|
1304
|
-
|
|
1305
|
-
const
|
|
1306
|
-
const
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1388
|
+
for (const fa of frame.animations || []) {
|
|
1389
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1390
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1391
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1392
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1393
|
+
const el = this.elements.get(elSel);
|
|
1394
|
+
if (!el || !el.isConnected) continue;
|
|
1395
|
+
for (const prop of fa.properties) {
|
|
1310
1396
|
if (prop.value) {
|
|
1311
|
-
|
|
1397
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1312
1398
|
}
|
|
1313
1399
|
}
|
|
1314
1400
|
}
|
|
1315
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1316
|
-
for (const anim of frame.animations || []) {
|
|
1317
|
-
const fc = anim.animationId.indexOf(":");
|
|
1318
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1319
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1320
|
-
}
|
|
1321
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1322
|
-
const el = rawEl;
|
|
1323
|
-
const sel = this.getSelector(el);
|
|
1324
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1325
|
-
el.style.removeProperty("opacity");
|
|
1326
|
-
el.style.removeProperty("transform");
|
|
1327
|
-
el.style.removeProperty("filter");
|
|
1328
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1329
|
-
}
|
|
1330
|
-
});
|
|
1331
1401
|
}
|
|
1332
1402
|
// ---------------------------------------------------------------------------
|
|
1333
1403
|
// release — tear down all scrub state and restore the page to normal
|
|
1334
1404
|
// ---------------------------------------------------------------------------
|
|
1335
1405
|
release() {
|
|
1406
|
+
for (const [, clone] of this.seekableClones) {
|
|
1407
|
+
try {
|
|
1408
|
+
clone.animation.cancel();
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
this.seekableClones.clear();
|
|
1336
1413
|
for (const entry of this.interceptedAnimations) {
|
|
1337
1414
|
try {
|
|
1338
1415
|
entry.animation.cancel();
|
|
@@ -1394,6 +1471,7 @@ var TimelineScrubber = class {
|
|
|
1394
1471
|
this.elements.clear();
|
|
1395
1472
|
this.frames.length = 0;
|
|
1396
1473
|
this.capturedPortals.clear();
|
|
1474
|
+
this.animFrameRanges.clear();
|
|
1397
1475
|
}
|
|
1398
1476
|
};
|
|
1399
1477
|
|
|
@@ -1480,7 +1558,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
|
|
|
1480
1558
|
animations: animExports
|
|
1481
1559
|
};
|
|
1482
1560
|
}
|
|
1483
|
-
function formatExportForLLM(exp, detail = "
|
|
1561
|
+
function formatExportForLLM(exp, detail = "moderate") {
|
|
1484
1562
|
const lines = [];
|
|
1485
1563
|
const grouped = /* @__PURE__ */ new Map();
|
|
1486
1564
|
for (const anim of exp.animations) {
|
|
@@ -1495,7 +1573,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1495
1573
|
const [from, to] = prop.range.split(" \u2192 ");
|
|
1496
1574
|
return !(from && to && from.trim() === to.trim());
|
|
1497
1575
|
}
|
|
1498
|
-
if (detail === "
|
|
1576
|
+
if (detail === "brief") {
|
|
1499
1577
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1500
1578
|
lines.push("");
|
|
1501
1579
|
for (const [, group] of grouped) {
|
|
@@ -1520,7 +1598,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1520
1598
|
}
|
|
1521
1599
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1522
1600
|
lines.push("");
|
|
1523
|
-
if (detail === "
|
|
1601
|
+
if (detail === "granular") {
|
|
1524
1602
|
lines.push("**Environment:**");
|
|
1525
1603
|
lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
|
|
1526
1604
|
lines.push(`- URL: ${window.location.href}`);
|
|
@@ -1587,7 +1665,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1587
1665
|
lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
|
|
1588
1666
|
lines.push("");
|
|
1589
1667
|
for (const line of cssPropLines) lines.push(line);
|
|
1590
|
-
if (detail === "detailed" || detail === "
|
|
1668
|
+
if (detail === "detailed" || detail === "granular") {
|
|
1591
1669
|
const allVars = {};
|
|
1592
1670
|
for (const anim of cssAnims) {
|
|
1593
1671
|
if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
|
|
@@ -1642,6 +1720,22 @@ var SaccadeEngine = class {
|
|
|
1642
1720
|
getSpeed() {
|
|
1643
1721
|
return this.timing.getSpeed();
|
|
1644
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Install the timing patches immediately, without changing speed.
|
|
1725
|
+
*
|
|
1726
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
1727
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
1728
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
1729
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
1730
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
1731
|
+
*/
|
|
1732
|
+
install() {
|
|
1733
|
+
this.timing.install();
|
|
1734
|
+
}
|
|
1735
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
1736
|
+
registerGSAP(gsap) {
|
|
1737
|
+
this.timing.registerGSAP(gsap);
|
|
1738
|
+
}
|
|
1645
1739
|
// -- Timeline recording ---------------------------------------------------
|
|
1646
1740
|
startRecording(boundingBox) {
|
|
1647
1741
|
if (this._state !== "idle") return;
|
|
@@ -1666,7 +1760,7 @@ var SaccadeEngine = class {
|
|
|
1666
1760
|
try {
|
|
1667
1761
|
capture = this.recorder.stopRecording();
|
|
1668
1762
|
} catch (e) {
|
|
1669
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1763
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1670
1764
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1671
1765
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1672
1766
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1687,7 +1781,8 @@ var SaccadeEngine = class {
|
|
|
1687
1781
|
frames: capture.frames,
|
|
1688
1782
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1689
1783
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1690
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1784
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1785
|
+
seekableClones: this.recorder.seekableClones
|
|
1691
1786
|
};
|
|
1692
1787
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1693
1788
|
this._state = "scrubbing";
|
|
@@ -1715,7 +1810,7 @@ var SaccadeEngine = class {
|
|
|
1715
1810
|
filter
|
|
1716
1811
|
);
|
|
1717
1812
|
}
|
|
1718
|
-
exportForLLM(timeMs, filter = "active", detail = "
|
|
1813
|
+
exportForLLM(timeMs, filter = "active", detail = "moderate") {
|
|
1719
1814
|
const exp = this.generateExport(timeMs, filter);
|
|
1720
1815
|
if (!exp) return "";
|
|
1721
1816
|
return formatExportForLLM(exp, detail);
|
|
@@ -1739,6 +1834,19 @@ var SaccadeEngine = class {
|
|
|
1739
1834
|
this._state = "idle";
|
|
1740
1835
|
}
|
|
1741
1836
|
};
|
|
1837
|
+
|
|
1838
|
+
// src/core/shared.ts
|
|
1839
|
+
var KEY = "__saccadeSharedEngine__";
|
|
1840
|
+
function getSharedEngine() {
|
|
1841
|
+
const g = globalThis;
|
|
1842
|
+
if (!g[KEY]) g[KEY] = new SaccadeEngine();
|
|
1843
|
+
return g[KEY];
|
|
1844
|
+
}
|
|
1845
|
+
function resetSharedEngine() {
|
|
1846
|
+
const g = globalThis;
|
|
1847
|
+
g[KEY]?.destroy();
|
|
1848
|
+
g[KEY] = null;
|
|
1849
|
+
}
|
|
1742
1850
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1743
1851
|
0 && (module.exports = {
|
|
1744
1852
|
SaccadeEngine,
|
|
@@ -1747,6 +1855,8 @@ var SaccadeEngine = class {
|
|
|
1747
1855
|
TimingController,
|
|
1748
1856
|
formatExportForLLM,
|
|
1749
1857
|
generateExport,
|
|
1750
|
-
getFrameAtTime
|
|
1858
|
+
getFrameAtTime,
|
|
1859
|
+
getSharedEngine,
|
|
1860
|
+
resetSharedEngine
|
|
1751
1861
|
});
|
|
1752
1862
|
//# sourceMappingURL=core.cjs.map
|