6
\$\begingroup\$

Assume you have a target that changes. Example:

// on average every 1000 frames, move the target
if (random(1000) == 0) {
    // pick a value between 0 and 999
    target = random(1000);
}

Now, I want to chase that value with both ease in and ease out. With just ease out a common solution is this

const kApproachFudge = 0.03;
value += (target - value) * kApproachFudge;

That only gives ease out though. Another might be to use a kind of arrive behavior with acceleration and deceleration but that seems like overkill and is often fiddly meaning if your deceleration is not fast enough you'll overshoot the target. If you max speed is too high you might also overshoot the target. For example

function main()
{
  g_a = new Arriver();

  g_canvas = document.getElementById("canvas");
  g_ctx = g_canvas.getContext("2d");
 
  var then = getTime();
  function mainLoop()
  {
    var now = getTime();
    var elapsedTime = Math.min(0.1, now - then);
    then = now;
    resizeCanvas();
    update(elapsedTime, g_ctx);

    requestId = requestAnimFrame(mainLoop, g_canvas);
  }
  mainLoop();
}

function update(elapsedTime, ctx)
{
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
 
    g_a.update(elapsedTime);
    if ((g_a.distToTarget < 1 && g_a.velocity.length() < 5) ||
        randInt(200) == 0)
    {
       g_a.setTarget(
         randInt(ctx.canvas.width - 100) + 50,
         randInt(ctx.canvas.height - 100) + 50);
    }
    drawCircle(ctx, g_a.target.x, g_a.target.y, 10, "#00ff00");
    drawCircle(ctx, g_a.value.x, g_a.value.y, 10, "#ff80ff");
    drawLine(ctx,
         g_a.value.x, g_a.value.y,
         g_a.value.x + g_a.velocity.x * 1, g_a.value.y + g_a.velocity.y * 1,
         "black");
    drawLine(ctx,
         g_a.value.x, g_a.value.y,
         g_a.value.x + g_a.acceleration.x * 1, g_a.value.y + g_a.acceleration.y * 1,
         "red");
}

function getTime()
{
  return(new Date()).getTime() * 0.001;
}

// return int from 0 to range - 1
function randInt(range)
{
  return Math.floor(Math.random() * range);
}

function drawLine(ctx, x1, y1, x2, y2, color)
{
  ctx.beginPath();
  ctx.strokeStyle = color;
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

function drawCircle(ctx, x, y, radius, color)
{
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2, false);
  ctx.fill();
}

function drawCircleLine(ctx, x, y, radius, color)
{
  ctx.strokeStyle = color;
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2, false);
  ctx.stroke();
}

function resizeCanvas()
{
  if (g_canvas.width != g_canvas.clientWidth ||
      g_canvas.height != g_canvas.clientHeight)
  {
    g_canvas.width = g_canvas.clientWidth;
    g_canvas.height = g_canvas.clientHeight;
  }
}

function Vec2(x, y) {
  this.set(x, y);
}

Vec2.prototype.set = function(x, y)
{
  this.x = x;
  this.y = y;
  return this;
};

Vec2.prototype.copy = function(b)
{
  this.x = b.x;
  this.y = b.y;
  return this;
};

Vec2.prototype.add = function(b, dest)
{
  dest = dest || this;
  dest.x = this.x + b.x;
  dest.y = this.y + b.y;
  return this;
};

Vec2.prototype.addScaled = function(b, scale, dest)
{
  dest = dest || this;
  dest.x = this.x + b.x * scale;
  dest.y = this.y + b.y * scale;
  return this;
};

Vec2.prototype.subtract = function(b, dest)
{
  dest = dest || this;
  dest.x = this.x - b.x;
  dest.y = this.y - b.y;
  return this;
};

Vec2.prototype.scale = function(b, dest)
{
  dest = dest || this;
  dest.x = this.x * b;
  dest.y = this.y * b;
  return this;
};

