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/index.mjs
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/react/
|
|
3
|
+
// src/react/Saccade.tsx
|
|
4
4
|
import { useRef as useRef6, useState as useState4, useEffect as useEffect4 } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
|
|
7
|
-
// src/react/
|
|
7
|
+
// src/react/SaccadeContext.tsx
|
|
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;
|
|
137
|
+
}
|
|
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);
|
|
121
149
|
}
|
|
122
|
-
/** Patch playbackRate on all active
|
|
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?.() || [];
|
|
@@ -1030,96 +1118,83 @@ var _TimelineRecorder = class _TimelineRecorder {
|
|
|
1030
1118
|
blocker.title = "Clear the timeline to interact with the page";
|
|
1031
1119
|
document.body.appendChild(blocker);
|
|
1032
1120
|
this.blockerEl = blocker;
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if (t && (t.includes(":hover") || t.includes(":focus"))) {
|
|
1051
|
-
allCss += t + "\n";
|
|
1121
|
+
setTimeout(() => {
|
|
1122
|
+
try {
|
|
1123
|
+
const lapseStyle = document.createElement("style");
|
|
1124
|
+
lapseStyle.id = "__lapse-state-rules";
|
|
1125
|
+
const hoverFocusRules = [];
|
|
1126
|
+
for (const sheet of document.styleSheets) {
|
|
1127
|
+
try {
|
|
1128
|
+
const walk = (rules) => {
|
|
1129
|
+
for (const rule of rules) {
|
|
1130
|
+
if (rule.cssRules) {
|
|
1131
|
+
walk(rule.cssRules);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
const t = rule.cssText;
|
|
1135
|
+
if (t && (t.includes(":hover") || t.includes(":focus"))) {
|
|
1136
|
+
hoverFocusRules.push(t);
|
|
1137
|
+
}
|
|
1052
1138
|
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
walk2(sheet.cssRules);
|
|
1057
|
-
} catch (_) {
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
const stateRegex = /([^{}]*(?::hover|:focus-visible|:focus-within|:focus(?!-))[^{}]*)\{([^{}]*)\}/g;
|
|
1061
|
-
let match;
|
|
1062
|
-
while ((match = stateRegex.exec(allCss)) !== null) {
|
|
1063
|
-
const selector = match[1].trim();
|
|
1064
|
-
const body = match[2].trim();
|
|
1065
|
-
if (!body) continue;
|
|
1066
|
-
const newBody = body.replace(
|
|
1067
|
-
/([^;:]+):\s*([^;]+)(;|$)/g,
|
|
1068
|
-
(m, prop, val, end) => {
|
|
1069
|
-
if (val.includes("!important")) return m;
|
|
1070
|
-
return prop + ": " + val.trim() + " !important" + end;
|
|
1139
|
+
};
|
|
1140
|
+
walk(sheet.cssRules);
|
|
1141
|
+
} catch (_) {
|
|
1071
1142
|
}
|
|
1072
|
-
);
|
|
1073
|
-
if (selector.includes(":hover")) {
|
|
1074
|
-
lapseStyle.textContent += selector.replace(/:hover/g, "[data-lapse-hover]") + " { " + newBody + " }\n";
|
|
1075
|
-
}
|
|
1076
|
-
if (selector.includes(":focus-visible")) {
|
|
1077
|
-
lapseStyle.textContent += selector.replace(/:focus-visible/g, "[data-lapse-focus]") + " { " + newBody + " }\n";
|
|
1078
|
-
} else if (selector.includes(":focus-within")) {
|
|
1079
|
-
lapseStyle.textContent += selector.replace(
|
|
1080
|
-
/:focus-within/g,
|
|
1081
|
-
":has([data-lapse-focus])"
|
|
1082
|
-
) + " { " + newBody + " }\n";
|
|
1083
|
-
} else if (selector.includes(":focus")) {
|
|
1084
|
-
lapseStyle.textContent += selector.replace(/:focus(?!-)/g, "[data-lapse-focus]") + " { " + newBody + " }\n";
|
|
1085
1143
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (SAFE_PROPS_SET.has(prop)) {
|
|
1100
|
-
el.style.setProperty(prop, value, "important");
|
|
1101
|
-
}
|
|
1144
|
+
const parts = [];
|
|
1145
|
+
for (const ruleText of hoverFocusRules) {
|
|
1146
|
+
const braceIdx = ruleText.indexOf("{");
|
|
1147
|
+
if (braceIdx === -1) continue;
|
|
1148
|
+
const selector = ruleText.slice(0, braceIdx).trim();
|
|
1149
|
+
const bodyEnd = ruleText.lastIndexOf("}");
|
|
1150
|
+
const body = ruleText.slice(braceIdx + 1, bodyEnd).trim();
|
|
1151
|
+
if (!body) continue;
|
|
1152
|
+
const newBody = body.replace(
|
|
1153
|
+
/([^;:]+):\s*([^;]+)(;|$)/g,
|
|
1154
|
+
(m, prop, val, end) => {
|
|
1155
|
+
if (val.includes("!important")) return m;
|
|
1156
|
+
return prop + ": " + val.trim() + " !important" + end;
|
|
1102
1157
|
}
|
|
1158
|
+
);
|
|
1159
|
+
if (selector.includes(":hover")) {
|
|
1160
|
+
parts.push(selector.replace(/:hover/g, "[data-lapse-hover]") + " { " + newBody + " }");
|
|
1103
1161
|
}
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
el.className = value;
|
|
1111
|
-
} else if (attr === "style") {
|
|
1112
|
-
} else if (attr === "value" && value != null) {
|
|
1113
|
-
;
|
|
1114
|
-
el.value = value;
|
|
1115
|
-
} else if (value == null) {
|
|
1116
|
-
el.removeAttribute(attr);
|
|
1117
|
-
} else {
|
|
1118
|
-
el.setAttribute(attr, value);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1162
|
+
if (selector.includes(":focus-visible")) {
|
|
1163
|
+
parts.push(selector.replace(/:focus-visible/g, "[data-lapse-focus]") + " { " + newBody + " }");
|
|
1164
|
+
} else if (selector.includes(":focus-within")) {
|
|
1165
|
+
parts.push(selector.replace(/:focus-within/g, ":has([data-lapse-focus])") + " { " + newBody + " }");
|
|
1166
|
+
} else if (selector.includes(":focus")) {
|
|
1167
|
+
parts.push(selector.replace(/:focus(?!-)/g, "[data-lapse-focus]") + " { " + newBody + " }");
|
|
1121
1168
|
}
|
|
1122
1169
|
}
|
|
1170
|
+
lapseStyle.textContent = parts.join("\n");
|
|
1171
|
+
document.head.appendChild(lapseStyle);
|
|
1172
|
+
this.lapseStyleEl = lapseStyle;
|
|
1173
|
+
} catch (_) {
|
|
1174
|
+
}
|
|
1175
|
+
}, 0);
|
|
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 (_) {
|
|
1123
1198
|
}
|
|
1124
1199
|
}
|
|
1125
1200
|
const duration = this.frames.length > 0 ? this.frames[this.frames.length - 1].time : 0;
|
|
@@ -1142,6 +1217,8 @@ var TimelineRecorder = _TimelineRecorder;
|
|
|
1142
1217
|
// src/core/scrubber.ts
|
|
1143
1218
|
var TimelineScrubber = class {
|
|
1144
1219
|
constructor(state) {
|
|
1220
|
+
/** Precomputed frame range per animation for O(1) before/after lookup. */
|
|
1221
|
+
this.animFrameRanges = /* @__PURE__ */ new Map();
|
|
1145
1222
|
/** Saved originals for restore on release */
|
|
1146
1223
|
this._originalAnimate = null;
|
|
1147
1224
|
this._originalRaf = null;
|
|
@@ -1151,36 +1228,24 @@ var TimelineScrubber = class {
|
|
|
1151
1228
|
this.frames = state.frames;
|
|
1152
1229
|
this.capturedPortals = state.capturedPortals;
|
|
1153
1230
|
this.interceptedAnimations = state.interceptedAnimations;
|
|
1154
|
-
this.
|
|
1231
|
+
this.seekableClones = state.seekableClones;
|
|
1155
1232
|
this._originalAnimate = window.__LAPSE_ORIGINAL_ANIMATE__ ?? null;
|
|
1156
1233
|
this._originalRaf = window.__LAPSE_ORIGINAL_RAF__ ?? null;
|
|
1157
1234
|
this._originalRemoveChild = window.__LAPSE_TIMELINE__?._removeChild ?? null;
|
|
1158
1235
|
this._originalRemove = window.__LAPSE_TIMELINE__?._remove ?? null;
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
let current = el;
|
|
1168
|
-
for (let i = 0; i < 5 && current && current.tagName && current.tagName !== "HTML"; i++) {
|
|
1169
|
-
const tag = current.tagName.toLowerCase();
|
|
1170
|
-
const parent = current.parentElement;
|
|
1171
|
-
if (parent) {
|
|
1172
|
-
const siblings = Array.from(parent.children);
|
|
1173
|
-
const idx = siblings.indexOf(current) + 1;
|
|
1174
|
-
parts.unshift(`${tag}:nth-child(${idx})`);
|
|
1175
|
-
} else {
|
|
1176
|
-
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
|
+
}
|
|
1177
1244
|
}
|
|
1178
|
-
current = parent;
|
|
1179
1245
|
}
|
|
1180
|
-
return parts.join(" > ");
|
|
1181
1246
|
}
|
|
1182
1247
|
// ---------------------------------------------------------------------------
|
|
1183
|
-
// seekTo — scrub
|
|
1248
|
+
// seekTo — scrub to a specific timestamp using WAAPI-native seeking
|
|
1184
1249
|
// ---------------------------------------------------------------------------
|
|
1185
1250
|
seekTo(timeMs) {
|
|
1186
1251
|
if (!this.frames.length) return;
|
|
@@ -1228,6 +1293,34 @@ var TimelineScrubber = class {
|
|
|
1228
1293
|
}
|
|
1229
1294
|
}
|
|
1230
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
|
+
}
|
|
1231
1324
|
for (const entry of this.interceptedAnimations) {
|
|
1232
1325
|
try {
|
|
1233
1326
|
const anim = entry.animation;
|
|
@@ -1242,34 +1335,13 @@ var TimelineScrubber = class {
|
|
|
1242
1335
|
const el = this.elements.get(sel);
|
|
1243
1336
|
if (!el || !el.isConnected) continue;
|
|
1244
1337
|
if (el.closest?.("[data-lapse-panel]")) continue;
|
|
1245
|
-
const hasAnimation = (frame.animations || []).some(
|
|
1246
|
-
(a) => a.animationId.endsWith(":" + sel) || a.animationId.includes(":" + sel.split(" > ").pop())
|
|
1247
|
-
);
|
|
1248
1338
|
const snapTyped = snap;
|
|
1249
|
-
if (snapTyped.__styles) {
|
|
1250
|
-
for (const [prop, value] of Object.entries(snapTyped.__styles)) {
|
|
1251
|
-
if (this.SAFE_PROPS_SET.has(prop) || hasAnimation) {
|
|
1252
|
-
el.style.setProperty(prop, value, "important");
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
1339
|
if (snapTyped.__attrs) {
|
|
1257
1340
|
for (const [attr, value] of Object.entries(snapTyped.__attrs)) {
|
|
1341
|
+
if (attr === "class" || attr === "style") continue;
|
|
1258
1342
|
if (attr === "checked") {
|
|
1259
1343
|
;
|
|
1260
1344
|
el.checked = value === "true";
|
|
1261
|
-
} else if (attr === "class") {
|
|
1262
|
-
if (value != null) el.className = value;
|
|
1263
|
-
} else if (attr === "style") {
|
|
1264
|
-
if (value) {
|
|
1265
|
-
el.setAttribute("style", value);
|
|
1266
|
-
el.style.transition = "none";
|
|
1267
|
-
if (snapTyped.__styles) {
|
|
1268
|
-
for (const [prop, val] of Object.entries(snapTyped.__styles)) {
|
|
1269
|
-
el.style.setProperty(prop, val, "important");
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
1345
|
} else if (attr === "value") {
|
|
1274
1346
|
if (value != null) el.value = value;
|
|
1275
1347
|
} else if (value == null) {
|
|
@@ -1280,39 +1352,31 @@ var TimelineScrubber = class {
|
|
|
1280
1352
|
}
|
|
1281
1353
|
}
|
|
1282
1354
|
}
|
|
1283
|
-
for (const
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
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) {
|
|
1290
1363
|
if (prop.value) {
|
|
1291
|
-
|
|
1364
|
+
el.style.setProperty(prop.property, prop.value, "important");
|
|
1292
1365
|
}
|
|
1293
1366
|
}
|
|
1294
1367
|
}
|
|
1295
|
-
const animatedSels = /* @__PURE__ */ new Set();
|
|
1296
|
-
for (const anim of frame.animations || []) {
|
|
1297
|
-
const fc = anim.animationId.indexOf(":");
|
|
1298
|
-
const sc = anim.animationId.indexOf(":", fc + 1);
|
|
1299
|
-
if (sc >= 0) animatedSels.add(anim.animationId.substring(sc + 1));
|
|
1300
|
-
}
|
|
1301
|
-
document.querySelectorAll(".checkbox-indicator, .radio-indicator").forEach((rawEl) => {
|
|
1302
|
-
const el = rawEl;
|
|
1303
|
-
const sel = this.getSelector(el);
|
|
1304
|
-
if (sel && !animatedSels.has(sel)) {
|
|
1305
|
-
el.style.removeProperty("opacity");
|
|
1306
|
-
el.style.removeProperty("transform");
|
|
1307
|
-
el.style.removeProperty("filter");
|
|
1308
|
-
el.style.removeProperty("stroke-dashoffset");
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
1368
|
}
|
|
1312
1369
|
// ---------------------------------------------------------------------------
|
|
1313
1370
|
// release — tear down all scrub state and restore the page to normal
|
|
1314
1371
|
// ---------------------------------------------------------------------------
|
|
1315
1372
|
release() {
|
|
1373
|
+
for (const [, clone] of this.seekableClones) {
|
|
1374
|
+
try {
|
|
1375
|
+
clone.animation.cancel();
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
this.seekableClones.clear();
|
|
1316
1380
|
for (const entry of this.interceptedAnimations) {
|
|
1317
1381
|
try {
|
|
1318
1382
|
entry.animation.cancel();
|
|
@@ -1374,6 +1438,7 @@ var TimelineScrubber = class {
|
|
|
1374
1438
|
this.elements.clear();
|
|
1375
1439
|
this.frames.length = 0;
|
|
1376
1440
|
this.capturedPortals.clear();
|
|
1441
|
+
this.animFrameRanges.clear();
|
|
1377
1442
|
}
|
|
1378
1443
|
};
|
|
1379
1444
|
|
|
@@ -1600,7 +1665,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1600
1665
|
}
|
|
1601
1666
|
|
|
1602
1667
|
// src/core/engine.ts
|
|
1603
|
-
var
|
|
1668
|
+
var SaccadeEngine = class {
|
|
1604
1669
|
constructor() {
|
|
1605
1670
|
this.timing = new TimingController();
|
|
1606
1671
|
this.recorder = new TimelineRecorder();
|
|
@@ -1642,14 +1707,33 @@ var LapseEngine = class {
|
|
|
1642
1707
|
boundingBox: null
|
|
1643
1708
|
};
|
|
1644
1709
|
}
|
|
1645
|
-
|
|
1710
|
+
let capture;
|
|
1711
|
+
try {
|
|
1712
|
+
capture = this.recorder.stopRecording();
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
if (typeof console !== "undefined") console.error("[Saccade] stopRecording failed:", e);
|
|
1715
|
+
document.getElementById("__lapse-scrub-blocker")?.remove();
|
|
1716
|
+
document.getElementById("__lapse-no-transitions")?.remove();
|
|
1717
|
+
document.getElementById("__lapse-state-rules")?.remove();
|
|
1718
|
+
this._state = "idle";
|
|
1719
|
+
this.notify();
|
|
1720
|
+
return {
|
|
1721
|
+
startTime: 0,
|
|
1722
|
+
endTime: 0,
|
|
1723
|
+
duration: 0,
|
|
1724
|
+
animations: [],
|
|
1725
|
+
frames: [],
|
|
1726
|
+
boundingBox: null
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1646
1729
|
this.capture = capture;
|
|
1647
1730
|
const scrubberState = {
|
|
1648
1731
|
elements: this.recorder.elements,
|
|
1649
1732
|
frames: capture.frames,
|
|
1650
1733
|
capturedPortals: this.recorder.capturedPortalIds,
|
|
1651
1734
|
interceptedAnimations: this.recorder.interceptedAnimations,
|
|
1652
|
-
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET
|
|
1735
|
+
SAFE_PROPS_SET: this.recorder.SAFE_PROPS_SET,
|
|
1736
|
+
seekableClones: this.recorder.seekableClones
|
|
1653
1737
|
};
|
|
1654
1738
|
this.scrubber = new TimelineScrubber(scrubberState);
|
|
1655
1739
|
this._state = "scrubbing";
|
|
@@ -1702,23 +1786,23 @@ var LapseEngine = class {
|
|
|
1702
1786
|
}
|
|
1703
1787
|
};
|
|
1704
1788
|
|
|
1705
|
-
// src/react/
|
|
1789
|
+
// src/react/SaccadeContext.tsx
|
|
1706
1790
|
import { jsx } from "react/jsx-runtime";
|
|
1707
|
-
var
|
|
1708
|
-
function
|
|
1791
|
+
var SaccadeContext = createContext(null);
|
|
1792
|
+
function SaccadeProvider({ children }) {
|
|
1709
1793
|
const engineRef = useRef(null);
|
|
1710
1794
|
if (!engineRef.current) {
|
|
1711
|
-
engineRef.current = new
|
|
1795
|
+
engineRef.current = new SaccadeEngine();
|
|
1712
1796
|
}
|
|
1713
|
-
return /* @__PURE__ */ jsx(
|
|
1797
|
+
return /* @__PURE__ */ jsx(SaccadeContext.Provider, { value: engineRef.current, children });
|
|
1714
1798
|
}
|
|
1715
|
-
function
|
|
1716
|
-
const engine = useContext(
|
|
1717
|
-
if (!engine) throw new Error("
|
|
1799
|
+
function useSaccadeEngine() {
|
|
1800
|
+
const engine = useContext(SaccadeContext);
|
|
1801
|
+
if (!engine) throw new Error("useSaccadeEngine must be used within <SaccadeProvider>");
|
|
1718
1802
|
return engine;
|
|
1719
1803
|
}
|
|
1720
1804
|
|
|
1721
|
-
// src/react/
|
|
1805
|
+
// src/react/SaccadePanel.tsx
|
|
1722
1806
|
import { useRef as useRef5, useCallback as useCallback5 } from "react";
|
|
1723
1807
|
|
|
1724
1808
|
// src/react/Timeline.tsx
|
|
@@ -2112,7 +2196,7 @@ function SpeedControl({ speed, isPaused, onSetSpeed, onTogglePause }) {
|
|
|
2112
2196
|
import { useState as useState2, useCallback as useCallback3, useEffect as useEffect2, useRef as useRef4, useSyncExternalStore } from "react";
|
|
2113
2197
|
var DETAIL_LEVELS = ["compact", "standard", "detailed", "forensic"];
|
|
2114
2198
|
function useTimeline() {
|
|
2115
|
-
const engine =
|
|
2199
|
+
const engine = useSaccadeEngine();
|
|
2116
2200
|
const state = useSyncExternalStore(
|
|
2117
2201
|
(cb) => engine.subscribe(cb),
|
|
2118
2202
|
() => engine.state
|
|
@@ -2146,9 +2230,13 @@ function useTimeline() {
|
|
|
2146
2230
|
[engine]
|
|
2147
2231
|
);
|
|
2148
2232
|
const stopRecording = useCallback3(() => {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2233
|
+
try {
|
|
2234
|
+
const result = engine.stopRecording();
|
|
2235
|
+
setCapture(result);
|
|
2236
|
+
setScrubTime(0);
|
|
2237
|
+
} catch (e) {
|
|
2238
|
+
console.error("[Saccade] stopRecording failed:", e);
|
|
2239
|
+
}
|
|
2152
2240
|
}, [engine]);
|
|
2153
2241
|
const seek = useCallback3(
|
|
2154
2242
|
(timeMs) => {
|
|
@@ -2217,7 +2305,7 @@ function useTimeline() {
|
|
|
2217
2305
|
// src/react/useSpeed.ts
|
|
2218
2306
|
import { useState as useState3, useCallback as useCallback4, useEffect as useEffect3 } from "react";
|
|
2219
2307
|
function useSpeed() {
|
|
2220
|
-
const engine =
|
|
2308
|
+
const engine = useSaccadeEngine();
|
|
2221
2309
|
const [speed, setSpeedState] = useState3(1);
|
|
2222
2310
|
const [previousSpeed, setPreviousSpeed] = useState3(1);
|
|
2223
2311
|
const [isPaused, setIsPaused] = useState3(false);
|
|
@@ -2280,9 +2368,9 @@ function useSpeed() {
|
|
|
2280
2368
|
return { speed, isPaused, setSpeed, togglePause };
|
|
2281
2369
|
}
|
|
2282
2370
|
|
|
2283
|
-
// src/react/
|
|
2371
|
+
// src/react/SaccadePanel.tsx
|
|
2284
2372
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2285
|
-
function
|
|
2373
|
+
function SaccadePanel() {
|
|
2286
2374
|
const timeline = useTimeline();
|
|
2287
2375
|
const { speed, isPaused, setSpeed, togglePause } = useSpeed();
|
|
2288
2376
|
const panelRef = useRef5(null);
|
|
@@ -2742,17 +2830,18 @@ var PANEL_STYLES = (
|
|
|
2742
2830
|
`
|
|
2743
2831
|
);
|
|
2744
2832
|
|
|
2745
|
-
// src/react/
|
|
2833
|
+
// src/react/Saccade.tsx
|
|
2746
2834
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2747
|
-
function
|
|
2748
|
-
const hostRef = useRef6(null);
|
|
2835
|
+
function Saccade({ position = "bottom-left" }) {
|
|
2749
2836
|
const [shadowRoot, setShadowRoot] = useState4(null);
|
|
2837
|
+
const hostRef = useRef6(null);
|
|
2750
2838
|
useEffect4(() => {
|
|
2751
|
-
const host =
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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;
|
|
2756
2845
|
const shadow = host.attachShadow({ mode: "open" });
|
|
2757
2846
|
const style = document.createElement("style");
|
|
2758
2847
|
style.textContent = PANEL_STYLES;
|
|
@@ -2760,32 +2849,22 @@ function Lapse({ position = "bottom-left" }) {
|
|
|
2760
2849
|
const mount = document.createElement("div");
|
|
2761
2850
|
shadow.appendChild(mount);
|
|
2762
2851
|
setShadowRoot(shadow);
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
zIndex: 2147483647,
|
|
2773
|
-
// max int — must sit above the scrub blocker (z-index: 999999)
|
|
2774
|
-
pointerEvents: "auto",
|
|
2775
|
-
...positionOffset
|
|
2776
|
-
},
|
|
2777
|
-
children: shadowRoot && createPortal(
|
|
2778
|
-
/* @__PURE__ */ jsx5(LapseProvider, { children: /* @__PURE__ */ jsx5(LapsePanel, {}) }),
|
|
2779
|
-
shadowRoot.lastElementChild || shadowRoot
|
|
2780
|
-
)
|
|
2781
|
-
}
|
|
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
|
|
2782
2861
|
);
|
|
2783
2862
|
}
|
|
2784
2863
|
export {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2864
|
+
Saccade,
|
|
2865
|
+
SaccadeEngine,
|
|
2866
|
+
SaccadeProvider,
|
|
2867
|
+
useSaccadeEngine,
|
|
2789
2868
|
useSpeed,
|
|
2790
2869
|
useTimeline
|
|
2791
2870
|
};
|