Custom tweening

I made a custom TweenService for fun and learning. Here’s the module script:

local easingStyles = {}
-- Contains a bunch of functions which input a number from 0 to 1, and outputs a similar number based on the curve
-- Each function also inputs a direction for the curve

local function inOut(i:number,style:(number,EnumItem) -> number,...):number
	if i < .5 then
		return style(i * 2,Enum.EasingDirection.In,...) / 2
	else
		return style((i - .5) * 2,Enum.EasingDirection.Out,...) / 2 + .5
	end
end

local function createPolynomialFunction(degree:number): (number,EnumItem) -> number
	local polynomialFunction
	polynomialFunction = function(i:number,direction:EnumItem):number
		i = math.clamp(i,0,1) 
		if direction == Enum.EasingDirection.In then
			return i ^ degree
		elseif direction == Enum.EasingDirection.Out then
			return i ^ (1 / degree)
		elseif direction == Enum.EasingDirection.InOut then
			return inOut(i,polynomialFunction)
		end 
	end
	return polynomialFunction
end

easingStyles.Linear = createPolynomialFunction(1)
easingStyles.Quadratic = createPolynomialFunction(2)
easingStyles.Cubic = createPolynomialFunction(3)
easingStyles.Quartic = createPolynomialFunction(4)
easingStyles.Quintic = createPolynomialFunction(5)
easingStyles.Exponential = function(i:number,direction:EnumItem):number
	i = math.clamp(i,0,1) 
	if direction == Enum.EasingDirection.In then
		return 2 ^ (10 * (i - 1))
	elseif direction == Enum.EasingDirection.Out then
		return 1 - 2 ^ (-10 * i)
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Exponential)
	end
end
easingStyles.Sine = function(i:number,direction:EnumItem):number
	i = math.clamp(i,0,1) 
	local halfPi = math.pi / 2
	if direction == Enum.EasingDirection.In then
		return math.sin((i - 1) * halfPi) + 1
	elseif direction == Enum.EasingDirection.Out then
		return math.sin(i * halfPi)
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Sine)
	end
end
easingStyles.Circular = function(i:number,direction:EnumItem):number
	i = math.clamp(i,0,1) 
	if direction == Enum.EasingDirection.In then
		return 1 - math.sin(math.acos(i))
	elseif direction == Enum.EasingDirection.Out then
		return math.sin(math.acos(i + (.5 - i) * 2))
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Circular)
	end
end
easingStyles.Back = function(i:number,direction:EnumItem,steepness:number):number
	i = math.clamp(i,0,1)
	steepness = steepness or 1.3 
	local max = steepness ^ .2
	if direction == Enum.EasingDirection.In then
		local subtraction = (steepness - 1) * i
		return (i * max) ^ 5 - subtraction
	elseif direction == Enum.EasingDirection.Out then
		local subtraction = (steepness - 1) * (1 - i)
		return 1 - ((max - i * max) ^ 5 - subtraction) 
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Back,steepness)
	end
end

local function create(object:Instance,time:number,properties:{[string]:any},style:() -> number,...)
	local startValues = {}
	for name,_ in properties do
		startValues[name] = object[name]
	end
	
	local function updateProperties(alpha:number)
		for name,endValue in properties do
			local propertyType = typeof(endValue)
			if typeof(object[name]) ~= propertyType then return end
			local startValue:number = startValues[name]
			local newValue:any
			local lerpingTypes = {"CFrame","Vector2","Vector3","Color3","UDim2"}
			if propertyType == "number" then
				object[name] = math.lerp(startValue,endValue,alpha)
			elseif table.find(lerpingTypes,propertyType) then
				object[name] = startValue:Lerp(endValue,alpha)
			elseif propertyType == "UDim" then
				local as,ao = startValue.Scale,startValue.Offset
				local bs,bo = endValue.Scale,endValue.Offset
				object[name] = UDim.new(math.lerp(as,bs,alpha),math.lerp(ao,bo,alpha))
			end		
		end 
	end
	
	local total = 0
	repeat 
		total += task.wait()
		updateProperties(style(total / time,...))
	until total >= time
	
	updateProperties(1)
end


return {
	EasingStyle = easingStyles,
	Tween = create
}

I left out 2 styles, Bounce and Elastic, because they’re more complex.


Compared to the other styles, which are just lines with a simple curve:

Does anyone have ideas for how I can add Bounce and Elastic?

3 Likes

check this github:

1 Like

Here’s my functions for every easing style. They’re as close as possible to their TweenService counterparts (with the largest delta documented per each). I also took a good deal in benchmarking and making them as performant as possible.

For numerical stability, I haven’t fully checked if they all hold any correctness properties like monotonicity, but I can guarantee you they are exact (F(0) == 0 and F(1) == 1) which should be good enough for most cases.

--!strict
--!native
--!optimize 2