Vec2.prototype.truncate = function(max, dest)
{
  dest = dest || this;
  var length = this.length();
  if (length > max)
  {
    this.scale(max / length, dest);
  }
  return this;
};

Vec2.prototype.lengthSq = function() {
  return this.x * this.x + this.y * this.y;
};

Vec2.prototype.length = function() {
  return Math.sqrt(this.lengthSq());
};

Vec2.prototype.normalize = function(dest) {
  dest = dest || this;
  var length = this.length();
  if (length > 0.000001)
  {
    dest.x /= length;
    dest.y /= length;
  }
  return this;
};

function Arriver()
{
  this.value = new Vec2(0,0);
  this.target = new Vec2(0,0);
  this.velocity = new Vec2(0,0);
  this.maxSpeed = 150;
  this.deceleration = 0.6;
  this.zero = new Vec2(0,0);
  this.tempDelta = new Vec2(0,0);
  this.force = new Vec2(0, 0);
  this.acceleration = new Vec2(0,0);
  this.mass = 1;
  this.distToTarget = 0;
};

Arriver.prototype.setValue = function(x, y) {
  this.value.set(x, y);
}

Arriver.prototype.setTarget = function(x, y) {
  this.target.set(x, y);
  this.target.subtract(this.value, this.tempDelta);
  this.distToTarget = this.tempDelta.length();
}

Arriver.prototype.calcDesiredForce_ = function(elapsedTime) {
  this.target.subtract(this.value, this.tempDelta);

  this.distToTarget = this.tempDelta.length();
  if (this.distToTarget <= 0)
  {
    return this.zero;
  }

  var speed = this.distToTarget / this.deceleration;
  speed = Math.min(speed, this.maxSpeed);

  this.tempDelta.scale(speed / this.distToTarget);
  this.tempDelta.subtract(this.velocity);

  return this.tempDelta;
};

Arriver.prototype.update = function(elapsedTime) {
  this.force.copy(this.calcDesiredForce_());
  this.force.scale(1 / this.mass, this.acceleration);
  this.velocity.addScaled(this.acceleration, elapsedTime);
  this.velocity.truncate(this.maxSpeed);
  this.value.addScaled(this.velocity, elapsedTime);
};

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
  return window.requestAnimationFrame  ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame    ||
    window.oRequestAnimationFrame      ||
    window.msRequestAnimationFrame     ||
    function( callback ){
      window.setTimeout(callback, 1000 / 60);
  };
})();

main();
html, body {
  width: 100%;
  height: 100%;
  border: 0px;
  padding: 0px;
  margin: 0px;
  font-family: sans-serif;
  overflow: hidden;
}
canvas {
  width: 100%;
  height: 100%;
}
#legend {
  position: absolute;
  left: 1em;
  top: 1em;
  z-index: 2;
}
<canvas id="canvas" width="1024" height="1024"></canvas>
<pre id="legend">
---[ legend ]---
pink:  vehicle
green: target
black: velocity
red:   acceleration
</pre>

I can sit there and just max force and max velocity and mass and deceleration but then it's so damn fiddley. If I want it to go faster I have to first adjust max vel, then maybe I have to adjust max force to it started quicker but then it will over shoot the target and I have to adjust other factors. I have to imagine this is a solved problem.

Is there a simple algorithm that ALWAYS gives good results?

note: I'm not looking for an ease in / ease out function between 2 fixed values. I have that in my toolbox and there are several answers here as well. There's probably an answer for this too but I didn't find it.


The answers claimed to be duplicate ** DO NOT ANSWER THIS QUESTION **

Those answers assume a known starting distance to target and pick the 1/2 way mark as the spot to start slowing but if the target is moving there is no 1/2 way spot. Since you'll be recalculating every frame you'll always be a 0% of the way to target from your current position

