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/index.cjs
CHANGED
|
@@ -38,16 +38,21 @@ var import_react_dom = require("react-dom");
|
|
|
38
38
|
var import_react = require("react");
|
|
39
39
|
|
|
40
40
|
// src/core/timing.ts
|
|
41
|
+
var MEDIA_RATE_MIN = 0.0625;
|
|
42
|
+
var MEDIA_RATE_MAX = 16;
|
|
43
|
+
var ZERO_SPEED_DIVISOR = 1e-4;
|
|
41
44
|
var TimingController = class {
|
|
42
45
|
constructor() {
|
|
43
46
|
this.speed = 1;
|
|
44
47
|
this.virtualBaseline = 0;
|
|
45
48
|
this.intervalMap = /* @__PURE__ */ new Map();
|
|
46
49
|
this.nextIntervalId = 1e6;
|
|
47
|
-
this.mediaObserver = null;
|
|
48
|
-
this.animObserver = null;
|
|
49
50
|
this._origAnimate = null;
|
|
50
51
|
this.installed = false;
|
|
52
|
+
this.animPollId = 0;
|
|
53
|
+
// WeakMap tracking for animations and media
|
|
54
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
51
56
|
this._raf = requestAnimationFrame.bind(window);
|
|
52
57
|
this._caf = cancelAnimationFrame.bind(window);
|
|
53
58
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -57,19 +62,34 @@ var TimingController = class {
|
|
|
57
62
|
this._perfNow = performance.now.bind(performance);
|
|
58
63
|
this._dateNow = Date.now;
|
|
59
64
|
this.realBaseline = this._perfNow();
|
|
65
|
+
this.dateRealBaseline = this._dateNow();
|
|
66
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
60
67
|
}
|
|
61
68
|
getVirtualTime() {
|
|
62
69
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
63
70
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
64
71
|
}
|
|
72
|
+
getVirtualDateNow() {
|
|
73
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
74
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
75
|
+
}
|
|
65
76
|
reanchor() {
|
|
66
77
|
const virtualNow = this.getVirtualTime();
|
|
67
78
|
this.realBaseline = this._perfNow();
|
|
68
79
|
this.virtualBaseline = virtualNow;
|
|
80
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
81
|
+
this.dateRealBaseline = this._dateNow();
|
|
82
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
83
|
+
}
|
|
84
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
85
|
+
get speedDivisor() {
|
|
86
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
69
87
|
}
|
|
70
88
|
/** Install timing patches. Safe to call multiple times. */
|
|
71
89
|
install() {
|
|
72
90
|
if (this.installed) return;
|
|
91
|
+
if (window.__saccadeInstalled) return;
|
|
92
|
+
window.__saccadeInstalled = true;
|
|
73
93
|
this.installed = true;
|
|
74
94
|
const self = this;
|
|
75
95
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -78,21 +98,27 @@ var TimingController = class {
|
|
|
78
98
|
Element.prototype.animate = function(...args) {
|
|
79
99
|
const anim = origAnimate.apply(this, args);
|
|
80
100
|
if (self.speed !== 1) {
|
|
81
|
-
|
|
101
|
+
const originalRate = anim.playbackRate;
|
|
102
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
103
|
+
anim.playbackRate = applied;
|
|
104
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
82
105
|
}
|
|
83
106
|
return anim;
|
|
84
107
|
};
|
|
85
108
|
performance.now = () => self.getVirtualTime();
|
|
86
|
-
|
|
87
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
109
|
+
Date.now = () => self.getVirtualDateNow();
|
|
88
110
|
window.requestAnimationFrame = (callback) => {
|
|
89
111
|
return self._raf(() => {
|
|
112
|
+
if (self.speed === 0) {
|
|
113
|
+
window.requestAnimationFrame(callback);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
90
116
|
callback(self.getVirtualTime());
|
|
91
117
|
});
|
|
92
118
|
};
|
|
93
119
|
window.cancelAnimationFrame = this._caf;
|
|
94
120
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
95
|
-
const scaledDelay = (delay ?? 0) /
|
|
121
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
96
122
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
97
123
|
});
|
|
98
124
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -100,7 +126,7 @@ var TimingController = class {
|
|
|
100
126
|
const id = self.nextIntervalId++;
|
|
101
127
|
const baseDelay = delay ?? 0;
|
|
102
128
|
function tick() {
|
|
103
|
-
const scaledDelay = baseDelay /
|
|
129
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
104
130
|
const realId = self._setTimeout(() => {
|
|
105
131
|
if (typeof handler === "function") {
|
|
106
132
|
;
|
|
@@ -125,61 +151,93 @@ var TimingController = class {
|
|
|
125
151
|
self._clearInterval(id);
|
|
126
152
|
}
|
|
127
153
|
});
|
|
128
|
-
this.
|
|
129
|
-
for (const mutation of mutations) {
|
|
130
|
-
for (const node of mutation.addedNodes) {
|
|
131
|
-
if (node instanceof HTMLVideoElement || node instanceof HTMLAudioElement) {
|
|
132
|
-
node.playbackRate = self.speed;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
if (document.body) {
|
|
138
|
-
this.mediaObserver.observe(document.body, { childList: true, subtree: true });
|
|
139
|
-
}
|
|
154
|
+
this.startAnimationPoll();
|
|
140
155
|
}
|
|
141
156
|
/** Set playback speed. Requires install() first. */
|
|
142
157
|
setSpeed(newSpeed) {
|
|
143
158
|
if (!this.installed) this.install();
|
|
144
159
|
this.reanchor();
|
|
145
160
|
this.speed = newSpeed;
|
|
146
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
147
|
-
;
|
|
148
|
-
el.playbackRate = newSpeed;
|
|
149
|
-
});
|
|
150
161
|
this.patchAnimations();
|
|
162
|
+
this.patchMedia();
|
|
163
|
+
this.patchGSAP();
|
|
164
|
+
}
|
|
165
|
+
getSpeed() {
|
|
166
|
+
return this.speed;
|
|
151
167
|
}
|
|
152
|
-
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Animation polling — per-frame via original rAF
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
startAnimationPoll() {
|
|
172
|
+
const poll = () => {
|
|
173
|
+
if (!this.installed) return;
|
|
174
|
+
this.patchAnimations();
|
|
175
|
+
this.patchMedia();
|
|
176
|
+
this.animPollId = this._raf(poll);
|
|
177
|
+
};
|
|
178
|
+
this.animPollId = this._raf(poll);
|
|
179
|
+
}
|
|
180
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
153
181
|
patchAnimations() {
|
|
154
182
|
try {
|
|
155
183
|
const anims = document.getAnimations();
|
|
156
184
|
for (const anim of anims) {
|
|
157
185
|
const target = anim.effect?.target;
|
|
158
186
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
159
|
-
|
|
187
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
188
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
189
|
+
let tracked = this.trackedAnims.get(anim);
|
|
190
|
+
if (!tracked) {
|
|
191
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
192
|
+
this.trackedAnims.set(anim, tracked);
|
|
193
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
194
|
+
tracked.original = anim.playbackRate;
|
|
195
|
+
}
|
|
196
|
+
const desired = tracked.original * effectiveSpeed;
|
|
197
|
+
if (anim.playbackRate !== desired) {
|
|
198
|
+
anim.playbackRate = desired;
|
|
199
|
+
tracked.applied = desired;
|
|
200
|
+
}
|
|
160
201
|
}
|
|
161
202
|
} catch {
|
|
162
203
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
204
|
+
}
|
|
205
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
206
|
+
patchMedia() {
|
|
207
|
+
try {
|
|
208
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
209
|
+
const el = node;
|
|
210
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
211
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
212
|
+
let tracked = this.trackedMedia.get(el);
|
|
213
|
+
if (!tracked) {
|
|
214
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
215
|
+
this.trackedMedia.set(el, tracked);
|
|
216
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
217
|
+
tracked.original = el.playbackRate;
|
|
218
|
+
}
|
|
219
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
220
|
+
if (el.playbackRate !== desired) {
|
|
221
|
+
el.playbackRate = desired;
|
|
222
|
+
tracked.applied = desired;
|
|
176
223
|
}
|
|
177
|
-
}
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
178
226
|
}
|
|
179
227
|
}
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
/** Sync GSAP's global timeline if present. */
|
|
229
|
+
patchGSAP() {
|
|
230
|
+
try {
|
|
231
|
+
const gsap = window.gsap;
|
|
232
|
+
if (gsap?.globalTimeline) {
|
|
233
|
+
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
182
237
|
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Cleanup
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
183
241
|
/** Restore all patched APIs to originals. */
|
|
184
242
|
destroy() {
|
|
185
243
|
if (!this.installed) return;
|
|
@@ -199,19 +257,32 @@ var TimingController = class {
|
|
|
199
257
|
Element.prototype.animate = this._origAnimate;
|
|
200
258
|
this._origAnimate = null;
|
|
201
259
|
}
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this._clearInterval(this.animObserver);
|
|
206
|
-
this.animObserver = null;
|
|
260
|
+
if (this.animPollId) {
|
|
261
|
+
this._caf(this.animPollId);
|
|
262
|
+
this.animPollId = 0;
|
|
207
263
|
}
|
|
208
264
|
try {
|
|
209
265
|
for (const anim of document.getAnimations()) {
|
|
210
|
-
|
|
266
|
+
const tracked = this.trackedAnims.get(anim);
|
|
267
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
211
268
|
}
|
|
212
269
|
} catch {
|
|
213
270
|
}
|
|
271
|
+
try {
|
|
272
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
273
|
+
const el = node;
|
|
274
|
+
const tracked = this.trackedMedia.get(el);
|
|
275
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const gsap = window.gsap;
|
|
281
|
+
if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
214
284
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
285
|
+
delete window.__saccadeInstalled;
|
|
215
286
|
this.installed = false;
|
|
216
287
|
}
|
|
217
288
|
};
|
|
@@ -281,6 +352,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
281
352
|
"data-hover",
|
|
282
353
|
"data-at-boundary",
|
|
283
354
|
"data-scrubbing",
|
|
355
|
+
"data-starting-style",
|
|
356
|
+
"data-ending-style",
|
|
357
|
+
"data-panel-open",
|
|
358
|
+
"data-hidden",
|
|
284
359
|
"aria-checked",
|
|
285
360
|
"aria-selected",
|
|
286
361
|
"aria-expanded",
|
|
@@ -294,6 +369,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
294
369
|
"checked",
|
|
295
370
|
"disabled",
|
|
296
371
|
"hidden",
|
|
372
|
+
"inert",
|
|
297
373
|
"value",
|
|
298
374
|
"class",
|
|
299
375
|
"style"
|
|
@@ -425,6 +501,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
425
501
|
// ---- WAAPI interception --------------------------------------------------
|
|
426
502
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
427
503
|
this.interceptedAnimations = [];
|
|
504
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
505
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
428
506
|
this.hiddenSince = null;
|
|
429
507
|
this.onVisibilityChange = null;
|
|
430
508
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -475,6 +553,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
475
553
|
this.portalIdCounter = 0;
|
|
476
554
|
this.currentPortalIds.clear();
|
|
477
555
|
this.capturedPortals.clear();
|
|
556
|
+
this.seekableClones.clear();
|
|
478
557
|
this.prevInlineStyles.clear();
|
|
479
558
|
this.jsAnimStartTimes.clear();
|
|
480
559
|
this.jsAnimLastSeen.clear();
|
|
@@ -828,6 +907,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
828
907
|
} catch (_) {
|
|
829
908
|
}
|
|
830
909
|
}
|
|
910
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
911
|
+
const clean = {};
|
|
912
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
913
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
914
|
+
}
|
|
915
|
+
return clean;
|
|
916
|
+
});
|
|
831
917
|
this.animations.set(id, {
|
|
832
918
|
id,
|
|
833
919
|
name,
|
|
@@ -839,7 +925,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
839
925
|
type,
|
|
840
926
|
source,
|
|
841
927
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
842
|
-
conflicts
|
|
928
|
+
conflicts,
|
|
929
|
+
rawKeyframes: cleanedKeyframes,
|
|
930
|
+
rawTiming: { ...timing, fill: "both" }
|
|
843
931
|
});
|
|
844
932
|
}
|
|
845
933
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1115,38 +1203,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1115
1203
|
} catch (_) {
|
|
1116
1204
|
}
|
|
1117
1205
|
}, 0);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
;
|
|
1141
|
-
el.value = value;
|
|
1142
|
-
} else if (value == null) {
|
|
1143
|
-
el.removeAttribute(attr);
|
|
1144
|
-
} else {
|
|
1145
|
-
el.setAttribute(attr, value);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1206
|
+
this.seekableClones.clear();
|
|
1207
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1208
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1209
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1210
|
+
const firstColon = animId.indexOf(":");
|
|
1211
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1212
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1213
|
+
const el = this.elements.get(elSelector);
|
|
1214
|
+
if (!el?.isConnected) continue;
|
|
1215
|
+
try {
|
|
1216
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1217
|
+
...animInfo.rawTiming,
|
|
1218
|
+
fill: "both"
|
|
1219
|
+
});
|
|
1220
|
+
clone.pause();
|
|
1221
|
+
clone.currentTime = 0;
|
|
1222
|
+
this.seekableClones.set(animId, {
|
|
1223
|
+
animation: clone,
|
|
1224
|
+
element: el,
|
|
1225
|
+
effect: clone.effect
|
|
1226
|
+
});
|
|
1227
|
+
} catch (_) {
|
|
1150
1228
|
}
|
|
1151
1229
|
}
|
|
1152
1230
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1169,6 +1247,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1169
1247
|
// src/core/scrubber.ts
|
|
1170
1248
|
var TimelineScrubber = class {
|
|
1171
1249
|
constructor(state) {
|
|
1250
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1251
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1172
1252
|
/** Saved originals for restore on release */
|
|
1173
1253
|
this._originalAnimate = null;
|
|
1174
1254
|
this._originalRaf = null;
|
|
@@ -1178,36 +1258,24 @@ var TimelineScrubber = class {
|
|
|
1178
1258
|
this.frames = state.frames;
|
|
1179
1259
|
this.capturedPortals = state.capturedPortals;
|
|
1180
1260
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1181
|
-
this.
|
|
1261
|
+
this.seekableClones = state.seekableClones;
|
|
1182
1262
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1183
1263
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1184
1264
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1185
1265
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
let current = el;
|
|
1195
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1196
|
-
const tag = current.tagName.toLowerCase();
|
|
1197
|
-
const parent = current.parentElement;
|
|
1198
|
-
if (parent) {
|
|
1199
|
-
const siblings = Array.from(parent.children);
|
|
1200
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1201
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1202
|
-
} else {
|
|
1203
|
-
parts.unshift(tag);
|
|
1266
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1267
|
+
for (const fa of this.frames[i].animations) {
|
|
1268
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1269
|
+
if (!range) {
|
|
1270
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1271
|
+
} else {
|
|
1272
|
+
range.last = i;
|
|
1273
|
+
}
|
|
1204
1274
|
}
|
|
1205
|
-
current = parent;
|
|
1206
1275
|
}
|
|
1207
|
-
return parts.join(" > ");
|
|
1208
1276
|
}
|
|
1209
1277
|
// ---------------------------------------------------------------------------
|
|
1210
|
-
// seekTo — scrub
|
|
1278
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1211
1279
|
// ---------------------------------------------------------------------------
|
|
1212
1280
|
seekTo(timeMs) {
|
|
1213
1281
|
if (!this.frames.length) return;
|
|
@@ -1255,6 +1323,34 @@ var TimelineScrubber = class {
|
|
|
1255
1323
|
}
|
|
1256
1324
|
}
|
|
1257
1325
|
}
|
|
1326
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1327
|
+
for (const fa of frame.animations || []) {
|
|
1328
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1329
|
+
}
|
|
1330
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1331
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1332
|
+
try {
|
|
1333
|
+
if (frameAnim) {
|
|
1334
|
+
if (!clone.animation.effect) {
|
|
1335
|
+
clone.animation.effect = clone.effect;
|
|
1336
|
+
}
|
|
1337
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1338
|
+
} else {
|
|
1339
|
+
const range = this.animFrameRanges.get(animId);
|
|
1340
|
+
if (!range || lo < range.first) {
|
|
1341
|
+
clone.animation.effect = null;
|
|
1342
|
+
} else {
|
|
1343
|
+
if (!clone.animation.effect) {
|
|
1344
|
+
clone.animation.effect = clone.effect;
|
|
1345
|
+
}
|
|
1346
|
+
const timing = clone.effect.getTiming();
|
|
1347
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1348
|
+
clone.animation.currentTime = endTime;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1258
1354
|
for (const entry of this.interceptedAnimations) {
|
|
1259
1355
|
try {
|
|
1260
1356
|
const anim = entry.animation;
|
|
@@ -1269,34 +1365,13 @@ var TimelineScrubber = class {
|
|
|
1269
1365
|
const el = this.elements.get(sel);
|
|
1270
1366
|
if (!el || !el.isConnected) continue;
|
|
1271
1367
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1272
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1273
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1274
|
-
);
|
|
1275
1368
|
const snapTyped = snap;
|
|
1276
|
-
if (snapTyped.__styles) {
|
|
1277
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1278
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1279
|
-
el.style.setProperty(prop, value, "important");
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
1369
|
if (snapTyped.__attrs) {
|
|
1284
1370
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1371
|
+
if (attr === "class" || attr === "style") continue;
|
|
1285
1372
|
if (attr === "checked") {
|
|
1286
1373
|
;
|
|
1287
1374
|
el.checked = value === "true";
|
|
1288
|
-
} else if (attr === "class") {
|
|
1289
|
-
if (value != null) el.className = value;
|
|
1290
|
-
} else if (attr === "style") {
|
|
1291
|
-
if (value) {
|
|
1292
|
-
el.setAttribute("style", value);
|
|
1293
|
-
el.style.transition = "none";
|
|
1294
|
-
if (snapTyped.__styles) {
|
|
1295
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1296
|
-
el.style.setProperty(prop, val, "important");
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
1375
|
} else if (attr === "value") {
|
|
1301
1376
|
if (value != null) el.value = value;
|
|
1302
1377
|
} else if (value == null) {
|
|
@@ -1307,39 +1382,31 @@ var TimelineScrubber = class {
|
|
|
1307
1382
|
}
|
|
1308
1383
|
}
|
|
1309
1384
|
}
|
|
1310
|
-
for (const
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1385
|
+
for (const fa of frame.animations || []) {
|
|
1386
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1387
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1388
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1389
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1390
|
+
const el = this.elements.get(elSel);
|
|
1391
|
+
if (!el || !el.isConnected) continue;
|
|
1392
|
+
for (const prop of fa.properties) {
|
|
1317
1393
|
if (prop.value) {
|
|
1318
|
-
|
|
1394
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1319
1395
|
}
|
|
1320
1396
|
}
|
|
1321
1397
|
}
|
|
1322
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1323
|
-
for (const anim of frame.animations || []) {
|
|
1324
|
-
const fc = anim.animationId.indexOf(":");
|
|
1325
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1326
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1327
|
-
}
|
|
1328
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1329
|
-
const el = rawEl;
|
|
1330
|
-
const sel = this.getSelector(el);
|
|
1331
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1332
|
-
el.style.removeProperty("opacity");
|
|
1333
|
-
el.style.removeProperty("transform");
|
|
1334
|
-
el.style.removeProperty("filter");
|
|
1335
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
1398
|
}
|
|
1339
1399
|
// ---------------------------------------------------------------------------
|
|
1340
1400
|
// release — tear down all scrub state and restore the page to normal
|
|
1341
1401
|
// ---------------------------------------------------------------------------
|
|
1342
1402
|
release() {
|
|
1403
|
+
for (const [, clone] of this.seekableClones) {
|
|
1404
|
+
try {
|
|
1405
|
+
clone.animation.cancel();
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
this.seekableClones.clear();
|
|
1343
1410
|
for (const entry of this.interceptedAnimations) {
|
|
1344
1411
|
try {
|
|
1345
1412
|
entry.animation.cancel();
|
|
@@ -1401,6 +1468,7 @@ var TimelineScrubber = class {
|
|
|
1401
1468
|
this.elements.clear();
|
|
1402
1469
|
this.frames.length = 0;
|
|
1403
1470
|
this.capturedPortals.clear();
|
|
1471
|
+
this.animFrameRanges.clear();
|
|
1404
1472
|
}
|
|
1405
1473
|
};
|
|
1406
1474
|
|
|
@@ -1673,7 +1741,7 @@ var SaccadeEngine = class {
|
|
|
1673
1741
|
try {
|
|
1674
1742
|
capture = this.recorder.stopRecording();
|
|
1675
1743
|
} catch (e) {
|
|
1676
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1744
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1677
1745
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1678
1746
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1679
1747
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1694,7 +1762,8 @@ var SaccadeEngine = class {
|
|
|
1694
1762
|
frames: capture.frames,
|
|
1695
1763
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1696
1764
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1697
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1765
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1766
|
+
seekableClones: this.recorder.seekableClones
|
|
1698
1767
|
};
|
|
1699
1768
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1700
1769
|
this._state = "scrubbing";
|
|
@@ -2794,14 +2863,15 @@ var PANEL_STYLES = (
|
|
|
2794
2863
|
// src/react/Saccade.tsx
|
|
2795
2864
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
2796
2865
|
function Saccade({ position = "bottom-left" }) {
|
|
2797
|
-
const hostRef = (0, import_react7.useRef)(null);
|
|
2798
2866
|
const [shadowRoot, setShadowRoot] = (0, import_react7.useState)(null);
|
|
2867
|
+
const hostRef = (0, import_react7.useRef)(null);
|
|
2799
2868
|
(0, import_react7.useEffect)(() => {
|
|
2800
|
-
const host =
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2869
|
+
const host = document.createElement("div");
|
|
2870
|
+
host.setAttribute("data-lapse-panel", "");
|
|
2871
|
+
const positionOffset = position === "top-left" ? "top:20px;left:20px" : position === "top-right" ? "top:20px;right:20px" : position === "bottom-left" ? "bottom:20px;left:20px" : "bottom:20px;right:20px";
|
|
2872
|
+
host.style.cssText = `position:fixed;z-index:2147483647;pointer-events:auto;${positionOffset}`;
|
|
2873
|
+
document.body.appendChild(host);
|
|
2874
|
+
hostRef.current = host;
|
|
2805
2875
|
const shadow = host.attachShadow({ mode: "open" });
|
|
2806
2876
|
const style = document.createElement("style");
|
|
2807
2877
|
style.textContent = PANEL_STYLES;
|
|
@@ -2809,25 +2879,15 @@ function Saccade({ position = "bottom-left" }) {
|
|
|
2809
2879
|
const mount = document.createElement("div");
|
|
2810
2880
|
shadow.appendChild(mount);
|
|
2811
2881
|
setShadowRoot(shadow);
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
zIndex: 2147483647,
|
|
2822
|
-
// max int — must sit above the scrub blocker (z-index: 999999)
|
|
2823
|
-
pointerEvents: "auto",
|
|
2824
|
-
...positionOffset
|
|
2825
|
-
},
|
|
2826
|
-
children: shadowRoot && (0, import_react_dom.createPortal)(
|
|
2827
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadePanel, {}) }),
|
|
2828
|
-
shadowRoot.lastElementChild || shadowRoot
|
|
2829
|
-
)
|
|
2830
|
-
}
|
|
2882
|
+
return () => {
|
|
2883
|
+
host.remove();
|
|
2884
|
+
hostRef.current = null;
|
|
2885
|
+
};
|
|
2886
|
+
}, [position]);
|
|
2887
|
+
if (!shadowRoot) return null;
|
|
2888
|
+
return (0, import_react_dom.createPortal)(
|
|
2889
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadePanel, {}) }),
|
|
2890
|
+
shadowRoot.lastElementChild || shadowRoot
|
|
2831
2891
|
);
|
|
2832
2892
|
}
|
|
2833
2893
|
// Annotate the CommonJS export names for ESM import in node:
|