@tldraw/utils 4.2.0-next.094f21ae35eb → 4.2.0-next.3634f876bff4

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-cjs/index.js CHANGED
@@ -168,7 +168,7 @@ var import_version2 = require("./lib/version");
168
168
  var import_warn = require("./lib/warn");
169
169
  (0, import_version.registerTldrawLibraryVersion)(
170
170
  "@tldraw/utils",
171
- "4.2.0-next.094f21ae35eb",
171
+ "4.2.0-next.3634f876bff4",
172
172
  "cjs"
173
173
  );
174
174
  //# sourceMappingURL=index.js.map
@@ -25,21 +25,14 @@ module.exports = __toCommonJS(throttle_exports);
25
25
  const isTest = () => typeof process !== "undefined" && process.env.NODE_ENV === "test" && // @ts-expect-error
26
26
  !globalThis.__FORCE_RAF_IN_TESTS__;
27
27
  const fpsQueue = [];
28
- const targetFps = 120;
29
- const timingVarianceFactor = 0.9;
30
- const targetTimePerFrame = Math.floor(1e3 / targetFps) * timingVarianceFactor;
28
+ const targetFps = 60;
29
+ const targetTimePerFrame = Math.floor(1e3 / targetFps) * 0.9;
31
30
  let frameRaf;
32
31
  let flushRaf;
33
32
  let lastFlushTime = -targetTimePerFrame;
34
- const customFpsLastRunTime = /* @__PURE__ */ new WeakMap();
35
- const customFpsGetters = /* @__PURE__ */ new WeakMap();
36
33
  const flush = () => {
37
34
  const queue = fpsQueue.splice(0, fpsQueue.length);
38
35
  for (const fn of queue) {
39
- const getTargetFps = customFpsGetters.get(fn);
40
- if (getTargetFps) {
41
- customFpsLastRunTime.set(fn, Date.now());
42
- }
43
36
  fn();
44
37
  }
45
38
  };
@@ -67,7 +60,7 @@ function tick(isOnNextFrame = false) {
67
60
  });
68
61
  }
69
62
  }