\$\endgroup\$
  • 1
    \$\begingroup\$ Possible duplicate of Move a 2D point to a target (first accelerate towards the target, then decelerate) \$\endgroup\$ – izb Apr 18 '16 at 13:45
  • 1
    \$\begingroup\$ @izb I don't think it's a duplicate; this here specifies that the target is moving and that the chasing should be changed. \$\endgroup\$ – Alexandre Vaillancourt Apr 18 '16 at 14:24
  • \$\begingroup\$ This question is 2 years older than the other question. The other question should be marked a duplicate of this question assuming they are even duplicates \$\endgroup\$ – gman Apr 18 '16 at 14:30
  • \$\begingroup\$ @gman Age of a question isn't really that important for "what is a duplicate of what" issues; the quality, clarity and canonical fit of the question and answers is more important. \$\endgroup\$ – Josh Apr 18 '16 at 18:00
  • \$\begingroup\$ Regardless it's not a duplicate. \$\endgroup\$ – gman Apr 19 '16 at 5:41
1
\$\begingroup\$

I think your behaviorally-based approach is right on the money - and I'm not sure that overshooting your target is the problem that you're making it out to be. What you're describing is essentially a 1d steering behavior, and overshoot is an inevitable element of that behavior. Think of it this way: suppose you started at x=0 and you're after a current target of target_x=1000, with your cur_x=800 when a new target is chosen 'behind you' at target_x=700. You presumably wouldn't want your 'chase' to stop on a dime and flip velocity instantaneously, because that would be an implausible acceleration and a discontinuous velocity — basically, a 'kink' in your curve. Instead, you'd keep incrementing cur_x a little until you were able to change your velocity to the point of starting to decrement it towards target_x. The same behavior should be true if target_x=750, or if target_x=799 — and it stands to reason by 'continuity of behavior' that it should still be true if target_x is just ahead of you, say target_x=850.

As far as the specifics on either end of the curve, I think the best way to handle 'ease out' from a stopped position would be your suggested approach of putting a limit on acceleration; for symmetry purposes, then, you'd probably want your ease-in to be symmetric, with just enough lookahead to know when it should start decelerating in order to hit the target position 'on the nose'. Alternately, if you don't care about precisely hitting your target when it ultimately 'holds still', you could use an exponentially damped approach where you have a target velocity v given by v=dx/dt = C*|x_target-x| (with C the constant controlling the damping behavior) and you clamp your acceleration attempting to reach that target velocity; this will give asymmetric ease-in and ease-out (since ease-out from a stop will be quadratic, but ease-in to a stop will be exponential) but makes the updates slightly easier since there's no real lookahead involved.

\$\endgroup\$
1
\$\begingroup\$

Daniel Shiffman described your desired behavior in chapter 6 of his book The Nature of Code. That link leads directly to a live example of the behaviour. All code snippets are in Java.

Basically, you keep track of a velocity. Then calculate a desired velocity which points towards the target but has a magnitude equal to the maximum speed. Then you calculate a steering vector which is equal to your desired velocity minus your actual velocity. Then you add steering to your velocity.

To slow down and stop at the target, your set a threshold radius around your target. When your object is within this radius, you set your desired velocity depending on how close you are to the target.

Please read the chapter for a proper explanation with diagrams, code snippets and live examples.

I implemented this technique today to animate the motion of the camera following the player. This is really easy to implement and looks really nice. It does take some fiddling to get working though. Trying to get the perfect values of the constants took me about an hour but my camera moves really nicely now.

I don't think there is one perfect solution that doesn't require fiddling with constants because everyone's desired behaviour is different.

\$\endgroup\$
0
\$\begingroup\$

You should have a look at the animations functions in jQuery. I use them to get the math right in my own easing functions.

Here is an example of them in action... http://jqueryui.com/demos/effect/easing.html

I should add that each of these returns a value used like this..

