Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Erratic spring behaviour #7010

Open
robertadamsonsmith opened this issue Dec 13, 2021 · 3 comments
Open

Erratic spring behaviour #7010

robertadamsonsmith opened this issue Dec 13, 2021 · 3 comments

Comments

@robertadamsonsmith
Copy link

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:

  • The computer is under heavy load, and unable to process ticks in a timely fashion
  • The svelte app is itself running heavy calculations or a garbage collection cycle is occurring which temporarily blocks the thread
  • The user switches tab while a spring is running, and then returns to the tab at some point

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
Copy link

dit7ya commented Dec 1, 2023

Is there any workaround to this?

@JuchokJuk
Copy link

JuchokJuk commented Apr 25, 2024

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
Copy link

JuchokJuk commented Apr 27, 2024

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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants