motion icon indicating copy to clipboard operation
motion copied to clipboard

[FEATURE] Reduce frame rate via global setting

Open bockoblur opened this issue 7 months ago • 5 comments

Context

I need to reduce the frame rate for all animations to 30 or 25fps in order to have smoother animations on some low-end machines (namely Rapsberry Pi 3) which can't quite handle 60 or more fps at 1920×1080 resolution.

In animejs, for example, I can set the frameRate globally like this:

  import { engine } from "animejs";
  // ...
  engine.defaults.frameRate = 25;

Possible solutions

  • Ideally, global settings like in animejs, with explicit frame rate setting would be great.
  • Maybe simpler solution would be to have an option to drop every second frame, which would probably also do the trick of reducing the frame rate to ~30fps.

What I tried

I tried taking over the requestAnimationFrame function and dropping the frames if not enough time has passed since the last call, but it seems that motion uses some other mechanism for rendering since I did not see the difference. Or I am not getting this right. Code follows.

import { useEffect } from 'react';

const frameRateValues = [5, 10, 15, 30, 60] as const;

export type FrameRate = typeof frameRateValues[number];

const originalRAF = window.requestAnimationFrame;

export function useThrottleFrameRate(fps: FrameRate = 30) {
  useEffect(() => {
    if (!frameRateValues.includes(fps)) {
      console.warn("FPS must be one of " + frameRateValues.join(", "));
      return;
    }

    const interval = 1000 / fps; // Time per frame in milliseconds
    let lastTime = 0; // Track the last frame time

    if (fps >= 60) {
      // If FPS is 60 or higher, use the original requestAnimationFrame
      return;
    }

    // Replace requestAnimationFrame with a throttled version
    window.requestAnimationFrame = (callback) => {
      return originalRAF((time) => {
        if (time - lastTime >= interval) {
          lastTime = time; // Update the last frame time
          callback(time);
        } else {
          // Skip this frame
          window.requestAnimationFrame(callback);
        }
      });
    };

    // Restore the original requestAnimationFrame on cleanup
    return () => {
      //console.warn("Restored original RAF")
      window.requestAnimationFrame = originalRAF;
    };
  }, [fps]);
}

Any help or nudge in the right direction will be appriciated

bockoblur avatar Apr 17 '25 08:04 bockoblur

I'm using Motion for a world that exists at 15 fps 😂

Lots of character animations are done at this rate. It conjures up stop-motion vibes!

Our brand persona at Khan Aademy has animated character reactions (via Rive) at 15fps, but when it moves about the screen that’s done with Motion, and the silky smooth movement is at odds with the aesthetic.

How about a way to set the fps manually, both per animation and globally?

jonahgoldsaito avatar Apr 19 '25 14:04 jonahgoldsaito

I quite like this idea. I’ll have a think. Setting FPS that isn’t a fraction of the display can be a bit jerky and defeat the purpose somewhat. Likewise if it were “15fps” you’d still want the selected frames to be the exact same or it’ll look odd. So there’s a few things to consider.

mattgperry avatar Apr 19 '25 14:04 mattgperry

I quite like this idea. I’ll have a think. Setting FPS that isn’t a fraction of the display can be a bit jerky and defeat the purpose somewhat. Likewise if it were “15fps” you’d still want the selected frames to be the exact same or it’ll look odd. So there’s a few things to consider.

Thank you. 🤞🏻 As I wrote in the original post, I'm fine with fraction of the display rate (120/60/30/15) if there's no other way.

bockoblur avatar Apr 20 '25 06:04 bockoblur

@bockoblur did you tried to patch window.requestAnimationFrame before any motion code is loaded?

There is my monkey patch


const originalRequestAnimationFrame = window.requestAnimationFrame;

function createRequestAnimationFrame(fpsLimit: number) {
  const frameDuration = 1000 / fpsLimit;
  let lastCallTime = 0;

  function requestAnimationFrameWithLimit(
    callback: FrameRequestCallback
  ): number {
    const now = performance.now();

    if (now - lastCallTime >= frameDuration) {
      lastCallTime = now;
      callback(lastCallTime);
    }

    return originalRequestAnimationFrame(() =>
      requestAnimationFrameWithLimit(callback)
    );
  }

  return requestAnimationFrameWithLimit;
}

// Override original with new one
window.requestAnimationFrame = createRequestAnimationFrame(15);

const test: FrameRequestCallback = (time) => {
  console.log("hi", time);

  window.requestAnimationFrame(test);
};

window.requestAnimationFrame(test);

May be, the good idea is to add posibility to pass your own frameloop function, with you own implementation. It will give more control.

Profesor08 avatar Apr 20 '25 11:04 Profesor08

@bockoblur did you tried to patch window.requestAnimationFrame before any motion code is loaded?

@Profesor08 I just tried it and it works. Thanks! However, I still think that it would be better to have this as an option in the motion itself. I was never a big fan of these monkey patches... 😉

May be, the good idea is to add posibility to pass your own frameloop function, with you own implementation. It will give more control.

That seems like a great idea for advanced use cases. But for simplicity, global setting would be more than enough for my use case.

Maybe something like adding the property to MotionGlobalConfig

export const MotionGlobalConfig: {
    skipAnimations: boolean
    useManualTiming: boolean
    WillChange?: any
    frameRate?: number  // adding an optional prop here would not be a breaking change
} = {
    skipAnimations: false,
    useManualTiming: false,
   // if frameRate is not defined use default, otherwise apply the logic to skip frames.
}

bockoblur avatar Apr 20 '25 19:04 bockoblur