-- Note: All constants (like math.pi * 2) are constant-folded
-- at level 2 optimizations (live games)

-- Delta: 7.953643921254638e-08
local function QuadIn(Value: number): number
	return Value ^ 2
end

-- Delta: 9.290313685017537e-08
local function QuadOut(Value: number): number
	return Value * 2 - Value ^ 2
end

-- Delta: 8.506011894837684e-08
local function QuadInOut(Value: number): number
	if Value < 0.5 then
		return Value ^ 2 * 2
	end
	
	return 1 - (2 - Value * 2) ^ 2 / 2
end


-- Delta: 9.810449030922541e-08
local function CubicIn(Value: number): number
	return Value ^ 3
end

-- Delta: 1.0041937237303955e-07
local function CubicOut(Value: number): number
	return 1 - (1 - Value) ^ 3
end

-- Delta: 1.1605999650221577e-07
local function CubicInOut(Value: number): number
	if Value < 0.5 then
		return Value ^ 3 * 4
	end
	
	return 1 - (2 - Value * 2) ^ 3 / 2
end


-- Delta: 1.2738682553248282e-07
local function QuartIn(Value: number): number
	local Base = Value ^ 2
	return Base * Base
end

-- Delta: 1.2738682264590295e-07
local function QuartOut(Value: number): number
	local Base = (1 - Value) ^ 2
	return 1 - Base * Base
end

-- Delta: 1.2579216368546753e-07
local function QuartInOut(Value: number): number
	if Value < 0.5 then
		local Base = Value ^ 2
		return Value * Value * 8
	end
	
	local Base = (2 - 2 * Value) ^ 2
	return 1 - Base * Base / 2
end


-- Delta: 1.504616977676676e-07
local function QuintIn(Value: number): number
	return Value * Value * Value * Value * Value
end

-- Delta: 1.5046169454802083e-07
local function QuintOut(Value: number): number
	local Base = 1 - Value
	return 1 - Base * Base * Base * Base * Base
end

-- Delta: 1.5110067619339418e-07
local function QuintInOut(Value: number): number
	if Value < 0.5 then
		return Value * Value * Value * Value * Value * 16
	end
	
	local Base = 2 - 2 * Value
	return 1 - Base * Base * Base * Base * Base / 2
end


-- Delta: 4.357429542745095e-07
local function CircularIn(Value: number): number
	return 1 - (1 - Value ^ 2) ^ 0.5
end

-- Delta: 4.3574297162868314e-07
local function CircularOut(Value: number): number
	return (1 - (Value - 1) ^ 2) ^ 0.5
end

-- Delta: 2.061581554357872e-07
local function CircularInOut(Value: number): number
	if Value < 0.5 then
		return (1 - (1 - 4 * Value ^ 2) ^ 0.5) / 2
	end
	
	return (1 + (1 - (2 - 2 * Value) ^ 2) ^ 0.5) / 2
end


-- Delta: 2.0641033593449265e-07
local function ExponentialIn(Value: number): number
	if Value == 0 then
		return 0
	end
	
	return 2 ^ (10 * Value - 10)
end

-- Delta: 2.0641033138257825e-07
local function ExponentialOut(Value: number): number
	if Value == 1 then
		return 1
	end
	
	return 1 - 2 ^ (Value * -10)
end

-- Delta: 1.7445670108529043e-07
local function ExponentialInOut(Value: number): number
	if Value == 0 then
		return 0
	end
	
	if Value == 1 then
		return 1
	end
	
	if Value < 0.5 then
		return 2 ^ (20 * Value - 11)
	end
	
	return 1 - 2 ^ (9 - 20 * Value)
end


-- Delta: 6.874036007076256e-08
local function SineIn(Value: number): number
	if Value == 1 then
		return 1
	end
	
	return 1 - math.cos(Value * (math.pi / 2))
end

-- Delta: 8.671046580754904e-08
local function SineOut(Value: number): number
	return math.sin(Value * (math.pi / 2))
end

-- Delta: 8.216054458998201e-08
local function SineInOut(Value: number): number
	return 0.5 - math.cos(Value * math.pi) / 2
end


-- Note: The Constants in Back are Fine-Tuned so Value = 1 Results in 1 Without Branching
-- and also Having a Small Delta

-- Delta: 2.2366793106520788e-07
local function BackIn(Value: number): number
	return Value ^ 3 * 2.7015799999999999 - Value ^ 2 * 1.7015799999999999
end

-- Delta: 2.236679272904496e-07
local function BackOut(Value: number): number
	local Base = Value - 1
	return 1 + Base ^ 3 * 2.7015799999999999 + Base ^ 2 * 1.7015799999999999
end

-- Delta: 1.716145916486056e-07
local function BackInOut(Value: number): number
	if Value < 0.5 then
		return Value ^ 3 * 10.80632 - Value ^ 2 * 3.40316 
	end
	
	return Value * 25.61264 - Value ^ 2 * 29.0158 - 6.40316 + Value ^ 3 * 10.80632
