UNPKG

@tldraw/utils

Version:

tldraw infinite canvas SDK (private utilities).

8 lines (7 loc) 9.75 kB
{ "version": 3, "sources": ["../../src/lib/throttle.ts"], "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"], "mappings": "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", "names": [] }