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 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 (CSS transitions, CSS animations, JS timers, `requestAnimationFrame`, video/audio)
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
- if (gsap?.globalTimeline) {
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
- if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
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 = "standard") {
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 === "compact") {
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 === "forensic") {
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 === "forensic") {
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 = "standard") {
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