Splidejs / splide-extension-auto-scroll

The Splide extension for continuously scrolling the slider.
https://splidejs.com/
MIT License
24 stars 40 forks source link

Stabilizing speed across devices with different framerates #10

Open axis80 opened 1 year ago

axis80 commented 1 year ago

Checks

Version

0.5.3

Description

Currently the speed of the auto-scroll extension is specified in pixel/frame units. Unfortunately, because different devices can operate at different framerates, this can result in the slider moving at different speeds on different devices.

For example, take the demo on the Splide web site at https://splidejs.com/extensions/auto-scroll/#speed. That displays at one speed on my desktop PC (which is 60fps) and a different speed on my Google Pixel 6 (which is 90fps, but can dynamically drop lower to save battery). It does appear that it's about 50% faster on my phone than on my PC. I can send you a video if you like.

I spent many hours yesterday trying to solve this by monitoring the current framerate with fps-observer and changing the Autoscroll speed option on the fly. Unfortunately the results were not very good.

So, now I am looking at modifying the autoscroll extension in some way, to stabilize the speed across devices with different framerates. I don't have any complete solutions in mind but I have some ideas. (calculating fps by measuring the time between RequestAnimationFrame calls, averaging that over time, then using that average value to modulate the speed of the slider so it matches the speed set in the main Splide config.)

First, though, I wanted to check in with you. Does my explanation make sense? Do you think this is worth solving? Do you have any preferred/suggested way of solving it? I can fumble my way through it and submit a PR, but if you have some more elegant way of approaching it, then I would love to hear your thoughts.

Thanks!

Reproduction Link

No response

Steps to Reproduce

  1. View the autoscroll demo at https://splidejs.com/extensions/auto-scroll/#speed on a device that is 60fps
  2. View the autoscroll demo at https://splidejs.com/extensions/auto-scroll/#speed on a device that is 90fps
  3. Note that the two sliders are moving at different speeds.

Expected Behaviour

I expect to see the two sliders moving at the same speed. It is, I believe, reasonable for a designer to want the user experience to have that level of consistency across multiple devices.

axis80 commented 1 year ago

Someone else had previously left a comment suggesting the use of delta timing - i.e. measuring the time delta (difference) between the current and previous frame and then multiplying that by the desired speed in pixels per second. That comment is no longer available for some reason, but the suggestion made sense to me and I am now planning to implement it that way.

I plan to modify this extension so that it honors the speed parameter in the main Splide config. There is, however, a speed parameter on this extension's config as well, and I want to preserve compatibility with existing installations. So, I'll introduce a third parameter called speedMode which can be set to "main" or "extension" indicating which speed variable is to be honored. By default this will be set to "extension" so that existing installations will continue to function the same even after updating to a new version.

If you ever want to eliminate the speed parameter on the extension and instead always honor the one in the main Splide config, you could choose to mark the one on the extension as deprecated and make that transition.

Stop me if I am handling this in a way that you don't like. Otherwise I'll send a PR in the next week or so.

axis80 commented 1 year ago

I've forked this repo and have been working on a pull request to fix the issue described above.

A demo of my code is here: https://codepen.io/axis80/pen/XWxVjxK

It's working great on my desktop and the timing is perfect. If I set the speed to 5000ms in the main Splide config object, then the slider travels the distance of one slide in exactly 5000ms.

Unfortunately it is once again displaying at a different speed on my Google Pixel 6 phone. It's going much slower, and I can't quite figure out why. Here is the code in question - perhaps someone else can tell me what's wrong with this:

  /**
   * Returns the position to move.
   *
   * @param position - The current position.
   *
   * @return A computed destination.
   */
  function computeDestination( position: number ): number {

    let pixelsToMoveThisFrame = 0;
    if ( autoScrollOptions.speedMode === 'main' && lastRenderTimestamp) {
      const elapsedTime = performance.now() - lastRenderTimestamp;
      const pixelsPerSecond = slideSize( null, null ) / ( options.speed / 1000 );
      pixelsToMoveThisFrame = pixelsPerSecond * ( elapsedTime / 1000 );
    } else {
      pixelsToMoveThisFrame = autoScrollOptions.speed || 1;
    }
    position += orient( pixelsToMoveThisFrame );

    if ( Splide.is( SLIDE ) ) {
      position = clamp( position, getLimit( false ), getLimit( true ) );
    }

    return position;
  }

The only thing I could think of is that the Pixel 6 has a higher pixel density ratio, in addition to having a higher framerate. But, that shouldn't come into play because AFAIK the code above is dealing with virtual/CSS pixels and not physical ones. Right?

If anyone happens across this and has any suggestions on how to fix it, I'm all ears. I'll keep working on it myself, but right now I am stumped and don't really know what to try next.

aniplayIt commented 1 year ago

Having the same issue, comparing a macbook to a windows pc at 144fps the autoscroll is going really fast. As a test I tried to set a dynamic speed based on the browser window fps. This is working but since the fps is not stable until a couple of seconds the initial speed is terribly fast. Giving some values on an iPhone I should have a speed of 1.3 and on a 144fps 0.4. I think this should be fixed, adapting the speed to the window fps is too flaky.

PS. I'm using it in svelte

const times = [];
let speed;
let loops = 250;

function refreshLoop() {
  window.requestAnimationFrame(() => {
      if (loops > 0) {
          const now = performance.now();
          while (times.length > 0 && times[0] <= now - 1000) {
              times.shift();
          }
          times.push(now);
          speed = 60 / times.length;
          loops--;
          refreshLoop();
      } else {
          slider.splide.Components.AutoScroll.play();
          window.requestAnimationFrame(() => null);
      }
  });
}