currentValue = startValue + easeFunction(x, t, b, c, d) * (endValue - startValue)

 // jQuery Ease Functions
 x = 0 - 1 as float of completion
 t = elapsed time in ms
 b = 0
 c = 1
 d = duration in ms


    easeInQuad: function (x, t, b, c, d) {
        return c*(t/=d)*t + b;
    },
    easeOutQuad: function (x, t, b, c, d) {
        return -c *(t/=d)*(t-2) + b;
    },
    easeInOutQuad: function (x, t, b, c, d) {
        if ((t/=d/2) < 1) return c/2*t*t + b;
        return -c/2 * ((--t)*(t-2) - 1) + b;
    },
    easeInCubic: function (x, t, b, c, d) {
        return c*(t/=d)*t*t + b;
    },
    easeOutCubic: function (x, t, b, c, d) {
        return c*((t=t/d-1)*t*t + 1) + b;
    },
    easeInOutCubic: function (x, t, b, c, d) {
        if ((t/=d/2) < 1) return c/2*t*t*t + b;
        return c/2*((t-=2)*t*t + 2) + b;
    },
    easeInQuart: function (x, t, b, c, d) {
        return c*(t/=d)*t*t*t + b;
    },
    easeOutQuart: function (x, t, b, c, d) {
        return -c * ((t=t/d-1)*t*t*t - 1) + b;
    },
    easeInOutQuart: function (x, t, b, c, d) {
        if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
        return -c/2 * ((t-=2)*t*t*t - 2) + b;
    },
    easeInQuint: function (x, t, b, c, d) {
        return c*(t/=d)*t*t*t*t + b;
    },
    easeOutQuint: function (x, t, b, c, d) {
        return c*((t=t/d-1)*t*t*t*t + 1) + b;
    },
    easeInOutQuint: function (x, t, b, c, d) {
        if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
        return c/2*((t-=2)*t*t*t*t + 2) + b;
    },
    easeInSine: function (x, t, b, c, d) {
        return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
    },
    easeOutSine: function (x, t, b, c, d) {
        return c * Math.sin(t/d * (Math.PI/2)) + b;
    },
    easeInOutSine: function (x, t, b, c, d) {
        return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
    },
    easeInExpo: function (x, t, b, c, d) {
        return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
    },
    easeOutExpo: function (x, t, b, c, d) {
        return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
    },
    easeInOutExpo: function (x, t, b, c, d) {
        if (t==0) return b;
        if (t==d) return b+c;
        if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
        return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
    },
\$\endgroup\$
  • 1
    \$\begingroup\$ That's a great answer! \$\endgroup\$ – Petr Abdulin May 15 '12 at 8:12
  • 2
    \$\begingroup\$ ...except that the poster explicitly stated that they already had a number of ease-in/ease-out functions and that that wasn't what they were after? \$\endgroup\$ – Steven Stadnicki May 15 '12 at 19:37
  • \$\begingroup\$ oh right, I not sure I understand what gman needs exactly... if you want your object to ease in and out towards the target, you just restart the animation when the target changes. Perhaps instead gman is looking for an object that retains its velocity in the direction it was traveling before the target changed. In that case, I would treat it more like a physics sim, where the object has a velocity, and apply an amount of force to the object in the direction of the target based on how far away from the target it is. something like that. \$\endgroup\$ – Jay Kyburz May 15 '12 at 22:11
  • 1
    \$\begingroup\$ This is explicitly NOT the answer I was looking for as I tried to make clear in the original post. Those JQuery answers are for fixed values. I need an answer for moving values. Nice examples though. \$\endgroup\$ – gman May 16 '12 at 6:30
  • \$\begingroup\$ Yes, a physics sim is more what I was looking for. What I don't like about the physics sim approach though is that it has settings which if not set correctly for a given situation end up making bad behavior. For example if a max velocity is too high or a force too small then a simple A -> B movement will overshoot B. What I want is an example that never fails. Maybe it's the physics example except with some automatic setting of maxVel, maxForce, acceleration, etc so that they always work for any given value and target. \$\endgroup\$ – gman May 16 '12 at 6:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.