Luau Easing Styles Implementation

TweenService gives you the ability to retrieve an alpha value given a percentage with GetValue. However, the math behind it isn’t exposed. Pure implementations are faster (1, 2) and thus can be used as a slight boost in performance.

Easing styles forked from Easing Functions Cheat Sheet, also including a few extra easing styles.

--!strict

local ease = {}
function ease.InLinear(x: number): number
	return x
end
function ease.OutLinear(x: number): number
	return x
end
function ease.InOutLinear(x: number): number
	return x
end

function ease.InQuad(x: number): number
	return x * x
end
function ease.OutQuad(x: number): number
	return 1 - (1 - x) * (1 - x)
end
function ease.InOutQuad(x: number): number
	return x < 0.5 and 2 * x * x or 1 - math.pow(-2 * x + 2, 2) / 2
end

function ease.InCubic(x: number): number
	return math.pow(x, 3)
end
function ease.OutCubic(x: number): number
	return 1 - math.pow(1 - x, 3)
end
function ease.InOutCubic(x: number): number
	return x < 0.5 and 4 * math.pow(x, 3) or 1 - math.pow(-2 * x + 2, 3) / 2
end

function ease.InQuart(x: number): number
	return math.pow(x, 4)
end
function ease.OutQuart(x: number): number
	return 1 - math.pow(1 - x, 4)
end
function ease.InOutQuart(x: number): number
	return x < 0.5 and 8 * math.pow(x, 4) or 1 - math.pow(-2 * x + 2, 4) / 2
end

function ease.InQuint(x: number): number
	return math.pow(x, 5)
end
function ease.OutQuint(x: number): number
	return 1 - math.pow(1 - x, 5)
end
function ease.InOutQuint(x: number): number
	return x < 0.5 and 16 * math.pow(x, 5) or 1 - math.pow(-2 * x + 2, 5) / 2
end

function ease.InHexic(x: number): number
	return math.pow(x, 6)
end
function ease.OutHexic(x: number): number
	return 1 - math.pow(1 - x, 6)
end
function ease.InOutHexic(x: number): number
	return x < 0.5 and 32 * math.pow(x, 6) or 1 - math.pow(-2 * x + 2, 6) / 2
end

function ease.InSeptic(x: number): number
	return math.pow(x, 7)
end
function ease.OutSeptic(x: number): number
	return 1 - math.pow(1 - x, 7)
end
function ease.InOutSeptic(x: number): number
	return x < 0.5 and 64 * math.pow(x, 7) or 1 - math.pow(-2 * x + 2, 7) / 2
end

function ease.InOctic(x: number): number
	return math.pow(x, 8)
end
function ease.OutOctic(x: number): number
	return 1 - math.pow(1 - x, 8)
end
function ease.InOutOctic(x: number): number
	return x < 0.5 and 128 * math.pow(x, 8) or 1 - math.pow(-2 * x + 2, 8) / 2
end

function ease.InCirc(x: number): number
	return 1 - math.sqrt(1 - math.pow(x, 2))
end
function ease.OutCirc(x: number): number
	return math.sqrt(1 - math.pow(x - 1, 2))
end
function ease.InOutCirc(x: number): number
	return x < 0.5
		and (1 - math.sqrt(1 - math.pow(2 * x, 2))) / 2
		or (math.sqrt(1 - math.pow(-2 * x + 2, 2)) + 1) / 2
end

function ease.InExponential(x: number): number
	return x == 0 and 0 or math.pow(2, 10 * x - 10)
end
function ease.OutExponential(x: number): number
	return x == 1 and 1 or 1 - math.pow(2, -10 * x)
end
function ease.InOutExponential(x: number): number
	return x == 0
		and 0
		or x == 1
		and 1
		or x < 0.5 and math.pow(2, 20 * x - 10) / 2
		or (2 - math.pow(2, -20 * x + 10)) / 2;
end

