@tldraw/utils
Version:
tldraw infinite canvas SDK (private utilities).
8 lines (7 loc) • 6.08 kB
Source Map (JSON)
{
"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\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"],
"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;",
"names": []
}