I'm trying to calculate the decay or velocity in a requestFrame
loop based on the fact that I know the velocity (v) and the distance I want to travel. I also know the ms
for each frame.
So a simple decay algorithm is:
velocity *= 0.9
This slows down smooth and nicely, but I want the animated element to stop at a given position (snap to grid). So how can I calculate the deceleration accurately?
The deceleration will be computed by dividing the final velocity minus the initial velocity, by the amount of time is taken for this drop in velocity.
The deceleration will be computed by dividing the final velocity minus the initial velocity, by the amount of time is taken for this drop in velocity. The formula for acceleration can be used here, with a negative sign, to identify the deceleration value. The Formula for Deceleration It is computed as:
U = initial velocity, t = time taken, s = distance covered. Deceleration Formula is used to calculate the deceleration of the given body in motion. It is expressed in meter per second square (m/s 2).
Deceleration occurs when acceleration has an opposite direction as the object's velocity. In physics, deceleration definition always refers to the slowing down of an object, which means that the magnitude of the velocity decreases. Deceleration is characterized by opposite directions between the acceleration and the velocity.
The acceleration calculator is based on three various acceleration equations, where the third is derived from Newton's work: 1 a = (v_f - v_i) / Δt, 2 a = 2 * (Δd - v_i * Δt) / Δt², 3 a = F / m,
I have to admit it is not clear if in your scenario there are 1, 2 or 3 dimensions, I'll speak about a linear movement; remember that in a multidimensional environment velocity, acceleration and distance are vectors.
I would use the uniform acceleration formulas:
S = v0 * t + 1/2 * a * t ^ 2
vt = v0 + a * t
From your question it seems that the acceleration and the time should be the outputs of the problem.
Last thing not clear in your question is that before you stated "I know the distance I want to travel" later you need the movement end on a grid, these two sentences seems to be in contrast... I would handle this rounding the ending position at the beginning of the computation process.
Now our problem has following inputs:
Let's start expressing the time in function of the acceleration (2nd formula)
t = (vt - v0) / a
vt = 0, so
t = - v0 / a
let's substitute this in the 1st formula
S = - v0 ^ 2 / a + 1/2 * a (- v0 / a) ^ 2 = - (v0 ^ 2) / (2 * a)
from here we can find the acceleration
a = - (v0 ^ 2) / (2 * S)
and from the 2nd formula the time
t = - v0 / a
As we said, at the beginning of the process, we need to round the distance to a position snap to the grid:
rD = roundDistanceToGrid(D);
a = - velocity * velocity / 2 / rD;
t = - velocity / a;
t
will not be an integer multiplier of dt
from now on, until the time elapsed is lesser than t
, at each frame just
velocity += a * dt;
on the first frame after the time has elapsed, to fix errors due to rounding, set velocity at zero and place the object exactly on the grid.
For decay coefficient q
and n
steps (elementary time intervals) distance is sum of geometric progression
D = v0 * (1 - q ** n) / (1 - q)
We cand find q
for given D
, v0
and n
(is the latter known?) with simple numerical methods.
Also note that velocity never becomes zero, so you perhaps have to use some threshold value to stop. If velocity diminishes linearly (constant deceleration) rather than exponentially, then things are simpler.
Short answer:
d = 99 // Distance
v = 11 // Velocity
// Negative acceleration is deceleration:
acceleration = -0.5 * v * v / (d - 0.5 * v)
Derivation:
Start with equation for motion with constant acceleration:
s1 = s0 + v0 + a*t*t/2
and equation for acceleration:
a = dv/dt (change in velocity over change in time)
and solve for a
:
We know dv = -v0
because the final velocity is 0.
So t = dt = -v0/a
Plug t
into the first equation and solve for a
to get:
a = -0.5 * v0*v0 / (s1 - s0)
s1 - s0
is simply the distance traveled d
. For some reason I had to subtract half the velocity from d
to get the correct results....
Proof by simulation: You can try entering different velocities and distances into the simulation below.
requestFrame
will result relatively large time steps.function run() {
console.log('Simulating:')
d = getNumber('d') // Distance
v = getNumber('v') // Velocity
p = 0 // Position
a = -0.5 * v * v / (d - 0.5 * v) // Acceleration
log = [{p, v, a}]
while (v > 0) {
p = p + v;
d = d - p;
v = v + a;
data = {p, v, a};
console.log(data) // For StackOverflow console
log.push(data)
}
console.table(log); // For browser dev console
}
function getNumber(id) {
return Number(document.getElementById(id).value)
}
<div>Distance <input id=d value=10 /></div>
<div>Velocity <input id=v value=1 /></div>
<div><button onclick='run()'>Run Simulation (Open dev console first to get full data in a nicely formatted table)</button></div>
Short answer: a = e**(-v0*dt/d)
, where d
is your distance, a
the decay constant, dt
the time per frame and v0
the initial velocity.
Why?
The algorithm you gave is an exponential decay. If you want to do it this way, you cant use the uniform acceleration equations in this answer.
The implicit formulation v[n] = v[n-1] * a
(e.g. with a=0.9 and v[0] = 1.0) at every frame n
can be written explicitly as as v = v0*a**(n)
. Or better in terms of the time t
as v = v0*a**(t/dt)
, where dt = 1/fps
(frames per second) and t = n*dt
(n = 1, 2, 3, ....
).
Note: this will never be 0! However, the traveling object still travels a finite distance.
The distance traveled d
is the integral of that function: d = v0*dt * a**t / ln(a)
. After some time, the object will be close to -v0*dt/ln(a)
. Solving for a
gives the result above.
Note: this is an analytic result, your numeric result will be somewhere close to that.
This is much more of a software-engineering problem than a math/physics problem. The math/physics is quite trivial. The difficult thing here is handling the varying frame/tick rate of the browser. Math/physics will not apply too practically for a problem advanced by discrete time steps of varying durations.
Here is some code which solves the problem; note you can click "destabilize" to watch it work under very unstable frame/tick rates (you'll see in the implementation that this simulation of lag is an honest one!)
Ideally hit the "full page" button:
let elem = document.querySelector('.model');
let rangeElem = document.querySelector('.range');
let fpsElem = document.querySelector('.fps');
let destabilizeElem = document.querySelector('.destabilize');
destabilizeElem.addEventListener('click', evt => {
destabilizeElem.classList.toggle('active');
evt.stopPropagation();
evt.preventDefault();
});
let model = {
pos: [ 0, 0 ],
vel: [ 0, 0 ],
startPos: [ 0, 0 ],
range: 100
};
let reset = ({ startMs, range, vel, ang=0 }) => {
// Start again with `range` representing how far the model
// should travel and `vel` representing its initial speed.
// We will calculate `velMult` to be a value multiplied
// against `vel` each frame, such that the model will
// asymptotically reach a distance of `range`
let [ velX, velY ] = [ Math.sin(ang) * vel, Math.cos(ang) * vel ];
// Note the box-shadow on `rangeElem` is 2px wide, so to
// see the exact range it represents we should subtract
// half that amount. This way the middle of the border
// truly represents a distance of `range`!
rangeElem.style.width = rangeElem.style.height = `${(range - 1) << 1}px`;
rangeElem.style.marginLeft = rangeElem.style.marginTop = `-${range - 1}px`;
elem.transform = 'translate(0, 0)';
model.pos = [ 0, 0 ];
model.vel = [ velX, velY ];
model.startPos = [ 0, 0 ];
model.range = range;
};
let ms = performance.now();
let frame = () => {
let prevFrame = ms;
let dms = (ms = performance.now()) - prevFrame;
let dt = dms * 0.001;
elem.style.transform = `translate(${model.pos[0]}px, ${model.pos[1]}px)`;
// Now `velMult` is different every frame:
let velMag = Math.hypot(...model.vel);
let dx = model.pos[0] - model.startPos[0];
let dy = model.pos[1] - model.startPos[1];
let rangeRemaining = model.range - Math.hypot(dx, dy);
let velMult = 1 - Math.max(0, Math.min(1, dt * velMag / rangeRemaining));
model.pos[0] += model.vel[0] * dt;
model.pos[1] += model.vel[1] * dt;
model.vel[0] *= velMult;
model.vel[1] *= velMult;
fpsElem.textContent = `dms: ${dms.toFixed(2)}`;
// Reset once the velocity has multiplied nearly to 0
if (velMag < 0.05) {
reset({
startMs: ms,
// Note that without `Math.round` results will be *visually* inaccurate
// This is simply a result of css truncating floats in some cases
range: Math.round(50 + Math.random() * 300),
vel: 600 + Math.random() * 1200,
ang: Math.random() * 2 * Math.PI
});
}
};
(async () => {
while (true) {
await new Promise(r => window.requestAnimationFrame(r));
if (destabilizeElem.classList.contains('active')) {
await new Promise(r => setTimeout(r, Math.round(Math.random() * 100)));
}
frame();
}
})();
html, body {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
overflow: hidden;
}
.origin {
position: absolute;
overflow: visible;
left: 50%; top: 50%;
}
.model {
position: absolute;
width: 30px; height: 30px;
margin-left: -15px; margin-top: -15px;
border-radius: 100%;
box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.8);
}
.model::before {
content: ''; position: absolute; display: block;
left: 50%; top: 50%;
width: 4px; height: 4px; margin-left: -2px; margin-top: -2px;
border-radius: 100%;
box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.8);
}
.range {
position: absolute;
width: 100px; height: 100px;
margin-left: -50px; margin-top: -50px;
border-radius: 100%;
box-shadow: 0 0 0 2px rgba(200, 0, 0, 0.5);
}
.fps {
position: absolute;
right: 0; bottom: 0;
height: 20px; line-height: 20px;
white-space: nowrap; overflow: hidden;
padding: 10px;
font-family: monospace;
background-color: rgba(0, 0, 0, 0.1);
}
.destabilize {
position: absolute;
right: 0; bottom: 45px;
height: 20px; line-height: 20px;
white-space: nowrap; overflow: hidden;
padding: 10px;
font-family: monospace;
box-shadow: inset 0 0 0 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.destabilize.active { box-shadow: inset 0 0 0 4px rgba(255, 130, 0, 0.9); }
<div class="origin">
<div class="model"></div>
<div class="range"></div>
</div>
<div class="destabilize">Destabilize</div>
<div class="fps"></div>
The trick here is to adapt the braking in realtime to the framerate.
In a discrete model where after every secsPerStep
a position
increments by a velocity
, the velocity
then multiplies by some brakingFactor
, and there is some target distance
to achieve, we know that:
brakingFactor = 1 - constantSecsPerStep * initialVelocity / distance
This of course only works if constantSecsPerStep
is always constant. For a varying secsPerStep
I used this formula instead:
updatedBrakingFactor = 1 - durationOfCurrentTick * currentVelocity / remainingDistance
It sounds like you wanted what I will call a "pure" solution, where there is no explicit "agenda" that establishes the location to which the decelerating object will snap (no data such as "intended destination" should exist). Unfortunately, I claim there must be at least some data which establishes this agenda, and that the model is not undergoing some arbitrary movement. The updatedBrakingFactor
formula requires knowledge of the remainingDistance
, not the initial distance. There will need to be data to derive this (in the code I decided to store the "start position" of the model, but the "start time" could also be used).
Note that mathematically, the velocity of the model never quite becomes 0
- therefore a heuristic is needed to approximate when the model has "arrived". I opted to wait for the instantaneous velocity to fall under some small threshold.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With