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/README.md
CHANGED
|
@@ -27,9 +27,48 @@ function App() {
|
|
|
27
27
|
|
|
28
28
|
A floating panel appears in the corner. Use it to control speed, record, and scrub.
|
|
29
29
|
|
|
30
|
+
## Slowing every animation library
|
|
31
|
+
|
|
32
|
+
Saccade slows anything driven by the standard time sources — CSS transitions,
|
|
33
|
+
CSS `@keyframes`, the Web Animations API, `requestAnimationFrame` loops, video
|
|
34
|
+
and audio, and JS libraries that read `Date.now`/`performance.now`/rAF such as
|
|
35
|
+
**Framer Motion**. To reach them all reliably you need two things:
|
|
36
|
+
|
|
37
|
+
### 1. Install before your app code (recommended)
|
|
38
|
+
|
|
39
|
+
Libraries that cache a time function before saccade patches it never see
|
|
40
|
+
slow-mo. Because `<Saccade>` mounts inside React, its patches go in *after*
|
|
41
|
+
anything imported earlier. Import the side-effect entry **first** in your app
|
|
42
|
+
entry so the timing APIs are patched before any other module runs:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// main.tsx — must be the first import
|
|
46
|
+
import 'saccade/install'
|
|
47
|
+
|
|
48
|
+
import { createRoot } from 'react-dom/client'
|
|
49
|
+
import { App } from './App'
|
|
50
|
+
// …
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This patches the same shared engine `<Saccade>` uses, so the panel still
|
|
54
|
+
controls everything. It's optional — without it, anything imported before
|
|
55
|
+
`<Saccade>` mounts may not slow down.
|
|
56
|
+
|
|
57
|
+
### 2. Register GSAP (ES-module imports only)
|
|
58
|
+
|
|
59
|
+
GSAP is auto-detected when loaded as a global (UMD/CDN). When you `import` it as
|
|
60
|
+
an ES module, `window.gsap` is undefined, so hand saccade your instance once:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { gsap } from 'gsap'
|
|
64
|
+
import { getSharedEngine } from 'saccade'
|
|
65
|
+
|
|
66
|
+
getSharedEngine().registerGSAP(gsap)
|
|
67
|
+
```
|
|
68
|
+
|
|
30
69
|
## Features
|
|
31
70
|
|
|
32
|
-
- **Speed control** — Slow down or speed up all animations
|
|
71
|
+
- **Speed control** — Slow down or speed up all animations: CSS transitions, CSS `@keyframes`, the Web Animations API, `requestAnimationFrame`, JS timers, video/audio, GSAP, and Framer Motion
|
|
33
72
|
- **Timeline recording** — Record interactions, then scrub through captured frames to inspect mid-transition states
|
|
34
73
|
- **LLM export** — Copy structured animation state (properties, keyframes, easing, progress) as markdown for AI coding agents
|
|
35
74
|
- **Shadow DOM isolation** — Panel styles never leak into your app
|
|
@@ -38,13 +77,14 @@ A floating panel appears in the corner. Use it to control speed, record, and scr
|
|
|
38
77
|
|
|
39
78
|
## How it works
|
|
40
79
|
|
|
41
|
-
Saccade patches timing APIs (`setTimeout`, `setInterval`, `requestAnimationFrame`, `performance.now`, `Date.now`) to scale time by a configurable factor. During recording, it snapshots computed styles, attributes, and animation state every frame. In scrub mode, it replays those snapshots by applying inline styles directly to the DOM.
|
|
80
|
+
Saccade patches timing APIs (`setTimeout`, `setInterval`, `requestAnimationFrame`, `performance.now`, `Date.now`) to scale time by a configurable factor — this covers `requestAnimationFrame` loops and time-reading libraries like Framer Motion. CSS transitions, `@keyframes`, and Web Animations API animations are scaled via their `playbackRate`; GSAP via its global timeline `timeScale`. During recording, it snapshots computed styles, attributes, and animation state every frame. In scrub mode, it replays those snapshots by applying inline styles directly to the DOM.
|
|
42
81
|
|
|
43
82
|
## Props
|
|
44
83
|
|
|
45
84
|
| Prop | Type | Default | Description |
|
|
46
85
|
|------|------|---------|-------------|
|
|
47
86
|
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'bottom-left'` | Panel position |
|
|
87
|
+
| `engine` | `SaccadeEngine` | shared singleton | Engine the panel drives. Defaults to the process-wide shared engine, so app code (via `getSharedEngine()`) and the panel control the same instance. Pass your own only to isolate. |
|
|
48
88
|
|
|
49
89
|
## Core API
|
|
50
90
|
|
|
@@ -59,6 +99,13 @@ const engine = new SaccadeEngine()
|
|
|
59
99
|
engine.setSpeed(0.25) // quarter speed
|
|
60
100
|
engine.getSpeed()
|
|
61
101
|
|
|
102
|
+
// Patch timing APIs now, without changing speed (win the early-load race).
|
|
103
|
+
// setSpeed/startRecording also install on demand, so this is only needed up front.
|
|
104
|
+
engine.install()
|
|
105
|
+
|
|
106
|
+
// Register a module-imported GSAP instance (window.gsap fallback otherwise)
|
|
107
|
+
engine.registerGSAP(gsap)
|
|
108
|
+
|
|
62
109
|
// Recording
|
|
63
110
|
engine.startRecording()
|
|
64
111
|
const capture = engine.stopRecording()
|
|
@@ -74,6 +121,22 @@ const markdown = engine.exportForLLM(500, 'active', 'standard')
|
|
|
74
121
|
engine.destroy()
|
|
75
122
|
```
|
|
76
123
|
|
|
124
|
+
### Shared engine
|
|
125
|
+
|
|
126
|
+
`new SaccadeEngine()` gives you an isolated engine. If you also render
|
|
127
|
+
`<Saccade>` and want app code to drive the same one the panel does, use the
|
|
128
|
+
process-wide singleton instead:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { getSharedEngine } from 'saccade' // or 'saccade/core'
|
|
132
|
+
|
|
133
|
+
const engine = getSharedEngine() // same instance <Saccade> uses by default
|
|
134
|
+
engine.setSpeed(0.5)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`saccade/install` installs this shared engine, so importing it and calling
|
|
138
|
+
`getSharedEngine()` always refer to the same instance.
|
|
139
|
+
|
|
77
140
|
## React Hooks
|
|
78
141
|
|
|
79
142
|
```tsx
|
package/dist/core.cjs
CHANGED
|
@@ -26,7 +26,9 @@ __export(core_exports, {
|
|
|
26
26
|
TimingController: () => TimingController,
|
|
27
27
|
formatExportForLLM: () => formatExportForLLM,
|
|
28
28
|
generateExport: () => generateExport,
|
|
29
|
-
getFrameAtTime: () => getFrameAtTime
|
|
29
|
+
getFrameAtTime: () => getFrameAtTime,
|
|
30
|
+
getSharedEngine: () => getSharedEngine,
|
|
31
|
+
resetSharedEngine: () => resetSharedEngine
|
|
30
32
|
});
|
|
31
33
|
module.exports = __toCommonJS(core_exports);
|
|
32
34
|
|
|
@@ -43,6 +45,7 @@ var TimingController = class {
|
|
|
43
45
|
this._origAnimate = null;
|
|
44
46
|
this.installed = false;
|
|
45
47
|
this.animPollId = 0;
|
|
48
|
+
this.gsapInstance = null;
|
|
46
49
|
// WeakMap tracking for animations and media
|
|
47
50
|
this.trackedAnims = /* @__PURE__ */ new WeakMap();
|
|
48
51
|
this.trackedMedia = /* @__PURE__ */ new WeakMap();
|
|
@@ -218,13 +221,19 @@ var TimingController = class {
|
|
|
218
221
|
} catch {
|
|
219
222
|
}
|
|
220
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Register a GSAP instance (for ES-module imports where window.gsap is
|
|
226
|
+
* undefined). Applies the current timeScale immediately if speed !== 1.
|
|
227
|
+
*/
|
|
228
|
+
registerGSAP(gsap) {
|
|
229
|
+
this.gsapInstance = gsap;
|
|
230
|
+
if (this.speed !== 1) this.patchGSAP();
|
|
231
|
+
}
|
|
221
232
|
/** Sync GSAP's global timeline if present. */
|
|
222
233
|
patchGSAP() {
|
|
223
234
|
try {
|
|
224
|
-
const gsap = window.gsap;
|
|
225
|
-
|
|
226
|
-
gsap.globalTimeline.timeScale(this.speed || 1e-3);
|
|
227
|
-
}
|
|
235
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
236
|
+
gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
|
|
228
237
|
} catch {
|
|
229
238
|
}
|
|
230
239
|
}
|
|
@@ -270,10 +279,11 @@ var TimingController = class {
|
|
|
270
279
|
} catch {
|
|
271
280
|
}
|
|
272
281
|
try {
|
|
273
|
-
const gsap = window.gsap;
|
|
274
|
-
|
|
282
|
+
const gsap = this.gsapInstance ?? window.gsap;
|
|
283
|
+
gsap?.globalTimeline?.timeScale(1);
|
|
275
284
|
} catch {
|
|
276
285
|
}
|
|
286
|
+
this.gsapInstance = null;
|
|
277
287
|
delete window.__LAPSE_ORIGINAL_RAF__;
|
|
278
288
|
delete window.__saccadeInstalled;
|
|
279
289
|
this.installed = false;
|
|
@@ -1548,7 +1558,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
|
|
|
1548
1558
|
animations: animExports
|
|
1549
1559
|
};
|
|
1550
1560
|
}
|
|
1551
|
-
function formatExportForLLM(exp, detail = "
|
|
1561
|
+
function formatExportForLLM(exp, detail = "moderate") {
|
|
1552
1562
|
const lines = [];
|
|
1553
1563
|
const grouped = /* @__PURE__ */ new Map();
|
|
1554
1564
|
for (const anim of exp.animations) {
|
|
@@ -1563,7 +1573,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1563
1573
|
const [from, to] = prop.range.split(" \u2192 ");
|
|
1564
1574
|
return !(from && to && from.trim() === to.trim());
|
|
1565
1575
|
}
|
|
1566
|
-
if (detail === "
|
|
1576
|
+
if (detail === "brief") {
|
|
1567
1577
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1568
1578
|
lines.push("");
|
|
1569
1579
|
for (const [, group] of grouped) {
|
|
@@ -1588,7 +1598,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1588
1598
|
}
|
|
1589
1599
|
lines.push(`# Animation State at ${exp.timestamp}`);
|
|
1590
1600
|
lines.push("");
|
|
1591
|
-
if (detail === "
|
|
1601
|
+
if (detail === "granular") {
|
|
1592
1602
|
lines.push("**Environment:**");
|
|
1593
1603
|
lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
|
|
1594
1604
|
lines.push(`- URL: ${window.location.href}`);
|
|
@@ -1655,7 +1665,7 @@ function formatExportForLLM(exp, detail = "standard") {
|
|
|
1655
1665
|
lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
|
|
1656
1666
|
lines.push("");
|
|
1657
1667
|
for (const line of cssPropLines) lines.push(line);
|
|
1658
|
-
if (detail === "detailed" || detail === "
|
|
1668
|
+
if (detail === "detailed" || detail === "granular") {
|
|
1659
1669
|
const allVars = {};
|
|
1660
1670
|
for (const anim of cssAnims) {
|
|
1661
1671
|
if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
|
|
@@ -1710,6 +1720,22 @@ var SaccadeEngine = class {
|
|
|
1710
1720
|
getSpeed() {
|
|
1711
1721
|
return this.timing.getSpeed();
|
|
1712
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Install the timing patches immediately, without changing speed.
|
|
1725
|
+
*
|
|
1726
|
+
* Call this as early as possible (before app code, GSAP, or Framer Motion
|
|
1727
|
+
* run) so they capture the patched time functions rather than the originals.
|
|
1728
|
+
* Idempotent and harmless to call more than once. `setSpeed` and
|
|
1729
|
+
* `startRecording` also install on demand, so this is only needed to win the
|
|
1730
|
+
* early-load race against libraries that cache `Date.now`/`performance.now`.
|
|
1731
|
+
*/
|
|
1732
|
+
install() {
|
|
1733
|
+
this.timing.install();
|
|
1734
|
+
}
|
|
1735
|
+
/** Register a module-imported GSAP instance so saccade can slow it. */
|
|
1736
|
+
registerGSAP(gsap) {
|
|
1737
|
+
this.timing.registerGSAP(gsap);
|
|
1738
|
+
}
|
|
1713
1739
|
// -- Timeline recording ---------------------------------------------------
|
|
1714
1740
|
startRecording(boundingBox) {
|
|
1715
1741
|
if (this._state !== "idle") return;
|
|
@@ -1784,7 +1810,7 @@ var SaccadeEngine = class {
|
|
|
1784
1810
|
filter
|
|
1785
1811
|
);
|
|
1786
1812
|
}
|
|
1787
|
-
exportForLLM(timeMs, filter = "active", detail = "
|
|
1813
|
+
exportForLLM(timeMs, filter = "active", detail = "moderate") {
|
|
1788
1814
|
const exp = this.generateExport(timeMs, filter);
|
|
1789
1815
|
if (!exp) return "";
|
|
1790
1816
|
return formatExportForLLM(exp, detail);
|
|
@@ -1808,6 +1834,19 @@ var SaccadeEngine = class {
|
|
|
1808
1834
|
this._state = "idle";
|
|
1809
1835
|
}
|
|
1810
1836
|
};
|
|
1837
|
+
|
|
1838
|
+
// src/core/shared.ts
|
|
1839
|
+
var KEY = "__saccadeSharedEngine__";
|
|
1840
|
+
function getSharedEngine() {
|
|
1841
|
+
const g = globalThis;
|
|
1842
|
+
if (!g[KEY]) g[KEY] = new SaccadeEngine();
|
|
1843
|
+
return g[KEY];
|
|
1844
|
+
}
|
|
1845
|
+
function resetSharedEngine() {
|
|
1846
|
+
const g = globalThis;
|
|
1847
|
+
g[KEY]?.destroy();
|
|
1848
|
+
g[KEY] = null;
|
|
1849
|
+
}
|
|
1811
1850
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1812
1851
|
0 && (module.exports = {
|
|
1813
1852
|
SaccadeEngine,
|
|
@@ -1816,6 +1855,8 @@ var SaccadeEngine = class {
|
|
|
1816
1855
|
TimingController,
|
|
1817
1856
|
formatExportForLLM,
|
|
1818
1857
|
generateExport,
|
|
1819
|
-
getFrameAtTime
|
|
1858
|
+
getFrameAtTime,
|
|
1859
|
+
getSharedEngine,
|
|
1860
|
+
resetSharedEngine
|
|
1820
1861
|
});
|
|
1821
1862
|
//# sourceMappingURL=core.cjs.map
|