Source: utils/enterFrame.js

import { FPSCounter } from "./FPSCounter";

/**
 * Creates an enter frame loop that calls a callback function at a specified FPS limit
 * @param {function} callback - The function to call on each frame
 * @param {number} fpsLimit - The maximum FPS limit (0 for unlimited)
 * @returns {object} An object with methods to control the loop
 */
export const enterFrame = (callback, fpsLimit = 0) => {
  const FPS = FPSCounter();

  let updateFunction,
    maxFPS,
    maxMS,
    correctedMS,
    then = Date.now(),
    requestAnimationFrameId,
    isPlaying = false;

  const updateCallback = () => {
    FPS.update();
    callback(FPS.fps, FPS.delay);
  };

  const updateWithLimit = () => {
    const now = Date.now();
    const diff = now - then;
    if (diff >= correctedMS) {
      correctedMS = 2 * maxMS - diff;
      then = now;
      updateCallback();
    }
  };

  const render = () => {
    if (isPlaying) {
      updateFunction();
      requestAnimationFrameId = requestAnimationFrame(render);
    }
  };

  const scope = {
    /**
     * Returns whether the enter frame loop is currently playing
     * @returns {boolean} True if the loop is playing, false otherwise
     */
    isPlaying: () => isPlaying,

    /**
     * Clears the FPS limit, allowing the loop to run at unlimited FPS
     */
    clearMaxFPS: () => {
      maxFPS = 1 / 0;
      updateFunction = updateCallback;

      return scope;
    },

    /**
     * Returns the maximum FPS limit
     * @returns {number} The maximum FPS limit
     */
    getMaxFPS: () => maxFPS,

    /**
     * Sets the maximum FPS limit
     * @param {number} fpsLimit - The maximum FPS limit (0 for unlimited)
     */
    setMaxFPS: (fpsLimit) => {
      if (!fpsLimit || fpsLimit <= 0) return scope.clearMaxFPS();

      maxFPS = fpsLimit;
      correctedMS = maxMS = Math.floor(1000 / maxFPS) - 1;
      updateFunction = updateWithLimit;

      return scope;
    },

    /**
     * Starts the enter frame loop
     */
    start: () => {
      if (!isPlaying) {
        isPlaying = true;
        render();
      }

      return scope;
    },

    /**
     * Stops the enter frame loop
     */
    stop: () => {
      cancelAnimationFrame(requestAnimationFrameId);
      isPlaying = false;

      return scope;
    },
  };

  scope.setMaxFPS(fpsLimit).start();

  return scope;
};