function ease.InSine(x: number): number
	return 1 - math.cos((x * math.pi) / 2)
end
function ease.OutSine(x: number): number
	return math.sin((x * math.pi) / 2)
end
function ease.InOutSine(x: number): number
	return -(math.cos(math.pi * x) - 1) / 2
end

function ease.InBack(x: number): number
	local c1 = 1.70158
	local c3 = c1 + 1

	return c3 * math.pow(x, 3) - c1 * math.pow(x, 2)
end
function ease.OutBack(x: number): number
	local c1 = 1.70158
	local c3 = c1 + 1

	return 1 + c3 * math.pow(x - 1, 3) + c1 * math.pow(x - 1, 2)
end
function ease.InOutBack(x: number): number
	local c1 = 1.70158;
	local c2 = c1 * 1.525;

	return x < 0.5
		and (math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2
		or (math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2
end

function ease.InBounce(x: number): number
	return 1 - ease.OutBounce(1 - x)
end
function ease.OutBounce(x: number): number
	local n1 = 7.5625;
	local d1 = 2.75;

	if (x < 1 / d1) then
		return n1 * math.pow(x, 2)
	elseif (x < 2 / d1) then
		x -= 1.5 / d1
		return n1 * math.pow(x, 2) + 0.75
	elseif (x < 2.5 / d1) then
		x -= 2.25 / d1
		return n1 * math.pow(x, 2) + 0.9375
	else
		x -= 2.625 / d1
		return n1 * math.pow(x, 2) + 0.984375
	end
end
function ease.InOutBounce(x: number): number
	return
		x < 0.5
		and (1 - ease.OutBounce(1 - 2 * x)) / 2
		or (1 + ease.OutBounce(2 * x - 1)) / 2;
end

function ease.InElastic(x: number): number
	local c4 = (2 * math.pi) / 3

	return x == 0
		and 0
		or x == 1
		and 1
		or -math.pow(2, 10 * x - 10) * math.sin((x * 10 - 10.75) * c4)
end
function ease.OutElastic(x: number): number
	local c4 = (2 * math.pi) / 3;

	return x == 0
		and 0
		or x == 1
		and 1
		or math.pow(2, -10 * x) * math.sin((x * 10 - 0.75) * c4) + 1;
end
function ease.InOutElastic(x: number): number
	local c5 = (2 * math.pi) / 4.5

	return x == 0
		and 0
		or x == 1
		and 1
		or x < 0.5
		and -(math.pow(2, 20 * x - 10) * math.sin((20 * x - 11.125) * c5)) / 2
		or (math.pow(2, -20 * x + 10) * math.sin((20 * x - 11.125) * c5)) / 2 + 1
end

return ease
14 Likes

What about TweenService:GetValue()?

4 Likes

Whoops! Good catch! I’ll fix that now.

1 Like

Hmm… Maybe his functions work faster than the TweenService:GetValue(). We would need to do a benchmark.

1 Like

It’s around 2.5x faster. However, that can be due to the function call overhead as well as the extra processing :GetValue() needs for the extra arguments.

local TS = game:GetService("TweenService")
local ease = --> path to module

local t0 = os.clock()
for i = 1, 10000000 do
    ease.InSine(0.7)
end
print("Lua Implementation:", os.clock() - t0)

local t1 = os.clock()
for i = 1, 10000000 do
    TS:GetValue(0.7, Enum.EasingStyle.Sine, Enum.EasingDirection.In)
end
print("TweenService:", os.clock() - t1)
1 Like

Can you also benchmark it with the native code generation? Using the --!native at the start of the code?

1 Like

honestly a 2.5x speed increase is enough to make this useful

Natively generated code does yield significantly better results (~4x):

(same code as above, but with the --!native tag)

Roblox is planning to push out automatic native code generation to applicable code in the future, so pure Luau implementations can be quite a lot faster.

1 Like

I just thought about it, but in my game, I currently have a workaround for using TweenService:GetValue() in parallel. TweenService:GetValue() cannot be used in parallel, so I have to run it in serial before stuff gets cached
(Hoping this changes soon)

Is your pure lua implementation a one to one recreation of the TweenService funcitons?

Would be nice if there’s a caching system for repetitive calls to further boost performance, similar to Python’s @cache functool

But the caching system wouldn’t work well in parallel unless you want to use SharedTables, which are notorious for having super slow read/write accesses

This isn’t a substitute for TweenService methods. The resource only includes the math behind all the interpolation styles. You can, however, use this resource as a replacement for GetValue() as it’s faster and works in parallel.

2 Likes

Went out of my way to hand optimize all these functions. I’ve tested and guaranteed all of them are equal to the old functions and TweenService/GetValue.

Some functions in my benchmarks ran 3x faster than before, making a 7.5x speed difference to GetValue.

I left out Hexic-Octic since they aren’t in normal TweenService, but it should be easy for anyone to add them in.

--!strict
--!native

local Easing = {
	Linear = {
		In = function(Value: number): number
			return Value
		end,
		
		Out = function(Value: number): number
			return Value
		end,
		
		InOut = function(Value: number): number
			return Value
		end,
	},
	
	Quad = {
		In = function(Value: number): number
			return Value * Value
		end,
		
		Out = function(Value: number): number
			local Base = 1 - Value
			return 1 - Base * Base
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return 2 * Value * Value
			end
			
			local Base = -2 * Value + 2
			return 1 - Base * Base * 0.5
		end,
	},
	
	Cubic = {
		In = function(Value: number): number
			return Value * Value * Value
		end,
		
		Out = function(Value: number): number
			local Base = 1 - Value
			return 1 - Base * Base * Base
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return 4 * Value * Value * Value
			end
			
			local Base = -2 * Value + 2
			return 1 - Base * Base * Base * 0.5
		end,
	},
	
	Quart = {
		In = function(Value: number): number
			return Value * Value * Value * Value
		end,
		
		Out = function(Value: number): number
			local Base = 1 - Value
			return 1 - Base * Base * Base * Base
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return 8 * Value * Value * Value * Value
			end
			
			local Base = -2 * Value + 2
			return 1 - Base * Base * Base * Base * 0.5
		end,
	},
	
	Quint = {
		In = function(Value: number): number
			return Value * Value * Value * Value * Value
		end,
		
		Out = function(Value: number): number
			local Base = 1 - Value
			return 1 - Base * Base * Base * Base * Base
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return 16 * Value * Value * Value * Value * Value
			end
			
			local Base = -2 * Value + 2
			return 1 - Base * Base * Base * Base * Base * 0.5
		end,
	},
	
	Circular = {
		In = function(Value: number): number
			return 1 - (1 - Value * Value) ^ 0.5
		end,
		
		Out = function(Value: number): number
			local Base = Value - 1
			return (1 - Base * Base) ^ 0.5
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return (1 - (1 - 4 * Value * Value) ^ 0.5) * 0.5
			end
			
			local Base = -2 * Value + 2
			return (1 + (1 - Base * Base) ^ 0.5) * 0.5
		end,
	},
	
	Exponential = {
		In = function(Value: number): number
			return 2 ^ (10 * Value - 10)
		end,
		Out = function(Value: number): number
			return 1 - 2 ^ (-10 * Value)
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return 2 ^ (20 * Value - 11)
			end
			
			return 1 - 2 ^ (-20 * Value + 9)
		end,
	},
	
	Sine = {
		In = function(Value: number): number
			return 1 - math.cos(Value * 1.5707963267948966)
		end,
		
		Out = function(Value: number): number
			return math.sin(Value * 1.5707963267948966)
		end,
		
		InOut = function(Value: number): number
			return (math.cos(3.141592653589793 * Value) - 1) * -0.5
		end,
	},
	
	Back = {
		In = function(Value: number): number
			return 2.70158 * Value * Value * Value - 1.70158 * Value * Value
		end,
		
		Out = function(Value: number): number
			local Base = Value - 1
			return 1 + 2.70158 * Base * Base * Base + 1.70158 * Base * Base
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				local Base = 2 * Value
				return Base * Base * (3.5949095 * Base - 2.5949095) * 0.5
			end
			
			local Base = 2 * Value - 2
			return Base * Base * (3.5949095 * Base + 2.5949095) + 2 * 0.5
		end,
	},
	
	Bounce = {
		In = function(Value: number): number
			local Compliment = 1 - Value
			
			if Compliment < 0.36363636363636365 then
				return 1 - 7.5625 * Compliment * Compliment
			end
			
			if Compliment < 0.7272727272727273 then
				local Base = Compliment - 0.5454545454545454
				return 1 - 7.5625 * Base * Base + 0.75
			end
			
			if Compliment < 0.9090909090909091 then
				local Base = Compliment - 0.9090909090909091
				return 1 - 7.5625 * Base * Base + 0.9375
			end
			
			local Base = Compliment - 0.9545454545454546
			return 1 - 7.5625 * Base * Base + 0.984375
		end,
		
		Out = function(Value: number): number
			if Value < 0.36363636363636365 then
				return 7.5625 * Value * Value
			end
			
			if Value < 0.7272727272727273 then
				local Base = Value - 0.5454545454545454
				return 7.5625 * Base * Base + 0.75
			end
			
			if Value < 0.9090909090909091 then
				local Base = Value - 0.9090909090909091
				return 7.5625 * Base * Base + 0.9375
			end
			
			local Base = Value - 0.9545454545454546
			return 7.5625 * Base * Base + 0.984375
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				local Compliment = 1 - 2 * Value
				
				if Compliment < 0.36363636363636365 then
					return (1 - 7.5625 * Compliment * Compliment) * 0.5
				end
				
				if Compliment < 0.7272727272727273 then
					local Base = Compliment - 0.5454545454545454
					return (1 - 7.5625 * Base * Base + 0.75) * 0.5
				end
				
				if Compliment < 0.9090909090909091 then
					local Base = Compliment - 0.9090909090909091
					return (1 - 7.5625 * Base * Base + 0.9375) * 0.5
				end
				
				local Base = Compliment - 0.9545454545454546
				return (1 - 7.5625 * Base * Base + 0.984375) * 0.5
			end
			
			local Compliment = 2 * Value - 1
			
			if Compliment < 0.36363636363636365 then
				return (1 + 7.5625 * Compliment * Compliment) * 0.5
			end
			
			if Compliment < 0.7272727272727273 then
				local Base = Compliment - 0.5454545454545454
				return (1 + 7.5625 * Base * Base + 0.75) * 0.5
			end
			
			if Compliment < 0.9090909090909091 then
				local Base = Compliment - 0.9090909090909091
				return (1 + 7.5625 * Base * Base + 0.9375) * 0.5
			end
			
			local Base = Compliment - 0.9545454545454546
			return (1 + 7.5625 * Base * Base + 0.984375) * 0.5
		end,
	},
	
	Elastic = {
		In = function(Value: number): number
			return -2 ^ (10 * Value - 10) * math.sin((Value * 10 - 10.75) * 2.0943951023931953)
		end,
		
		Out = function(Value: number): number
			return 2 ^ (-10 * Value) * math.sin((Value * 10 - 0.75) * 2.0943951023931953) + 1
		end,
		
		InOut = function(Value: number): number
			if Value < 0.5 then
				return -2 ^ (20 * Value - 10) * math.sin((20 * Value - 11.125) * 1.3962634015954636) * 0.5
			end
			
			return 2 ^ -20 * Value + 10 * math.sin(20 * Value - 11.125 * 1.3962634015954636) * 0.5 + 1
		end,
	},
}

return Easing
5 Likes