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.mjs
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// src/core/timing.ts
|
|
2
|
+
var MEDIA_RATE_MIN = 0.0625;
|
|
3
|
+
var MEDIA_RATE_MAX = 16;
|
|
4
|
+
var ZERO_SPEED_DIVISOR = 1e-4;
|
|
2
5
|
var TimingController = class {
|
|
3
6
|
constructor() {
|
|
4
7
|
this.speed = 1;
|
|
5
8
|
this.virtualBaseline = 0;
|
|
6
9
|
this.intervalMap = /* @__PURE__ */ new Map();
|
|
7
10
|
this.nextIntervalId = 1e6;
|
|
8
|
-
this.mediaObserver = null;
|
|
9
|
-
this.animObserver = null;
|
|
10
11
|
this._origAnimate = null;
|
|
11
12
|
this.installed = false;
|
|
13
|
+
this.animPollId = 0;
|
|
14
|
+
this.gsapInstance = null;
|
|
15
|
+
// WeakMap tracking for animations and media
|
|
16
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
12
18
|
this._raf = requestAnimationFrame.bind(window);
|
|
13
19
|
this._caf = cancelAnimationFrame.bind(window);
|
|
14
20
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -18,19 +24,34 @@ var TimingController = class {
|
|
|
18
24
|
this._perfNow = performance.now.bind(performance);
|
|
19
25
|
this._dateNow = Date.now;
|
|
20
26
|
this.realBaseline = this._perfNow();
|
|
27
|
+
this.dateRealBaseline = this._dateNow();
|
|
28
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
21
29
|
}
|
|
22
30
|
getVirtualTime() {
|
|
23
31
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
24
32
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
25
33
|
}
|
|
34
|
+
getVirtualDateNow() {
|
|
35
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
36
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
37
|
+
}
|
|
26
38
|
reanchor() {
|
|
27
39
|
const virtualNow = this.getVirtualTime();
|
|
28
40
|
this.realBaseline = this._perfNow();
|
|
29
41
|
this.virtualBaseline = virtualNow;
|
|
42
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
43
|
+
this.dateRealBaseline = this._dateNow();
|
|
44
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
45
|
+
}
|
|
46
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
47
|
+
get speedDivisor() {
|
|
48
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
30
49
|
}
|
|
31
50
|
/** Install timing patches. Safe to call multiple times. */
|
|
32
51
|
install() {
|
|
33
52
|
if (this.installed) return;
|
|
53
|
+
if (window.__saccadeInstalled) return;
|
|
54
|
+
window.__saccadeInstalled = true;
|
|
34
55
|
this.installed = true;
|
|
35
56
|
const self = this;
|
|
36
57
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -39,21 +60,27 @@ var TimingController = class {
|
|
|
39
60
|
Element.prototype.animate = function(...args) {
|
|
40
61
|
const anim = origAnimate.apply(this, args);
|
|
41
62
|
if (self.speed !== 1) {
|
|
42
|
-
|
|
63
|
+
const originalRate = anim.playbackRate;
|
|
64
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
65
|
+
anim.playbackRate = applied;
|
|
66
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
43
67
|
}
|
|
44
68
|
return anim;
|
|
45
69
|
};
|
|
46
70
|
performance.now = () => self.getVirtualTime();
|
|
47
|
-
|
|
48
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
71
|
+
Date.now = () => self.getVirtualDateNow();
|
|
49
72
|
window.requestAnimationFrame = (callback) => {
|
|
50
73
|
return self._raf(() => {
|
|
74
|
+
if (self.speed === 0) {
|
|
75
|
+
window.requestAnimationFrame(callback);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
51
78
|
callback(self.getVirtualTime());
|
|
52
79
|
});
|
|
53
80
|
};
|
|
54
81
|
window.cancelAnimationFrame = this._caf;
|
|
55
82
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
56
|
-
const scaledDelay = (delay ?? 0) /
|
|
83
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
57
84
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
58
85
|
});
|
|
59
86
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -61,7 +88,7 @@ var TimingController = class {
|
|
|
61
88
|
const id = self.nextIntervalId++;
|
|
62
89
|
const baseDelay = delay ?? 0;
|
|
63
90
|
function tick() {
|
|
64
|
-
const scaledDelay = baseDelay /
|
|
91
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
65
92
|
const realId = self._setTimeout(() => {
|
|
66
93
|
if (typeof handler === "function") {
|
|
67
94
|
;
|
|
@@ -86,61 +113,99 @@ var TimingController = class {
|
|
|
86
113
|
self._clearInterval(id);
|
|
87
114
|
}
|
|
88
115
|
});
|
|
89
|
-
this.
|
|
90
|
-
for (const mutation of mutations) {
|
|
91
|
-
for (const node of mutation.addedNodes) {
|
|
92
|
-
if (node instanceof HTMLVideoElement || node instanceof HTMLAudioElement) {
|
|
93
|
-
node.playbackRate = self.speed;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
if (document.body) {
|
|
99
|
-
this.mediaObserver.observe(document.body, { childList: true, subtree: true });
|
|
100
|
-
}
|
|
116
|
+
this.startAnimationPoll();
|
|
101
117
|
}
|
|
102
118
|
/** Set playback speed. Requires install() first. */
|
|
103
119
|
setSpeed(newSpeed) {
|
|
104
120
|
if (!this.installed) this.install();
|
|
105
121
|
this.reanchor();
|
|
106
122
|
this.speed = newSpeed;
|
|
107
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
108
|
-
;
|
|
109
|
-
el.playbackRate = newSpeed;
|
|
110
|
-
});
|
|
111
123
|
this.patchAnimations();
|
|
124
|
+
this.patchMedia();
|
|
125
|
+
this.patchGSAP();
|
|
126
|
+
}
|
|
127
|
+
getSpeed() {
|
|
128
|
+
return this.speed;
|
|
112
129
|
}
|
|
113
|
-
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Animation polling — per-frame via original rAF
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
startAnimationPoll() {
|
|
134
|
+
const poll = () => {
|
|
135
|
+
if (!this.installed) return;
|
|
136
|
+
this.patchAnimations();
|
|
137
|
+
this.patchMedia();
|
|
138
|
+
this.animPollId = this._raf(poll);
|
|
139
|
+
};
|
|
140
|
+
this.animPollId = this._raf(poll);
|
|
141
|
+
}
|
|
142
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
114
143
|
patchAnimations() {
|
|
115
144
|
try {
|
|
116
145
|
const anims = document.getAnimations();
|
|
117
146
|
for (const anim of anims) {
|
|
118
147
|
const target = anim.effect?.target;
|
|
119
148
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
120
|
-
|
|
149
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
150
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
151
|
+
let tracked = this.trackedAnims.get(anim);
|
|
152
|
+
if (!tracked) {
|
|
153
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
154
|
+
this.trackedAnims.set(anim, tracked);
|
|
155
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
156
|
+
tracked.original = anim.playbackRate;
|
|
157
|
+
}
|
|
158
|
+
const desired = tracked.original * effectiveSpeed;
|
|
159
|
+
if (anim.playbackRate !== desired) {
|
|
160
|
+
anim.playbackRate = desired;
|
|
161
|
+
tracked.applied = desired;
|
|
162
|
+
}
|
|
121
163
|
}
|
|
122
164
|
} catch {
|
|
123
165
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
166
|
+
}
|
|
167
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
168
|
+
patchMedia() {
|
|
169
|
+
try {
|
|
170
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
171
|
+
const el = node;
|
|
172
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
173
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
174
|
+
let tracked = this.trackedMedia.get(el);
|
|
175
|
+
if (!tracked) {
|
|
176
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
177
|
+
this.trackedMedia.set(el, tracked);
|
|
178
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
179
|
+
tracked.original = el.playbackRate;
|
|
137
180
|
}
|
|
138
|
-
|
|
181
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
182
|
+
if (el.playbackRate !== desired) {
|
|
183
|
+
el.playbackRate = desired;
|
|
184
|
+
tracked.applied = desired;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
139
188
|
}
|
|
140
189
|
}
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Register a GSAP instance (for ES-module imports where window.gsap is
|
|
192
|
+
* undefined). Applies the current timeScale immediately if speed !== 1.
|
|
193
|
+
*/
|
|
194
|
+
registerGSAP(gsap) {
|
|
195
|
+
this.gsapInstance = gsap;
|
|
196
|
+
if (this.speed !== 1) this.patchGSAP();
|
|
197
|
+
}
|
|
198
|
+
/** Sync GSAP's global timeline if present. */
|
|
199
|
+
patchGSAP() {
|
|
200
|
+
try {
|
|
201
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
202
|
+
gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
143
205
|
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Cleanup
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
144
209
|
/** Restore all patched APIs to originals. */
|
|
145
210
|
destroy() {
|
|
146
211
|
if (!this.installed) return;
|
|
@@ -160,19 +225,33 @@ var TimingController = class {
|
|
|
160
225
|
Element.prototype.animate = this._origAnimate;
|
|
161
226
|
this._origAnimate = null;
|
|
162
227
|
}
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this._clearInterval(this.animObserver);
|
|
167
|
-
this.animObserver = null;
|
|
228
|
+
if (this.animPollId) {
|
|
229
|
+
this._caf(this.animPollId);
|
|
230
|
+
this.animPollId = 0;
|
|
168
231
|
}
|
|
169
232
|
try {
|
|
170
233
|
for (const anim of document.getAnimations()) {
|
|
171
|
-
|
|
234
|
+
const tracked = this.trackedAnims.get(anim);
|
|
235
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
172
236
|
}
|
|
173
237
|
} catch {
|
|
174
238
|
}
|
|
239
|
+
try {
|
|
240
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
241
|
+
const el = node;
|
|
242
|
+
const tracked = this.trackedMedia.get(el);
|
|
243
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
244
|
+
});
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
249
|
+
gsap?.globalTimeline?.timeScale(1);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
this.gsapInstance = null;
|
|
175
253
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
254
|
+
delete window.__saccadeInstalled;
|
|
176
255
|
this.installed = false;
|
|
177
256
|
}
|
|
178
257
|
};
|
|
@@ -242,6 +321,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
242
321
|
"data-hover",
|
|
243
322
|
"data-at-boundary",
|
|
244
323
|
"data-scrubbing",
|
|
324
|
+
"data-starting-style",
|
|
325
|
+
"data-ending-style",
|
|
326
|
+
"data-panel-open",
|
|
327
|
+
"data-hidden",
|
|
245
328
|
"aria-checked",
|
|
246
329
|
"aria-selected",
|
|
247
330
|
"aria-expanded",
|
|
@@ -255,6 +338,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
255
338
|
"checked",
|
|
256
339
|
"disabled",
|
|
257
340
|
"hidden",
|
|
341
|
+
"inert",
|
|
258
342
|
"value",
|
|
259
343
|
"class",
|
|
260
344
|
"style"
|
|
@@ -386,6 +470,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
386
470
|
// ---- WAAPI interception --------------------------------------------------
|
|
387
471
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
388
472
|
this.interceptedAnimations = [];
|
|
473
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
474
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
389
475
|
this.hiddenSince = null;
|
|
390
476
|
this.onVisibilityChange = null;
|
|
391
477
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -436,6 +522,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
436
522
|
this.portalIdCounter = 0;
|
|
437
523
|
this.currentPortalIds.clear();
|
|
438
524
|
this.capturedPortals.clear();
|
|
525
|
+
this.seekableClones.clear();
|
|
439
526
|
this.prevInlineStyles.clear();
|
|
440
527
|
this.jsAnimStartTimes.clear();
|
|
441
528
|
this.jsAnimLastSeen.clear();
|
|
@@ -789,6 +876,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
789
876
|
} catch (_) {
|
|
790
877
|
}
|
|
791
878
|
}
|
|
879
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
880
|
+
const clean = {};
|
|
881
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
882
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
883
|
+
}
|
|
884
|
+
return clean;
|
|
885
|
+
});
|
|
792
886
|
this.animations.set(id, {
|
|
793
887
|
id,
|
|
794
888
|
name,
|
|
@@ -800,7 +894,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
800
894
|
type,
|
|
801
895
|
source,
|
|
802
896
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
803
|
-
conflicts
|
|
897
|
+
conflicts,
|
|
898
|
+
rawKeyframes: cleanedKeyframes,
|
|
899
|
+
rawTiming: { ...timing, fill: "both" }
|
|
804
900
|
});
|
|
805
901
|
}
|
|
806
902
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1076,38 +1172,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1076
1172
|
} catch (_) {
|
|
1077
1173
|
}
|
|
1078
1174
|
}, 0);
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
;
|
|
1102
|
-
el.value = value;
|
|
1103
|
-
} else if (value == null) {
|
|
1104
|
-
el.removeAttribute(attr);
|
|
1105
|
-
} else {
|
|
1106
|
-
el.setAttribute(attr, value);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1175
|
+
this.seekableClones.clear();
|
|
1176
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1177
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1178
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1179
|
+
const firstColon = animId.indexOf(":");
|
|
1180
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1181
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1182
|
+
const el = this.elements.get(elSelector);
|
|
1183
|
+
if (!el?.isConnected) continue;
|
|
1184
|
+
try {
|
|
1185
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1186
|
+
...animInfo.rawTiming,
|
|
1187
|
+
fill: "both"
|
|
1188
|
+
});
|
|
1189
|
+
clone.pause();
|
|
1190
|
+
clone.currentTime = 0;
|
|
1191
|
+
this.seekableClones.set(animId, {
|
|
1192
|
+
animation: clone,
|
|
1193
|
+
element: el,
|
|
1194
|
+
effect: clone.effect
|
|
1195
|
+
});
|
|
1196
|
+
} catch (_) {
|
|
1111
1197
|
}
|
|
1112
1198
|
}
|
|
1113
1199
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1130,6 +1216,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1130
1216
|
// src/core/scrubber.ts
|
|
1131
1217
|
var TimelineScrubber = class {
|
|
1132
1218
|
constructor(state) {
|
|
1219
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1220
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1133
1221
|
/** Saved originals for restore on release */
|
|
1134
1222
|
this._originalAnimate = null;
|
|
1135
1223
|
this._originalRaf = null;
|
|
@@ -1139,36 +1227,24 @@ var TimelineScrubber = class {
|
|
|
1139
1227
|
this.frames = state.frames;
|
|
1140
1228
|
this.capturedPortals = state.capturedPortals;
|
|
1141
1229
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1142
|
-
this.
|
|
1230
|
+
this.seekableClones = state.seekableClones;
|
|
1143
1231
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1144
1232
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1145
1233
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1146
1234
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
let current = el;
|
|
1156
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1157
|
-
const tag = current.tagName.toLowerCase();
|
|
1158
|
-
const parent = current.parentElement;
|
|
1159
|
-
if (parent) {
|
|
1160
|
-
const siblings = Array.from(parent.children);
|
|
1161
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1162
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1163
|
-
} else {
|
|
1164
|
-
parts.unshift(tag);
|
|
1235
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1236
|
+
for (const fa of this.frames[i].animations) {
|
|
1237
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1238
|
+
if (!range) {
|
|
1239
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1240
|
+
} else {
|
|
1241
|
+
range.last = i;
|
|
1242
|
+
}
|
|
1165
1243
|
}
|
|
1166
|
-
current = parent;
|
|
1167
1244
|
}
|
|
1168
|
-
return parts.join(" > ");
|
|
1169
1245
|
}
|
|
1170
1246
|
// ---------------------------------------------------------------------------
|
|
1171
|
-
// seekTo — scrub
|
|
1247
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1172
1248
|
// ---------------------------------------------------------------------------
|
|
1173
1249
|
seekTo(timeMs) {
|
|
1174
1250
|
if (!this.frames.length) return;
|
|
@@ -1216,6 +1292,34 @@ var TimelineScrubber = class {
|
|
|
1216
1292
|
}
|
|
1217
1293
|
}
|
|
1218
1294
|
}
|
|
1295
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1296
|
+
for (const fa of frame.animations || []) {
|
|
1297
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1298
|
+
}
|
|
1299
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1300
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1301
|
+
try {
|
|
1302
|
+
if (frameAnim) {
|
|
1303
|
+
if (!clone.animation.effect) {
|
|
1304
|
+
clone.animation.effect = clone.effect;
|
|
1305
|
+
}
|
|
1306
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1307
|
+
} else {
|
|
1308
|
+
const range = this.animFrameRanges.get(animId);
|
|
1309
|
+
if (!range || lo < range.first) {
|
|
1310
|
+
clone.animation.effect = null;
|
|
1311
|
+
} else {
|
|
1312
|
+
if (!clone.animation.effect) {
|
|
1313
|
+
clone.animation.effect = clone.effect;
|
|
1314
|
+
}
|
|
1315
|
+
const timing = clone.effect.getTiming();
|
|
1316
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1317
|
+
clone.animation.currentTime = endTime;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1219
1323
|
for (const entry of this.interceptedAnimations) {
|
|
1220
1324
|
try {
|
|
1221
1325
|
const anim = entry.animation;
|
|
@@ -1230,34 +1334,13 @@ var TimelineScrubber = class {
|
|
|
1230
1334
|
const el = this.elements.get(sel);
|
|
1231
1335
|
if (!el || !el.isConnected) continue;
|
|
1232
1336
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1233
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1234
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1235
|
-
);
|
|
1236
1337
|
const snapTyped = snap;
|
|
1237
|
-
if (snapTyped.__styles) {
|
|
1238
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1239
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1240
|
-
el.style.setProperty(prop, value, "important");
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
1338
|
if (snapTyped.__attrs) {
|
|
1245
1339
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1340
|
+
if (attr === "class" || attr === "style") continue;
|
|
1246
1341
|
if (attr === "checked") {
|
|
1247
1342
|
;
|
|
1248
1343
|
el.checked = value === "true";
|
|
1249
|
-
} else if (attr === "class") {
|
|
1250
|
-
if (value != null) el.className = value;
|
|
1251
|
-
} else if (attr === "style") {
|
|
1252
|
-
if (value) {
|
|
1253
|
-
el.setAttribute("style", value);
|
|
1254
|
-
el.style.transition = "none";
|
|
1255
|
-
if (snapTyped.__styles) {
|
|
1256
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1257
|
-
el.style.setProperty(prop, val, "important");
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
1344
|
} else if (attr === "value") {
|
|
1262
1345
|
if (value != null) el.value = value;
|
|
1263
1346
|
} else if (value == null) {
|
|
@@ -1268,39 +1351,31 @@ var TimelineScrubber = class {
|
|
|
1268
1351
|
}
|
|
1269
1352
|
}
|
|
1270
1353
|
}
|
|
1271
|
-
for (const
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1354
|
+
for (const fa of frame.animations || []) {
|
|
1355
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1356
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1357
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1358
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1359
|
+
const el = this.elements.get(elSel);
|
|
1360
|
+
if (!el || !el.isConnected) continue;
|
|
1361
|
+
for (const prop of fa.properties) {
|
|
1278
1362
|
if (prop.value) {
|
|
1279
|
-
|
|
1363
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1280
1364
|
}
|
|
1281
1365
|
}
|
|
1282
1366
|
}
|
|
1283
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1284
|
-
for (const anim of frame.animations || []) {
|
|
1285
|
-
const fc = anim.animationId.indexOf(":");
|
|
1286
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1287
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1288
|
-
}
|
|
1289
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1290
|
-
const el = rawEl;
|
|
1291
|
-
const sel = this.getSelector(el);
|
|
1292
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1293
|
-
el.style.removeProperty("opacity");
|
|
1294
|
-
el.style.removeProperty("transform");
|
|
1295
|
-
el.style.removeProperty("filter");
|
|
1296
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
1367
|
}
|
|
1300
1368
|
// ---------------------------------------------------------------------------
|
|
1301
1369
|
// release — tear down all scrub state and restore the page to normal
|
|
1302
1370
|
// ---------------------------------------------------------------------------
|
|
1303
1371
|
release() {
|
|
1372
|
+
for (const [, clone] of this.seekableClones) {
|
|
1373
|
+
try {
|
|
1374
|
+
clone.animation.cancel();
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
this.seekableClones.clear();
|
|
1304
1379
|
for (const entry of this.interceptedAnimations) {
|
|
1305
1380
|
try {
|
|
1306
1381
|
entry.animation.cancel();
|
|
@@ -1362,6 +1437,7 @@ var TimelineScrubber = class {
|
|
|
1362
1437
|
this.elements.clear();
|
|
1363
1438
|
this.frames.length = 0;
|
|
1364
1439
|
this.capturedPortals.clear();
|
|
1440
|
+
this.animFrameRanges.clear();
|
|
1365
1441
|
}
|
|
1366
1442
|
};
|
|
1367
1443
|
|
|
@@ -1448,7 +1524,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
|
|
|
1448
1524
|
animations: animExports
|
|
1449
1525
|
};
|
|
1450
1526
|
}
|
|
1451
|
-
function formatExportForLLM(exp, detail = "
|
|
1527
|
+
function formatExportForLLM(exp, detail = "moderate") {
|
|
1452
1528
|
const lines = [];
|
|
1453
1529
|
const grouped = /* @__PURE__ */ new Map();
|
|
1454
1530
|
for (const anim of exp.animations) {
|
|
@@ -1463,7 +1539,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1463
1539
|
const [from, to] = prop.range.split(" \u2192 ");
|
|
1464
1540
|
return !(from && to && from.trim() === to.trim());
|
|
1465
1541
|
}
|
|
1466
|
-
if (detail === "
|
|
1542
|
+
if (detail === "brief") {
|
|
1467
1543
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1468
1544
|
lines.push("");
|
|
1469
1545
|
for (const [, group] of grouped) {
|
|
@@ -1488,7 +1564,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1488
1564
|
}
|
|
1489
1565
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1490
1566
|
lines.push("");
|
|
1491
|
-
if (detail === "
|
|
1567
|
+
if (detail === "granular") {
|
|
1492
1568
|
lines.push("**Environment:**");
|
|
1493
1569
|
lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
|
|
1494
1570
|
lines.push(`- URL: ${window.location.href}`);
|
|
@@ -1555,7 +1631,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1555
1631
|
lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
|
|
1556
1632
|
lines.push("");
|
|
1557
1633
|
for (const line of cssPropLines) lines.push(line);
|
|
1558
|
-
if (detail === "detailed" || detail === "
|
|
1634
|
+
if (detail === "detailed" || detail === "granular") {
|
|
1559
1635
|
const allVars = {};
|
|
1560
1636
|
for (const anim of cssAnims) {
|
|
1561
1637
|
if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
|
|
@@ -1610,6 +1686,22 @@ var SaccadeEngine = class {
|
|
|
1610
1686
|
getSpeed() {
|
|
1611
1687
|
return this.timing.getSpeed();
|
|
1612
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Install the timing patches immediately, without changing speed.
|
|
1691
|
+
*
|
|
1692
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
1693
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
1694
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
1695
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
1696
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
1697
|
+
*/
|
|
1698
|
+
install() {
|
|
1699
|
+
this.timing.install();
|
|
1700
|
+
}
|
|
1701
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
1702
|
+
registerGSAP(gsap) {
|
|
1703
|
+
this.timing.registerGSAP(gsap);
|
|
1704
|
+
}
|
|
1613
1705
|
// -- Timeline recording ---------------------------------------------------
|
|
1614
1706
|
startRecording(boundingBox) {
|
|
1615
1707
|
if (this._state !== "idle") return;
|
|
@@ -1634,7 +1726,7 @@ var SaccadeEngine = class {
|
|
|
1634
1726
|
try {
|
|
1635
1727
|
capture = this.recorder.stopRecording();
|
|
1636
1728
|
} catch (e) {
|
|
1637
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1729
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1638
1730
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1639
1731
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1640
1732
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1655,7 +1747,8 @@ var SaccadeEngine = class {
|
|
|
1655
1747
|
frames: capture.frames,
|
|
1656
1748
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1657
1749
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1658
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1750
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1751
|
+
seekableClones: this.recorder.seekableClones
|
|
1659
1752
|
};
|
|
1660
1753
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1661
1754
|
this._state = "scrubbing";
|
|
@@ -1683,7 +1776,7 @@ var SaccadeEngine = class {
|
|
|
1683
1776
|
filter
|
|
1684
1777
|
);
|
|
1685
1778
|
}
|
|
1686
|
-
exportForLLM(timeMs, filter = "active", detail = "
|
|
1779
|
+
exportForLLM(timeMs, filter = "active", detail = "moderate") {
|
|
1687
1780
|
const exp = this.generateExport(timeMs, filter);
|
|
1688
1781
|
if (!exp) return "";
|
|
1689
1782
|
return formatExportForLLM(exp, detail);
|
|
@@ -1707,6 +1800,19 @@ var SaccadeEngine = class {
|
|
|
1707
1800
|
this._state = "idle";
|
|
1708
1801
|
}
|
|
1709
1802
|
};
|
|
1803
|
+
|
|
1804
|
+
// src/core/shared.ts
|
|
1805
|
+
var KEY = "__saccadeSharedEngine__";
|
|
1806
|
+
function getSharedEngine() {
|
|
1807
|
+
const g = globalThis;
|
|
1808
|
+
if (!g[KEY]) g[KEY] = new SaccadeEngine();
|
|
1809
|
+
return g[KEY];
|
|
1810
|
+
}
|
|
1811
|
+
function resetSharedEngine() {
|
|
1812
|
+
const g = globalThis;
|
|
1813
|
+
g[KEY]?.destroy();
|
|
1814
|
+
g[KEY] = null;
|
|
1815
|
+
}
|
|
1710
1816
|
export {
|
|
1711
1817
|
SaccadeEngine,
|
|
1712
1818
|
TimelineRecorder,
|
|
@@ -1714,6 +1820,8 @@ export {
|
|
|
1714
1820
|
TimingController,
|
|
1715
1821
|
formatExportForLLM,
|
|
1716
1822
|
generateExport,
|
|
1717
|
-
getFrameAtTime
|
|
1823
|
+
getFrameAtTime,
|
|
1824
|
+
getSharedEngine,
|
|
1825
|
+
resetSharedEngine
|
|
1718
1826
|
};
|
|
1719
1827
|
//# sourceMappingURL=core.mjs.map
|