saccade 0.0.3 → 0.1.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/dist/core.cjs +219 -150
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +40 -10
- package/dist/core.d.ts +40 -10
- package/dist/core.mjs +219 -150
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +235 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.mjs +235 -175
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -31,16 +31,21 @@ __export(core_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(core_exports);
|
|
32
32
|
|
|
33
33
|
// src/core/timing.ts
|
|
34
|
+
var MEDIA_RATE_MIN = 0.0625;
|
|
35
|
+
var MEDIA_RATE_MAX = 16;
|
|
36
|
+
var ZERO_SPEED_DIVISOR = 1e-4;
|
|
34
37
|
var TimingController = class {
|
|
35
38
|
constructor() {
|
|
36
39
|
this.speed = 1;
|
|
37
40
|
this.virtualBaseline = 0;
|
|
38
41
|
this.intervalMap = /* @__PURE__ */ new Map();
|
|
39
42
|
this.nextIntervalId = 1e6;
|
|
40
|
-
this.mediaObserver = null;
|
|
41
|
-
this.animObserver = null;
|
|
42
43
|
this._origAnimate = null;
|
|
43
44
|
this.installed = false;
|
|
45
|
+
this.animPollId = 0;
|
|
46
|
+
// WeakMap tracking for animations and media
|
|
47
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
48
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
44
49
|
this._raf = requestAnimationFrame.bind(window);
|
|
45
50
|
this._caf = cancelAnimationFrame.bind(window);
|
|
46
51
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -50,19 +55,34 @@ var TimingController = class {
|
|
|
50
55
|
this._perfNow = performance.now.bind(performance);
|
|
51
56
|
this._dateNow = Date.now;
|
|
52
57
|
this.realBaseline = this._perfNow();
|
|
58
|
+
this.dateRealBaseline = this._dateNow();
|
|
59
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
53
60
|
}
|
|
54
61
|
getVirtualTime() {
|
|
55
62
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
56
63
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
57
64
|
}
|
|
65
|
+
getVirtualDateNow() {
|
|
66
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
67
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
68
|
+
}
|
|
58
69
|
reanchor() {
|
|
59
70
|
const virtualNow = this.getVirtualTime();
|
|
60
71
|
this.realBaseline = this._perfNow();
|
|
61
72
|
this.virtualBaseline = virtualNow;
|
|
73
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
74
|
+
this.dateRealBaseline = this._dateNow();
|
|
75
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
76
|
+
}
|
|
77
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
78
|
+
get speedDivisor() {
|
|
79
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
62
80
|
}
|
|
63
81
|
/** Install timing patches. Safe to call multiple times. */
|
|
64
82
|
install() {
|
|
65
83
|
if (this.installed) return;
|
|
84
|
+
if (window.__saccadeInstalled) return;
|
|
85
|
+
window.__saccadeInstalled = true;
|
|
66
86
|
this.installed = true;
|
|
67
87
|
const self = this;
|
|
68
88
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -71,21 +91,27 @@ var TimingController = class {
|
|
|
71
91
|
Element.prototype.animate = function(...args) {
|
|
72
92
|
const anim = origAnimate.apply(this, args);
|
|
73
93
|
if (self.speed !== 1) {
|
|
74
|
-
|
|
94
|
+
const originalRate = anim.playbackRate;
|
|
95
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
96
|
+
anim.playbackRate = applied;
|
|
97
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
75
98
|
}
|
|
76
99
|
return anim;
|
|
77
100
|
};
|
|
78
101
|
performance.now = () => self.getVirtualTime();
|
|
79
|
-
|
|
80
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
102
|
+
Date.now = () => self.getVirtualDateNow();
|
|
81
103
|
window.requestAnimationFrame = (callback) => {
|
|
82
104
|
return self._raf(() => {
|
|
105
|
+
if (self.speed === 0) {
|
|
106
|
+
window.requestAnimationFrame(callback);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
83
109
|
callback(self.getVirtualTime());
|
|
84
110
|
});
|
|
85
111
|
};
|
|
86
112
|
window.cancelAnimationFrame = this._caf;
|
|
87
113
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
88
|
-
const scaledDelay = (delay ?? 0) /
|
|
114
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
89
115
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
90
116
|
});
|
|
91
117
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -93,7 +119,7 @@ var TimingController = class {
|
|
|
93
119
|
const id = self.nextIntervalId++;
|
|
94
120
|
const baseDelay = delay ?? 0;
|
|
95
121
|
function tick() {
|
|
96
|
-
const scaledDelay = baseDelay /
|
|
122
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
97
123
|
const realId = self._setTimeout(() => {
|
|
98
124
|
if (typeof handler === "function") {
|
|
99
125
|
;
|
|
@@ -118,61 +144,93 @@ var TimingController = class {
|
|
|
118
144
|
self._clearInterval(id);
|
|
119
145
|
}
|
|
120
146
|
});
|
|
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
|
-
}
|
|
147
|
+
this.startAnimationPoll();
|
|
133
148
|
}
|
|
134
149
|
/** Set playback speed. Requires install() first. */
|
|
135
150
|
setSpeed(newSpeed) {
|
|
136
151
|
if (!this.installed) this.install();
|
|
137
152
|
this.reanchor();
|
|
138
153
|
this.speed = newSpeed;
|
|
139
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
140
|
-
;
|
|
141
|
-
el.playbackRate = newSpeed;
|
|
142
|
-
});
|
|
143
154
|
this.patchAnimations();
|
|
155
|
+
this.patchMedia();
|
|
156
|
+
this.patchGSAP();
|
|
144
157
|
}
|
|
145
|
-
|
|
158
|
+
getSpeed() {
|
|
159
|
+
return this.speed;
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Animation polling — per-frame via original rAF
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
startAnimationPoll() {
|
|
165
|
+
const poll = () => {
|
|
166
|
+
if (!this.installed) return;
|
|
167
|
+
this.patchAnimations();
|
|
168
|
+
this.patchMedia();
|
|
169
|
+
this.animPollId = this._raf(poll);
|
|
170
|
+
};
|
|
171
|
+
this.animPollId = this._raf(poll);
|
|
172
|
+
}
|
|
173
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
146
174
|
patchAnimations() {
|
|
147
175
|
try {
|
|
148
176
|
const anims = document.getAnimations();
|
|
149
177
|
for (const anim of anims) {
|
|
150
178
|
const target = anim.effect?.target;
|
|
151
179
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
152
|
-
|
|
180
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
181
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
182
|
+
let tracked = this.trackedAnims.get(anim);
|
|
183
|
+
if (!tracked) {
|
|
184
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
185
|
+
this.trackedAnims.set(anim, tracked);
|
|
186
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
187
|
+
tracked.original = anim.playbackRate;
|
|
188
|
+
}
|
|
189
|
+
const desired = tracked.original * effectiveSpeed;
|
|
190
|
+
if (anim.playbackRate !== desired) {
|
|
191
|
+
anim.playbackRate = desired;
|
|
192
|
+
tracked.applied = desired;
|
|
193
|
+
}
|
|
153
194
|
}
|
|
154
195
|
} catch {
|
|
155
196
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
197
|
+
}
|
|
198
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
199
|
+
patchMedia() {
|
|
200
|
+
try {
|
|
201
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
202
|
+
const el = node;
|
|
203
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
204
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
205
|
+
let tracked = this.trackedMedia.get(el);
|
|
206
|
+
if (!tracked) {
|
|
207
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
208
|
+
this.trackedMedia.set(el, tracked);
|
|
209
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
210
|
+
tracked.original = el.playbackRate;
|
|
211
|
+
}
|
|
212
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
213
|
+
if (el.playbackRate !== desired) {
|
|
214
|
+
el.playbackRate = desired;
|
|
215
|
+
tracked.applied = desired;
|
|
169
216
|
}
|
|
170
|
-
}
|
|
217
|
+
});
|
|
218
|
+
} catch {
|
|
171
219
|
}
|
|
172
220
|
}
|
|
173
|
-
|
|
174
|
-
|
|
221
|
+
/** Sync GSAP's global timeline if present. */
|
|
222
|
+
patchGSAP() {
|
|
223
|
+
try {
|
|
224
|
+
const gsap = window.gsap;
|
|
225
|
+
if (gsap?.globalTimeline) {
|
|
226
|
+
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
175
230
|
}
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Cleanup
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
176
234
|
/** Restore all patched APIs to originals. */
|
|
177
235
|
destroy() {
|
|
178
236
|
if (!this.installed) return;
|
|
@@ -192,19 +250,32 @@ var TimingController = class {
|
|
|
192
250
|
Element.prototype.animate = this._origAnimate;
|
|
193
251
|
this._origAnimate = null;
|
|
194
252
|
}
|
|
195
|
-
this.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this._clearInterval(this.animObserver);
|
|
199
|
-
this.animObserver = null;
|
|
253
|
+
if (this.animPollId) {
|
|
254
|
+
this._caf(this.animPollId);
|
|
255
|
+
this.animPollId = 0;
|
|
200
256
|
}
|
|
201
257
|
try {
|
|
202
258
|
for (const anim of document.getAnimations()) {
|
|
203
|
-
|
|
259
|
+
const tracked = this.trackedAnims.get(anim);
|
|
260
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
204
261
|
}
|
|
205
262
|
} catch {
|
|
206
263
|
}
|
|
264
|
+
try {
|
|
265
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
266
|
+
const el = node;
|
|
267
|
+
const tracked = this.trackedMedia.get(el);
|
|
268
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
269
|
+
});
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const gsap = window.gsap;
|
|
274
|
+
if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
207
277
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
278
|
+
delete window.__saccadeInstalled;
|
|
208
279
|
this.installed = false;
|
|
209
280
|
}
|
|
210
281
|
};
|
|
@@ -274,6 +345,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
274
345
|
"data-hover",
|
|
275
346
|
"data-at-boundary",
|
|
276
347
|
"data-scrubbing",
|
|
348
|
+
"data-starting-style",
|
|
349
|
+
"data-ending-style",
|
|
350
|
+
"data-panel-open",
|
|
351
|
+
"data-hidden",
|
|
277
352
|
"aria-checked",
|
|
278
353
|
"aria-selected",
|
|
279
354
|
"aria-expanded",
|
|
@@ -287,6 +362,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
287
362
|
"checked",
|
|
288
363
|
"disabled",
|
|
289
364
|
"hidden",
|
|
365
|
+
"inert",
|
|
290
366
|
"value",
|
|
291
367
|
"class",
|
|
292
368
|
"style"
|
|
@@ -418,6 +494,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
418
494
|
// ---- WAAPI interception --------------------------------------------------
|
|
419
495
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
420
496
|
this.interceptedAnimations = [];
|
|
497
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
498
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
421
499
|
this.hiddenSince = null;
|
|
422
500
|
this.onVisibilityChange = null;
|
|
423
501
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -468,6 +546,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
468
546
|
this.portalIdCounter = 0;
|
|
469
547
|
this.currentPortalIds.clear();
|
|
470
548
|
this.capturedPortals.clear();
|
|
549
|
+
this.seekableClones.clear();
|
|
471
550
|
this.prevInlineStyles.clear();
|
|
472
551
|
this.jsAnimStartTimes.clear();
|
|
473
552
|
this.jsAnimLastSeen.clear();
|
|
@@ -821,6 +900,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
821
900
|
} catch (_) {
|
|
822
901
|
}
|
|
823
902
|
}
|
|
903
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
904
|
+
const clean = {};
|
|
905
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
906
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
907
|
+
}
|
|
908
|
+
return clean;
|
|
909
|
+
});
|
|
824
910
|
this.animations.set(id, {
|
|
825
911
|
id,
|
|
826
912
|
name,
|
|
@@ -832,7 +918,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
832
918
|
type,
|
|
833
919
|
source,
|
|
834
920
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
835
|
-
conflicts
|
|
921
|
+
conflicts,
|
|
922
|
+
rawKeyframes: cleanedKeyframes,
|
|
923
|
+
rawTiming: { ...timing, fill: "both" }
|
|
836
924
|
});
|
|
837
925
|
}
|
|
838
926
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1108,38 +1196,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1108
1196
|
} catch (_) {
|
|
1109
1197
|
}
|
|
1110
1198
|
}, 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
|
-
}
|
|
1199
|
+
this.seekableClones.clear();
|
|
1200
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1201
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1202
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1203
|
+
const firstColon = animId.indexOf(":");
|
|
1204
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1205
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1206
|
+
const el = this.elements.get(elSelector);
|
|
1207
|
+
if (!el?.isConnected) continue;
|
|
1208
|
+
try {
|
|
1209
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1210
|
+
...animInfo.rawTiming,
|
|
1211
|
+
fill: "both"
|
|
1212
|
+
});
|
|
1213
|
+
clone.pause();
|
|
1214
|
+
clone.currentTime = 0;
|
|
1215
|
+
this.seekableClones.set(animId, {
|
|
1216
|
+
animation: clone,
|
|
1217
|
+
element: el,
|
|
1218
|
+
effect: clone.effect
|
|
1219
|
+
});
|
|
1220
|
+
} catch (_) {
|
|
1143
1221
|
}
|
|
1144
1222
|
}
|
|
1145
1223
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1162,6 +1240,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1162
1240
|
// src/core/scrubber.ts
|
|
1163
1241
|
var TimelineScrubber = class {
|
|
1164
1242
|
constructor(state) {
|
|
1243
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1244
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1165
1245
|
/** Saved originals for restore on release */
|
|
1166
1246
|
this._originalAnimate = null;
|
|
1167
1247
|
this._originalRaf = null;
|
|
@@ -1171,36 +1251,24 @@ var TimelineScrubber = class {
|
|
|
1171
1251
|
this.frames = state.frames;
|
|
1172
1252
|
this.capturedPortals = state.capturedPortals;
|
|
1173
1253
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1174
|
-
this.
|
|
1254
|
+
this.seekableClones = state.seekableClones;
|
|
1175
1255
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1176
1256
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1177
1257
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1178
1258
|
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);
|
|
1259
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1260
|
+
for (const fa of this.frames[i].animations) {
|
|
1261
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1262
|
+
if (!range) {
|
|
1263
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1264
|
+
} else {
|
|
1265
|
+
range.last = i;
|
|
1266
|
+
}
|
|
1197
1267
|
}
|
|
1198
|
-
current = parent;
|
|
1199
1268
|
}
|
|
1200
|
-
return parts.join(" > ");
|
|
1201
1269
|
}
|
|
1202
1270
|
// ---------------------------------------------------------------------------
|
|
1203
|
-
// seekTo — scrub
|
|
1271
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1204
1272
|
// ---------------------------------------------------------------------------
|
|
1205
1273
|
seekTo(timeMs) {
|
|
1206
1274
|
if (!this.frames.length) return;
|
|
@@ -1248,6 +1316,34 @@ var TimelineScrubber = class {
|
|
|
1248
1316
|
}
|
|
1249
1317
|
}
|
|
1250
1318
|
}
|
|
1319
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const fa of frame.animations || []) {
|
|
1321
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1322
|
+
}
|
|
1323
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1324
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1325
|
+
try {
|
|
1326
|
+
if (frameAnim) {
|
|
1327
|
+
if (!clone.animation.effect) {
|
|
1328
|
+
clone.animation.effect = clone.effect;
|
|
1329
|
+
}
|
|
1330
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1331
|
+
} else {
|
|
1332
|
+
const range = this.animFrameRanges.get(animId);
|
|
1333
|
+
if (!range || lo < range.first) {
|
|
1334
|
+
clone.animation.effect = null;
|
|
1335
|
+
} else {
|
|
1336
|
+
if (!clone.animation.effect) {
|
|
1337
|
+
clone.animation.effect = clone.effect;
|
|
1338
|
+
}
|
|
1339
|
+
const timing = clone.effect.getTiming();
|
|
1340
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1341
|
+
clone.animation.currentTime = endTime;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1251
1347
|
for (const entry of this.interceptedAnimations) {
|
|
1252
1348
|
try {
|
|
1253
1349
|
const anim = entry.animation;
|
|
@@ -1262,34 +1358,13 @@ var TimelineScrubber = class {
|
|
|
1262
1358
|
const el = this.elements.get(sel);
|
|
1263
1359
|
if (!el || !el.isConnected) continue;
|
|
1264
1360
|
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
1361
|
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
1362
|
if (snapTyped.__attrs) {
|
|
1277
1363
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1364
|
+
if (attr === "class" || attr === "style") continue;
|
|
1278
1365
|
if (attr === "checked") {
|
|
1279
1366
|
;
|
|
1280
1367
|
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
1368
|
} else if (attr === "value") {
|
|
1294
1369
|
if (value != null) el.value = value;
|
|
1295
1370
|
} else if (value == null) {
|
|
@@ -1300,39 +1375,31 @@ var TimelineScrubber = class {
|
|
|
1300
1375
|
}
|
|
1301
1376
|
}
|
|
1302
1377
|
}
|
|
1303
|
-
for (const
|
|
1304
|
-
|
|
1305
|
-
const
|
|
1306
|
-
const
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1378
|
+
for (const fa of frame.animations || []) {
|
|
1379
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1380
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1381
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1382
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1383
|
+
const el = this.elements.get(elSel);
|
|
1384
|
+
if (!el || !el.isConnected) continue;
|
|
1385
|
+
for (const prop of fa.properties) {
|
|
1310
1386
|
if (prop.value) {
|
|
1311
|
-
|
|
1387
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1312
1388
|
}
|
|
1313
1389
|
}
|
|
1314
1390
|
}
|
|
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
1391
|
}
|
|
1332
1392
|
// ---------------------------------------------------------------------------
|
|
1333
1393
|
// release — tear down all scrub state and restore the page to normal
|
|
1334
1394
|
// ---------------------------------------------------------------------------
|
|
1335
1395
|
release() {
|
|
1396
|
+
for (const [, clone] of this.seekableClones) {
|
|
1397
|
+
try {
|
|
1398
|
+
clone.animation.cancel();
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
this.seekableClones.clear();
|
|
1336
1403
|
for (const entry of this.interceptedAnimations) {
|
|
1337
1404
|
try {
|
|
1338
1405
|
entry.animation.cancel();
|
|
@@ -1394,6 +1461,7 @@ var TimelineScrubber = class {
|
|
|
1394
1461
|
this.elements.clear();
|
|
1395
1462
|
this.frames.length = 0;
|
|
1396
1463
|
this.capturedPortals.clear();
|
|
1464
|
+
this.animFrameRanges.clear();
|
|
1397
1465
|
}
|
|
1398
1466
|
};
|
|
1399
1467
|
|
|
@@ -1666,7 +1734,7 @@ var SaccadeEngine = class {
|
|
|
1666
1734
|
try {
|
|
1667
1735
|
capture = this.recorder.stopRecording();
|
|
1668
1736
|
} catch (e) {
|
|
1669
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1737
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1670
1738
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1671
1739
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1672
1740
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1687,7 +1755,8 @@ var SaccadeEngine = class {
|
|
|
1687
1755
|
frames: capture.frames,
|
|
1688
1756
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1689
1757
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1690
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1758
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1759
|
+
seekableClones: this.recorder.seekableClones
|
|
1691
1760
|
};
|
|
1692
1761
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1693
1762
|
this._state = "scrubbing";
|