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.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
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
|
+
// WeakMap tracking for animations and media
|
|
15
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
12
17
|
this._raf = requestAnimationFrame.bind(window);
|
|
13
18
|
this._caf = cancelAnimationFrame.bind(window);
|
|
14
19
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -18,19 +23,34 @@ var TimingController = class {
|
|
|
18
23
|
this._perfNow = performance.now.bind(performance);
|
|
19
24
|
this._dateNow = Date.now;
|
|
20
25
|
this.realBaseline = this._perfNow();
|
|
26
|
+
this.dateRealBaseline = this._dateNow();
|
|
27
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
21
28
|
}
|
|
22
29
|
getVirtualTime() {
|
|
23
30
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
24
31
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
25
32
|
}
|
|
33
|
+
getVirtualDateNow() {
|
|
34
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
35
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
36
|
+
}
|
|
26
37
|
reanchor() {
|
|
27
38
|
const virtualNow = this.getVirtualTime();
|
|
28
39
|
this.realBaseline = this._perfNow();
|
|
29
40
|
this.virtualBaseline = virtualNow;
|
|
41
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
42
|
+
this.dateRealBaseline = this._dateNow();
|
|
43
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
44
|
+
}
|
|
45
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
46
|
+
get speedDivisor() {
|
|
47
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
30
48
|
}
|
|
31
49
|
/** Install timing patches. Safe to call multiple times. */
|
|
32
50
|
install() {
|
|
33
51
|
if (this.installed) return;
|
|
52
|
+
if (window.__saccadeInstalled) return;
|
|
53
|
+
window.__saccadeInstalled = true;
|
|
34
54
|
this.installed = true;
|
|
35
55
|
const self = this;
|
|
36
56
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -39,21 +59,27 @@ var TimingController = class {
|
|
|
39
59
|
Element.prototype.animate = function(...args) {
|
|
40
60
|
const anim = origAnimate.apply(this, args);
|
|
41
61
|
if (self.speed !== 1) {
|
|
42
|
-
|
|
62
|
+
const originalRate = anim.playbackRate;
|
|
63
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
64
|
+
anim.playbackRate = applied;
|
|
65
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
43
66
|
}
|
|
44
67
|
return anim;
|
|
45
68
|
};
|
|
46
69
|
performance.now = () => self.getVirtualTime();
|
|
47
|
-
|
|
48
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
70
|
+
Date.now = () => self.getVirtualDateNow();
|
|
49
71
|
window.requestAnimationFrame = (callback) => {
|
|
50
72
|
return self._raf(() => {
|
|
73
|
+
if (self.speed === 0) {
|
|
74
|
+
window.requestAnimationFrame(callback);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
51
77
|
callback(self.getVirtualTime());
|
|
52
78
|
});
|
|
53
79
|
};
|
|
54
80
|
window.cancelAnimationFrame = this._caf;
|
|
55
81
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
56
|
-
const scaledDelay = (delay ?? 0) /
|
|
82
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
57
83
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
58
84
|
});
|
|
59
85
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -61,7 +87,7 @@ var TimingController = class {
|
|
|
61
87
|
const id = self.nextIntervalId++;
|
|
62
88
|
const baseDelay = delay ?? 0;
|
|
63
89
|
function tick() {
|
|
64
|
-
const scaledDelay = baseDelay /
|
|
90
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
65
91
|
const realId = self._setTimeout(() => {
|
|
66
92
|
if (typeof handler === "function") {
|
|
67
93
|
;
|
|
@@ -86,61 +112,93 @@ var TimingController = class {
|
|
|
86
112
|
self._clearInterval(id);
|
|
87
113
|
}
|
|
88
114
|
});
|
|
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
|
-
}
|
|
115
|
+
this.startAnimationPoll();
|
|
101
116
|
}
|
|
102
117
|
/** Set playback speed. Requires install() first. */
|
|
103
118
|
setSpeed(newSpeed) {
|
|
104
119
|
if (!this.installed) this.install();
|
|
105
120
|
this.reanchor();
|
|
106
121
|
this.speed = newSpeed;
|
|
107
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
108
|
-
;
|
|
109
|
-
el.playbackRate = newSpeed;
|
|
110
|
-
});
|
|
111
122
|
this.patchAnimations();
|
|
123
|
+
this.patchMedia();
|
|
124
|
+
this.patchGSAP();
|
|
112
125
|
}
|
|
113
|
-
|
|
126
|
+
getSpeed() {
|
|
127
|
+
return this.speed;
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Animation polling — per-frame via original rAF
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
startAnimationPoll() {
|
|
133
|
+
const poll = () => {
|
|
134
|
+
if (!this.installed) return;
|
|
135
|
+
this.patchAnimations();
|
|
136
|
+
this.patchMedia();
|
|
137
|
+
this.animPollId = this._raf(poll);
|
|
138
|
+
};
|
|
139
|
+
this.animPollId = this._raf(poll);
|
|
140
|
+
}
|
|
141
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
114
142
|
patchAnimations() {
|
|
115
143
|
try {
|
|
116
144
|
const anims = document.getAnimations();
|
|
117
145
|
for (const anim of anims) {
|
|
118
146
|
const target = anim.effect?.target;
|
|
119
147
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
120
|
-
|
|
148
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
149
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
150
|
+
let tracked = this.trackedAnims.get(anim);
|
|
151
|
+
if (!tracked) {
|
|
152
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
153
|
+
this.trackedAnims.set(anim, tracked);
|
|
154
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
155
|
+
tracked.original = anim.playbackRate;
|
|
156
|
+
}
|
|
157
|
+
const desired = tracked.original * effectiveSpeed;
|
|
158
|
+
if (anim.playbackRate !== desired) {
|
|
159
|
+
anim.playbackRate = desired;
|
|
160
|
+
tracked.applied = desired;
|
|
161
|
+
}
|
|
121
162
|
}
|
|
122
163
|
} catch {
|
|
123
164
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
165
|
+
}
|
|
166
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
167
|
+
patchMedia() {
|
|
168
|
+
try {
|
|
169
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
170
|
+
const el = node;
|
|
171
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
172
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
173
|
+
let tracked = this.trackedMedia.get(el);
|
|
174
|
+
if (!tracked) {
|
|
175
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
176
|
+
this.trackedMedia.set(el, tracked);
|
|
177
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
178
|
+
tracked.original = el.playbackRate;
|
|
179
|
+
}
|
|
180
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
181
|
+
if (el.playbackRate !== desired) {
|
|
182
|
+
el.playbackRate = desired;
|
|
183
|
+
tracked.applied = desired;
|
|
137
184
|
}
|
|
138
|
-
}
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
139
187
|
}
|
|
140
188
|
}
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
/** Sync GSAP's global timeline if present. */
|
|
190
|
+
patchGSAP() {
|
|
191
|
+
try {
|
|
192
|
+
const gsap = window.gsap;
|
|
193
|
+
if (gsap?.globalTimeline) {
|
|
194
|
+
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
143
198
|
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Cleanup
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
144
202
|
/** Restore all patched APIs to originals. */
|
|
145
203
|
destroy() {
|
|
146
204
|
if (!this.installed) return;
|
|
@@ -160,19 +218,32 @@ var TimingController = class {
|
|
|
160
218
|
Element.prototype.animate = this._origAnimate;
|
|
161
219
|
this._origAnimate = null;
|
|
162
220
|
}
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this._clearInterval(this.animObserver);
|
|
167
|
-
this.animObserver = null;
|
|
221
|
+
if (this.animPollId) {
|
|
222
|
+
this._caf(this.animPollId);
|
|
223
|
+
this.animPollId = 0;
|
|
168
224
|
}
|
|
169
225
|
try {
|
|
170
226
|
for (const anim of document.getAnimations()) {
|
|
171
|
-
|
|
227
|
+
const tracked = this.trackedAnims.get(anim);
|
|
228
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
172
229
|
}
|
|
173
230
|
} catch {
|
|
174
231
|
}
|
|
232
|
+
try {
|
|
233
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
234
|
+
const el = node;
|
|
235
|
+
const tracked = this.trackedMedia.get(el);
|
|
236
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const gsap = window.gsap;
|
|
242
|
+
if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
175
245
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
246
|
+
delete window.__saccadeInstalled;
|
|
176
247
|
this.installed = false;
|
|
177
248
|
}
|
|
178
249
|
};
|
|
@@ -242,6 +313,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
242
313
|
"data-hover",
|
|
243
314
|
"data-at-boundary",
|
|
244
315
|
"data-scrubbing",
|
|
316
|
+
"data-starting-style",
|
|
317
|
+
"data-ending-style",
|
|
318
|
+
"data-panel-open",
|
|
319
|
+
"data-hidden",
|
|
245
320
|
"aria-checked",
|
|
246
321
|
"aria-selected",
|
|
247
322
|
"aria-expanded",
|
|
@@ -255,6 +330,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
255
330
|
"checked",
|
|
256
331
|
"disabled",
|
|
257
332
|
"hidden",
|
|
333
|
+
"inert",
|
|
258
334
|
"value",
|
|
259
335
|
"class",
|
|
260
336
|
"style"
|
|
@@ -386,6 +462,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
386
462
|
// ---- WAAPI interception --------------------------------------------------
|
|
387
463
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
388
464
|
this.interceptedAnimations = [];
|
|
465
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
466
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
389
467
|
this.hiddenSince = null;
|
|
390
468
|
this.onVisibilityChange = null;
|
|
391
469
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -436,6 +514,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
436
514
|
this.portalIdCounter = 0;
|
|
437
515
|
this.currentPortalIds.clear();
|
|
438
516
|
this.capturedPortals.clear();
|
|
517
|
+
this.seekableClones.clear();
|
|
439
518
|
this.prevInlineStyles.clear();
|
|
440
519
|
this.jsAnimStartTimes.clear();
|
|
441
520
|
this.jsAnimLastSeen.clear();
|
|
@@ -789,6 +868,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
789
868
|
} catch (_) {
|
|
790
869
|
}
|
|
791
870
|
}
|
|
871
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
872
|
+
const clean = {};
|
|
873
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
874
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
875
|
+
}
|
|
876
|
+
return clean;
|
|
877
|
+
});
|
|
792
878
|
this.animations.set(id, {
|
|
793
879
|
id,
|
|
794
880
|
name,
|
|
@@ -800,7 +886,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
800
886
|
type,
|
|
801
887
|
source,
|
|
802
888
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
803
|
-
conflicts
|
|
889
|
+
conflicts,
|
|
890
|
+
rawKeyframes: cleanedKeyframes,
|
|
891
|
+
rawTiming: { ...timing, fill: "both" }
|
|
804
892
|
});
|
|
805
893
|
}
|
|
806
894
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1076,38 +1164,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1076
1164
|
} catch (_) {
|
|
1077
1165
|
}
|
|
1078
1166
|
}, 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
|
-
}
|
|
1167
|
+
this.seekableClones.clear();
|
|
1168
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1169
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1170
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1171
|
+
const firstColon = animId.indexOf(":");
|
|
1172
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1173
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1174
|
+
const el = this.elements.get(elSelector);
|
|
1175
|
+
if (!el?.isConnected) continue;
|
|
1176
|
+
try {
|
|
1177
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1178
|
+
...animInfo.rawTiming,
|
|
1179
|
+
fill: "both"
|
|
1180
|
+
});
|
|
1181
|
+
clone.pause();
|
|
1182
|
+
clone.currentTime = 0;
|
|
1183
|
+
this.seekableClones.set(animId, {
|
|
1184
|
+
animation: clone,
|
|
1185
|
+
element: el,
|
|
1186
|
+
effect: clone.effect
|
|
1187
|
+
});
|
|
1188
|
+
} catch (_) {
|
|
1111
1189
|
}
|
|
1112
1190
|
}
|
|
1113
1191
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1130,6 +1208,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1130
1208
|
// src/core/scrubber.ts
|
|
1131
1209
|
var TimelineScrubber = class {
|
|
1132
1210
|
constructor(state) {
|
|
1211
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1212
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1133
1213
|
/** Saved originals for restore on release */
|
|
1134
1214
|
this._originalAnimate = null;
|
|
1135
1215
|
this._originalRaf = null;
|
|
@@ -1139,36 +1219,24 @@ var TimelineScrubber = class {
|
|
|
1139
1219
|
this.frames = state.frames;
|
|
1140
1220
|
this.capturedPortals = state.capturedPortals;
|
|
1141
1221
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1142
|
-
this.
|
|
1222
|
+
this.seekableClones = state.seekableClones;
|
|
1143
1223
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1144
1224
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1145
1225
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1146
1226
|
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);
|
|
1227
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1228
|
+
for (const fa of this.frames[i].animations) {
|
|
1229
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1230
|
+
if (!range) {
|
|
1231
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1232
|
+
} else {
|
|
1233
|
+
range.last = i;
|
|
1234
|
+
}
|
|
1165
1235
|
}
|
|
1166
|
-
current = parent;
|
|
1167
1236
|
}
|
|
1168
|
-
return parts.join(" > ");
|
|
1169
1237
|
}
|
|
1170
1238
|
// ---------------------------------------------------------------------------
|
|
1171
|
-
// seekTo — scrub
|
|
1239
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1172
1240
|
// ---------------------------------------------------------------------------
|
|
1173
1241
|
seekTo(timeMs) {
|
|
1174
1242
|
if (!this.frames.length) return;
|
|
@@ -1216,6 +1284,34 @@ var TimelineScrubber = class {
|
|
|
1216
1284
|
}
|
|
1217
1285
|
}
|
|
1218
1286
|
}
|
|
1287
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1288
|
+
for (const fa of frame.animations || []) {
|
|
1289
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1290
|
+
}
|
|
1291
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1292
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1293
|
+
try {
|
|
1294
|
+
if (frameAnim) {
|
|
1295
|
+
if (!clone.animation.effect) {
|
|
1296
|
+
clone.animation.effect = clone.effect;
|
|
1297
|
+
}
|
|
1298
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1299
|
+
} else {
|
|
1300
|
+
const range = this.animFrameRanges.get(animId);
|
|
1301
|
+
if (!range || lo < range.first) {
|
|
1302
|
+
clone.animation.effect = null;
|
|
1303
|
+
} else {
|
|
1304
|
+
if (!clone.animation.effect) {
|
|
1305
|
+
clone.animation.effect = clone.effect;
|
|
1306
|
+
}
|
|
1307
|
+
const timing = clone.effect.getTiming();
|
|
1308
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1309
|
+
clone.animation.currentTime = endTime;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1219
1315
|
for (const entry of this.interceptedAnimations) {
|
|
1220
1316
|
try {
|
|
1221
1317
|
const anim = entry.animation;
|
|
@@ -1230,34 +1326,13 @@ var TimelineScrubber = class {
|
|
|
1230
1326
|
const el = this.elements.get(sel);
|
|
1231
1327
|
if (!el || !el.isConnected) continue;
|
|
1232
1328
|
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
1329
|
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
1330
|
if (snapTyped.__attrs) {
|
|
1245
1331
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1332
|
+
if (attr === "class" || attr === "style") continue;
|
|
1246
1333
|
if (attr === "checked") {
|
|
1247
1334
|
;
|
|
1248
1335
|
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
1336
|
} else if (attr === "value") {
|
|
1262
1337
|
if (value != null) el.value = value;
|
|
1263
1338
|
} else if (value == null) {
|
|
@@ -1268,39 +1343,31 @@ var TimelineScrubber = class {
|
|
|
1268
1343
|
}
|
|
1269
1344
|
}
|
|
1270
1345
|
}
|
|
1271
|
-
for (const
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1346
|
+
for (const fa of frame.animations || []) {
|
|
1347
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1348
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1349
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1350
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1351
|
+
const el = this.elements.get(elSel);
|
|
1352
|
+
if (!el || !el.isConnected) continue;
|
|
1353
|
+
for (const prop of fa.properties) {
|
|
1278
1354
|
if (prop.value) {
|
|
1279
|
-
|
|
1355
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1280
1356
|
}
|
|
1281
1357
|
}
|
|
1282
1358
|
}
|
|
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
1359
|
}
|
|
1300
1360
|
// ---------------------------------------------------------------------------
|
|
1301
1361
|
// release — tear down all scrub state and restore the page to normal
|
|
1302
1362
|
// ---------------------------------------------------------------------------
|
|
1303
1363
|
release() {
|
|
1364
|
+
for (const [, clone] of this.seekableClones) {
|
|
1365
|
+
try {
|
|
1366
|
+
clone.animation.cancel();
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
this.seekableClones.clear();
|
|
1304
1371
|
for (const entry of this.interceptedAnimations) {
|
|
1305
1372
|
try {
|
|
1306
1373
|
entry.animation.cancel();
|
|
@@ -1362,6 +1429,7 @@ var TimelineScrubber = class {
|
|
|
1362
1429
|
this.elements.clear();
|
|
1363
1430
|
this.frames.length = 0;
|
|
1364
1431
|
this.capturedPortals.clear();
|
|
1432
|
+
this.animFrameRanges.clear();
|
|
1365
1433
|
}
|
|
1366
1434
|
};
|
|
1367
1435
|
|
|
@@ -1634,7 +1702,7 @@ var SaccadeEngine = class {
|
|
|
1634
1702
|
try {
|
|
1635
1703
|
capture = this.recorder.stopRecording();
|
|
1636
1704
|
} catch (e) {
|
|
1637
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1705
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1638
1706
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1639
1707
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1640
1708
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1655,7 +1723,8 @@ var SaccadeEngine = class {
|
|
|
1655
1723
|
frames: capture.frames,
|
|
1656
1724
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1657
1725
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1658
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1726
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1727
|
+
seekableClones: this.recorder.seekableClones
|
|
1659
1728
|
};
|
|
1660
1729
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1661
1730
|
this._state = "scrubbing";
|