@tldraw/utils 4.3.0 → 4.4.0-canary.15cff7ea86f8

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.
@@ -1,87 +1,131 @@
1
1
  const isTest = () => typeof process !== "undefined" && process.env.NODE_ENV === "test" && // @ts-expect-error
2
2
  !globalThis.__FORCE_RAF_IN_TESTS__;
3
- const fpsQueue = [];
4
- const targetFps = 60;
5
- const targetTimePerFrame = Math.floor(1e3 / targetFps) * 0.9;
6
- let frameRaf;
7
- let flushRaf;
8
- let lastFlushTime = -targetTimePerFrame;
9
- const flush = () => {
10
- const queue = fpsQueue.splice(0, fpsQueue.length);
11
- for (const fn of queue) {
12
- fn();
3
+ const timingVarianceFactor = 0.9;
4
+ const getTargetTimePerFrame = (targetFps) => Math.floor(1e3 / targetFps) * timingVarianceFactor;
5
+ class FpsScheduler {
6
+ targetFps;
7
+ targetTimePerFrame;
8
+ fpsQueue = [];
9
+ frameRaf;
10
+ flushRaf;
11
+ lastFlushTime;
12
+ constructor(targetFps = 120) {
13
+ this.targetFps = targetFps;
14
+ this.targetTimePerFrame = getTargetTimePerFrame(targetFps);
15
+ this.lastFlushTime = -this.targetTimePerFrame;
13
16
  }
14
- };
15
- function tick(isOnNextFrame = false) {
16
- if (frameRaf) return;
17
- const now = Date.now();
18
- const elapsed = now - lastFlushTime;
19
- if (elapsed < targetTimePerFrame) {
20
- frameRaf = requestAnimationFrame(() => {
21
- frameRaf = void 0;
22
- tick(true);
23
- });
24
- return;
17
+ updateTargetFps(targetFps) {
18
+ if (targetFps === this.targetFps) return;
19
+ this.targetFps = targetFps;
20
+ this.targetTimePerFrame = getTargetTimePerFrame(targetFps);
21
+ this.lastFlushTime = -this.targetTimePerFrame;
25
22
  }
26
- if (isOnNextFrame) {
27
- if (flushRaf) return;
28
- lastFlushTime = now;
29
- flush();
30
- } else {
31
- if (flushRaf) return;
32
- flushRaf = requestAnimationFrame(() => {
33
- flushRaf = void 0;
34
- lastFlushTime = now;
35
- flush();
36
- });
23
+ flush() {
24
+ const queue = this.fpsQueue.splice(0, this.fpsQueue.length);
25
+ for (const fn of queue) {
26
+ fn();
27
+ }
37
28
  }
38
- }
39
- function fpsThrottle(fn) {
40
- if (isTest()) {
41
- fn.cancel = () => {
42
- if (frameRaf) {
43
- cancelAnimationFrame(frameRaf);
44
- frameRaf = void 0;
29
+ tick(isOnNextFrame = false) {
30
+ if (this.frameRaf) return;
31
+ const now = Date.now();
32
+ const elapsed = now - this.lastFlushTime;
33
+ if (elapsed < this.targetTimePerFrame) {
34
+ this.frameRaf = requestAnimationFrame(() => {
35
+ this.frameRaf = void 0;
36
+ this.tick(true);
37
+ });
38
+ return;
39
+ }
40
+ if (isOnNextFrame) {
41
+ if (this.flushRaf) return;
42
+ this.lastFlushTime = now;
43
+ this.flush();
44
+ } else {
45
+ if (this.flushRaf) return;
46
+ this.flushRaf = requestAnimationFrame(() => {
47
+ this.flushRaf = void 0;
48
+ this.lastFlushTime = Date.now();
49
+ this.flush();
50
+ });
51
+ }
52
+ }
53
+ /**
54
+ * Creates a throttled version of a function that executes at most once per frame.
55
+ * The default target frame rate is set by the FpsScheduler instance.
56
+ * Subsequent calls within the same frame are ignored, ensuring smooth performance
57
+ * for high-frequency events like mouse movements or scroll events.
58
+ *
59
+ * @param fn - The function to throttle, optionally with a cancel method
60
+ * @returns A throttled function with an optional cancel method to remove pending calls
61
+ *
62
+ * @public
63
+ */
64
+ fpsThrottle(fn) {
65
+ if (isTest()) {
66
+ fn.cancel = () => {
67
+ if (this.frameRaf) {
68
+ cancelAnimationFrame(this.frameRaf);
69
+ this.frameRaf = void 0;
70
+ }
71
+ if (this.flushRaf) {
72
+ cancelAnimationFrame(this.flushRaf);
73
+ this.flushRaf = void 0;
74
+ }
75
+ };
76
+ return fn;
77
+ }
78
+ const throttledFn = () => {
79
+ if (this.fpsQueue.includes(fn)) {
80
+ return;
45
81
  }
46
- if (flushRaf) {
47
- cancelAnimationFrame(flushRaf);
48
- flushRaf = void 0;
82
+ this.fpsQueue.push(fn);
83
+ this.tick();
84
+ };
85
+ throttledFn.cancel = () => {
86
+ const index = this.fpsQueue.indexOf(fn);
87
+ if (index > -1) {
88
+ this.fpsQueue.splice(index, 1);
49
89
  }
50
90
  };
51
- return fn;
91
+ return throttledFn;
52
92
  }
53
- const throttledFn = () => {
54
- if (fpsQueue.includes(fn)) {
55
- return;
93
+ /**
94
+ * Schedules a function to execute on the next animation frame.
95
+ * If the same function is passed multiple times before the frame executes,
96
+ * it will only be called once, effectively batching multiple calls.
97
+ *
98
+ * @param fn - The function to execute on the next frame
99
+ * @returns A cancel function that can prevent execution if called before the next frame
100
+ *
101
+ * @public
102
+ */
103
+ throttleToNextFrame(fn) {
104
+ if (isTest()) {
105
+ fn();
106
+ return () => void 0;
56
107
  }
57
- fpsQueue.push(fn);
58
- tick();
59
- };
60
- throttledFn.cancel = () => {
61
- const index = fpsQueue.indexOf(fn);
62
- if (index > -1) {
63
- fpsQueue.splice(index, 1);
108
+ if (!this.fpsQueue.includes(fn)) {
109
+ this.fpsQueue.push(fn);
110
+ this.tick();
64
111
  }
65
- };
66
- return throttledFn;
112
+ return () => {
113
+ const index = this.fpsQueue.indexOf(fn);
114
+ if (index > -1) {
115
+ this.fpsQueue.splice(index, 1);
116
+ }
117
+ };
118
+ }
119
+ }
120
+ const defaultScheduler = new FpsScheduler(120);
121
+ function fpsThrottle(fn) {
122
+ return defaultScheduler.fpsThrottle(fn);
67
123
  }
68
124
  function throttleToNextFrame(fn) {
69
- if (isTest()) {
70
- fn();
71
- return () => void 0;
72
- }
73
- if (!fpsQueue.includes(fn)) {
74
- fpsQueue.push(fn);
75
- tick();
76
- }
77
- return () => {
78
- const index = fpsQueue.indexOf(fn);
79
- if (index > -1) {
80
- fpsQueue.splice(index, 1);
81
- }
82
- };
125
+ return defaultScheduler.throttleToNextFrame(fn);
83
126
  }
84
127
  export {
128
+ FpsScheduler,
85
129
  fpsThrottle,
86
130
  throttleToNextFrame
87
131
  };
@@ -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 = 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;",
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\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 getTargetTimePerFrame = (targetFps: number) =>\n\tMath.floor(1000 / targetFps) * timingVarianceFactor\n\n/**\n * A scheduler class that manages a queue of functions to be executed at a target frame rate.\n * Each instance maintains its own queue and state, allowing for separate throttling contexts\n * (e.g., UI operations vs network sync operations).\n *\n * @public\n */\nexport class FpsScheduler {\n\tprivate targetFps: number\n\tprivate targetTimePerFrame: number\n\tprivate fpsQueue: Array<() => void> = []\n\tprivate frameRaf: undefined | number\n\tprivate flushRaf: undefined | number\n\tprivate lastFlushTime: number\n\n\tconstructor(targetFps: number = 120) {\n\t\tthis.targetFps = targetFps\n\t\tthis.targetTimePerFrame = getTargetTimePerFrame(targetFps)\n\t\tthis.lastFlushTime = -this.targetTimePerFrame\n\t}\n\n\tupdateTargetFps(targetFps: number) {\n\t\tif (targetFps === this.targetFps) return\n\t\tthis.targetFps = targetFps\n\t\tthis.targetTimePerFrame = getTargetTimePerFrame(targetFps)\n\t\tthis.lastFlushTime = -this.targetTimePerFrame\n\t}\n\n\tprivate flush() {\n\t\tconst queue = this.fpsQueue.splice(0, this.fpsQueue.length)\n\t\tfor (const fn of queue) {\n\t\t\tfn()\n\t\t}\n\t}\n\n\tprivate tick(isOnNextFrame = false) {\n\t\tif (this.frameRaf) return\n\n\t\tconst now = Date.now()\n\t\tconst elapsed = now - this.lastFlushTime\n\n\t\tif (elapsed < this.targetTimePerFrame) {\n\t\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\t// eslint-disable-next-line no-restricted-globals\n\t\t\tthis.frameRaf = requestAnimationFrame(() => {\n\t\t\t\tthis.frameRaf = undefined\n\t\t\t\tthis.tick(true)\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tif (isOnNextFrame) {\n\t\t\t// If we've already waited for the next frame to run the tick, then we can flush immediately\n\t\t\tif (this.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\t\tthis.lastFlushTime = now\n\t\t\tthis.flush()\n\t\t} else {\n\t\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\t\tif (this.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\t// eslint-disable-next-line no-restricted-globals\n\t\t\tthis.flushRaf = requestAnimationFrame(() => {\n\t\t\t\tthis.flushRaf = undefined\n\t\t\t\tthis.lastFlushTime = Date.now()\n\t\t\t\tthis.flush()\n\t\t\t})\n\t\t}\n\t}\n\n\t/**\n\t * Creates a throttled version of a function that executes at most once per frame.\n\t * The default target frame rate is set by the FpsScheduler instance.\n\t * Subsequent calls within the same frame are ignored, ensuring smooth performance\n\t * for high-frequency events like mouse movements or scroll events.\n\t *\n\t * @param fn - The function to throttle, optionally with a cancel method\n\t * @returns A throttled function with an optional cancel method to remove pending calls\n\t *\n\t * @public\n\t */\n\tfpsThrottle(fn: { (): void; cancel?(): void }): {\n\t\t(): void\n\t\tcancel?(): void\n\t} {\n\t\tif (isTest()) {\n\t\t\tfn.cancel = () => {\n\t\t\t\tif (this.frameRaf) {\n\t\t\t\t\tcancelAnimationFrame(this.frameRaf)\n\t\t\t\t\tthis.frameRaf = undefined\n\t\t\t\t}\n\t\t\t\tif (this.flushRaf) {\n\t\t\t\t\tcancelAnimationFrame(this.flushRaf)\n\t\t\t\t\tthis.flushRaf = undefined\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fn\n\t\t}\n\n\t\tconst throttledFn = () => {\n\t\t\tif (this.fpsQueue.includes(fn)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fpsQueue.push(fn)\n\t\t\tthis.tick()\n\t\t}\n\t\tthrottledFn.cancel = () => {\n\t\t\tconst index = this.fpsQueue.indexOf(fn)\n\t\t\tif (index > -1) {\n\t\t\t\tthis.fpsQueue.splice(index, 1)\n\t\t\t}\n\t\t}\n\t\treturn throttledFn\n\t}\n\n\t/**\n\t * Schedules a function to execute on the next animation frame.\n\t * If the same function is passed multiple times before the frame executes,\n\t * it will only be called once, effectively batching multiple calls.\n\t *\n\t * @param fn - The function to execute on the next frame\n\t * @returns A cancel function that can prevent execution if called before the next frame\n\t *\n\t * @public\n\t */\n\tthrottleToNextFrame(fn: () => void): () => void {\n\t\tif (isTest()) {\n\t\t\tfn()\n\t\t\treturn () => void null // noop\n\t\t}\n\n\t\tif (!this.fpsQueue.includes(fn)) {\n\t\t\tthis.fpsQueue.push(fn)\n\t\t\tthis.tick()\n\t\t}\n\n\t\treturn () => {\n\t\t\tconst index = this.fpsQueue.indexOf(fn)\n\t\t\tif (index > -1) {\n\t\t\t\tthis.fpsQueue.splice(index, 1)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Default instance for UI operations\nconst defaultScheduler = new FpsScheduler(120)\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 * Uses the default throttle instance for UI operations. If you need a separate\n * throttling queue (e.g., for network operations), create your own Throttle instance.\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 * // 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 *\n * @internal\n */\nexport function fpsThrottle(fn: { (): void; cancel?(): void }): {\n\t(): void\n\tcancel?(): void\n} {\n\treturn defaultScheduler.fpsThrottle(fn)\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 * Uses the default throttle instance for UI operations.\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\treturn defaultScheduler.throttleToNextFrame(fn)\n}\n"],
5
+ "mappings": "AAAA,MAAM,SAAS,MACd,OAAO,YAAY,eACnB,QAAQ,IAAI,aAAa;AAEzB,CAAC,WAAW;AAIb,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB,CAAC,cAC9B,KAAK,MAAM,MAAO,SAAS,IAAI;AASzB,MAAM,aAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA,WAA8B,CAAC;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,YAAoB,KAAK;AACpC,SAAK,YAAY;AACjB,SAAK,qBAAqB,sBAAsB,SAAS;AACzD,SAAK,gBAAgB,CAAC,KAAK;AAAA,EAC5B;AAAA,EAEA,gBAAgB,WAAmB;AAClC,QAAI,cAAc,KAAK,UAAW;AAClC,SAAK,YAAY;AACjB,SAAK,qBAAqB,sBAAsB,SAAS;AACzD,SAAK,gBAAgB,CAAC,KAAK;AAAA,EAC5B;AAAA,EAEQ,QAAQ;AACf,UAAM,QAAQ,KAAK,SAAS,OAAO,GAAG,KAAK,SAAS,MAAM;AAC1D,eAAW,MAAM,OAAO;AACvB,SAAG;AAAA,IACJ;AAAA,EACD;AAAA,EAEQ,KAAK,gBAAgB,OAAO;AACnC,QAAI,KAAK,SAAU;AAEnB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,UAAU,MAAM,KAAK;AAE3B,QAAI,UAAU,KAAK,oBAAoB;AAGtC,WAAK,WAAW,sBAAsB,MAAM;AAC3C,aAAK,WAAW;AAChB,aAAK,KAAK,IAAI;AAAA,MACf,CAAC;AACD;AAAA,IACD;AAEA,QAAI,eAAe;AAElB,UAAI,KAAK,SAAU;AACnB,WAAK,gBAAgB;AACrB,WAAK,MAAM;AAAA,IACZ,OAAO;AAEN,UAAI,KAAK,SAAU;AAEnB,WAAK,WAAW,sBAAsB,MAAM;AAC3C,aAAK,WAAW;AAChB,aAAK,gBAAgB,KAAK,IAAI;AAC9B,aAAK,MAAM;AAAA,MACZ,CAAC;AAAA,IACF;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,YAAY,IAGV;AACD,QAAI,OAAO,GAAG;AACb,SAAG,SAAS,MAAM;AACjB,YAAI,KAAK,UAAU;AAClB,+BAAqB,KAAK,QAAQ;AAClC,eAAK,WAAW;AAAA,QACjB;AACA,YAAI,KAAK,UAAU;AAClB,+BAAqB,KAAK,QAAQ;AAClC,eAAK,WAAW;AAAA,QACjB;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAEA,UAAM,cAAc,MAAM;AACzB,UAAI,KAAK,SAAS,SAAS,EAAE,GAAG;AAC/B;AAAA,MACD;AACA,WAAK,SAAS,KAAK,EAAE;AACrB,WAAK,KAAK;AAAA,IACX;AACA,gBAAY,SAAS,MAAM;AAC1B,YAAM,QAAQ,KAAK,SAAS,QAAQ,EAAE;AACtC,UAAI,QAAQ,IAAI;AACf,aAAK,SAAS,OAAO,OAAO,CAAC;AAAA,MAC9B;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,oBAAoB,IAA4B;AAC/C,QAAI,OAAO,GAAG;AACb,SAAG;AACH,aAAO,MAAM;AAAA,IACd;AAEA,QAAI,CAAC,KAAK,SAAS,SAAS,EAAE,GAAG;AAChC,WAAK,SAAS,KAAK,EAAE;AACrB,WAAK,KAAK;AAAA,IACX;AAEA,WAAO,MAAM;AACZ,YAAM,QAAQ,KAAK,SAAS,QAAQ,EAAE;AACtC,UAAI,QAAQ,IAAI;AACf,aAAK,SAAS,OAAO,OAAO,CAAC;AAAA,MAC9B;AAAA,IACD;AAAA,EACD;AACD;AAGA,MAAM,mBAAmB,IAAI,aAAa,GAAG;AA+BtC,SAAS,YAAY,IAG1B;AACD,SAAO,iBAAiB,YAAY,EAAE;AACvC;AAgCO,SAAS,oBAAoB,IAA4B;AAC/D,SAAO,iBAAiB,oBAAoB,EAAE;AAC/C;",
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.3.0",
4
+ "version": "4.4.0-canary.15cff7ea86f8",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
package/src/index.ts CHANGED
@@ -91,7 +91,7 @@ export {
91
91
  setInSessionStorage,
92
92
  } from './lib/storage'
93
93
  export { stringEnum } from './lib/stringEnum'
94
- export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
94
+ export { FpsScheduler, fpsThrottle, throttleToNextFrame } from './lib/throttle'
95
95
  export { Timers } from './lib/timers'
96
96
  export {
97
97
  type Expand,