Linear Interpolation (lerp)

February 15th, 2020

From http://howlingmoonsoftware.com/wordpress/827-2/

It’s super useful, and can be used for all sorts of things. With a higher constant, it’s a good way to smooth out jittery input from a mouse or joystick. With a low constant, it’s a nice way to smoothly animate a progress bar or a camera following a player. It will never overshoot the target value even if it’s changing, and it changes the speed based on how far away it is so it will always quickly converge on the target. Pretty good for a one liner!

He states the standard use as:

value = lerp(value, targetValue, 0.1);

And then the improved version as:

Let’s replace the simple lerp constant with an exponential function that involves time, and see how it works.

// exp() works just as well, probably with little to no measurable performance difference.
value = lerp(target, value, exp2(-rate*deltaTime))

In this version, rate controls how quickly the value converges on the target. With a rate of 1.0, the value will move halfway to the target each second. If you double a rate, the value will move in twice as fast. If you halve the rate, it will move in half as fast. Couldn’t be easier.

Even better, it’s framerate independent. If you lerp this way 60 times with a delta time of 1/60 s, it will be the same result as lerping 30 times with 1/30 s, or once with 1 s. No fixed time step required nor the jittery movement it causes. However, do keep in mind that if your target value is changing over time (such as one object following another) you won’t get the exact same behavior. It’s close enough for many uses though.

https://github.com/dclelland/Lerp/blob/master/Lerp.swift

But also recall easing functions from YUI as implemented in Lua for word game.

import('gamecore/timer/timer')
simpleClass = require 'loop.simple'

t_constants = {
	types = {
		ease_in_and_out,
		ease_in,
		ease_out,
		linear,
	}
}
game.transition = simpleClass.class{
	type = t_constants.types.ease_in_and_out,
	length = 1.0,
	pause = false,
}

game.transition.constants = t_constants
t_constants = nil

function game.transition:__init( object)
	if object.from == nil then error("Transition expected 'from' values") end
	if object.to == nil then error("Transition expected 'to' values") end
	object.easing = object.easing or game.transition.easing.easeBoth
	object.bounce = object.bounce or false
	
	local result = simpleClass.rawnew( self, object)
	result.current = {}
	for k, v in pairs( result.from) do
		result.current[k] = result.from[k]
	end
	result.start_time = getTime()
	result.now_time = result.start_time
	result.end_time = result.start_time + result.length
	result.percent_complete = 0 -- from 0 to 1.0
	result.raw_percent_complete = 0
	return result
end

function game.transition:update( timeToAdd, pause, forceComplete)
	if not self.done then
		if not pause or not self.pause then
			self.now_time = self.now_time + timeToAdd
			if forceComplete then
				self.now_time = self.end_time
			end
		end
		if self.now_time >= self.end_time then
			self.done = true
			self.raw_percent_complete = 1.0
			--self.percent_complete = 1.0
			self.percent_complete = self.easing( self.raw_percent_complete, 0, 1, 1)
			
			if self.final == nil then self.final = self.to end
			for k, v in pairs( self.from) do
				self.current[k] = self.final[k]
			end
		else
			self.raw_percent_complete = self.now_time - self.start_time
			self.raw_percent_complete = self.raw_percent_complete / self.length
			self.percent_complete = self.raw_percent_complete
			self.percent_complete = self.easing( self.raw_percent_complete, 0, 1, 1)
			for k, v in pairs( self.from) do
				self.current[k] = self.from[k] + ((self.to[k] - self.from[k]) * self.percent_complete)
			end
		end
		if self.updateFunction ~= nil then
			self.updateFunction( self.object, self)
		end
		if self.done and self.completeFunction ~= nil then
			self.completeFunction( self.object, self)
		end
	end
end

function game.transition.add( object)
	local newTransition = game.transition( object)
	table.insert( game.timer.current, newTransition)
	newTransition:update( 0)
end


game.transition.easing = {}
game.transition.easing.easeBoth = function( t, b, c, d)
	local result
	local tt = t / (d / 2)
	if tt < 1 then
		result = c/2*tt*tt+b
	else
		t = tt - 1
		result = -c/2*(t*(t-2)-1) + b
	end
	return result
end

game.transition.easing.easeIn = function( t, b, c, d)
	local tt = t / d
	return c * (tt) * (tt) + b
end

game.transition.easing.easeOut = function( t, b, c, d)
	local tt = t / d
	return -c * (tt) * (tt-2) + b
end

game.transition.easing.easeBounce = function( t, b, c, d)
	local result
	if t < .5 then result = game.transition.easing.easeOut( t * 2, b, c, d)
	else result = game.transition.easing.easeOut( 1 - ((t - .5) * 2), b, c, d)
	end
	return result
end

game.transition.easing.bounceOut = function( t, b, c, d)
	local result
	t = t / d
	if ((t) < (1/2.75)) then
		result =  c*(7.5625*t*t) + b;
	elseif (t < (2/2.75)) then
		t = t - (1.5/2.75)
		result = c*(7.5625*(t)*t + .75) + b;
	elseif (t < (2.5/2.75)) then
		t = t - (2.25/2.75)
		result = c*(7.5625*(t)*t + .9375) + b;
	else
		t = t -(2.625/2.75)
		result = c*(7.5625*(t)*t + .984375) + b;
	end
	return result
end

game.transition.easing.bounceIn = function( t, b, c, d)
	return c - game.transition.easing.bounceOut(d-t, 0, c, d) + b;
end

-- Backtracks slightly, then reverses direction and moves to end
game.transition.easing.backIn = function (t, b, c, d, s) -- s is optional; how far to overshoot
	if (s == nil) then
		s = 1.70158;
	end
	t = t / d
	return c*(t)*t*((s+1)*t - s) + b;
end

game.transition.easing.backOut = function(t, b, c, d, s) -- s is optional; how far to overshoot
	if (s == nil) then
		s = 1.70158;
	end
	t = t / d - 1
	return c*((t)*t*((s+1)*t + s) + 1) + b;
end