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/dist/index.cjs CHANGED
@@ -24,6 +24,8 @@ __export(src_exports, {
24
24
  Saccade: () => Saccade,
25
25
  SaccadeEngine: () => SaccadeEngine,
26
26
  SaccadeProvider: () => SaccadeProvider,
27
+ getSharedEngine: () => getSharedEngine,
28
+ resetSharedEngine: () => resetSharedEngine,
27
29
  useSaccadeEngine: () => useSaccadeEngine,
28
30
  useSpeed: () => useSpeed,
29
31
  useTimeline: () => useTimeline
@@ -50,6 +52,7 @@ var TimingController = class {
50
52
  this._origAnimate = null;
51
53
  this.installed = false;
52
54
  this.animPollId = 0;
55
+ this.gsapInstance = null;
53
56
  // WeakMap tracking for animations and media
54
57
  this.trackedAnims = /* @__PURE__ */ new WeakMap();
55
58
  this.trackedMedia = /* @__PURE__ */ new WeakMap();
@@ -225,13 +228,19 @@ var TimingController = class {
225
228
  } catch {
226
229
  }
227
230
  }
231
+ /**
232
+ * Register a GSAP instance (for ES-module imports where window.gsap is
233
+ * undefined). Applies the current timeScale immediately if speed !== 1.
234
+ */
235
+ registerGSAP(gsap) {
236
+ this.gsapInstance = gsap;
237
+ if (this.speed !== 1) this.patchGSAP();
238
+ }
228
239
  /** Sync GSAP's global timeline if present. */
229
240
  patchGSAP() {
230
241
  try {
231
- const gsap = window.gsap;
232
- if (gsap?.globalTimeline) {
233
- gsap.globalTimeline.timeScale(this.speed || 1e-3);
234
- }
242
+ const gsap = this.gsapInstance ?? window.gsap;
243
+ gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
235
244
  } catch {
236
245
  }
237
246
  }
@@ -277,10 +286,11 @@ var TimingController = class {
277
286
  } catch {
278
287
  }
279
288
  try {
280
- const gsap = window.gsap;
281
- if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
289
+ const gsap = this.gsapInstance ?? window.gsap;
290
+ gsap?.globalTimeline?.timeScale(1);
282
291
  } catch {
283
292
  }
293
+ this.gsapInstance = null;
284
294
  delete window.__LAPSE_ORIGINAL_RAF__;
285
295
  delete window.__saccadeInstalled;
286
296
  this.installed = false;
@@ -1555,7 +1565,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
1555
1565
  animations: animExports
1556
1566
  };
1557
1567
  }
