sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.06k stars 4.08k forks source link

Erratic spring behaviour #7010

Open robertadamsonsmith opened 2 years ago

robertadamsonsmith commented 2 years ago

Describe the bug

If the time between frames is high (i.e. more than a small fraction of a second), it can cause springs to rapidly jump to unexpectedly large or small values. This creates a poor user experience, and can cause error conditions (such as when it wasn't anticipated that an overdamped spring could overshoot the target.)

The time between frames can increase for three main reasons:

I've experienced all 3 cases, and this effects all uses of springs to varying extents.

(I will submit a pull request separately that is intended to fix this)

Reproduction

https://svelte.dev/repl/8a52664675434394899f3e58d8f542a3?version=3.44.2

Logs

No response

System Info

System:
    OS: Windows 10 10.0.19042
    CPU: (4) x64 Intel(R) Core(TM) i5-4670K CPU @ 3.40GHz
    Memory: 6.92 GB / 15.93 GB
  Binaries:
    Node: 16.0.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.13.0 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 7.10.0 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (96.0.1054.53)
    Internet Explorer: 11.0.19041.1202
  npmPackages:
    rollup: ^2.3.4 => 2.61.0
    svelte: C:/Projects/svelte => 3.44.2

Severity

annoyance

dit7ya commented 9 months ago

Is there any workaround to this?

JuchokJuk commented 4 months ago

The bug is caused by too long deltaTime between frames of the requestAnimationFrame, the calculation of the spring physics is not intended for such a long “prediction”. Since requestAnimationFrame does not call a callback while the tab with the page is invisible deltaTime becomes too big. Limiting deltaTime to a maximum of 1/24 of a second will help here, or taking into account the time that the tab was closed using the visibility API, for example

109 line of svelte/packages/src/motion/spring.js

dt: ((now - last_time) * 60) / 1000

could be changed to

dt: ((Math.min(now - last_time, 1000 / 24)) * 60) / 1000
JuchokJuk commented 4 months ago

Is there any workaround to this?

Recently, I wrote a similar function to svelte/spring. I needed a faster version of the spring, so I removed support for objects and arrays, and also fixed a bug with excessively large delta time.

import { writable } from 'svelte/store';

export function spring(value: number, { stiffness = 0.15, damping = 0.8, precision = 0.01 }) {
    const store = writable(value);

    let lastTime: number;

    let lastValue = value;
    let currentValue = value;
    let targetValue = value;

    let running = false;

    function set(newValue: number) {
        targetValue = newValue;
        if (!running) {
            running = true;
            lastTime = performance.now();
            requestAnimationFrame(loop);
        }
    }

    function loop() {
        const currentTime = performance.now();
        const deltaTime = Math.min(currentTime - lastTime, 42) * 0.06;

        const delta = targetValue - currentValue;
        const velocity = (currentValue - lastValue) / deltaTime;
        const spring = stiffness * delta;
        const damper = damping * velocity;
        const acceleration = spring - damper;
        const d = (velocity + acceleration) * deltaTime;

        lastValue = currentValue;

        if (Math.abs(d) < precision && Math.abs(delta) < precision) {
            store.set((currentValue = targetValue));
            running = false;
        } else {
            store.set((currentValue = currentValue + d));
            lastTime = currentTime;
            requestAnimationFrame(loop);
        }
    }

    return {
        set,
        subscribe: store.subscribe
    };
}

Currently, the function does not support SSR. To add support, you need to replace requestAnimationFrame with raf as follows:

const is_client = typeof window !== 'undefined';
function noop() {}
const raf = is_client ? (cb) => requestAnimationFrame(cb) : noop;
abegehr commented 3 weeks ago

I also ran into this. Anyone found a way to solve it while keeping svelte/motion's spring function?