@tldraw/utils 4.2.0-canary.fe9b43f7b4a1 → 4.2.0-internal.d43dfb4f2e76
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-
|
|
171
|
+
"4.2.0-internal.d43dfb4f2e76",
|
|
172
172
|
"cjs"
|
|
173
173
|
);
|
|
174
174
|
//# sourceMappingURL=index.js.map
|
package/dist-cjs/lib/throttle.js
CHANGED
|
@@ -25,14 +25,21 @@ 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 =
|
|
29
|
-
const
|
|
28
|
+
const targetFps = 120;
|
|
29
|
+
const timingVarianceFactor = 0.9;
|
|
30
|
+
const targetTimePerFrame = Math.floor(1e3 / targetFps) * timingVarianceFactor;
|
|
30
31
|
let frameRaf;
|
|
31
32
|
let flushRaf;
|
|
32
33
|
let lastFlushTime = -targetTimePerFrame;
|
|
34
|
+
const customFpsLastRunTime = /* @__PURE__ */ new WeakMap();
|
|
35
|
+
const customFpsGetters = /* @__PURE__ */ new WeakMap();
|
|
33
36
|
const flush = () => {
|
|
34
37
|
const queue = fpsQueue.splice(0, fpsQueue.length);
|
|
35
38
|
for (const fn of queue) {
|
|
39
|
+
const getTargetFps = customFpsGetters.get(fn);
|
|
40
|
+
if (getTargetFps) {
|
|
41
|
+
customFpsLastRunTime.set(fn, Date.now());
|
|
42
|
+
}
|
|
36
43
|
fn();
|
|
37
44
|
}
|
|
38
45
|
};
|
|
@@ -60,7 +67,7 @@ function tick(isOnNextFrame = false) {
|
|
|
60
67
|
});
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
|
-
function fpsThrottle(fn) {
|
|
70
|
+
function fpsThrottle(fn, getTargetFps) {
|
|
64
71
|
if (isTest()) {
|
|
65
72
|
fn.cancel = () => {
|
|
66
73
|
if (frameRaf) {
|
|
@@ -74,7 +81,18 @@ function fpsThrottle(fn) {
|
|
|
74
81
|
};
|
|
75
82
|
return fn;
|
|
76
83
|
}
|
|
84
|
+
if (getTargetFps) {
|
|
85
|
+
customFpsGetters.set(fn, getTargetFps);
|
|
86
|
+
}
|
|
77
87
|
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
|
+
}
|
|
78
96
|
if (fpsQueue.includes(fn)) {
|
|
79
97
|
return;
|
|
80
98
|
}
|
|
@@ -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 =
|
|
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;
|
|
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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -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-
|
|
104
|
+
"4.2.0-internal.d43dfb4f2e76",
|
|
105
105
|
"esm"
|
|
106
106
|
);
|
|
107
107
|
export {
|
|
@@ -1,14 +1,21 @@
|
|
|
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 =
|
|
5
|
-
const
|
|
4
|
+
const targetFps = 120;
|
|
5
|
+
const timingVarianceFactor = 0.9;
|
|
6
|
+
const targetTimePerFrame = Math.floor(1e3 / targetFps) * timingVarianceFactor;
|
|
6
7
|
let frameRaf;
|
|
7
8
|
let flushRaf;
|
|
8
9
|
let lastFlushTime = -targetTimePerFrame;
|
|
10
|
+
const customFpsLastRunTime = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
const customFpsGetters = /* @__PURE__ */ new WeakMap();
|
|
9
12
|
const flush = () => {
|
|
10
13
|
const queue = fpsQueue.splice(0, fpsQueue.length);
|
|
11
14
|
for (const fn of queue) {
|
|
15
|
+
const getTargetFps = customFpsGetters.get(fn);
|
|
16
|
+
if (getTargetFps) {
|
|
17
|
+
customFpsLastRunTime.set(fn, Date.now());
|
|
18
|
+
}
|
|
12
19
|
fn();
|
|
13
20
|
}
|
|
14
21
|
};
|
|
@@ -36,7 +43,7 @@ function tick(isOnNextFrame = false) {
|
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
|
-
function fpsThrottle(fn) {
|
|
46
|
+
function fpsThrottle(fn, getTargetFps) {
|
|
40
47
|
if (isTest()) {
|
|
41
48
|
fn.cancel = () => {
|
|
42
49
|
if (frameRaf) {
|
|
@@ -50,7 +57,18 @@ function fpsThrottle(fn) {
|
|
|
50
57
|
};
|
|
51
58
|
return fn;
|
|
52
59
|
}
|
|
60
|
+
if (getTargetFps) {
|
|
61
|
+
customFpsGetters.set(fn, getTargetFps);
|
|
62
|
+
}
|
|
53
63
|
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
|
+
}
|
|
54
72
|
if (fpsQueue.includes(fn)) {
|
|
55
73
|
return;
|
|
56
74
|
}
|
|
@@ -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 =
|
|
5
|
-
"mappings": "AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAEb,MAAM,WAA8B,CAAC;AACrC,MAAM,YAAY;
|
|
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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
package/src/lib/throttle.ts
CHANGED
|
@@ -5,15 +5,28 @@ const isTest = () =>
|
|
|
5
5
|
!globalThis.__FORCE_RAF_IN_TESTS__
|
|
6
6
|
|
|
7
7
|
const fpsQueue: Array<() => void> = []
|
|
8
|
-
const targetFps =
|
|
9
|
-
|
|
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
|
|
10
13
|
let frameRaf: undefined | number
|
|
11
14
|
let flushRaf: undefined | number
|
|
12
15
|
let lastFlushTime = -targetTimePerFrame
|
|
13
16
|
|
|
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
|
+
|
|
14
22
|
const flush = () => {
|
|
15
23
|
const queue = fpsQueue.splice(0, fpsQueue.length)
|
|
16
24
|
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
|
+
}
|
|
17
30
|
fn()
|
|
18
31
|
}
|
|
19
32
|
}
|
|
@@ -52,30 +65,41 @@ function tick(isOnNextFrame = false) {
|
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
/**
|
|
55
|
-
* Creates a throttled version of a function that executes at most once per frame
|
|
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.
|
|
56
70
|
* Subsequent calls within the same frame are ignored, ensuring smooth performance
|
|
57
71
|
* for high-frequency events like mouse movements or scroll events.
|
|
58
72
|
*
|
|
59
73
|
* @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
|
|
60
75
|
* @returns A throttled function with an optional cancel method to remove pending calls
|
|
61
76
|
*
|
|
62
77
|
* @example
|
|
63
78
|
* ```ts
|
|
79
|
+
* // Default 120fps throttling
|
|
64
80
|
* const updateCanvas = fpsThrottle(() => {
|
|
65
|
-
* // This will run at most once per frame (~
|
|
81
|
+
* // This will run at most once per frame (~8.33ms)
|
|
66
82
|
* redrawCanvas()
|
|
67
83
|
* })
|
|
68
84
|
*
|
|
69
|
-
* // Call as often as you want - automatically throttled to
|
|
85
|
+
* // Call as often as you want - automatically throttled to 120fps
|
|
70
86
|
* document.addEventListener('mousemove', updateCanvas)
|
|
71
87
|
*
|
|
72
88
|
* // Cancel pending calls if needed
|
|
73
89
|
* updateCanvas.cancel?.()
|
|
90
|
+
*
|
|
91
|
+
* // Custom FPS throttling for less critical updates
|
|
92
|
+
* const slowUpdate = fpsThrottle(() => {
|
|
93
|
+
* heavyComputation()
|
|
94
|
+
* }, () => 30) // Throttle to 30fps
|
|
74
95
|
* ```
|
|
75
96
|
*
|
|
76
97
|
* @internal
|
|
77
98
|
*/
|
|
78
|
-
export function fpsThrottle(
|
|
99
|
+
export function fpsThrottle(
|
|
100
|
+
fn: { (): void; cancel?(): void },
|
|
101
|
+
getTargetFps?: () => number
|
|
102
|
+
): {
|
|
79
103
|
(): void
|
|
80
104
|
cancel?(): void
|
|
81
105
|
} {
|
|
@@ -93,7 +117,23 @@ export function fpsThrottle(fn: { (): void; cancel?(): void }): {
|
|
|
93
117
|
return fn
|
|
94
118
|
}
|
|
95
119
|
|
|
120
|
+
// Store custom FPS getter if provided
|
|
121
|
+
if (getTargetFps) {
|
|
122
|
+
customFpsGetters.set(fn, getTargetFps)
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
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
|
+
}
|
|
97
137
|
if (fpsQueue.includes(fn)) {
|
|
98
138
|
return
|
|
99
139
|
}
|
|
@@ -110,7 +150,7 @@ export function fpsThrottle(fn: { (): void; cancel?(): void }): {
|
|
|
110
150
|
}
|
|
111
151
|
|
|
112
152
|
/**
|
|
113
|
-
* Schedules a function to execute on the next animation frame, targeting
|
|
153
|
+
* Schedules a function to execute on the next animation frame, targeting 120fps.
|
|
114
154
|
* If the same function is passed multiple times before the frame executes,
|
|
115
155
|
* it will only be called once, effectively batching multiple calls.
|
|
116
156
|
*
|