1558
- function formatExportForLLM(exp, detail = "standard") {
1568
+ function formatExportForLLM(exp, detail = "moderate") {
1559
1569
  const lines = [];
1560
1570
  const grouped = /* @__PURE__ */ new Map();
1561
1571
  for (const anim of exp.animations) {
@@ -1570,7 +1580,7 @@ function formatExportForLLM(exp, detail = "standard") {
1570
1580
  const [from, to] = prop.range.split(" \u2192 ");
1571
1581
  return !(from && to && from.trim() === to.trim());
1572
1582
  }
1573
- if (detail === "compact") {
1583
+ if (detail === "brief") {
1574
1584
  lines.push(`# Animation State at ${exp.timestamp}`);
1575
1585
  lines.push("");
1576
1586
  for (const [, group] of grouped) {
@@ -1595,7 +1605,7 @@ function formatExportForLLM(exp, detail = "standard") {
1595
1605
  }
1596
1606
  lines.push(`# Animation State at ${exp.timestamp}`);
1597
1607
  lines.push("");
1598
- if (detail === "forensic") {
1608
+ if (detail === "granular") {
1599
1609
  lines.push("**Environment:**");
1600
1610
  lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
1601
1611
  lines.push(`- URL: ${window.location.href}`);
@@ -1662,7 +1672,7 @@ function formatExportForLLM(exp, detail = "standard") {
1662
1672
  lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
1663
1673
  lines.push("");
1664
1674
  for (const line of cssPropLines) lines.push(line);
1665
- if (detail === "detailed" || detail === "forensic") {
1675
+ if (detail === "detailed" || detail === "granular") {
1666
1676
  const allVars = {};
1667
1677
  for (const anim of cssAnims) {
1668
1678
  if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
@@ -1717,6 +1727,22 @@ var SaccadeEngine = class {
1717
1727
  getSpeed() {
1718
1728
  return this.timing.getSpeed();
1719
1729
  }
1730
+ /**
1731
+ * Install the timing patches immediately, without changing speed.
1732
+ *
1733
+ * Call this as early as possible (before app code, GSAP, or Framer Motion
1734
+ * run) so they capture the patched time functions rather than the originals.
1735
+ * Idempotent and harmless to call more than once. `setSpeed` and
1736
+ * `startRecording` also install on demand, so this is only needed to win the
1737
+ * early-load race against libraries that cache `Date.now`/`performance.now`.
1738
+ */
1739
+ install() {
1740
+ this.timing.install();
1741
+ }
1742
+ /** Register a module-imported GSAP instance so saccade can slow it. */
1743
+ registerGSAP(gsap) {
1744
+ this.timing.registerGSAP(gsap);
1745
+ }
1720
1746
  // -- Timeline recording ---------------------------------------------------
1721
1747
  startRecording(boundingBox) {
1722
1748
  if (this._state !== "idle") return;
@@ -1791,7 +1817,7 @@ var SaccadeEngine = class {
1791
1817
  filter
1792
1818
  );
1793
1819
  }
1794
- exportForLLM(timeMs, filter = "active", detail = "standard") {
1820
+ exportForLLM(timeMs, filter = "active", detail = "moderate") {
1795
1821
  const exp = this.generateExport(timeMs, filter);
1796
1822
  if (!exp) return "";
1797
1823
  return formatExportForLLM(exp, detail);
@@ -1816,13 +1842,29 @@ var SaccadeEngine = class {
1816
1842
  }
1817
1843
  };
1818
1844
 
1845
+ // src/core/shared.ts
1846
+ var KEY = "__saccadeSharedEngine__";
1847
+ function getSharedEngine() {
1848
+ const g = globalThis;
1849
+ if (!g[KEY]) g[KEY] = new SaccadeEngine();
1850
+ return g[KEY];
1851
+ }
1852
+ function resetSharedEngine() {
1853
+ const g = globalThis;
1854
+ g[KEY]?.destroy();
1855
+ g[KEY] = null;
1856
+ }
1857
+
1819
1858
  // src/react/SaccadeContext.tsx
1820
1859
  var import_jsx_runtime = require("react/jsx-runtime");
1821
1860
  var SaccadeContext = (0, import_react.createContext)(null);
1822
- function SaccadeProvider({ children }) {
1861
+ function SaccadeProvider({
1862
+ children,
1863
+ engine
1864
+ }) {
1823
1865
  const engineRef = (0, import_react.useRef)(null);
1824
1866
  if (!engineRef.current) {
1825
- engineRef.current = new SaccadeEngine();
1867
+ engineRef.current = engine ?? getSharedEngine();
1826
1868
  }
1827
1869
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SaccadeContext.Provider, { value: engineRef.current, children });
1828
1870
  }
@@ -1839,10 +1881,10 @@ var import_react6 = require("react");
1839
1881
  var import_react2 = require("react");
1840
1882
  var import_jsx_runtime2 = require("react/jsx-runtime");
1841
1883
  var DETAIL_LABELS = {
1842
- compact: "Compact",
1843
- standard: "Standard",
1884
+ brief: "Brief",
1885
+ moderate: "Moderate",
1844
1886
  detailed: "Detailed",
1845
- forensic: "Forensic"
1887
+ granular: "Granular"
1846
1888
  };
1847
1889
  function CopyCheckIcon({ copied }) {
1848
1890
  const spring = "cubic-bezier(0.34, 1.15, 0.64, 1)";
@@ -1896,10 +1938,10 @@ function CopyCheckIcon({ copied }) {
1896
1938
  ] });
1897
1939
  }
1898
1940
  var DETAIL_BRIGHT_COUNT = {
1899
- compact: 1,
1900
- standard: 2,
1941
+ brief: 1,
1942
+ moderate: 2,
1901
1943
  detailed: 3,
1902
- forensic: 4
1944
+ granular: 4
1903
1945
  };
1904
1946
  function DetailIcon({ level }) {
1905
1947
  const bright = DETAIL_BRIGHT_COUNT[level];
@@ -2224,7 +2266,9 @@ function SpeedControl({ speed, isPaused, onSetSpeed, onTogglePause }) {
2224
2266
 
2225
2267
  // src/react/useTimeline.ts
2226
2268
  var import_react4 = require("react");
2227
- var DETAIL_LEVELS = ["compact", "standard", "detailed", "forensic"];
2269
+ var _realSetTimeout = setTimeout.bind(window);
2270
+ var _realClearTimeout = clearTimeout.bind(window);
2271
+ var DETAIL_LEVELS = ["brief", "moderate", "detailed", "granular"];
2228
2272
  function useTimeline() {
2229
2273
  const engine = useSaccadeEngine();
2230
2274
  const state = (0, import_react4.useSyncExternalStore)(
@@ -2235,7 +2279,7 @@ function useTimeline() {
2235
2279
  const [scrubTime, setScrubTime] = (0, import_react4.useState)(0);
2236
2280
  const [copied, setCopied] = (0, import_react4.useState)(false);
2237
2281
  const [exportFilter, setExportFilter] = (0, import_react4.useState)("all-animations");
2238
- const [detailLevel, setDetailLevel] = (0, import_react4.useState)("standard");
2282
+ const [detailLevel, setDetailLevel] = (0, import_react4.useState)("moderate");
2239
2283
  const copiedTimeout = (0, import_react4.useRef)(null);
2240
2284
  const pendingSeek = (0, import_react4.useRef)(null);
2241
2285
  const rafId = (0, import_react4.useRef)(0);
@@ -2309,8 +2353,8 @@ function useTimeline() {
2309
2353
  navigator.clipboard.writeText(text).catch(() => {
2310
2354
  });
2311
2355
  setCopied(true);
2312
- if (copiedTimeout.current) clearTimeout(copiedTimeout.current);
2313
- copiedTimeout.current = setTimeout(() => setCopied(false), 2e3);
2356
+ if (copiedTimeout.current) _realClearTimeout(copiedTimeout.current);
2357
+ copiedTimeout.current = _realSetTimeout(() => setCopied(false), 1800);
2314
2358
  return text;
2315
2359
  },
2316
2360
  [engine, capture, scrubTime, exportFilter, detailLevel]
@@ -2862,7 +2906,7 @@ var PANEL_STYLES = (
2862
2906
 
2863
2907
  // src/react/Saccade.tsx
2864
2908
  var import_jsx_runtime5 = require("react/jsx-runtime");
2865
- function Saccade({ position = "bottom-left" }) {
2909
+ function Saccade({ position = "bottom-left", engine }) {
2866
2910
  const [shadowRoot, setShadowRoot] = (0, import_react7.useState)(null);
2867
2911
  const hostRef = (0, import_react7.useRef)(null);
2868
2912
  (0, import_react7.useEffect)(() => {
@@ -2886,7 +2930,7 @@ function Saccade({ position = "bottom-left" }) {
2886
2930
  }, [position]);
2887
2931
  if (!shadowRoot) return null;
2888
2932
  return (0, import_react_dom.createPortal)(
2889
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadePanel, {}) }),
2933
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadeProvider, { engine, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SaccadePanel, {}) }),
2890
2934
  shadowRoot.lastElementChild || shadowRoot
2891
2935
  );
2892
2936
  }
@@ -2895,6 +2939,8 @@ function Saccade({ position = "bottom-left" }) {
2895
2939
  Saccade,
2896
2940
  SaccadeEngine,
2897
2941
  SaccadeProvider,
2942
+ getSharedEngine,
2943
+ resetSharedEngine,
2898
2944
  useSaccadeEngine,
2899
2945
  useSpeed,
2900
2946
  useTimeline