svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Erratic spring behaviour

Open robertadamsonsmith opened this issue 3 years ago • 4 comments

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

robertadamsonsmith avatar Dec 13 '21 00:12 robertadamsonsmith

Is there any workaround to this?

dit7ya avatar Dec 01 '23 14:12 dit7ya

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 avatar Apr 25 '24 18:04 JuchokJuk

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;

JuchokJuk avatar Apr 27 '24 12:04 JuchokJuk

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

abegehr avatar Aug 05 '24 11:08 abegehr