end


-- Delta: 1.4909839396448632e-07
local function BounceIn(Value: number): number
	if Value < 1 / 11 then
		return 0.015625 - (7.5625 * (1 / 22 - Value) ^ 2)
	end
	
	if Value < 3 / 11 then
		return 0.0625 - (7.5625 * (2 / 11 - Value) ^ 2)
	end
	
	if Value < 7 / 11 then
		return 0.25 - (7.5625 * (5 / 11 - Value) ^ 2)
	end
	
	return 1 - (7.5625 * (1 - Value) ^ 2)
end

-- Delta: 2.535781873369558e-07
local function BounceOut(Value: number): number
	if Value < 4 / 11 then
		return 7.5625 * Value ^ 2
	end
	
	if Value < 8 / 11 then
		return 7.5625 * (Value - 6 / 11) ^ 2 + 0.75
	end
	
	if Value < 10 / 11 then
		return 7.5625 * (Value - 9 / 11) ^ 2 + 0.9375
	end
	
	return 7.5625 * (Value - 21 / 22) ^ 2 + 0.984375
end

-- Note: For Bounce InOut, the function call is faster than trying to inline the formula

-- Delta: 1.6928482327038807e-07
local function BounceInOut(Value: number): number
	if Value < 0.5 then
		return (1 - BounceOut(1 - 2 * Value)) / 2
	end
	
	return (1 + BounceOut(2 * Value - 1)) / 2
end


-- Delta: 4.4274472726923975e-07
local function ElasticIn(Value: number): number
	if Value == 0 then
		return 0
	end
	
	return -2 ^ (10 * Value - 10) * math.sin(math.pi * 6.5 * Value - math.pi * 7)
end

-- Delta: 4.7254703794408215e-07
local function ElasticOut(Value: number): number
	return 1 + 2 ^ (-10 * Value) * math.sin(math.pi * -6.5 * Value - math.pi / 2)
end

-- Delta: 4.167113005770773e-07
local function ElasticInOut(Value: number): number
	if Value == 0 then
		return 0
	end
	
	if Value < 0.5 then
		return -2 ^ (10 * (2 * Value - 1)) * math.sin(math.pi * 13 * Value - math.pi * 7) / 2
	end
	
	return 1 - 2 ^ (-10 * (2 * Value - 1)) * math.sin(math.pi * 13 * Value - math.pi * 6) / 2
end
2 Likes

Thanks, this is what I already wrote:

easingStyles.Bounce = function(i:number,direction:EnumItem,bounciness:number):number
	i = math.clamp(i,0,1) 
	bounciness = (bounciness and math.clamp(bounciness,0,math.huge)) or 1
	if direction == Enum.EasingDirection.In then
		local function getHeightAndCosine():(number,number)
			if i < .1 then
				return .05 * bounciness, i * 20 - 1
			elseif i < .3 then
				return .1 * bounciness, (i - .1) * (20 / 2) - 1
			elseif i < .6 then
				return .3 * bounciness, (i - .3) * (20 / 3) - 1
			else 
				return 1, (i - .6) * (20 / 8) - 1
			end
		end

		local height,cosine = getHeightAndCosine()
		return math.sin(math.acos(cosine)) * height
	elseif direction == Enum.EasingDirection.Out then
		return 1 - easingStyles.Bounce(1 - i,Enum.EasingDirection.In,bounciness)
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Bounce,bounciness)
	end
end
easingStyles.Elastic = function(i:number,direction:EnumItem,elasticity):number
	i = math.clamp(i,0,1) 
	elasticity = (elasticity and math.clamp(elasticity,0,math.huge)) or 1
	if direction == Enum.EasingDirection.In then
		local directionIndex = i * 24
		local directionChangeTable = {
			{20,4,-.3 * elasticity,1},
			{16,4,.15 * elasticity,-.3 * elasticity},
			{13,3,-.04 * elasticity,.15 * elasticity},
			{10,3,.04 * elasticity,-.04 * elasticity},
			{8,2,-.02 * elasticity,.04 * elasticity},
			{0,8,0,-.02 * elasticity}		
		}
		
		for _,info in directionChangeTable do
			local indexStart,maxIndexDifference,valueStart,valueEnd = unpack(info)
			if directionIndex >= indexStart then
				local alpha = (directionIndex - indexStart) / maxIndexDifference
				return math.lerp(valueStart,valueEnd,alpha)
			end
		end
	elseif direction == Enum.EasingDirection.Out then
		return 1 - easingStyles.Elastic(1 - i,Enum.EasingDirection.In)
	elseif direction == Enum.EasingDirection.InOut then
		return inOut(i,easingStyles.Elastic)
	end
end

Bounce is pretty similiar, but Elastic is very different because using my function it doesn’t curve, it’s just straight lines.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.