...,
autoScroll: {
    speed,
    pauseOnFocus: false
},
...
axis80 commented 1 year ago

@aniplayIt I'm sorry to hear that you are also having trouble with this, but it's nice to know that I'm not crazy.

I had set this aside for a little while but I would still like to find a solution for it. I am sick and wacked out on NyQuil at the moment, so I will have to write back in more detail once I'm feeling better.

If I'm reading your code correctly, it waits for 250 animation frames to complete, returns the average fps over the last 1 second, and then starts Splide using that calculated speed. Is that correct? My code skills are not at their best right now.

aniplayIt commented 1 year ago

Yes but this is just a test, it cannot be considered a solution. The problem there was that devices tend to have an increasing fps value during the first seconds of the page load, so it needs to be "stable" to set the correct slider speed. However, this could also lead to wrong framerate because it's not a fixed value during time.

axis80 commented 1 year ago

I've made some progress in figuring out why my code wasn't working. The browser FPS is apparently being calculated incorrectly. On my desktop PC it correctly reports 60fps, but on my Pixel 7 mobile it is reporting 132fps, which exceeds the device's maximum of 90fps. That is why it's running more slowly on my phone - because it thinks it has a massively high framerate. Next step is to debug why it's happening and fix it.

axis80 commented 1 year ago

I cannot figure out why it is calculating the framerate as 132fps on my mobile device (Chrome for Android on Pixel 7).

Here's a simple FPS test that runs without Splide: https://fpstest.axis80.com/. It is correctly reporting the fps as 90fps. If you view the source code on that page you can see how it is making the fps calculation.

When I insert a similar calculation into the AutoScroll.ts move() function (which supposedly gets called once for every animation frame) it reports the framerate as 132fps (and sometimes as high as 140fps). I traced it back to RequestInterval and I don't see anything wrong. But, obviously something is wrong or it wouldn't be reporting the fps as 132.

aniplayIt commented 1 year ago

Honestly I don't get why the framerate is the basis from where decide the slider speed. On other sliders it is simply managed with a timed transition. Can you tell me more details about why the implementation had been decided to be fps based?

axis80 commented 1 year ago

@aniplayIt This autoscroll extension currently requires the slider speed to be specified in pixels per frame. Because different devices have different framerates, this means that the slider ends up scrolling at different speeds on different devices. On a device with a framerate of 90fps the slide will move 1.5x faster than a device with 60fps.

Instead of specifying the speed in pixels/frame, I would like to specify it in either pixels/second or seconds per slide. That way the slider will display at the same speed on all devices. In order to do the necessary calculations, we need to first know the current device fps. Once we know that we can convert between the different speed settings and equalize the speed on all devices. fps is just an intermediate value used in calculations.

By timed transition, do you mean that other sliders allow you to set the slide transition time to, say, 2000ms for example? If so, then that is how the main Splide slider works, but not this auto-scroll extension. I am trying to make this auto-scroll extension work the same way as the main Splide slider.

axis80 commented 1 year ago

I just spent some more time working on this and didn't have any luck. At this point I am looking to hire someone to fix this. I have tried reaching out to @NaotoshiFujita a couple times but have not gotten a response. If anyone reading this has the technical skill to fix this, please PM me. In the meantime I'll reach out to some of my contacts.

axis80 commented 1 year ago

I wanted to follow up with a few notes on the pull request that was just submitted by @dmitriynazaratiy. He was able to implement a solution that makes the scroll speed consistent across devices with different framerates, though it looks quite a bit different than what I described above.

He pointed out that it is basically not possible for the extension to honor the speed parameter from the main Splide config. That parameter refers to the transition time between each slide. That is fine when moving by one slide (or one page) at a time. The problem with using that in continuous scroll mode is that Splide allows for slides to have different widths. So, it's really not possible to translate that "speed of each transition" setting into a continuous scroll speed in any meaningful way. (And this probably explains why @NaotoshiFujita initially implemented the speed setting the way he did.)

So, what @dmitriynazaratiy came up with is an fpsLock setting that ties the extension's speed parameter to a particular framerate. If the device's framerate differs from the fpsLock rate, the library will now adjust the speed accordingly. The result is that you can view two identical sliders side-by-side on devices with different framerates and they will run at the same speed.

He also introduced some internal calculations in the extension that use the concept of a virtual viewport. This stabilizes animation speeds across varying screen and slider sizes. It counteracts the issue of animations being tied to pixels per frame, which previously caused perceived speed inconsistencies on different screens or slider sizes.

For now I can use his branch in my project, but I hope that the Pull Request can be accepted so it will become a permanent part of the AutoScroll extension.

Lawlight02 commented 2 weeks ago

Hi, I've resolved the issue by modifying the update() function in splide-extension-auto-scroll.js.

var fps = 120;
var lastDelta = 0;

function update(deltaTime) {
  if (!paused) {
    if (deltaTime - lastDelta > 1000 / fps) {
      lastDelta = deltaTime;
      rate = interval ? min$1((now() - startTime) / interval, 1) : 1;
      onUpdate && onUpdate(rate);

      if (rate >= 1) {
        onInterval();
        startTime = now();

        if (limit && ++count >= limit) {
          return pause();
        }
      }
    }

    raf(update);
  }
}

Hope it helps!