David Hellsing
David Hellsing

Reputation: 108520

Calculate deceleration based on velocity and grid

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?

Upvotes: 9

Views: 1118

Answers (6)

Gershom Maes
Gershom Maes

Reputation: 8178

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.

Upvotes: 2

MBo
MBo

Reputation: 80297

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.

Upvotes: 4

Daniele Ricci
Daniele Ricci

Reputation: 15837

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:

  1. D (distance), known
  2. v0 (initial velocity), known
  3. vt (ending velocity), know: 0
  4. dt (delta time), known: the time between two consecutive frames, expressed in seconds (not in milliseconds)

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.

Upvotes: 4

lebowski
lebowski

Reputation: 91

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.

Upvotes: 2

AbdelAziz AbdelLatef
AbdelAziz AbdelLatef

Reputation: 3744

You can use the formula you suggested velocity *= r to achieve your goal. However, it will require - theoretically - infinity time for your object to travel the distance d you want, as the velocity will never actually reach zero using successive multiplication. Practically, it will reach zero after it reaches the lowest value that your value can consider greater than zero, but it will take a lot of time too.
To get the value r you need, starting with velocity V0 and assuming the time interval of your frames is ms, the value r can be calculated as:

r = 1 - V0 * ms / D;

There is another alternative, to reduce the velocity by a constant value dv each frame, this value can be calculated as:

dv = ms * Math.pow(V0, 2) / (2 * D - ms * V0);

The travelled distance for the second case may not always be D, this will happen only if the value 2 * D / ms / V0 is an integer. Otherwise the object will travel an extra distance, and you will have to make sure to stop the movement if the velocity becomes negative, you can do a modification to the velocity at the last step to overcome this problem.

The mathematical details can be found in my answer to that question.

Upvotes: 0

Leftium
Leftium

Reputation: 17923

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:

  1. We know dv = -v0 because the final velocity is 0.

  2. So t = dt = -v0/a

  3. Plug t into the first equation and solve for a to get:

    a = -0.5 * v0*v0 / (s1 - s0)

  4. 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.

  • Note the final position is a little off because the equations assume continuous motion (infinitely small time steps), but requestFrame will result relatively large time steps.
  • For the same reason, acceleration must be calculated only once at the beginning of motion and saved. I tried recalculating the acceleration for every frame, and rounding/simulation errors get too large as the final position is reached.

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>

Upvotes: 1

Related Questions