Skip to content

Commit c059fc7

Browse files
single file module
1 parent e93a4bd commit c059fc7

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed

oscillation.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const MS_PER_FRAME = 1000 / 60;
2+
const SEC_PER_FRAME = 1 / 60;
3+
const MAX_SKIP_FRAME = 10;
4+
5+
// TODO resolve first frame issue (the first render in normal conditions suppose to start with initial state)
6+
7+
/**
8+
* @template {ArrayLike<number>} T
9+
* @param {T} start
10+
* @param {T} destination
11+
* @param {AbortSignal} signal
12+
* @param {(values: T) => void} callback
13+
*/
14+
export function motion(start, destination, signal, callback) {
15+
let current = Float64Array.from(start);
16+
let velocity = new Float64Array(current.length);
17+
let interpolated = current.slice();
18+
let config = spring(springs.noWobble);
19+
20+
let accumulatedMs = 0;
21+
/** @param {number} delta */
22+
function update(delta) {
23+
// check for accumulated time since we don't fully own the render cycle
24+
accumulatedMs += delta;
25+
26+
// if accumulated time is increasingly huge, probably the window was suspended
27+
// restarting the loop allows resuming animation when the window is active again
28+
if (accumulatedMs > MS_PER_FRAME * MAX_SKIP_FRAME) {
29+
accumulatedMs = 0;
30+
return false;
31+
}
32+
33+
// rendering cycle is not consistent and we need to take this into account
34+
while (accumulatedMs >= MS_PER_FRAME) {
35+
accumulatedMs -= MS_PER_FRAME;
36+
for (let i = 0, t; i < current.length; i++) {
37+
t = step(current[i], velocity[i], destination[i], config);
38+
current[i] = t[0];
39+
velocity[i] = t[1];
40+
}
41+
}
42+
43+
for (let i = 0, t, completion = accumulatedMs / MS_PER_FRAME; i < current.length; i++) {
44+
t = step(current[i], velocity[i], destination[i], config);
45+
interpolated[i] = current[i] + (t[0] - current[i]) * completion;
46+
}
47+
48+
return true;
49+
}
50+
51+
let lastTimestamp = performance.now();
52+
requestAnimationFrame(function loop(timestamp) {
53+
if (signal.aborted || complete(current, velocity, destination)) return;
54+
if (update(timestamp - lastTimestamp)) callback(interpolated);
55+
lastTimestamp = timestamp;
56+
requestAnimationFrame(loop);
57+
});
58+
}
59+
60+
function complete(x, vx, dx) {
61+
for (let i = 0; i < x.length; i++) {
62+
if (vx[i] !== 0 || x[i] !== dx[i]) return false;
63+
}
64+
return true;
65+
}
66+
67+
let springs = {
68+
noWobble: { damping: 0.997, frequency: 0.4818, precision: 0.01 },
69+
gentle: { damping: 0.639, frequency: 0.5735, precision: 0.01 },
70+
wobbly: { damping: 0.4472, frequency: 0.4683, precision: 0.01 },
71+
stiff: { damping: 0.69, frequency: 0.4335, precision: 0.01 },
72+
};
73+
74+
function spring(config = springs.noWobble) {
75+
let damping = (4 * Math.PI * config.damping) / config.frequency;
76+
let stiffness = ((2 * Math.PI) / config.frequency) ** 2;
77+
return { damping, stiffness, precision: config.precision };
78+
}
79+
80+
// step function is assumed to be returning a tuple which values are immediately extracted
81+
// given how often this function is called, saving some precious memory by reusing the same tuple
82+
// even though it doesn't use much memory, it is GC that we should be afraid of
83+
let tuple = new Float64Array(2);
84+
function step(x, v, destX, config) {
85+
// Spring stiffness, in kg / s^2
86+
87+
// for animations, destX is really spring length (spring at rest). initial
88+
// position is considered as the stretched/compressed position of a spring
89+
let Fspring = -config.stiffness * (x - destX);
90+
91+
// Damping, in kg / s
92+
let Fdamper = -config.damping * v;
93+
94+
// (Fspring + Fdamper) / mass
95+
let a = Fspring + Fdamper;
96+
97+
let newV = v + a * SEC_PER_FRAME;
98+
let newX = x + newV * SEC_PER_FRAME;
99+
100+
if (Math.abs(newV) < config.precision && Math.abs(newX - destX) < config.precision) {
101+
tuple[0] = destX;
102+
tuple[1] = 0;
103+
return tuple;
104+
}
105+
106+
tuple[0] = newX;
107+
tuple[1] = newV;
108+
return tuple;
109+
}

0 commit comments

Comments
 (0)