saccade 0.1.0 → 0.2.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 +65 -2
- package/dist/core.cjs +54 -13
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +24 -2
- package/dist/core.d.ts +24 -2
- package/dist/core.mjs +51 -12
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +71 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -10
- package/dist/index.d.ts +34 -10
- package/dist/index.mjs +69 -25
- package/dist/index.mjs.map +1 -1
- package/dist/install.cjs +1846 -0
- package/dist/install.cjs.map +1 -0
- package/dist/install.d.cts +129 -0
- package/dist/install.d.ts +129 -0
- package/dist/install.mjs +1819 -0
- package/dist/install.mjs.map +1 -0
- package/package.json +12 -1
package/dist/index.d.cts
CHANGED
|
@@ -2,15 +2,8 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
|
-
type SaccadeProps = {
|
|
7
|
-
/** Panel position. Default: 'bottom-left' */
|
|
8
|
-
position?: SaccadePosition;
|
|
9
|
-
};
|
|
10
|
-
declare function Saccade({ position }: SaccadeProps): react.ReactPortal | null;
|
|
11
|
-
|
|
12
5
|
type ExportFilter = 'active' | 'all-animations' | 'all-elements';
|
|
13
|
-
type OutputDetailLevel = '
|
|
6
|
+
type OutputDetailLevel = 'brief' | 'moderate' | 'detailed' | 'granular';
|
|
14
7
|
type Rect = {
|
|
15
8
|
x: number;
|
|
16
9
|
y: number;
|
|
@@ -112,6 +105,18 @@ declare class SaccadeEngine {
|
|
|
112
105
|
getCapture(): TimelineCapture | null;
|
|
113
106
|
setSpeed(speed: number): void;
|
|
114
107
|
getSpeed(): number;
|
|
108
|
+
/**
|
|
109
|
+
* Install the timing patches immediately, without changing speed.
|
|
110
|
+
*
|
|
111
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
112
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
113
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
114
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
115
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
116
|
+
*/
|
|
117
|
+
install(): void;
|
|
118
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
119
|
+
registerGSAP(gsap: any): void;
|
|
115
120
|
startRecording(boundingBox?: Rect | null): void;
|
|
116
121
|
stopRecording(): TimelineCapture;
|
|
117
122
|
seekTo(timeMs: number): void;
|
|
@@ -123,8 +128,23 @@ declare class SaccadeEngine {
|
|
|
123
128
|
destroy(): void;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
132
|
+
type SaccadeProps = {
|
|
133
|
+
/** Panel position. Default: 'bottom-left' */
|
|
134
|
+
position?: SaccadePosition;
|
|
135
|
+
/** Engine the panel should drive. Defaults to the shared singleton, so app
|
|
136
|
+
* code can control the same engine the panel does (e.g. via
|
|
137
|
+
* `getSharedEngine()` or `import 'saccade/install'`). Pass your own only if
|
|
138
|
+
* you deliberately want an isolated engine. */
|
|
139
|
+
engine?: SaccadeEngine;
|
|
140
|
+
};
|
|
141
|
+
declare function Saccade({ position, engine }: SaccadeProps): react.ReactPortal | null;
|
|
142
|
+
|
|
143
|
+
declare function SaccadeProvider({ children, engine }: {
|
|
127
144
|
children: ReactNode;
|
|
145
|
+
/** Engine to provide. Defaults to the process-wide shared singleton so the
|
|
146
|
+
* panel and app code (and `saccade/install`) all drive the same instance. */
|
|
147
|
+
engine?: SaccadeEngine;
|
|
128
148
|
}): react_jsx_runtime.JSX.Element;
|
|
129
149
|
declare function useSaccadeEngine(): SaccadeEngine;
|
|
130
150
|
|
|
@@ -151,4 +171,8 @@ declare function useSpeed(): {
|
|
|
151
171
|
togglePause: () => void;
|
|
152
172
|
};
|
|
153
173
|
|
|
154
|
-
|
|
174
|
+
declare function getSharedEngine(): SaccadeEngine;
|
|
175
|
+
/** Test/teardown hook — drops the singleton so the next get() makes a fresh one. */
|
|
176
|
+
declare function resetSharedEngine(): void;
|
|
177
|
+
|
|
178
|
+
export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, getSharedEngine, resetSharedEngine, useSaccadeEngine, useSpeed, useTimeline };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,15 +2,8 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
|
-
type SaccadeProps = {
|
|
7
|
-
/** Panel position. Default: 'bottom-left' */
|
|
8
|
-
position?: SaccadePosition;
|
|
9
|
-
};
|
|
10
|
-
declare function Saccade({ position }: SaccadeProps): react.ReactPortal | null;
|
|
11
|
-
|
|
12
5
|
type ExportFilter = 'active' | 'all-animations' | 'all-elements';
|
|
13
|
-
type OutputDetailLevel = '
|
|
6
|
+
type OutputDetailLevel = 'brief' | 'moderate' | 'detailed' | 'granular';
|
|
14
7
|
type Rect = {
|
|
15
8
|
x: number;
|
|
16
9
|
y: number;
|
|
@@ -112,6 +105,18 @@ declare class SaccadeEngine {
|
|
|
112
105
|
getCapture(): TimelineCapture | null;
|
|
113
106
|
setSpeed(speed: number): void;
|
|
114
107
|
getSpeed(): number;
|
|
108
|
+
/**
|
|
109
|
+
* Install the timing patches immediately, without changing speed.
|
|
110
|
+
*
|
|
111
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
112
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
113
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
114
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
115
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
116
|
+
*/
|
|
117
|
+
install(): void;
|
|
118
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
119
|
+
registerGSAP(gsap: any): void;
|
|
115
120
|
startRecording(boundingBox?: Rect | null): void;
|
|
116
121
|
stopRecording(): TimelineCapture;
|
|
117
122
|
seekTo(timeMs: number): void;
|
|
@@ -123,8 +128,23 @@ declare class SaccadeEngine {
|
|
|
123
128
|
destroy(): void;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
132
|
+
type SaccadeProps = {
|
|
133
|
+
/** Panel position. Default: 'bottom-left' */
|
|
134
|
+
position?: SaccadePosition;
|
|
135
|
+
/** Engine the panel should drive. Defaults to the shared singleton, so app
|
|
136
|
+
* code can control the same engine the panel does (e.g. via
|
|
137
|
+
* `getSharedEngine()` or `import 'saccade/install'`). Pass your own only if
|
|
138
|
+
* you deliberately want an isolated engine. */
|
|
139
|
+
engine?: SaccadeEngine;
|
|
140
|
+
};
|
|
141
|
+
declare function Saccade({ position, engine }: SaccadeProps): react.ReactPortal | null;
|
|
142
|
+
|
|
143
|
+
declare function SaccadeProvider({ children, engine }: {
|
|
127
144
|
children: ReactNode;
|
|
145
|
+
/** Engine to provide. Defaults to the process-wide shared singleton so the
|
|
146
|
+
* panel and app code (and `saccade/install`) all drive the same instance. */
|
|
147
|
+
engine?: SaccadeEngine;
|
|
128
148
|
}): react_jsx_runtime.JSX.Element;
|
|
129
149
|
declare function useSaccadeEngine(): SaccadeEngine;
|
|
130
150
|
|
|
@@ -151,4 +171,8 @@ declare function useSpeed(): {
|
|
|
151
171
|
togglePause: () => void;
|
|
152
172
|
};
|
|
153
173
|
|
|
154
|
-
|
|
174
|
+
declare function getSharedEngine(): SaccadeEngine;
|
|
175
|
+
/** Test/teardown hook — drops the singleton so the next get() makes a fresh one. */
|
|
176
|
+
declare function resetSharedEngine(): void;
|
|
177
|
+
|
|
178
|
+
export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, getSharedEngine, resetSharedEngine, useSaccadeEngine, useSpeed, useTimeline };
|
package/dist/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ var TimingController = class {
|
|
|
20
20
|
this._origAnimate = null;
|
|
21
21
|
this.installed = false;
|
|
22
22
|
this.animPollId = 0;
|
|
23
|
+
this.gsapInstance = null;
|
|
23
24
|
// WeakMap tracking for animations and media
|
|
24
25
|
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
25
26
|
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
@@ -195,13 +196,19 @@ var TimingController = class {
|
|
|
195
196
|
} catch {
|
|
196
197
|
}
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Register a GSAP instance (for ES-module imports where window.gsap is
|
|
201
|
+
* undefined). Applies the current timeScale immediately if speed !== 1.
|
|
202
|
+
*/
|
|
203
|
+
registerGSAP(gsap) {
|
|
204
|
+
this.gsapInstance = gsap;
|
|
205
|
+
if (this.speed !== 1) this.patchGSAP();
|
|
206
|
+
}
|
|
198
207
|
/** Sync GSAP's global timeline if present. */
|
|
199
208
|
patchGSAP() {
|
|
200
209
|
try {
|
|
201
|
-
const gsap = window.gsap;
|
|
202
|
-
|
|
203
|
-
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
204
|
-
}
|
|
210
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
211
|
+
gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
|
|
205
212
|
} catch {
|
|
206
213
|
}
|
|
207
214
|
}
|
|
@@ -247,10 +254,11 @@ var TimingController = class {
|
|
|
247
254
|
} catch {
|
|
248
255
|
}
|
|
249
256
|
try {
|
|
250
|
-
const gsap = window.gsap;
|
|
251
|
-
|
|
257
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
258
|
+
gsap?.globalTimeline?.timeScale(1);
|
|
252
259
|
} catch {
|
|
253
260
|
}
|
|
261
|
+
this.gsapInstance = null;
|
|
254
262
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
255
263
|
delete window.__saccadeInstalled;
|
|
256
264
|
this.installed = false;
|
|
@@ -1525,7 +1533,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
|
|
|
1525
1533
|
animations: animExports
|
|
1526
1534
|
};
|
|
1527
1535
|
}
|
|
1528
|
-
function formatExportForLLM(exp, detail = "
|
|
1536
|
+
function formatExportForLLM(exp, detail = "moderate") {
|
|
1529
1537
|
const lines = [];
|
|
1530
1538
|
const grouped = /* @__PURE__ */ new Map();
|
|
1531
1539
|
for (const anim of exp.animations) {
|
|
@@ -1540,7 +1548,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1540
1548
|
const [from, to] = prop.range.split(" \u2192 ");
|
|
1541
1549
|
return !(from && to && from.trim() === to.trim());
|
|
1542
1550
|
}
|
|
1543
|
-
if (detail === "
|
|
1551
|
+
if (detail === "brief") {
|
|
1544
1552
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1545
1553
|
lines.push("");
|
|
1546
1554
|
for (const [, group] of grouped) {
|
|
@@ -1565,7 +1573,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1565
1573
|
}
|
|
1566
1574
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1567
1575
|
lines.push("");
|
|
1568
|
-
if (detail === "
|
|
1576
|
+
if (detail === "granular") {
|
|
1569
1577
|
lines.push("**Environment:**");
|
|
1570
1578
|
lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
|
|
1571
1579
|
lines.push(`- URL: ${window.location.href}`);
|
|
@@ -1632,7 +1640,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1632
1640
|
lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
|
|
1633
1641
|
lines.push("");
|
|
1634
1642
|
for (const line of cssPropLines) lines.push(line);
|
|
1635
|
-
if (detail === "detailed" || detail === "
|
|
1643
|
+
if (detail === "detailed" || detail === "granular") {
|
|
1636
1644
|
const allVars = {};
|
|
1637
1645
|
for (const anim of cssAnims) {
|
|
1638
1646
|
if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
|
|
@@ -1687,6 +1695,22 @@ var SaccadeEngine = class {
|
|
|
1687
1695
|
getSpeed() {
|
|
1688
1696
|
return this.timing.getSpeed();
|
|
1689
1697
|
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Install the timing patches immediately, without changing speed.
|
|
1700
|
+
*
|
|
1701
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
1702
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
1703
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
1704
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
1705
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
1706
|
+
*/
|
|
1707
|
+
install() {
|
|
1708
|
+
this.timing.install();
|
|
1709
|
+
}
|
|
1710
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
1711
|
+
registerGSAP(gsap) {
|
|
1712
|
+
this.timing.registerGSAP(gsap);
|
|
1713
|
+
}
|
|
1690
1714
|
// -- Timeline recording ---------------------------------------------------
|
|
1691
1715
|
startRecording(boundingBox) {
|
|
1692
1716
|
if (this._state !== "idle") return;
|
|
@@ -1761,7 +1785,7 @@ var SaccadeEngine = class {
|
|
|
1761
1785
|
filter
|
|
1762
1786
|
);
|
|
1763
1787
|
}
|
|
1764
|
-
exportForLLM(timeMs, filter = "active", detail = "
|
|
1788
|
+
exportForLLM(timeMs, filter = "active", detail = "moderate") {
|
|
1765
1789
|
const exp = this.generateExport(timeMs, filter);
|
|
1766
1790
|
if (!exp) return "";
|
|
1767
1791
|
return formatExportForLLM(exp, detail);
|
|
@@ -1786,13 +1810,29 @@ var SaccadeEngine = class {
|
|
|
1786
1810
|
}
|
|
1787
1811
|
};
|
|
1788
1812
|
|
|
1813
|
+
// src/core/shared.ts
|
|
1814
|
+
var KEY = "__saccadeSharedEngine__";
|
|
1815
|
+
function getSharedEngine() {
|
|
1816
|
+
const g = globalThis;
|
|
1817
|
+
if (!g[KEY]) g[KEY] = new SaccadeEngine();
|
|
1818
|
+
return g[KEY];
|
|
1819
|
+
}
|
|
1820
|
+
function resetSharedEngine() {
|
|
1821
|
+
const g = globalThis;
|
|
1822
|
+
g[KEY]?.destroy();
|
|
1823
|
+
g[KEY] = null;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1789
1826
|
// src/react/SaccadeContext.tsx
|
|
1790
1827
|
import { jsx } from "react/jsx-runtime";
|
|
1791
1828
|
var SaccadeContext = createContext(null);
|
|
1792
|
-
function SaccadeProvider({
|
|
1829
|
+
function SaccadeProvider({
|
|
1830
|
+
children,
|
|
1831
|
+
engine
|
|
1832
|
+
}) {
|
|
1793
1833
|
const engineRef = useRef(null);
|
|
1794
1834
|
if (!engineRef.current) {
|
|
1795
|
-
engineRef.current =
|
|
1835
|
+
engineRef.current = engine ?? getSharedEngine();
|
|
1796
1836
|
}
|
|
1797
1837
|
return /* @__PURE__ */ jsx(SaccadeContext.Provider, { value: engineRef.current, children });
|
|
1798
1838
|
}
|
|
@@ -1809,10 +1849,10 @@ import { useRef as useRef5, useCallback as useCallback5 } from "react";
|
|
|
1809
1849
|
import { useCallback, useRef as useRef2, useState, useEffect } from "react";
|
|
1810
1850
|
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1811
1851
|
var DETAIL_LABELS = {
|
|
1812
|
-
|
|
1813
|
-
|
|
1852
|
+
brief: "Brief",
|
|
1853
|
+
moderate: "Moderate",
|
|
1814
1854
|
detailed: "Detailed",
|
|
1815
|
-
|
|
1855
|
+
granular: "Granular"
|
|
1816
1856
|
};
|
|
1817
1857
|
function CopyCheckIcon({ copied }) {
|
|
1818
1858
|
const spring = "cubic-bezier(0.34, 1.15, 0.64, 1)";
|
|
@@ -1866,10 +1906,10 @@ function CopyCheckIcon({ copied }) {
|
|
|
1866
1906
|
] });
|
|
1867
1907
|
}
|
|
1868
1908
|
var DETAIL_BRIGHT_COUNT = {
|
|
1869
|
-
|
|
1870
|
-
|
|
1909
|
+
brief: 1,
|
|
1910
|
+
moderate: 2,
|
|
1871
1911
|
detailed: 3,
|
|
1872
|
-
|
|
1912
|
+
granular: 4
|
|
1873
1913
|
};
|
|
1874
1914
|
function DetailIcon({ level }) {
|
|
1875
1915
|
const bright = DETAIL_BRIGHT_COUNT[level];
|
|
@@ -2194,7 +2234,9 @@ function SpeedControl({ speed, isPaused, onSetSpeed, onTogglePause }) {
|
|
|
2194
2234
|
|
|
2195
2235
|
// src/react/useTimeline.ts
|
|
2196
2236
|
import { useState as useState2, useCallback as useCallback3, useEffect as useEffect2, useRef as useRef4, useSyncExternalStore } from "react";
|
|
2197
|
-
var
|
|
2237
|
+
var _realSetTimeout = setTimeout.bind(window);
|
|
2238
|
+
var _realClearTimeout = clearTimeout.bind(window);
|
|
2239
|
+
var DETAIL_LEVELS = ["brief", "moderate", "detailed", "granular"];
|
|
2198
2240
|
function useTimeline() {
|
|
2199
2241
|
const engine = useSaccadeEngine();
|
|
2200
2242
|
const state = useSyncExternalStore(
|
|
@@ -2205,7 +2247,7 @@ function useTimeline() {
|
|
|
2205
2247
|
const [scrubTime, setScrubTime] = useState2(0);
|
|
2206
2248
|
const [copied, setCopied] = useState2(false);
|
|
2207
2249
|
const [exportFilter, setExportFilter] = useState2("all-animations");
|
|
2208
|
-
const [detailLevel, setDetailLevel] = useState2("
|
|
2250
|
+
const [detailLevel, setDetailLevel] = useState2("moderate");
|
|
2209
2251
|
const copiedTimeout = useRef4(null);
|
|
2210
2252
|
const pendingSeek = useRef4(null);
|
|
2211
2253
|
const rafId = useRef4(0);
|
|
@@ -2279,8 +2321,8 @@ function useTimeline() {
|
|
|
2279
2321
|
navigator.clipboard.writeText(text).catch(() => {
|
|
2280
2322
|
});
|
|
2281
2323
|
setCopied(true);
|
|
2282
|
-
if (copiedTimeout.current)
|
|
2283
|
-
copiedTimeout.current =
|
|
2324
|
+
if (copiedTimeout.current) _realClearTimeout(copiedTimeout.current);
|
|
2325
|
+
copiedTimeout.current = _realSetTimeout(() => setCopied(false), 1800);
|
|
2284
2326
|
return text;
|
|
2285
2327
|
},
|
|
2286
2328
|
[engine, capture, scrubTime, exportFilter, detailLevel]
|
|
@@ -2832,7 +2874,7 @@ var PANEL_STYLES = (
|
|
|
2832
2874
|
|
|
2833
2875
|
// src/react/Saccade.tsx
|
|
2834
2876
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2835
|
-
function Saccade({ position = "bottom-left" }) {
|
|
2877
|
+
function Saccade({ position = "bottom-left", engine }) {
|
|
2836
2878
|
const [shadowRoot, setShadowRoot] = useState4(null);
|
|
2837
2879
|
const hostRef = useRef6(null);
|
|
2838
2880
|
useEffect4(() => {
|
|
@@ -2856,7 +2898,7 @@ function Saccade({ position = "bottom-left" }) {
|
|
|
2856
2898
|
}, [position]);
|
|
2857
2899
|
if (!shadowRoot) return null;
|
|
2858
2900
|
return createPortal(
|
|
2859
|
-
/* @__PURE__ */ jsx5(SaccadeProvider, { children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
|
|
2901
|
+
/* @__PURE__ */ jsx5(SaccadeProvider, { engine, children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
|
|
2860
2902
|
shadowRoot.lastElementChild || shadowRoot
|
|
2861
2903
|
);
|
|
2862
2904
|
}
|
|
@@ -2864,6 +2906,8 @@ export {
|
|
|
2864
2906
|
Saccade,
|
|
2865
2907
|
SaccadeEngine,
|
|
2866
2908
|
SaccadeProvider,
|
|
2909
|
+
getSharedEngine,
|
|
2910
|
+
resetSharedEngine,
|
|
2867
2911
|
useSaccadeEngine,
|
|
2868
2912
|
useSpeed,
|
|
2869
2913
|
useTimeline
|