Tempus Animation Frame Manager

repository·main·Indexed 43 days ago

https://github.com/darkroomengineering/tempus

A lightweight, high-performance animation frame manager for JavaScript that consolidates multiple requestAnimationFrame calls into a single shared loop. Version 1.0.0 provides tools for frame budget management, execution ordering, and profiling. It includes a React integration via the useTempus hook and ReactTempus component, a profiler overlay for visualizing frame budget, and the ability to patch the native requestAnimationFrame API to route third-party library loops through the Tempus engine.

Tokens
5.8K
Snippets
25
Records
32
Agent score
55%

What's inside Tempus

  1. Manage frame budget with state.budget()

    main

    To prevent blocking the main thread, use state.budget() to gate expensive or optional work. This is the live equivalent of requestIdleCallback's timeRemaining(). You can use it to skip work entirely if the frame is full, or to process work in chunks until the budget is exhausted.

    Note: The budget is calculated based on Tempus.targetFps (default 60).

    // Option 1: Run only if there is spare time
    Tempus.add(({ budget }) => {
      if (budget() > 0) doExpensiveWork()
    })
    
    // Option 2: Process work in chunks until budget runs out
    Tempus.add((state) => {
      while (state.budget() > 0) {
        doChunkOfWork()
      }
    })
  2. Patch requestAnimationFrame with ReactTempus

    main

    The ReactTempus component manages the global patching of the native requestAnimationFrame API. When mounted, it calls Tempus.patch(), routing all native animation frame requests through the Tempus loop. When unmounted, it calls Tempus.unpatch().

    To ensure consistent timing across your application, render <ReactTempus /> once near the root of your component tree. By default, patching is enabled (patch={true}). To prevent patching, pass patch={false}.

    import { ReactTempus } from 'tempus/react'
    
    // patch defaults to true; pass patch={false} to disable
    <ReactTempus patch />
  3. Integrate Tempus with GSAP, Lenis, or Three.js

    main

    Tempus is designed to be library-friendly. You can replace the internal loops of other animation tools with the Tempus loop.

    GSAP Integration: Remove GSAP's internal ticker and wrap its update in Tempus.add().

    Lenis Integration: Pass the time from the Tempus state to lenis.raf().

    Three.js Integration: Use the order option to ensure rendering happens after other logic (like scroll or physics).

    // GSAP
    gsap.ticker.remove(gsap.updateRoot)
    Tempus.add(({ time }) => {
      gsap.updateRoot(time / 1000)
    })
    
    // Lenis
    Tempus.add(({ time }) => lenis.raf(time))
    
    // Three.js
    Tempus.add(() => {
      renderer.render(scene, camera)
    }, { order: 1 })
  4. Use the TempusState budget() to optimize performance

    main

    Every callback registered with Tempus.add() receives a TempusState object. This object includes a budget() method that returns the remaining time (in milliseconds) available in the current frame before the next tick, based on a default targetFps of 60.

    Developers can use this to implement 'adaptive' logic: if the budget is low, skip expensive computations or reduce visual fidelity to maintain a smooth frame rate.

    import { Tempus } from 'tempus';
    
    Tempus.add((state) => {
      if (state.budget() > 5) {
        // Only perform expensive work if we have more than 5ms left in the frame
        performExpensiveCalculation();
      } else {
        // Skip or do a lightweight version
        performLightweightUpdate();
      }
    }, { label: 'adaptive-loop' });
  5. Use the Tempus profiler overlay

    main

    The tempus/profiler package provides a live, draggable UI overlay that visualizes how callbacks fill the frame budget. It shows a timeline of all Tempus.add() and Tempus.patch() callbacks.

    Installation: import { profiler } from 'tempus/profiler'

    Options:

    • corner: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' (default: 'top-left') - Pinning position.
    • fps: number (default: 5) - The refresh rate of the overlay itself.
    • container: HTMLElement (default: document.body) - The mount target.

    Note: The profiler is SSR-safe; it returns a no-op handle on the server.

    import { profiler } from 'tempus/profiler'
    
    const overlay = profiler({
      corner: 'top-left',
      fps: 5,
      container: document.body,
    })
    
    // To remove the overlay:
    overlay.destroy()
  6. Add an animation callback with Tempus.add()

    main

    Use Tempus.add(callback, options) to register a function to the shared animation loop. Every callback receives a state object containing timing and budget information.

    Callback State Object:

    • time: number - Elapsed time in ms since the loop started.
    • deltaTime: number - Time in ms since this callback's previous run.
    • frame: number - Current frame counter.
    • budget: () => number - Returns the milliseconds remaining in the current frame before exceeding the budget (calculated via 1000 / Tempus.targetFps).

    Options:

    • order: number (default: 0) - Execution order. Lower values run earlier (e.g., -1 runs before 0).
    • fps: number | string (default: Infinity) - Target frame rate. Use a number for absolute FPS or a string like '50%' for a fraction of the system refresh rate.
    • label: string - An optional name for identification in the profiler or Tempus.inspect().

    Returns: An unsubscribe function that, when called, removes the callback from the loop.

    import Tempus from "tempus"
    
    function animate({ time, deltaTime, frame, budget }) {
      console.log('frame', time, deltaTime)
    }
    
    // Add callback and get unsubscribe function
    const unsubscribe = Tempus.add(animate)
    
    // Later, to stop animating:
    unsubscribe()
  7. Configure useTempus options

    main

    The useTempus hook accepts an options object as its second argument. These options are forwarded directly to the core Tempus.add method.

    Available options include:

    • fps: Sets the target frame rate (e.g., 30 or a relative rate like '50%').
    • order: Determines execution priority (lower numbers run earlier in the loop).
    • label: A string identifier used for profiling in tempus/profiler and Tempus.inspect().
    useTempus(() => {}, {
      fps: 30,            // Will run at 30 FPS (or a relative rate like '50%')
      order: -1,          // Will run before other animations (lower runs first)
      label: 'my-effect', // Shown in tempus/profiler and Tempus.inspect()
    })
  8. Control loop playback

    main

    You can control the entire shared animation loop using the following methods:

    • Tempus.pause(): Stops the loop; no callbacks will be called.
    • Tempus.play(): Resumes the loop.
    • Tempus.restart(): Resets the elapsed time clock to 0 and resumes the loop.
    • Tempus.isPlaying: A boolean indicating if the loop is currently running.
    Tempus.pause()
    Tempus.play()
    Tempus.restart()
  9. Use the useTempus hook in React

    main

    The useTempus hook registers a callback function into the Tempus loop for the entire lifetime of the component. The callback is automatically removed when the component unmounts.

    The callback receives a single state object containing:

    • time: The current elapsed time.
    • deltaTime: The time elapsed since the last frame.
    • frame: The current frame index.
    • budget: The remaining time in the current frame budget.

    Because the hook always uses the latest callback without re-subscribing, you can safely use component state or props inside the callback without needing to wrap it in useCallback.

    import { useTempus } from 'tempus/react'
    
    function App() {
      useTempus(({ time, deltaTime, frame, budget }) => {
        console.log('frame', time, deltaTime, frame)
      })
    }
  10. Patch native requestAnimationFrame with Tempus.patch()

    main

    Calling Tempus.patch() intercepts all native requestAnimationFrame calls (including those from third-party libraries) and routes them into the single shared Tempus loop. This reduces per-frame overhead and allows Tempus to manage the execution order of all animations.

    Use Tempus.unpatch() to restore the original native requestAnimationFrame and cancelAnimationFrame behavior.

    // Absorb all native rAF calls into Tempus
    Tempus.patch()
    
    // Restore original behavior
    Tempus.unpatch()
  11. Inspect callback timing with Tempus.inspect()

    main

    Use Tempus.inspect() to get a normalized timing snapshot of every active callback (both those added via add() and those absorbed via patch()). This is the data source used by the profiler overlay.

    const snapshot = Tempus.inspect()
    // Returns: [{ label, samples, order, fps, source: 'add' | 'patch' }, ...]