saccade 0.0.2 → 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/README.md +6 -6
- package/dist/core.cjs +288 -204
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +43 -13
- package/dist/core.d.ts +43 -13
- package/dist/core.mjs +287 -203
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +334 -255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.mjs +330 -251
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/core/index.ts
|
|
21
21
|
var core_exports = {};
|
|
22
22
|
__export(core_exports, {
|
|
23
|
-
|
|
23
|
+
SaccadeEngine: () => SaccadeEngine,
|
|
24
24
|
TimelineRecorder: () => TimelineRecorder,
|
|
25
25
|
TimelineScrubber: () => TimelineScrubber,
|
|
26
26
|
TimingController: () => TimingController,
|
|
@@ -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();
|
|
157
|
+
}
|
|
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);
|
|
144
172
|
}
|
|
145
|
-
/** Patch playbackRate on all active
|
|
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;
|
|
169
211
|
}
|
|
170
|
-
|
|
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;
|
|
216
|
+
}
|
|
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?.() || [];
|
|
@@ -1053,96 +1141,83 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1053
1141
|
blocker.title = "Clear the timeline to interact with the page";
|
|
1054
1142
|
document.body.appendChild(blocker);
|
|
1055
1143
|
this.blockerEl = blocker;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (t && (t.includes(":hover") || t.includes(":focus"))) {
|
|
1074
|
-
allCss += t + "\n";
|
|
1144
|
+
setTimeout(() => {
|
|
1145
|
+
try {
|
|
1146
|
+
const lapseStyle = document.createElement("style");
|
|
1147
|
+
lapseStyle.id = "__lapse-state-rules";
|
|
1148
|
+
const hoverFocusRules = [];
|
|
1149
|
+
for (const sheet of document.styleSheets) {
|
|
1150
|
+
try {
|
|
1151
|
+
const walk = (rules) => {
|
|
1152
|
+
for (const rule of rules) {
|
|
1153
|
+
if (rule.cssRules) {
|
|
1154
|
+
walk(rule.cssRules);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const t = rule.cssText;
|
|
1158
|
+
if (t && (t.includes(":hover") || t.includes(":focus"))) {
|
|
1159
|
+
hoverFocusRules.push(t);
|
|
1160
|
+
}
|
|
1075
1161
|
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
walk2(sheet.cssRules);
|
|
1080
|
-
} catch (_) {
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
const stateRegex = /([^{}]*(?::hover|:focus-visible|:focus-within|:focus(?!-))[^{}]*)\{([^{}]*)\}/g;
|
|
1084
|
-
let match;
|
|
1085
|
-
while ((match = stateRegex.exec(allCss)) !== null) {
|
|
1086
|
-
const selector = match[1].trim();
|
|
1087
|
-
const body = match[2].trim();
|
|
1088
|
-
if (!body) continue;
|
|
1089
|
-
const newBody = body.replace(
|
|
1090
|
-
/([^;:]+):\s*([^;]+)(;|$)/g,
|
|
1091
|
-
(m, prop, val, end) => {
|
|
1092
|
-
if (val.includes("!important")) return m;
|
|
1093
|
-
return prop + ": " + val.trim() + " !important" + end;
|
|
1162
|
+
};
|
|
1163
|
+
walk(sheet.cssRules);
|
|
1164
|
+
} catch (_) {
|
|
1094
1165
|
}
|
|
1095
|
-
);
|
|
1096
|
-
if (selector.includes(":hover")) {
|
|
1097
|
-
lapseStyle.textContent += selector.replace(/:hover/g, "[data-lapse-hover]") + " { " + newBody + " }\n";
|
|
1098
1166
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
} catch (_) {
|
|
1113
|
-
}
|
|
1114
|
-
if (this.frames.length > 0) {
|
|
1115
|
-
const frame0 = this.frames[0];
|
|
1116
|
-
if (frame0.elementSnapshots) {
|
|
1117
|
-
for (const [sel, snap] of Object.entries(frame0.elementSnapshots)) {
|
|
1118
|
-
const el = this.elements.get(sel);
|
|
1119
|
-
if (!el || !el.isConnected) continue;
|
|
1120
|
-
if (snap.__styles) {
|
|
1121
|
-
for (const [prop, value] of Object.entries(snap.__styles)) {
|
|
1122
|
-
if (SAFE_PROPS_SET.has(prop)) {
|
|
1123
|
-
el.style.setProperty(prop, value, "important");
|
|
1124
|
-
}
|
|
1167
|
+
const parts = [];
|
|
1168
|
+
for (const ruleText of hoverFocusRules) {
|
|
1169
|
+
const braceIdx = ruleText.indexOf("{");
|
|
1170
|
+
if (braceIdx === -1) continue;
|
|
1171
|
+
const selector = ruleText.slice(0, braceIdx).trim();
|
|
1172
|
+
const bodyEnd = ruleText.lastIndexOf("}");
|
|
1173
|
+
const body = ruleText.slice(braceIdx + 1, bodyEnd).trim();
|
|
1174
|
+
if (!body) continue;
|
|
1175
|
+
const newBody = body.replace(
|
|
1176
|
+
/([^;:]+):\s*([^;]+)(;|$)/g,
|
|
1177
|
+
(m, prop, val, end) => {
|
|
1178
|
+
if (val.includes("!important")) return m;
|
|
1179
|
+
return prop + ": " + val.trim() + " !important" + end;
|
|
1125
1180
|
}
|
|
1181
|
+
);
|
|
1182
|
+
if (selector.includes(":hover")) {
|
|
1183
|
+
parts.push(selector.replace(/:hover/g, "[data-lapse-hover]") + " { " + newBody + " }");
|
|
1126
1184
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
el.className = value;
|
|
1134
|
-
} else if (attr === "style") {
|
|
1135
|
-
} else if (attr === "value" && value != null) {
|
|
1136
|
-
;
|
|
1137
|
-
el.value = value;
|
|
1138
|
-
} else if (value == null) {
|
|
1139
|
-
el.removeAttribute(attr);
|
|
1140
|
-
} else {
|
|
1141
|
-
el.setAttribute(attr, value);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1185
|
+
if (selector.includes(":focus-visible")) {
|
|
1186
|
+
parts.push(selector.replace(/:focus-visible/g, "[data-lapse-focus]") + " { " + newBody + " }");
|
|
1187
|
+
} else if (selector.includes(":focus-within")) {
|
|
1188
|
+
parts.push(selector.replace(/:focus-within/g, ":has([data-lapse-focus])") + " { " + newBody + " }");
|
|
1189
|
+
} else if (selector.includes(":focus")) {
|
|
1190
|
+
parts.push(selector.replace(/:focus(?!-)/g, "[data-lapse-focus]") + " { " + newBody + " }");
|
|
1144
1191
|
}
|
|
1145
1192
|
}
|
|
1193
|
+
lapseStyle.textContent = parts.join("\n");
|
|
1194
|
+
document.head.appendChild(lapseStyle);
|
|
1195
|
+
this.lapseStyleEl = lapseStyle;
|
|
1196
|
+
} catch (_) {
|
|
1197
|
+
}
|
|
1198
|
+
}, 0);
|
|
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 (_) {
|
|
1146
1221
|
}
|
|
1147
1222
|
}
|
|
1148
1223
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1165,6 +1240,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1165
1240
|
// src/core/scrubber.ts
|
|
1166
1241
|
var TimelineScrubber = class {
|
|
1167
1242
|
constructor(state) {
|
|
1243
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1244
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1168
1245
|
/** Saved originals for restore on release */
|
|
1169
1246
|
this._originalAnimate = null;
|
|
1170
1247
|
this._originalRaf = null;
|
|
@@ -1174,36 +1251,24 @@ var TimelineScrubber = class {
|
|
|
1174
1251
|
this.frames = state.frames;
|
|
1175
1252
|
this.capturedPortals = state.capturedPortals;
|
|
1176
1253
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1177
|
-
this.
|
|
1254
|
+
this.seekableClones = state.seekableClones;
|
|
1178
1255
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1179
1256
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1180
1257
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1181
1258
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
let current = el;
|
|
1191
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1192
|
-
const tag = current.tagName.toLowerCase();
|
|
1193
|
-
const parent = current.parentElement;
|
|
1194
|
-
if (parent) {
|
|
1195
|
-
const siblings = Array.from(parent.children);
|
|
1196
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1197
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1198
|
-
} else {
|
|
1199
|
-
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
|
+
}
|
|
1200
1267
|
}
|
|
1201
|
-
current = parent;
|
|
1202
1268
|
}
|
|
1203
|
-
return parts.join(" > ");
|
|
1204
1269
|
}
|
|
1205
1270
|
// ---------------------------------------------------------------------------
|
|
1206
|
-
// seekTo — scrub
|
|
1271
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1207
1272
|
// ---------------------------------------------------------------------------
|
|
1208
1273
|
seekTo(timeMs) {
|
|
1209
1274
|
if (!this.frames.length) return;
|
|
@@ -1251,6 +1316,34 @@ var TimelineScrubber = class {
|
|
|
1251
1316
|
}
|
|
1252
1317
|
}
|
|
1253
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
|
+
}
|
|
1254
1347
|
for (const entry of this.interceptedAnimations) {
|
|
1255
1348
|
try {
|
|
1256
1349
|
const anim = entry.animation;
|
|
@@ -1265,34 +1358,13 @@ var TimelineScrubber = class {
|
|
|
1265
1358
|
const el = this.elements.get(sel);
|
|
1266
1359
|
if (!el || !el.isConnected) continue;
|
|
1267
1360
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1268
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1269
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1270
|
-
);
|
|
1271
1361
|
const snapTyped = snap;
|
|
1272
|
-
if (snapTyped.__styles) {
|
|
1273
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1274
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1275
|
-
el.style.setProperty(prop, value, "important");
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
1362
|
if (snapTyped.__attrs) {
|
|
1280
1363
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1364
|
+
if (attr === "class" || attr === "style") continue;
|
|
1281
1365
|
if (attr === "checked") {
|
|
1282
1366
|
;
|
|
1283
1367
|
el.checked = value === "true";
|
|
1284
|
-
} else if (attr === "class") {
|
|
1285
|
-
if (value != null) el.className = value;
|
|
1286
|
-
} else if (attr === "style") {
|
|
1287
|
-
if (value) {
|
|
1288
|
-
el.setAttribute("style", value);
|
|
1289
|
-
el.style.transition = "none";
|
|
1290
|
-
if (snapTyped.__styles) {
|
|
1291
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1292
|
-
el.style.setProperty(prop, val, "important");
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
1368
|
} else if (attr === "value") {
|
|
1297
1369
|
if (value != null) el.value = value;
|
|
1298
1370
|
} else if (value == null) {
|
|
@@ -1303,39 +1375,31 @@ var TimelineScrubber = class {
|
|
|
1303
1375
|
}
|
|
1304
1376
|
}
|
|
1305
1377
|
}
|
|
1306
|
-
for (const
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
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) {
|
|
1313
1386
|
if (prop.value) {
|
|
1314
|
-
|
|
1387
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1315
1388
|
}
|
|
1316
1389
|
}
|
|
1317
1390
|
}
|
|
1318
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1319
|
-
for (const anim of frame.animations || []) {
|
|
1320
|
-
const fc = anim.animationId.indexOf(":");
|
|
1321
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1322
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1323
|
-
}
|
|
1324
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1325
|
-
const el = rawEl;
|
|
1326
|
-
const sel = this.getSelector(el);
|
|
1327
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1328
|
-
el.style.removeProperty("opacity");
|
|
1329
|
-
el.style.removeProperty("transform");
|
|
1330
|
-
el.style.removeProperty("filter");
|
|
1331
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
1334
1391
|
}
|
|
1335
1392
|
// ---------------------------------------------------------------------------
|
|
1336
1393
|
// release — tear down all scrub state and restore the page to normal
|
|
1337
1394
|
// ---------------------------------------------------------------------------
|
|
1338
1395
|
release() {
|
|
1396
|
+
for (const [, clone] of this.seekableClones) {
|
|
1397
|
+
try {
|
|
1398
|
+
clone.animation.cancel();
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
this.seekableClones.clear();
|
|
1339
1403
|
for (const entry of this.interceptedAnimations) {
|
|
1340
1404
|
try {
|
|
1341
1405
|
entry.animation.cancel();
|
|
@@ -1397,6 +1461,7 @@ var TimelineScrubber = class {
|
|
|
1397
1461
|
this.elements.clear();
|
|
1398
1462
|
this.frames.length = 0;
|
|
1399
1463
|
this.capturedPortals.clear();
|
|
1464
|
+
this.animFrameRanges.clear();
|
|
1400
1465
|
}
|
|
1401
1466
|
};
|
|
1402
1467
|
|
|
@@ -1623,7 +1688,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1623
1688
|
}
|
|
1624
1689
|
|
|
1625
1690
|
// src/core/engine.ts
|
|
1626
|
-
var
|
|
1691
|
+
var SaccadeEngine = class {
|
|
1627
1692
|
constructor() {
|
|
1628
1693
|
this.timing = new TimingController();
|
|
1629
1694
|
this.recorder = new TimelineRecorder();
|
|
@@ -1665,14 +1730,33 @@ var LapseEngine = class {
|
|
|
1665
1730
|
boundingBox: null
|
|
1666
1731
|
};
|
|
1667
1732
|
}
|
|
1668
|
-
|
|
1733
|
+
let capture;
|
|
1734
|
+
try {
|
|
1735
|
+
capture = this.recorder.stopRecording();
|
|
1736
|
+
} catch (e) {
|
|
1737
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1738
|
+
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1739
|
+
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1740
|
+
document.getElementById("__lapse-state-rules")?.remove();
|
|
1741
|
+
this._state = "idle";
|
|
1742
|
+
this.notify();
|
|
1743
|
+
return {
|
|
1744
|
+
startTime: 0,
|
|
1745
|
+
endTime: 0,
|
|
1746
|
+
duration: 0,
|
|
1747
|
+
animations: [],
|
|
1748
|
+
frames: [],
|
|
1749
|
+
boundingBox: null
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1669
1752
|
this.capture = capture;
|
|
1670
1753
|
const scrubberState = {
|
|
1671
1754
|
elements: this.recorder.elements,
|
|
1672
1755
|
frames: capture.frames,
|
|
1673
1756
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1674
1757
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1675
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1758
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1759
|
+
seekableClones: this.recorder.seekableClones
|
|
1676
1760
|
};
|
|
1677
1761
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1678
1762
|
this._state = "scrubbing";
|
|
@@ -1726,7 +1810,7 @@ var LapseEngine = class {
|
|
|
1726
1810
|
};
|
|
1727
1811
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1728
1812
|
0 && (module.exports = {
|
|
1729
|
-
|
|
1813
|
+
SaccadeEngine,
|
|
1730
1814
|
TimelineRecorder,
|
|
1731
1815
|
TimelineScrubber,
|
|
1732
1816
|
TimingController,
|