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