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.mjs
CHANGED
|
@@ -8,16 +8,21 @@ import { createPortal } from "react-dom";
|
|
|
8
8
|
import { createContext, useContext, useRef } from "react";
|
|
9
9
|
|
|
10
10
|
// src/core/timing.ts
|
|
11
|
+
var MEDIA_RATE_MIN = 0.0625;
|
|
12
|
+
var MEDIA_RATE_MAX = 16;
|
|
13
|
+
var ZERO_SPEED_DIVISOR = 1e-4;
|
|
11
14
|
var TimingController = class {
|
|
12
15
|
constructor() {
|
|
13
16
|
this.speed = 1;
|
|
14
17
|
this.virtualBaseline = 0;
|
|
15
18
|
this.intervalMap = /* @__PURE__ */ new Map();
|
|
16
19
|
this.nextIntervalId = 1e6;
|
|
17
|
-
this.mediaObserver = null;
|
|
18
|
-
this.animObserver = null;
|
|
19
20
|
this._origAnimate = null;
|
|
20
21
|
this.installed = false;
|
|
22
|
+
this.animPollId = 0;
|
|
23
|
+
// WeakMap tracking for animations and media
|
|
24
|
+
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
25
|
+
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
21
26
|
this._raf = requestAnimationFrame.bind(window);
|
|
22
27
|
this._caf = cancelAnimationFrame.bind(window);
|
|
23
28
|
this._setTimeout = setTimeout.bind(window);
|
|
@@ -27,19 +32,34 @@ var TimingController = class {
|
|
|
27
32
|
this._perfNow = performance.now.bind(performance);
|
|
28
33
|
this._dateNow = Date.now;
|
|
29
34
|
this.realBaseline = this._perfNow();
|
|
35
|
+
this.dateRealBaseline = this._dateNow();
|
|
36
|
+
this.dateVirtualBaseline = this.dateRealBaseline;
|
|
30
37
|
}
|
|
31
38
|
getVirtualTime() {
|
|
32
39
|
const realElapsed = this._perfNow() - this.realBaseline;
|
|
33
40
|
return this.virtualBaseline + realElapsed * this.speed;
|
|
34
41
|
}
|
|
42
|
+
getVirtualDateNow() {
|
|
43
|
+
const realElapsed = this._dateNow() - this.dateRealBaseline;
|
|
44
|
+
return this.dateVirtualBaseline + realElapsed * this.speed;
|
|
45
|
+
}
|
|
35
46
|
reanchor() {
|
|
36
47
|
const virtualNow = this.getVirtualTime();
|
|
37
48
|
this.realBaseline = this._perfNow();
|
|
38
49
|
this.virtualBaseline = virtualNow;
|
|
50
|
+
const virtualDateNow = this.getVirtualDateNow();
|
|
51
|
+
this.dateRealBaseline = this._dateNow();
|
|
52
|
+
this.dateVirtualBaseline = virtualDateNow;
|
|
53
|
+
}
|
|
54
|
+
/** Effective speed divisor — avoids division by zero at speed=0. */
|
|
55
|
+
get speedDivisor() {
|
|
56
|
+
return this.speed || ZERO_SPEED_DIVISOR;
|
|
39
57
|
}
|
|
40
58
|
/** Install timing patches. Safe to call multiple times. */
|
|
41
59
|
install() {
|
|
42
60
|
if (this.installed) return;
|
|
61
|
+
if (window.__saccadeInstalled) return;
|
|
62
|
+
window.__saccadeInstalled = true;
|
|
43
63
|
this.installed = true;
|
|
44
64
|
const self = this;
|
|
45
65
|
window.__LAPSE_ORIGINAL_RAF__ = this._raf;
|
|
@@ -48,21 +68,27 @@ var TimingController = class {
|
|
|
48
68
|
Element.prototype.animate = function(...args) {
|
|
49
69
|
const anim = origAnimate.apply(this, args);
|
|
50
70
|
if (self.speed !== 1) {
|
|
51
|
-
|
|
71
|
+
const originalRate = anim.playbackRate;
|
|
72
|
+
const applied = originalRate * (self.speed || 1e-3);
|
|
73
|
+
anim.playbackRate = applied;
|
|
74
|
+
self.trackedAnims.set(anim, { original: originalRate, applied });
|
|
52
75
|
}
|
|
53
76
|
return anim;
|
|
54
77
|
};
|
|
55
78
|
performance.now = () => self.getVirtualTime();
|
|
56
|
-
|
|
57
|
-
Date.now = () => dateBaseline + self.getVirtualTime();
|
|
79
|
+
Date.now = () => self.getVirtualDateNow();
|
|
58
80
|
window.requestAnimationFrame = (callback) => {
|
|
59
81
|
return self._raf(() => {
|
|
82
|
+
if (self.speed === 0) {
|
|
83
|
+
window.requestAnimationFrame(callback);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
60
86
|
callback(self.getVirtualTime());
|
|
61
87
|
});
|
|
62
88
|
};
|
|
63
89
|
window.cancelAnimationFrame = this._caf;
|
|
64
90
|
window.setTimeout = ((handler, delay, ...args) => {
|
|
65
|
-
const scaledDelay = (delay ?? 0) /
|
|
91
|
+
const scaledDelay = (delay ?? 0) / self.speedDivisor;
|
|
66
92
|
return self._setTimeout(handler, scaledDelay, ...args);
|
|
67
93
|
});
|
|
68
94
|
window.clearTimeout = this._clearTimeout;
|
|
@@ -70,7 +96,7 @@ var TimingController = class {
|
|
|
70
96
|
const id = self.nextIntervalId++;
|
|
71
97
|
const baseDelay = delay ?? 0;
|
|
72
98
|
function tick() {
|
|
73
|
-
const scaledDelay = baseDelay /
|
|
99
|
+
const scaledDelay = baseDelay / self.speedDivisor;
|
|
74
100
|
const realId = self._setTimeout(() => {
|
|
75
101
|
if (typeof handler === "function") {
|
|
76
102
|
;
|
|
@@ -95,61 +121,93 @@ var TimingController = class {
|
|
|
95
121
|
self._clearInterval(id);
|
|
96
122
|
}
|
|
97
123
|
});
|
|
98
|
-
this.
|
|
99
|
-
for (const mutation of mutations) {
|
|
100
|
-
for (const node of mutation.addedNodes) {
|
|
101
|
-
if (node instanceof HTMLVideoElement || node instanceof HTMLAudioElement) {
|
|
102
|
-
node.playbackRate = self.speed;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
if (document.body) {
|
|
108
|
-
this.mediaObserver.observe(document.body, { childList: true, subtree: true });
|
|
109
|
-
}
|
|
124
|
+
this.startAnimationPoll();
|
|
110
125
|
}
|
|
111
126
|
/** Set playback speed. Requires install() first. */
|
|
112
127
|
setSpeed(newSpeed) {
|
|
113
128
|
if (!this.installed) this.install();
|
|
114
129
|
this.reanchor();
|
|
115
130
|
this.speed = newSpeed;
|
|
116
|
-
document.querySelectorAll("video, audio").forEach((el) => {
|
|
117
|
-
;
|
|
118
|
-
el.playbackRate = newSpeed;
|
|
119
|
-
});
|
|
120
131
|
this.patchAnimations();
|
|
132
|
+
this.patchMedia();
|
|
133
|
+
this.patchGSAP();
|
|
134
|
+
}
|
|
135
|
+
getSpeed() {
|
|
136
|
+
return this.speed;
|
|
121
137
|
}
|
|
122
|
-
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Animation polling — per-frame via original rAF
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
startAnimationPoll() {
|
|
142
|
+
const poll = () => {
|
|
143
|
+
if (!this.installed) return;
|
|
144
|
+
this.patchAnimations();
|
|
145
|
+
this.patchMedia();
|
|
146
|
+
this.animPollId = this._raf(poll);
|
|
147
|
+
};
|
|
148
|
+
this.animPollId = this._raf(poll);
|
|
149
|
+
}
|
|
150
|
+
/** Patch playbackRate on all active animations via WAAPI. */
|
|
123
151
|
patchAnimations() {
|
|
124
152
|
try {
|
|
125
153
|
const anims = document.getAnimations();
|
|
126
154
|
for (const anim of anims) {
|
|
127
155
|
const target = anim.effect?.target;
|
|
128
156
|
if (target?.closest?.("[data-lapse-panel]")) continue;
|
|
129
|
-
|
|
157
|
+
if (target?.closest?.("[data-saccade-exclude]")) continue;
|
|
158
|
+
const effectiveSpeed = this.speed || 1e-3;
|
|
159
|
+
let tracked = this.trackedAnims.get(anim);
|
|
160
|
+
if (!tracked) {
|
|
161
|
+
tracked = { original: anim.playbackRate, applied: anim.playbackRate };
|
|
162
|
+
this.trackedAnims.set(anim, tracked);
|
|
163
|
+
} else if (anim.playbackRate !== tracked.applied) {
|
|
164
|
+
tracked.original = anim.playbackRate;
|
|
165
|
+
}
|
|
166
|
+
const desired = tracked.original * effectiveSpeed;
|
|
167
|
+
if (anim.playbackRate !== desired) {
|
|
168
|
+
anim.playbackRate = desired;
|
|
169
|
+
tracked.applied = desired;
|
|
170
|
+
}
|
|
130
171
|
}
|
|
131
172
|
} catch {
|
|
132
173
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
174
|
+
}
|
|
175
|
+
/** Patch playbackRate on all video/audio elements. */
|
|
176
|
+
patchMedia() {
|
|
177
|
+
try {
|
|
178
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
179
|
+
const el = node;
|
|
180
|
+
if (el.closest?.("[data-lapse-panel]")) return;
|
|
181
|
+
if (el.closest?.("[data-saccade-exclude]")) return;
|
|
182
|
+
let tracked = this.trackedMedia.get(el);
|
|
183
|
+
if (!tracked) {
|
|
184
|
+
tracked = { original: el.playbackRate, applied: el.playbackRate };
|
|
185
|
+
this.trackedMedia.set(el, tracked);
|
|
186
|
+
} else if (el.playbackRate !== tracked.applied) {
|
|
187
|
+
tracked.original = el.playbackRate;
|
|
188
|
+
}
|
|
189
|
+
const desired = Math.min(MEDIA_RATE_MAX, Math.max(MEDIA_RATE_MIN, tracked.original * (this.speed || MEDIA_RATE_MIN)));
|
|
190
|
+
if (el.playbackRate !== desired) {
|
|
191
|
+
el.playbackRate = desired;
|
|
192
|
+
tracked.applied = desired;
|
|
146
193
|
}
|
|
147
|
-
}
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
|
-
|
|
151
|
-
|
|
198
|
+
/** Sync GSAP's global timeline if present. */
|
|
199
|
+
patchGSAP() {
|
|
200
|
+
try {
|
|
201
|
+
const gsap = window.gsap;
|
|
202
|
+
if (gsap?.globalTimeline) {
|
|
203
|
+
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
152
207
|
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Cleanup
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
153
211
|
/** Restore all patched APIs to originals. */
|
|
154
212
|
destroy() {
|
|
155
213
|
if (!this.installed) return;
|
|
@@ -169,19 +227,32 @@ var TimingController = class {
|
|
|
169
227
|
Element.prototype.animate = this._origAnimate;
|
|
170
228
|
this._origAnimate = null;
|
|
171
229
|
}
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this._clearInterval(this.animObserver);
|
|
176
|
-
this.animObserver = null;
|
|
230
|
+
if (this.animPollId) {
|
|
231
|
+
this._caf(this.animPollId);
|
|
232
|
+
this.animPollId = 0;
|
|
177
233
|
}
|
|
178
234
|
try {
|
|
179
235
|
for (const anim of document.getAnimations()) {
|
|
180
|
-
|
|
236
|
+
const tracked = this.trackedAnims.get(anim);
|
|
237
|
+
anim.playbackRate = tracked?.original ?? 1;
|
|
181
238
|
}
|
|
182
239
|
} catch {
|
|
183
240
|
}
|
|
241
|
+
try {
|
|
242
|
+
document.querySelectorAll("video, audio").forEach((node) => {
|
|
243
|
+
const el = node;
|
|
244
|
+
const tracked = this.trackedMedia.get(el);
|
|
245
|
+
el.playbackRate = tracked?.original ?? 1;
|
|
246
|
+
});
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const gsap = window.gsap;
|
|
251
|
+
if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
184
254
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
255
|
+
delete window.__saccadeInstalled;
|
|
185
256
|
this.installed = false;
|
|
186
257
|
}
|
|
187
258
|
};
|
|
@@ -251,6 +322,10 @@ var SNAPSHOT_ATTRS = [
|
|
|
251
322
|
"data-hover",
|
|
252
323
|
"data-at-boundary",
|
|
253
324
|
"data-scrubbing",
|
|
325
|
+
"data-starting-style",
|
|
326
|
+
"data-ending-style",
|
|
327
|
+
"data-panel-open",
|
|
328
|
+
"data-hidden",
|
|
254
329
|
"aria-checked",
|
|
255
330
|
"aria-selected",
|
|
256
331
|
"aria-expanded",
|
|
@@ -264,6 +339,7 @@ var SNAPSHOT_ATTRS = [
|
|
|
264
339
|
"checked",
|
|
265
340
|
"disabled",
|
|
266
341
|
"hidden",
|
|
342
|
+
"inert",
|
|
267
343
|
"value",
|
|
268
344
|
"class",
|
|
269
345
|
"style"
|
|
@@ -395,6 +471,8 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
395
471
|
// ---- WAAPI interception --------------------------------------------------
|
|
396
472
|
/** Animations captured via Element.prototype.animate monkey-patch. */
|
|
397
473
|
this.interceptedAnimations = [];
|
|
474
|
+
// ---- Seekable WAAPI clones (created in stopRecording for scrubbing) -----
|
|
475
|
+
this.seekableClones = /* @__PURE__ */ new Map();
|
|
398
476
|
this.hiddenSince = null;
|
|
399
477
|
this.onVisibilityChange = null;
|
|
400
478
|
/** Set to true when the capture loop self-terminates due to limits. */
|
|
@@ -445,6 +523,7 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
445
523
|
this.portalIdCounter = 0;
|
|
446
524
|
this.currentPortalIds.clear();
|
|
447
525
|
this.capturedPortals.clear();
|
|
526
|
+
this.seekableClones.clear();
|
|
448
527
|
this.prevInlineStyles.clear();
|
|
449
528
|
this.jsAnimStartTimes.clear();
|
|
450
529
|
this.jsAnimLastSeen.clear();
|
|
@@ -798,6 +877,13 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
798
877
|
} catch (_) {
|
|
799
878
|
}
|
|
800
879
|
}
|
|
880
|
+
const cleanedKeyframes = keyframes2.map((kf) => {
|
|
881
|
+
const clean = {};
|
|
882
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
883
|
+
if (k !== "computedOffset" && k !== "composite") clean[k] = v;
|
|
884
|
+
}
|
|
885
|
+
return clean;
|
|
886
|
+
});
|
|
801
887
|
this.animations.set(id, {
|
|
802
888
|
id,
|
|
803
889
|
name,
|
|
@@ -809,7 +895,9 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
809
895
|
type,
|
|
810
896
|
source,
|
|
811
897
|
resolvedVars: Object.keys(resolvedVars).length > 0 ? resolvedVars : null,
|
|
812
|
-
conflicts
|
|
898
|
+
conflicts,
|
|
899
|
+
rawKeyframes: cleanedKeyframes,
|
|
900
|
+
rawTiming: { ...timing, fill: "both" }
|
|
813
901
|
});
|
|
814
902
|
}
|
|
815
903
|
const keyframes = a.effect?.getKeyframes?.() || [];
|
|
@@ -1085,38 +1173,28 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1085
1173
|
} catch (_) {
|
|
1086
1174
|
}
|
|
1087
1175
|
}, 0);
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
;
|
|
1111
|
-
el.value = value;
|
|
1112
|
-
} else if (value == null) {
|
|
1113
|
-
el.removeAttribute(attr);
|
|
1114
|
-
} else {
|
|
1115
|
-
el.setAttribute(attr, value);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1176
|
+
this.seekableClones.clear();
|
|
1177
|
+
for (const [animId, animInfo] of this.animations) {
|
|
1178
|
+
if (!animInfo.rawKeyframes?.length || !animInfo.rawTiming) continue;
|
|
1179
|
+
if (animInfo.type === "JSAnimation") continue;
|
|
1180
|
+
const firstColon = animId.indexOf(":");
|
|
1181
|
+
const secondColon = animId.indexOf(":", firstColon + 1);
|
|
1182
|
+
const elSelector = secondColon >= 0 ? animId.substring(secondColon + 1) : "";
|
|
1183
|
+
const el = this.elements.get(elSelector);
|
|
1184
|
+
if (!el?.isConnected) continue;
|
|
1185
|
+
try {
|
|
1186
|
+
const clone = el.animate(animInfo.rawKeyframes, {
|
|
1187
|
+
...animInfo.rawTiming,
|
|
1188
|
+
fill: "both"
|
|
1189
|
+
});
|
|
1190
|
+
clone.pause();
|
|
1191
|
+
clone.currentTime = 0;
|
|
1192
|
+
this.seekableClones.set(animId, {
|
|
1193
|
+
animation: clone,
|
|
1194
|
+
element: el,
|
|
1195
|
+
effect: clone.effect
|
|
1196
|
+
});
|
|
1197
|
+
} catch (_) {
|
|
1120
1198
|
}
|
|
1121
1199
|
}
|
|
1122
1200
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1139,6 +1217,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1139
1217
|
// src/core/scrubber.ts
|
|
1140
1218
|
var TimelineScrubber = class {
|
|
1141
1219
|
constructor(state) {
|
|
1220
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1221
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1142
1222
|
/** Saved originals for restore on release */
|
|
1143
1223
|
this._originalAnimate = null;
|
|
1144
1224
|
this._originalRaf = null;
|
|
@@ -1148,36 +1228,24 @@ var TimelineScrubber = class {
|
|
|
1148
1228
|
this.frames = state.frames;
|
|
1149
1229
|
this.capturedPortals = state.capturedPortals;
|
|
1150
1230
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1151
|
-
this.
|
|
1231
|
+
this.seekableClones = state.seekableClones;
|
|
1152
1232
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1153
1233
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1154
1234
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1155
1235
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
let current = el;
|
|
1165
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1166
|
-
const tag = current.tagName.toLowerCase();
|
|
1167
|
-
const parent = current.parentElement;
|
|
1168
|
-
if (parent) {
|
|
1169
|
-
const siblings = Array.from(parent.children);
|
|
1170
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1171
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1172
|
-
} else {
|
|
1173
|
-
parts.unshift(tag);
|
|
1236
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
1237
|
+
for (const fa of this.frames[i].animations) {
|
|
1238
|
+
const range = this.animFrameRanges.get(fa.animationId);
|
|
1239
|
+
if (!range) {
|
|
1240
|
+
this.animFrameRanges.set(fa.animationId, { first: i, last: i });
|
|
1241
|
+
} else {
|
|
1242
|
+
range.last = i;
|
|
1243
|
+
}
|
|
1174
1244
|
}
|
|
1175
|
-
current = parent;
|
|
1176
1245
|
}
|
|
1177
|
-
return parts.join(" > ");
|
|
1178
1246
|
}
|
|
1179
1247
|
// ---------------------------------------------------------------------------
|
|
1180
|
-
// seekTo — scrub
|
|
1248
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1181
1249
|
// ---------------------------------------------------------------------------
|
|
1182
1250
|
seekTo(timeMs) {
|
|
1183
1251
|
if (!this.frames.length) return;
|
|
@@ -1225,6 +1293,34 @@ var TimelineScrubber = class {
|
|
|
1225
1293
|
}
|
|
1226
1294
|
}
|
|
1227
1295
|
}
|
|
1296
|
+
const activeAnimIds = /* @__PURE__ */ new Map();
|
|
1297
|
+
for (const fa of frame.animations || []) {
|
|
1298
|
+
activeAnimIds.set(fa.animationId, fa);
|
|
1299
|
+
}
|
|
1300
|
+
for (const [animId, clone] of this.seekableClones) {
|
|
1301
|
+
const frameAnim = activeAnimIds.get(animId);
|
|
1302
|
+
try {
|
|
1303
|
+
if (frameAnim) {
|
|
1304
|
+
if (!clone.animation.effect) {
|
|
1305
|
+
clone.animation.effect = clone.effect;
|
|
1306
|
+
}
|
|
1307
|
+
clone.animation.currentTime = frameAnim.currentTime;
|
|
1308
|
+
} else {
|
|
1309
|
+
const range = this.animFrameRanges.get(animId);
|
|
1310
|
+
if (!range || lo < range.first) {
|
|
1311
|
+
clone.animation.effect = null;
|
|
1312
|
+
} else {
|
|
1313
|
+
if (!clone.animation.effect) {
|
|
1314
|
+
clone.animation.effect = clone.effect;
|
|
1315
|
+
}
|
|
1316
|
+
const timing = clone.effect.getTiming();
|
|
1317
|
+
const endTime = (typeof timing.duration === "number" ? timing.duration : 0) + (timing.delay || 0);
|
|
1318
|
+
clone.animation.currentTime = endTime;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1228
1324
|
for (const entry of this.interceptedAnimations) {
|
|
1229
1325
|
try {
|
|
1230
1326
|
const anim = entry.animation;
|
|
@@ -1239,34 +1335,13 @@ var TimelineScrubber = class {
|
|
|
1239
1335
|
const el = this.elements.get(sel);
|
|
1240
1336
|
if (!el || !el.isConnected) continue;
|
|
1241
1337
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1242
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1243
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1244
|
-
);
|
|
1245
1338
|
const snapTyped = snap;
|
|
1246
|
-
if (snapTyped.__styles) {
|
|
1247
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1248
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1249
|
-
el.style.setProperty(prop, value, "important");
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
1339
|
if (snapTyped.__attrs) {
|
|
1254
1340
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1341
|
+
if (attr === "class" || attr === "style") continue;
|
|
1255
1342
|
if (attr === "checked") {
|
|
1256
1343
|
;
|
|
1257
1344
|
el.checked = value === "true";
|
|
1258
|
-
} else if (attr === "class") {
|
|
1259
|
-
if (value != null) el.className = value;
|
|
1260
|
-
} else if (attr === "style") {
|
|
1261
|
-
if (value) {
|
|
1262
|
-
el.setAttribute("style", value);
|
|
1263
|
-
el.style.transition = "none";
|
|
1264
|
-
if (snapTyped.__styles) {
|
|
1265
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1266
|
-
el.style.setProperty(prop, val, "important");
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
1345
|
} else if (attr === "value") {
|
|
1271
1346
|
if (value != null) el.value = value;
|
|
1272
1347
|
} else if (value == null) {
|
|
@@ -1277,39 +1352,31 @@ var TimelineScrubber = class {
|
|
|
1277
1352
|
}
|
|
1278
1353
|
}
|
|
1279
1354
|
}
|
|
1280
|
-
for (const
|
|
1281
|
-
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1355
|
+
for (const fa of frame.animations || []) {
|
|
1356
|
+
if (!fa.animationId.startsWith("JSAnimation:")) continue;
|
|
1357
|
+
const firstColon = fa.animationId.indexOf(":");
|
|
1358
|
+
const secondColon = fa.animationId.indexOf(":", firstColon + 1);
|
|
1359
|
+
const elSel = secondColon >= 0 ? fa.animationId.substring(secondColon + 1) : "";
|
|
1360
|
+
const el = this.elements.get(elSel);
|
|
1361
|
+
if (!el || !el.isConnected) continue;
|
|
1362
|
+
for (const prop of fa.properties) {
|
|
1287
1363
|
if (prop.value) {
|
|
1288
|
-
|
|
1364
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1289
1365
|
}
|
|
1290
1366
|
}
|
|
1291
1367
|
}
|
|
1292
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1293
|
-
for (const anim of frame.animations || []) {
|
|
1294
|
-
const fc = anim.animationId.indexOf(":");
|
|
1295
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1296
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1297
|
-
}
|
|
1298
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1299
|
-
const el = rawEl;
|
|
1300
|
-
const sel = this.getSelector(el);
|
|
1301
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1302
|
-
el.style.removeProperty("opacity");
|
|
1303
|
-
el.style.removeProperty("transform");
|
|
1304
|
-
el.style.removeProperty("filter");
|
|
1305
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
1368
|
}
|
|
1309
1369
|
// ---------------------------------------------------------------------------
|
|
1310
1370
|
// release — tear down all scrub state and restore the page to normal
|
|
1311
1371
|
// ---------------------------------------------------------------------------
|
|
1312
1372
|
release() {
|
|
1373
|
+
for (const [, clone] of this.seekableClones) {
|
|
1374
|
+
try {
|
|
1375
|
+
clone.animation.cancel();
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
this.seekableClones.clear();
|
|
1313
1380
|
for (const entry of this.interceptedAnimations) {
|
|
1314
1381
|
try {
|
|
1315
1382
|
entry.animation.cancel();
|
|
@@ -1371,6 +1438,7 @@ var TimelineScrubber = class {
|
|
|
1371
1438
|
this.elements.clear();
|
|
1372
1439
|
this.frames.length = 0;
|
|
1373
1440
|
this.capturedPortals.clear();
|
|
1441
|
+
this.animFrameRanges.clear();
|
|
1374
1442
|
}
|
|
1375
1443
|
};
|
|
1376
1444
|
|
|
@@ -1643,7 +1711,7 @@ var SaccadeEngine = class {
|
|
|
1643
1711
|
try {
|
|
1644
1712
|
capture = this.recorder.stopRecording();
|
|
1645
1713
|
} catch (e) {
|
|
1646
|
-
console.error("[Saccade] stopRecording failed:", e);
|
|
1714
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1647
1715
|
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1648
1716
|
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1649
1717
|
document.getElementById("__lapse-state-rules")?.remove();
|
|
@@ -1664,7 +1732,8 @@ var SaccadeEngine = class {
|
|
|
1664
1732
|
frames: capture.frames,
|
|
1665
1733
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1666
1734
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1667
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1735
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1736
|
+
seekableClones: this.recorder.seekableClones
|
|
1668
1737
|
};
|
|
1669
1738
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1670
1739
|
this._state = "scrubbing";
|
|
@@ -2764,14 +2833,15 @@ var PANEL_STYLES = (
|
|
|
2764
2833
|
// src/react/Saccade.tsx
|
|
2765
2834
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2766
2835
|
function Saccade({ position = "bottom-left" }) {
|
|
2767
|
-
const hostRef = useRef6(null);
|
|
2768
2836
|
const [shadowRoot, setShadowRoot] = useState4(null);
|
|
2837
|
+
const hostRef = useRef6(null);
|
|
2769
2838
|
useEffect4(() => {
|
|
2770
|
-
const host =
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2839
|
+
const host = document.createElement("div");
|
|
2840
|
+
host.setAttribute("data-lapse-panel", "");
|
|
2841
|
+
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";
|
|
2842
|
+
host.style.cssText = `position:fixed;z-index:2147483647;pointer-events:auto;${positionOffset}`;
|
|
2843
|
+
document.body.appendChild(host);
|
|
2844
|
+
hostRef.current = host;
|
|
2775
2845
|
const shadow = host.attachShadow({ mode: "open" });
|
|
2776
2846
|
const style = document.createElement("style");
|
|
2777
2847
|
style.textContent = PANEL_STYLES;
|
|
@@ -2779,25 +2849,15 @@ function Saccade({ position = "bottom-left" }) {
|
|
|
2779
2849
|
const mount = document.createElement("div");
|
|
2780
2850
|
shadow.appendChild(mount);
|
|
2781
2851
|
setShadowRoot(shadow);
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
zIndex: 2147483647,
|
|
2792
|
-
// max int — must sit above the scrub blocker (z-index: 999999)
|
|
2793
|
-
pointerEvents: "auto",
|
|
2794
|
-
...positionOffset
|
|
2795
|
-
},
|
|
2796
|
-
children: shadowRoot && createPortal(
|
|
2797
|
-
/* @__PURE__ */ jsx5(SaccadeProvider, { children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
|
|
2798
|
-
shadowRoot.lastElementChild || shadowRoot
|
|
2799
|
-
)
|
|
2800
|
-
}
|
|
2852
|
+
return () => {
|
|
2853
|
+
host.remove();
|
|
2854
|
+
hostRef.current = null;
|
|
2855
|
+
};
|
|
2856
|
+
}, [position]);
|
|
2857
|
+
if (!shadowRoot) return null;
|
|
2858
|
+
return createPortal(
|
|
2859
|
+
/* @__PURE__ */ jsx5(SaccadeProvider, { children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
|
|
2860
|
+
shadowRoot.lastElementChild || shadowRoot
|
|
2801
2861
|
);
|
|
2802
2862
|
}
|
|
2803
2863
|
export {
|