70
- function fpsThrottle(fn, getTargetFps) {
63
+ function fpsThrottle(fn) {
71
64
  if (isTest()) {
72
65
  fn.cancel = () => {
73
66
  if (frameRaf) {
@@ -81,18 +74,7 @@ function fpsThrottle(fn, getTargetFps) {
81
74
  };
82
75
  return fn;
83
76
  }
84
- if (getTargetFps) {
85
- customFpsGetters.set(fn, getTargetFps);
86
- }
87
77
  const throttledFn = () => {
88
- if (getTargetFps) {
89
- const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity;
90
- const customTimePerFrame = Math.floor(1e3 / getTargetFps()) * timingVarianceFactor;
91
- const elapsed = Date.now() - lastRun;
92
- if (elapsed < customTimePerFrame) {
93
- return;
94
- }
95
- }
96
78
  if (fpsQueue.includes(fn)) {
97
79
  return;
98
80
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/throttle.ts"],
4
- "sourcesContent": ["const isTest = () =>\n\ttypeof process !== 'undefined' &&\n\tprocess.env.NODE_ENV === 'test' &&\n\t// @ts-expect-error\n\t!globalThis.__FORCE_RAF_IN_TESTS__\n\nconst fpsQueue: Array<() => void> = []\nconst targetFps = 120\n// Browsers aren't precise with frame timing - this factor prevents skipping frames unnecessarily\n// by aiming slightly below the theoretical frame duration (e.g., ~7.5ms instead of 8.33ms for 120fps)\nconst timingVarianceFactor = 0.9\nconst targetTimePerFrame = Math.floor(1000 / targetFps) * timingVarianceFactor // ~7ms\nlet frameRaf: undefined | number\nlet flushRaf: undefined | number\nlet lastFlushTime = -targetTimePerFrame\n\n// Track custom FPS timing per function\nconst customFpsLastRunTime = new WeakMap<() => void, number>()\n// Map function to its custom FPS getter\nconst customFpsGetters = new WeakMap<() => void, () => number>()\n\nconst flush = () => {\n\tconst queue = fpsQueue.splice(0, fpsQueue.length)\n\tfor (const fn of queue) {\n\t\t// If this function has custom FPS, update timestamp when executing\n\t\tconst getTargetFps = customFpsGetters.get(fn)\n\t\tif (getTargetFps) {\n\t\t\tcustomFpsLastRunTime.set(fn, Date.now())\n\t\t}\n\t\tfn()\n\t}\n}\n\nfunction tick(isOnNextFrame = false) {\n\tif (frameRaf) return\n\n\tconst now = Date.now()\n\tconst elapsed = now - lastFlushTime\n\n\tif (elapsed < targetTimePerFrame) {\n\t\t// If we're too early to flush, we need to wait until the next frame to try and flush again.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tframeRaf = requestAnimationFrame(() => {\n\t\t\tframeRaf = undefined\n\t\t\ttick(true)\n\t\t})\n\t\treturn\n\t}\n\n\tif (isOnNextFrame) {\n\t\t// If we've already waited for the next frame to run the tick, then we can flush immediately\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on this frame already, so we can do nothing here.\n\t\tlastFlushTime = now\n\t\tflush()\n\t} else {\n\t\t// If we haven't already waited for the next frame to run the tick, we need to wait until the next frame to flush.\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on the next frame already, so we can do nothing here.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tflushRaf = requestAnimationFrame(() => {\n\t\t\tflushRaf = undefined\n\t\t\tlastFlushTime = now\n\t\t\tflush()\n\t\t})\n\t}\n}\n\n/**\n * Creates a throttled version of a function that executes at most once per frame.\n * The default target frame rate is 120fps, but can be customized per function.\n * Subsequent calls within the same frame are ignored, ensuring smooth performance\n * for high-frequency events like mouse movements or scroll events.\n *\n * @param fn - The function to throttle, optionally with a cancel method\n * @param getTargetFps - Optional function that returns the current target FPS rate for custom throttling\n * @returns A throttled function with an optional cancel method to remove pending calls\n *\n * @example\n * ```ts\n * // Default 120fps throttling\n * const updateCanvas = fpsThrottle(() => {\n * // This will run at most once per frame (~8.33ms)\n * redrawCanvas()\n * })\n *\n * // Call as often as you want - automatically throttled to 120fps\n * document.addEventListener('mousemove', updateCanvas)\n *\n * // Cancel pending calls if needed\n * updateCanvas.cancel?.()\n *\n * // Custom FPS throttling for less critical updates\n * const slowUpdate = fpsThrottle(() => {\n * heavyComputation()\n * }, () => 30) // Throttle to 30fps\n * ```\n *\n * @internal\n */\nexport function fpsThrottle(\n\tfn: { (): void; cancel?(): void },\n\tgetTargetFps?: () => number\n): {\n\t(): void\n\tcancel?(): void\n} {\n\tif (isTest()) {\n\t\tfn.cancel = () => {\n\t\t\tif (frameRaf) {\n\t\t\t\tcancelAnimationFrame(frameRaf)\n\t\t\t\tframeRaf = undefined\n\t\t\t}\n\t\t\tif (flushRaf) {\n\t\t\t\tcancelAnimationFrame(flushRaf)\n\t\t\t\tflushRaf = undefined\n\t\t\t}\n\t\t}\n\t\treturn fn\n\t}\n\n\t// Store custom FPS getter if provided\n\tif (getTargetFps) {\n\t\tcustomFpsGetters.set(fn, getTargetFps)\n\t}\n\n\tconst throttledFn = () => {\n\t\t// Custom FPS - check timing before queuing\n\t\tif (getTargetFps) {\n\t\t\tconst lastRun = customFpsLastRunTime.get(fn) ?? -Infinity\n\t\t\tconst customTimePerFrame = Math.floor(1000 / getTargetFps()) * timingVarianceFactor\n\t\t\tconst elapsed = Date.now() - lastRun\n\n\t\t\tif (elapsed < customTimePerFrame) {\n\t\t\t\t// Not ready yet, don't queue\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif (fpsQueue.includes(fn)) {\n\t\t\treturn\n\t\t}\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\tthrottledFn.cancel = () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n\treturn throttledFn\n}\n\n/**\n * Schedules a function to execute on the next animation frame, targeting 120fps.\n * If the same function is passed multiple times before the frame executes,\n * it will only be called once, effectively batching multiple calls.\n *\n * @param fn - The function to execute on the next frame\n * @returns A cancel function that can prevent execution if called before the next frame\n *\n * @example\n * ```ts\n * const updateUI = throttleToNextFrame(() => {\n * // Batches multiple calls into the next animation frame\n * updateStatusBar()\n * refreshToolbar()\n * })\n *\n * // Multiple calls within the same frame are batched\n * updateUI() // Will execute\n * updateUI() // Ignored (same function already queued)\n * updateUI() // Ignored (same function already queued)\n *\n * // Get cancel function to prevent execution\n * const cancel = updateUI()\n * cancel() // Prevents execution if called before next frame\n * ```\n *\n * @internal\n */\nexport function throttleToNextFrame(fn: () => void): () => void {\n\tif (isTest()) {\n\t\tfn()\n\t\treturn () => void null // noop\n\t}\n\n\tif (!fpsQueue.includes(fn)) {\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\n\treturn () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAEb,MAAM,WAA8B,CAAC;AACrC,MAAM,YAAY;AAGlB,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,MAAM,MAAO,SAAS,IAAI;AAC1D,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAgB,CAAC;AAGrB,MAAM,uBAAuB,oBAAI,QAA4B;AAE7D,MAAM,mBAAmB,oBAAI,QAAkC;AAE/D,MAAM,QAAQ,MAAM;AACnB,QAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,MAAM;AAChD,aAAW,MAAM,OAAO;AAEvB,UAAM,eAAe,iBAAiB,IAAI,EAAE;AAC5C,QAAI,cAAc;AACjB,2BAAqB,IAAI,IAAI,KAAK,IAAI,CAAC;AAAA,IACxC;AACA,OAAG;AAAA,EACJ;AACD;AAEA,SAAS,KAAK,gBAAgB,OAAO;AACpC,MAAI,SAAU;AAEd,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,MAAM;AAEtB,MAAI,UAAU,oBAAoB;AAGjC,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,WAAK,IAAI;AAAA,IACV,CAAC;AACD;AAAA,EACD;AAEA,MAAI,eAAe;AAElB,QAAI,SAAU;AACd,oBAAgB;AAChB,UAAM;AAAA,EACP,OAAO;AAEN,QAAI,SAAU;AAEd,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,sBAAgB;AAChB,YAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AAkCO,SAAS,YACf,IACA,cAIC;AACD,MAAI,OAAO,GAAG;AACb,OAAG,SAAS,MAAM;AACjB,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AACA,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAGA,MAAI,cAAc;AACjB,qBAAiB,IAAI,IAAI,YAAY;AAAA,EACtC;AAEA,QAAM,cAAc,MAAM;AAEzB,QAAI,cAAc;AACjB,YAAM,UAAU,qBAAqB,IAAI,EAAE,KAAK;AAChD,YAAM,qBAAqB,KAAK,MAAM,MAAO,aAAa,CAAC,IAAI;AAC/D,YAAM,UAAU,KAAK,IAAI,IAAI;AAE7B,UAAI,UAAU,oBAAoB;AAEjC;AAAA,MACD;AAAA,IACD;AACA,QAAI,SAAS,SAAS,EAAE,GAAG;AAC1B;AAAA,IACD;AACA,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AACA,cAAY,SAAS,MAAM;AAC1B,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACA,SAAO;AACR;AA8BO,SAAS,oBAAoB,IAA4B;AAC/D,MAAI,OAAO,GAAG;AACb,OAAG;AACH,WAAO,MAAM;AAAA,EACd;AAEA,MAAI,CAAC,SAAS,SAAS,EAAE,GAAG;AAC3B,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AAEA,SAAO,MAAM;AACZ,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["const isTest = () =>\n\ttypeof process !== 'undefined' &&\n\tprocess.env.NODE_ENV === 'test' &&\n\t// @ts-expect-error\n\t!globalThis.__FORCE_RAF_IN_TESTS__\n\nconst fpsQueue: Array<() => void> = []\nconst targetFps = 60\nconst targetTimePerFrame = Math.floor(1000 / targetFps) * 0.9 // ~15ms - we allow for some variance as browsers aren't that precise.\nlet frameRaf: undefined | number\nlet flushRaf: undefined | number\nlet lastFlushTime = -targetTimePerFrame\n\nconst flush = () => {\n\tconst queue = fpsQueue.splice(0, fpsQueue.length)\n\tfor (const fn of queue) {\n\t\tfn()\n\t}\n}\n\nfunction tick(isOnNextFrame = false) {\n\tif (frameRaf) return\n\n\tconst now = Date.now()\n\tconst elapsed = now - lastFlushTime\n\n\tif (elapsed < targetTimePerFrame) {\n\t\t// If we're too early to flush, we need to wait until the next frame to try and flush again.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tframeRaf = requestAnimationFrame(() => {\n\t\t\tframeRaf = undefined\n\t\t\ttick(true)\n\t\t})\n\t\treturn\n\t}\n\n\tif (isOnNextFrame) {\n\t\t// If we've already waited for the next frame to run the tick, then we can flush immediately\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on this frame already, so we can do nothing here.\n\t\tlastFlushTime = now\n\t\tflush()\n\t} else {\n\t\t// If we haven't already waited for the next frame to run the tick, we need to wait until the next frame to flush.\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on the next frame already, so we can do nothing here.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tflushRaf = requestAnimationFrame(() => {\n\t\t\tflushRaf = undefined\n\t\t\tlastFlushTime = now\n\t\t\tflush()\n\t\t})\n\t}\n}\n\n/**\n * Creates a throttled version of a function that executes at most once per frame (60fps).\n * Subsequent calls within the same frame are ignored, ensuring smooth performance\n * for high-frequency events like mouse movements or scroll events.\n *\n * @param fn - The function to throttle, optionally with a cancel method\n * @returns A throttled function with an optional cancel method to remove pending calls\n *\n * @example\n * ```ts\n * const updateCanvas = fpsThrottle(() => {\n * // This will run at most once per frame (~16.67ms)\n * redrawCanvas()\n * })\n *\n * // Call as often as you want - automatically throttled to 60fps\n * document.addEventListener('mousemove', updateCanvas)\n *\n * // Cancel pending calls if needed\n * updateCanvas.cancel?.()\n * ```\n *\n * @internal\n */\nexport function fpsThrottle(fn: { (): void; cancel?(): void }): {\n\t(): void\n\tcancel?(): void\n} {\n\tif (isTest()) {\n\t\tfn.cancel = () => {\n\t\t\tif (frameRaf) {\n\t\t\t\tcancelAnimationFrame(frameRaf)\n\t\t\t\tframeRaf = undefined\n\t\t\t}\n\t\t\tif (flushRaf) {\n\t\t\t\tcancelAnimationFrame(flushRaf)\n\t\t\t\tflushRaf = undefined\n\t\t\t}\n\t\t}\n\t\treturn fn\n\t}\n\n\tconst throttledFn = () => {\n\t\tif (fpsQueue.includes(fn)) {\n\t\t\treturn\n\t\t}\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\tthrottledFn.cancel = () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n\treturn throttledFn\n}\n\n/**\n * Schedules a function to execute on the next animation frame, targeting 60fps.\n * If the same function is passed multiple times before the frame executes,\n * it will only be called once, effectively batching multiple calls.\n *\n * @param fn - The function to execute on the next frame\n * @returns A cancel function that can prevent execution if called before the next frame\n *\n * @example\n * ```ts\n * const updateUI = throttleToNextFrame(() => {\n * // Batches multiple calls into the next animation frame\n * updateStatusBar()\n * refreshToolbar()\n * })\n *\n * // Multiple calls within the same frame are batched\n * updateUI() // Will execute\n * updateUI() // Ignored (same function already queued)\n * updateUI() // Ignored (same function already queued)\n *\n * // Get cancel function to prevent execution\n * const cancel = updateUI()\n * cancel() // Prevents execution if called before next frame\n * ```\n *\n * @internal\n */\nexport function throttleToNextFrame(fn: () => void): () => void {\n\tif (isTest()) {\n\t\tfn()\n\t\treturn () => void null // noop\n\t}\n\n\tif (!fpsQueue.includes(fn)) {\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\n\treturn () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAEb,MAAM,WAA8B,CAAC;AACrC,MAAM,YAAY;AAClB,MAAM,qBAAqB,KAAK,MAAM,MAAO,SAAS,IAAI;AAC1D,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAgB,CAAC;AAErB,MAAM,QAAQ,MAAM;AACnB,QAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,MAAM;AAChD,aAAW,MAAM,OAAO;AACvB,OAAG;AAAA,EACJ;AACD;AAEA,SAAS,KAAK,gBAAgB,OAAO;AACpC,MAAI,SAAU;AAEd,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,MAAM;AAEtB,MAAI,UAAU,oBAAoB;AAGjC,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,WAAK,IAAI;AAAA,IACV,CAAC;AACD;AAAA,EACD;AAEA,MAAI,eAAe;AAElB,QAAI,SAAU;AACd,oBAAgB;AAChB,UAAM;AAAA,EACP,OAAO;AAEN,QAAI,SAAU;AAEd,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,sBAAgB;AAChB,YAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AA0BO,SAAS,YAAY,IAG1B;AACD,MAAI,OAAO,GAAG;AACb,OAAG,SAAS,MAAM;AACjB,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AACA,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,MAAM;AACzB,QAAI,SAAS,SAAS,EAAE,GAAG;AAC1B;AAAA,IACD;AACA,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AACA,cAAY,SAAS,MAAM;AAC1B,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACA,SAAO;AACR;AA8BO,SAAS,oBAAoB,IAA4B;AAC/D,MAAI,OAAO,GAAG;AACb,OAAG;AACH,WAAO,MAAM;AAAA,EACd;AAEA,MAAI,CAAC,SAAS,SAAS,EAAE,GAAG;AAC3B,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AAEA,SAAO,MAAM;AACZ,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
@@ -101,7 +101,7 @@ import { registerTldrawLibraryVersion as registerTldrawLibraryVersion2 } from ".
101
101
  import { warnDeprecatedGetter, warnOnce } from "./lib/warn.mjs";
102
102
  registerTldrawLibraryVersion(
103
103
  "@tldraw/utils",
104
- "4.2.0-next.094f21ae35eb",
104
+ "4.2.0-next.3634f876bff4",
105
105
  "esm"
106
106
  );
107
107
  export {
@@ -1,21 +1,14 @@
1
1
  const isTest = () => typeof process !== "undefined" && process.env.NODE_ENV === "test" && // @ts-expect-error
2
2
  !globalThis.__FORCE_RAF_IN_TESTS__;
3
3
  const fpsQueue = [];
4
- const targetFps = 120;
5
- const timingVarianceFactor = 0.9;
6
- const targetTimePerFrame = Math.floor(1e3 / targetFps) * timingVarianceFactor;
4
+ const targetFps = 60;
5
+ const targetTimePerFrame = Math.floor(1e3 / targetFps) * 0.9;
7
6
  let frameRaf;
8
7
  let flushRaf;
9
8
  let lastFlushTime = -targetTimePerFrame;
10
- const customFpsLastRunTime = /* @__PURE__ */ new WeakMap();
11
- const customFpsGetters = /* @__PURE__ */ new WeakMap();
12
9
  const flush = () => {
13
10
  const queue = fpsQueue.splice(0, fpsQueue.length);
14
11
  for (const fn of queue) {
15
- const getTargetFps = customFpsGetters.get(fn);
16
- if (getTargetFps) {
17
- customFpsLastRunTime.set(fn, Date.now());
18
- }
19
12
  fn();
20
13
  }
21
14
  };
@@ -43,7 +36,7 @@ function tick(isOnNextFrame = false) {
43
36
  });
44
37
  }
45
38
  }
46
- function fpsThrottle(fn, getTargetFps) {
39
+ function fpsThrottle(fn) {
47
40
  if (isTest()) {
48
41
  fn.cancel = () => {
49
42
  if (frameRaf) {
@@ -57,18 +50,7 @@ function fpsThrottle(fn, getTargetFps) {
57
50
  };
58
51
  return fn;
59
52
  }
60
- if (getTargetFps) {
61
- customFpsGetters.set(fn, getTargetFps);
62
- }
63
53
  const throttledFn = () => {
64
- if (getTargetFps) {
65
- const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity;
66
- const customTimePerFrame = Math.floor(1e3 / getTargetFps()) * timingVarianceFactor;
67
- const elapsed = Date.now() - lastRun;
68
- if (elapsed < customTimePerFrame) {
69
- return;
70
- }
71
- }
72
54
  if (fpsQueue.includes(fn)) {
73
55
  return;
74
56
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/throttle.ts"],
4
- "sourcesContent": ["const isTest = () =>\n\ttypeof process !== 'undefined' &&\n\tprocess.env.NODE_ENV === 'test' &&\n\t// @ts-expect-error\n\t!globalThis.__FORCE_RAF_IN_TESTS__\n\nconst fpsQueue: Array<() => void> = []\nconst targetFps = 120\n// Browsers aren't precise with frame timing - this factor prevents skipping frames unnecessarily\n// by aiming slightly below the theoretical frame duration (e.g., ~7.5ms instead of 8.33ms for 120fps)\nconst timingVarianceFactor = 0.9\nconst targetTimePerFrame = Math.floor(1000 / targetFps) * timingVarianceFactor // ~7ms\nlet frameRaf: undefined | number\nlet flushRaf: undefined | number\nlet lastFlushTime = -targetTimePerFrame\n\n// Track custom FPS timing per function\nconst customFpsLastRunTime = new WeakMap<() => void, number>()\n// Map function to its custom FPS getter\nconst customFpsGetters = new WeakMap<() => void, () => number>()\n\nconst flush = () => {\n\tconst queue = fpsQueue.splice(0, fpsQueue.length)\n\tfor (const fn of queue) {\n\t\t// If this function has custom FPS, update timestamp when executing\n\t\tconst getTargetFps = customFpsGetters.get(fn)\n\t\tif (getTargetFps) {\n\t\t\tcustomFpsLastRunTime.set(fn, Date.now())\n\t\t}\n\t\tfn()\n\t}\n}\n\nfunction tick(isOnNextFrame = false) {\n\tif (frameRaf) return\n\n\tconst now = Date.now()\n\tconst elapsed = now - lastFlushTime\n\n\tif (elapsed < targetTimePerFrame) {\n\t\t// If we're too early to flush, we need to wait until the next frame to try and flush again.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tframeRaf = requestAnimationFrame(() => {\n\t\t\tframeRaf = undefined\n\t\t\ttick(true)\n\t\t})\n\t\treturn\n\t}\n\n\tif (isOnNextFrame) {\n\t\t// If we've already waited for the next frame to run the tick, then we can flush immediately\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on this frame already, so we can do nothing here.\n\t\tlastFlushTime = now\n\t\tflush()\n\t} else {\n\t\t// If we haven't already waited for the next frame to run the tick, we need to wait until the next frame to flush.\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on the next frame already, so we can do nothing here.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tflushRaf = requestAnimationFrame(() => {\n\t\t\tflushRaf = undefined\n\t\t\tlastFlushTime = now\n\t\t\tflush()\n\t\t})\n\t}\n}\n\n/**\n * Creates a throttled version of a function that executes at most once per frame.\n * The default target frame rate is 120fps, but can be customized per function.\n * Subsequent calls within the same frame are ignored, ensuring smooth performance\n * for high-frequency events like mouse movements or scroll events.\n *\n * @param fn - The function to throttle, optionally with a cancel method\n * @param getTargetFps - Optional function that returns the current target FPS rate for custom throttling\n * @returns A throttled function with an optional cancel method to remove pending calls\n *\n * @example\n * ```ts\n * // Default 120fps throttling\n * const updateCanvas = fpsThrottle(() => {\n * // This will run at most once per frame (~8.33ms)\n * redrawCanvas()\n * })\n *\n * // Call as often as you want - automatically throttled to 120fps\n * document.addEventListener('mousemove', updateCanvas)\n *\n * // Cancel pending calls if needed\n * updateCanvas.cancel?.()\n *\n * // Custom FPS throttling for less critical updates\n * const slowUpdate = fpsThrottle(() => {\n * heavyComputation()\n * }, () => 30) // Throttle to 30fps\n * ```\n *\n * @internal\n */\nexport function fpsThrottle(\n\tfn: { (): void; cancel?(): void },\n\tgetTargetFps?: () => number\n): {\n\t(): void\n\tcancel?(): void\n} {\n\tif (isTest()) {\n\t\tfn.cancel = () => {\n\t\t\tif (frameRaf) {\n\t\t\t\tcancelAnimationFrame(frameRaf)\n\t\t\t\tframeRaf = undefined\n\t\t\t}\n\t\t\tif (flushRaf) {\n\t\t\t\tcancelAnimationFrame(flushRaf)\n\t\t\t\tflushRaf = undefined\n\t\t\t}\n\t\t}\n\t\treturn fn\n\t}\n\n\t// Store custom FPS getter if provided\n\tif (getTargetFps) {\n\t\tcustomFpsGetters.set(fn, getTargetFps)\n\t}\n\n\tconst throttledFn = () => {\n\t\t// Custom FPS - check timing before queuing\n\t\tif (getTargetFps) {\n\t\t\tconst lastRun = customFpsLastRunTime.get(fn) ?? -Infinity\n\t\t\tconst customTimePerFrame = Math.floor(1000 / getTargetFps()) * timingVarianceFactor\n\t\t\tconst elapsed = Date.now() - lastRun\n\n\t\t\tif (elapsed < customTimePerFrame) {\n\t\t\t\t// Not ready yet, don't queue\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif (fpsQueue.includes(fn)) {\n\t\t\treturn\n\t\t}\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\tthrottledFn.cancel = () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n\treturn throttledFn\n}\n\n/**\n * Schedules a function to execute on the next animation frame, targeting 120fps.\n * If the same function is passed multiple times before the frame executes,\n * it will only be called once, effectively batching multiple calls.\n *\n * @param fn - The function to execute on the next frame\n * @returns A cancel function that can prevent execution if called before the next frame\n *\n * @example\n * ```ts\n * const updateUI = throttleToNextFrame(() => {\n * // Batches multiple calls into the next animation frame\n * updateStatusBar()\n * refreshToolbar()\n * })\n *\n * // Multiple calls within the same frame are batched\n * updateUI() // Will execute\n * updateUI() // Ignored (same function already queued)\n * updateUI() // Ignored (same function already queued)\n *\n * // Get cancel function to prevent execution\n * const cancel = updateUI()\n * cancel() // Prevents execution if called before next frame\n * ```\n *\n * @internal\n */\nexport function throttleToNextFrame(fn: () => void): () => void {\n\tif (isTest()) {\n\t\tfn()\n\t\treturn () => void null // noop\n\t}\n\n\tif (!fpsQueue.includes(fn)) {\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\n\treturn () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n}\n"],
5
- "mappings": "AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAEb,MAAM,WAA8B,CAAC;AACrC,MAAM,YAAY;AAGlB,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,MAAM,MAAO,SAAS,IAAI;AAC1D,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAgB,CAAC;AAGrB,MAAM,uBAAuB,oBAAI,QAA4B;AAE7D,MAAM,mBAAmB,oBAAI,QAAkC;AAE/D,MAAM,QAAQ,MAAM;AACnB,QAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,MAAM;AAChD,aAAW,MAAM,OAAO;AAEvB,UAAM,eAAe,iBAAiB,IAAI,EAAE;AAC5C,QAAI,cAAc;AACjB,2BAAqB,IAAI,IAAI,KAAK,IAAI,CAAC;AAAA,IACxC;AACA,OAAG;AAAA,EACJ;AACD;AAEA,SAAS,KAAK,gBAAgB,OAAO;AACpC,MAAI,SAAU;AAEd,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,MAAM;AAEtB,MAAI,UAAU,oBAAoB;AAGjC,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,WAAK,IAAI;AAAA,IACV,CAAC;AACD;AAAA,EACD;AAEA,MAAI,eAAe;AAElB,QAAI,SAAU;AACd,oBAAgB;AAChB,UAAM;AAAA,EACP,OAAO;AAEN,QAAI,SAAU;AAEd,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,sBAAgB;AAChB,YAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AAkCO,SAAS,YACf,IACA,cAIC;AACD,MAAI,OAAO,GAAG;AACb,OAAG,SAAS,MAAM;AACjB,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AACA,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAGA,MAAI,cAAc;AACjB,qBAAiB,IAAI,IAAI,YAAY;AAAA,EACtC;AAEA,QAAM,cAAc,MAAM;AAEzB,QAAI,cAAc;AACjB,YAAM,UAAU,qBAAqB,IAAI,EAAE,KAAK;AAChD,YAAM,qBAAqB,KAAK,MAAM,MAAO,aAAa,CAAC,IAAI;AAC/D,YAAM,UAAU,KAAK,IAAI,IAAI;AAE7B,UAAI,UAAU,oBAAoB;AAEjC;AAAA,MACD;AAAA,IACD;AACA,QAAI,SAAS,SAAS,EAAE,GAAG;AAC1B;AAAA,IACD;AACA,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AACA,cAAY,SAAS,MAAM;AAC1B,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACA,SAAO;AACR;AA8BO,SAAS,oBAAoB,IAA4B;AAC/D,MAAI,OAAO,GAAG;AACb,OAAG;AACH,WAAO,MAAM;AAAA,EACd;AAEA,MAAI,CAAC,SAAS,SAAS,EAAE,GAAG;AAC3B,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AAEA,SAAO,MAAM;AACZ,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACD;",
4
+ "sourcesContent": ["const isTest = () =>\n\ttypeof process !== 'undefined' &&\n\tprocess.env.NODE_ENV === 'test' &&\n\t// @ts-expect-error\n\t!globalThis.__FORCE_RAF_IN_TESTS__\n\nconst fpsQueue: Array<() => void> = []\nconst targetFps = 60\nconst targetTimePerFrame = Math.floor(1000 / targetFps) * 0.9 // ~15ms - we allow for some variance as browsers aren't that precise.\nlet frameRaf: undefined | number\nlet flushRaf: undefined | number\nlet lastFlushTime = -targetTimePerFrame\n\nconst flush = () => {\n\tconst queue = fpsQueue.splice(0, fpsQueue.length)\n\tfor (const fn of queue) {\n\t\tfn()\n\t}\n}\n\nfunction tick(isOnNextFrame = false) {\n\tif (frameRaf) return\n\n\tconst now = Date.now()\n\tconst elapsed = now - lastFlushTime\n\n\tif (elapsed < targetTimePerFrame) {\n\t\t// If we're too early to flush, we need to wait until the next frame to try and flush again.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tframeRaf = requestAnimationFrame(() => {\n\t\t\tframeRaf = undefined\n\t\t\ttick(true)\n\t\t})\n\t\treturn\n\t}\n\n\tif (isOnNextFrame) {\n\t\t// If we've already waited for the next frame to run the tick, then we can flush immediately\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on this frame already, so we can do nothing here.\n\t\tlastFlushTime = now\n\t\tflush()\n\t} else {\n\t\t// If we haven't already waited for the next frame to run the tick, we need to wait until the next frame to flush.\n\t\tif (flushRaf) return // ...though if there's a flush raf, that means we'll be flushing on the next frame already, so we can do nothing here.\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tflushRaf = requestAnimationFrame(() => {\n\t\t\tflushRaf = undefined\n\t\t\tlastFlushTime = now\n\t\t\tflush()\n\t\t})\n\t}\n}\n\n/**\n * Creates a throttled version of a function that executes at most once per frame (60fps).\n * Subsequent calls within the same frame are ignored, ensuring smooth performance\n * for high-frequency events like mouse movements or scroll events.\n *\n * @param fn - The function to throttle, optionally with a cancel method\n * @returns A throttled function with an optional cancel method to remove pending calls\n *\n * @example\n * ```ts\n * const updateCanvas = fpsThrottle(() => {\n * // This will run at most once per frame (~16.67ms)\n * redrawCanvas()\n * })\n *\n * // Call as often as you want - automatically throttled to 60fps\n * document.addEventListener('mousemove', updateCanvas)\n *\n * // Cancel pending calls if needed\n * updateCanvas.cancel?.()\n * ```\n *\n * @internal\n */\nexport function fpsThrottle(fn: { (): void; cancel?(): void }): {\n\t(): void\n\tcancel?(): void\n} {\n\tif (isTest()) {\n\t\tfn.cancel = () => {\n\t\t\tif (frameRaf) {\n\t\t\t\tcancelAnimationFrame(frameRaf)\n\t\t\t\tframeRaf = undefined\n\t\t\t}\n\t\t\tif (flushRaf) {\n\t\t\t\tcancelAnimationFrame(flushRaf)\n\t\t\t\tflushRaf = undefined\n\t\t\t}\n\t\t}\n\t\treturn fn\n\t}\n\n\tconst throttledFn = () => {\n\t\tif (fpsQueue.includes(fn)) {\n\t\t\treturn\n\t\t}\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\tthrottledFn.cancel = () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n\treturn throttledFn\n}\n\n/**\n * Schedules a function to execute on the next animation frame, targeting 60fps.\n * If the same function is passed multiple times before the frame executes,\n * it will only be called once, effectively batching multiple calls.\n *\n * @param fn - The function to execute on the next frame\n * @returns A cancel function that can prevent execution if called before the next frame\n *\n * @example\n * ```ts\n * const updateUI = throttleToNextFrame(() => {\n * // Batches multiple calls into the next animation frame\n * updateStatusBar()\n * refreshToolbar()\n * })\n *\n * // Multiple calls within the same frame are batched\n * updateUI() // Will execute\n * updateUI() // Ignored (same function already queued)\n * updateUI() // Ignored (same function already queued)\n *\n * // Get cancel function to prevent execution\n * const cancel = updateUI()\n * cancel() // Prevents execution if called before next frame\n * ```\n *\n * @internal\n */\nexport function throttleToNextFrame(fn: () => void): () => void {\n\tif (isTest()) {\n\t\tfn()\n\t\treturn () => void null // noop\n\t}\n\n\tif (!fpsQueue.includes(fn)) {\n\t\tfpsQueue.push(fn)\n\t\ttick()\n\t}\n\n\treturn () => {\n\t\tconst index = fpsQueue.indexOf(fn)\n\t\tif (index > -1) {\n\t\t\tfpsQueue.splice(index, 1)\n\t\t}\n\t}\n}\n"],
5
+ "mappings": "AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAEb,MAAM,WAA8B,CAAC;AACrC,MAAM,YAAY;AAClB,MAAM,qBAAqB,KAAK,MAAM,MAAO,SAAS,IAAI;AAC1D,IAAI;AACJ,IAAI;AACJ,IAAI,gBAAgB,CAAC;AAErB,MAAM,QAAQ,MAAM;AACnB,QAAM,QAAQ,SAAS,OAAO,GAAG,SAAS,MAAM;AAChD,aAAW,MAAM,OAAO;AACvB,OAAG;AAAA,EACJ;AACD;AAEA,SAAS,KAAK,gBAAgB,OAAO;AACpC,MAAI,SAAU;AAEd,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,MAAM;AAEtB,MAAI,UAAU,oBAAoB;AAGjC,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,WAAK,IAAI;AAAA,IACV,CAAC;AACD;AAAA,EACD;AAEA,MAAI,eAAe;AAElB,QAAI,SAAU;AACd,oBAAgB;AAChB,UAAM;AAAA,EACP,OAAO;AAEN,QAAI,SAAU;AAEd,eAAW,sBAAsB,MAAM;AACtC,iBAAW;AACX,sBAAgB;AAChB,YAAM;AAAA,IACP,CAAC;AAAA,EACF;AACD;AA0BO,SAAS,YAAY,IAG1B;AACD,MAAI,OAAO,GAAG;AACb,OAAG,SAAS,MAAM;AACjB,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AACA,UAAI,UAAU;AACb,6BAAqB,QAAQ;AAC7B,mBAAW;AAAA,MACZ;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,MAAM;AACzB,QAAI,SAAS,SAAS,EAAE,GAAG;AAC1B;AAAA,IACD;AACA,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AACA,cAAY,SAAS,MAAM;AAC1B,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACA,SAAO;AACR;AA8BO,SAAS,oBAAoB,IAA4B;AAC/D,MAAI,OAAO,GAAG;AACb,OAAG;AACH,WAAO,MAAM;AAAA,EACd;AAEA,MAAI,CAAC,SAAS,SAAS,EAAE,GAAG;AAC3B,aAAS,KAAK,EAAE;AAChB,SAAK;AAAA,EACN;AAEA,SAAO,MAAM;AACZ,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,QAAI,QAAQ,IAAI;AACf,eAAS,OAAO,OAAO,CAAC;AAAA,IACzB;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/utils",
3
3
  "description": "tldraw infinite canvas SDK (private utilities).",
4
- "version": "4.2.0-next.094f21ae35eb",
4
+ "version": "4.2.0-next.3634f876bff4",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -5,28 +5,15 @@ const isTest = () =>
5
5
  !globalThis.__FORCE_RAF_IN_TESTS__
6
6
 
7
7
  const fpsQueue: Array<() => void> = []
8
- const targetFps = 120
9
- // Browsers aren't precise with frame timing - this factor prevents skipping frames unnecessarily
10
- // by aiming slightly below the theoretical frame duration (e.g., ~7.5ms instead of 8.33ms for 120fps)
11
- const timingVarianceFactor = 0.9
12
- const targetTimePerFrame = Math.floor(1000 / targetFps) * timingVarianceFactor // ~7ms
8
+ const targetFps = 60
9
+ const targetTimePerFrame = Math.floor(1000 / targetFps) * 0.9 // ~15ms - we allow for some variance as browsers aren't that precise.
13
10
  let frameRaf: undefined | number
14
11
  let flushRaf: undefined | number
15
12
  let lastFlushTime = -targetTimePerFrame
16
13
 
17
- // Track custom FPS timing per function
18
- const customFpsLastRunTime = new WeakMap<() => void, number>()
19
- // Map function to its custom FPS getter
20
- const customFpsGetters = new WeakMap<() => void, () => number>()
21
-
22
14
  const flush = () => {
23
15
  const queue = fpsQueue.splice(0, fpsQueue.length)
24
16
  for (const fn of queue) {
25
- // If this function has custom FPS, update timestamp when executing
26
- const getTargetFps = customFpsGetters.get(fn)
27
- if (getTargetFps) {
28
- customFpsLastRunTime.set(fn, Date.now())
29
- }
30
17
  fn()
31
18
  }
32
19
  }
@@ -65,41 +52,30 @@ function tick(isOnNextFrame = false) {
65
52
  }
66
53
 
67
54
  /**
68
- * Creates a throttled version of a function that executes at most once per frame.
69
- * The default target frame rate is 120fps, but can be customized per function.
55
+ * Creates a throttled version of a function that executes at most once per frame (60fps).
70
56
  * Subsequent calls within the same frame are ignored, ensuring smooth performance
71
57
  * for high-frequency events like mouse movements or scroll events.
72
58
  *
73
59
  * @param fn - The function to throttle, optionally with a cancel method
74
- * @param getTargetFps - Optional function that returns the current target FPS rate for custom throttling
75
60
  * @returns A throttled function with an optional cancel method to remove pending calls
76
61
  *
77
62
  * @example
78
63
  * ```ts
79
- * // Default 120fps throttling
80
64
  * const updateCanvas = fpsThrottle(() => {
81
- * // This will run at most once per frame (~8.33ms)
65
+ * // This will run at most once per frame (~16.67ms)
82
66
  * redrawCanvas()
83
67
  * })
84
68
  *
85
- * // Call as often as you want - automatically throttled to 120fps
69
+ * // Call as often as you want - automatically throttled to 60fps
86
70
  * document.addEventListener('mousemove', updateCanvas)
87
71
  *
88
72
  * // Cancel pending calls if needed
89
73
  * updateCanvas.cancel?.()
90
- *
91
- * // Custom FPS throttling for less critical updates
92
- * const slowUpdate = fpsThrottle(() => {
93
- * heavyComputation()
94
- * }, () => 30) // Throttle to 30fps
95
74
  * ```
96
75
  *
97
76
  * @internal
98
77
  */
99
- export function fpsThrottle(
100
- fn: { (): void; cancel?(): void },
101
- getTargetFps?: () => number
102
- ): {
78
+ export function fpsThrottle(fn: { (): void; cancel?(): void }): {
103
79
  (): void
104
80
  cancel?(): void
105
81
  } {
@@ -117,23 +93,7 @@ export function fpsThrottle(
117
93
  return fn
118
94
  }
119
95
 
120
- // Store custom FPS getter if provided
121
- if (getTargetFps) {
122
- customFpsGetters.set(fn, getTargetFps)
123
- }
124
-
125
96
  const throttledFn = () => {
126
- // Custom FPS - check timing before queuing
127
- if (getTargetFps) {
128
- const lastRun = customFpsLastRunTime.get(fn) ?? -Infinity
129
- const customTimePerFrame = Math.floor(1000 / getTargetFps()) * timingVarianceFactor
130
- const elapsed = Date.now() - lastRun
131
-
132
- if (elapsed < customTimePerFrame) {
133
- // Not ready yet, don't queue
134
- return
135
- }
136
- }
137
97
  if (fpsQueue.includes(fn)) {
138
98
  return
139
99
  }
@@ -150,7 +110,7 @@ export function fpsThrottle(
150
110
  }
151
111
 
152
112
  /**
153
- * Schedules a function to execute on the next animation frame, targeting 120fps.
113
+ * Schedules a function to execute on the next animation frame, targeting 60fps.
154
114
  * If the same function is passed multiple times before the frame executes,
155
115
  * it will only be called once, effectively batching multiple calls.
156
116
  *