Hello everyone
!
Have you ever tried to tween the color of a UIGradient? If you have, you probably noticed that it doesn’t work, since ColorSequences can’t be animated, just like many other data types.
That’s why I decided to create UIGradient Tweener!
This module lets you very easily animate the color of a UIGradient, as it follows roughly the same structure as a normal tween and works with any color sequence!
The module:
GradientTweener.rbxm (7.8 KB)
Roblox won’t let me publish it in the Creator Hub somehow(“Missusing Roblox Systems”)
Functions:
GradientTweener.Create:
(UIGradient: UIGradient, TweenInf: TweenInfo, NewColorSequence: ColorSequence) -> typeof(GradientTweener.Create())
The constructor function. It allows you to create a tween for a UIGradient.
GetTweenObjectFromGradient:
(UIGradient:UIGradient) -> typeof(GradientTweener.Create())?
Returns the Tween object associated with an UIGradient. If no tween object where for for a specific UIGradient, it returns nil.
Properties:
- self.Instance: UIGradient → The UIGradient that will be tweened
- self.TweenInfo: TweenInfo → The TweenInfo used (cannot be changed during execution)
- self.ColorSequence: ColorSequence → The target color sequence
Methods:
- self:Play() → Plays the tween
- self:Pause() → Pauses the tween
- self:Cancel() → Pauses the tween and put it at its original state
- self:Destroy() → Destroy the tween object
Example:
local GradientTweener = require(game.ReplicatedStorage.GradientTweener)
local frame = script.Parent
--The target ColorSequence
local new_color = ColorSequence.new(
{
ColorSequenceKeypoint.new(0, Color3.fromRGB(0, 0, 255)),
ColorSequenceKeypoint.new(.2, Color3.fromRGB(238, 0, 255)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 0, 4))
}
)
--Creating the tween object
local tween = GradientTweener.Create(
frame.UIGradient,
TweenInfo.new(2, Enum.EasingStyle.Exponential, Enum.EasingDirection.InOut, 0, true, 1),
new_color
)
--We play the tween !
tween:Play()
task.wait(1)
--Pausing the tween
tween:Pause()
task.wait(1)
--Resuming the tween.
tween:Play()
Result:
Advantages:
- Can tween any type of sequence
- Easy to use (If you know how to use TweenService, it uses a similar structure)
Disadvantages:
- No RBXScriptConnection to intercept events (will be fixed in a future update)
Source code (GradientTweener):
local m = {}
local ProgressionFormulas = require(script:WaitForChild("ProgressionFormulas"))
local rs = game:GetService("RunService")
local tweens = {}
tweens.__index = tweens
local createds = {}
type TweenObject = {
["Instance"]: UIGradient,
["TweenInfo"]: TweenInfo,
["ColorSequence"]: ColorSequence,
} & typeof(tweens)
--This function links the new keypoints to the old ones and returns the link table.
local function LinkKeypoints(Old:{ColorSequenceKeypoint}, New:{ColorSequenceKeypoint})
--It works because the amount of points is always equal to the amount of keypoints of the new sequence
--Which means that the table of the old sequence is bijective in the new one (due to the part of code above)
local links: {{{["Time"]:number, ["Value"]:number}}} = {}
--Doing what I said earlier î (this is an up arrow btw)
for index, old in Old do
local new_keypoint = New[index]
table.insert(links, {
{
Time = old.Time,
Value = old.Value
},
{
Time = new_keypoint.Time,
Value = new_keypoint.Value
}
})
end
return links
end
local function ReconcileKeypoints(current_sequence:ColorSequence, new_sequence:ColorSequence)
--This part of the code reconciles the keypoints of the old sequence to the new one
local current_keypoints = current_sequence.Keypoints
local new_keypoints = new_sequence.Keypoints
local current_amount = #current_keypoints
local new_amount = #new_keypoints
--If the amount of keypoints between both is different. We need to add lerped keypoints.
--What this part does is, it will add the missing keypoints to the old sequence so they can also be tweened.
if current_amount ~= new_amount then
--If the new sequence has more keypoints than the old one,
--we need to add the missing keypoints to the old sequence using color lerping.
--Getting the lowest and highest amount of keypoints so we can adjust them to have the same quantity of keypoints
--Tables are using the same address in memory, meaning using variables isn't a problem.
local lowest = current_amount < new_amount and current_keypoints or new_keypoints
local highest = current_amount > new_amount and current_keypoints or new_keypoints
for _, new in highest do
if #lowest == #highest then
break
end
local found = false
local key_time = new.Time
for _, old in lowest do
--If we found a keypoint with the same time, we don't need to do anything. It will be tweened.
if old.Time == key_time then
found = true
break
end
end
--If not keypoints were found in the old sequence, we need to add it.
if not found then
--Adding a new keypoint to the current time and with lerped color from the current sequence
--Blank values.
local previous_keypoint: ColorSequenceKeypoint, next_keypoint:ColorSequenceKeypoint =
ColorSequenceKeypoint.new(0, Color3.fromRGB(255,255,255)),
ColorSequenceKeypoint.new(0, Color3.fromRGB(255,255,255))
--Searching the previous and next keypoints to lerp color between them
for _, kp in lowest do
local t = kp.Time
if t < key_time and previous_keypoint.Time >= t then
previous_keypoint = kp
elseif t > key_time and next_keypoint.Time <= t then
next_keypoint = kp
end
end
--Guessing the keypoint using the time value of the new sequence and color lerping between
--previous and next keypoints
local keypoint_guess = ColorSequenceKeypoint.new(
key_time,
previous_keypoint.Value:Lerp(next_keypoint.Value, key_time)
)
table.insert(lowest, keypoint_guess)
end
end
end
--Sorting here is really important: It helps getting easly which keypoints in the current sequence should be associated
--with which one in the new sequence using time as sorter.
--So even if the keypoints have different time positions, they can still be linked using their indexes.
table.sort(current_keypoints, function(a, b)
return a.Time < b.Time
end)
table.sort(new_keypoints, function(a, b)
return a.Time < b.Time
end)
return {current_keypoints, new_keypoints}
end
--[[
Creates a tween object for the given UIGradient.
Note that if a tween was already inked to the UIGradient, it will be destroyed using <code>:Destroy()</code> method.
]]
function m.Create(SequenceInstance:UIGradient, TI: TweenInfo?, NewColorSequence:ColorSequence?): TweenObject
if not SequenceInstance then
error("GradientTweener:Create() requires a UIGradient object as the first argument")
end
local self = {}
local prev_object = m.GetTweenObjectFromGradient(SequenceInstance)
if prev_object then
prev_object:Destroy()
end
self.Instance = SequenceInstance
self.TweenInfo = TI or TweenInfo.new()
self.ColorSequence = NewColorSequence or ColorSequence.new(Color3.new(1,1,1))
self._tweening_connection = nil
self._global_progression = 0 --The progression of the whole tween, including reverse and delaytime
self._progression = 0 --The progression of the tween, not counting reverse and delaytime
self._playing = false
self._original_sequence = SequenceInstance.Color
table.insert(createds, self)
return setmetatable(self, tweens)
end
function m.GetTweenObjectFromGradient(Gradient:UIGradient): TweenObject
for _, v in createds do
if v.Instance == Gradient then
return v
end
end
end
--Plays or resume the tween.
function tweens:Play(): ()
--Initializing variables
local inst:UIGradient = self.Instance
local tweeninfo:TweenInfo = self.TweenInfo
local playing = self._playing
local current_sequence = inst.Color
--The progression must be overriden since the user called the method more than once.
if playing then
self._global_progression = 0
self._progression = 0
current_sequence = self._original_sequence
end
--Applying changes to the current sequence and the new one so they are easier to manipulate.
self._reconciled_keypoints = ReconcileKeypoints(current_sequence, self.ColorSequence)
local current_keypoints = self._reconciled_keypoints[1]
local new_keypoints = self._reconciled_keypoints[2]
local current_amount = #current_keypoints
local new_amount = #new_keypoints
local links = LinkKeypoints(current_keypoints, new_keypoints)
--Now that we have the same amount of keypoints, we can now start the tweening.
--These two variables are used to determine in which way the tween should go if the TweenInfo.Reverses is enabled.
local current_state = current_keypoints
local goal_state = new_keypoints
local reverses = self.TweenInfo.Reverses
local delay_time = self.TweenInfo.DelayTime
local loops_completed = 0
local target_loop = self.TweenInfo.RepeatCount + 1
--Prevents the tween to step. (Used for the TweenInfo.DelayTime feature))
local disabled = false
--If it reverses, it means that it will do twice more than the amount of times specified in TweenInfo.RepeatCount
if reverses then
target_loop *= 2
end
self._playing = true
self._tweening_connection = rs.Heartbeat:Connect(function(dt)
local progression_step = dt / self.TweenInfo.Time
if reverses then
--If the tween reverses, it means that the progression should take twice longer to reach 1
if delay_time > 0 then
progression_step = dt / ((self.TweenInfo.Time * 2) + delay_time)
else
progression_step = dt / (self.TweenInfo.Time * 2)
end
end
self._global_progression += progression_step
if disabled then return end
self._progression += dt / self.TweenInfo.Time -- 5 is the duration of the tween
if self._global_progression >= 1 then
self._global_progression = 0
end
if self._progression >= 1 then
loops_completed += 1
local disable_tween = false
if self.TweenInfo.RepeatCount ~= -1 then
if loops_completed >= target_loop then
disable_tween = true
end
end
if not disable_tween then
--Swapping the current sequence and the goal sequence
if current_state == current_keypoints then
current_state = new_keypoints
goal_state = current_keypoints
else
current_state = current_keypoints
goal_state = new_keypoints
end
--We do the same thing as before, but we start from the end of the sequence to the old sequence
links = LinkKeypoints(current_state, goal_state)
self._progression = 0
if delay_time > 0 then
disabled = true
task.wait(delay_time)
disabled = false
end
return
end
self._tweening_connection:Disconnect()
self._tweening_connection = nil
--Tweening is done !
return
end
local step_keypoints: {ColorSequenceKeypoint} = {}
local ease_time = ProgressionFormulas[self.TweenInfo.EasingStyle.Name]
[self.TweenInfo.EasingDirection.Name](self._progression)
--Creating the step keypoints to make the step sequence for the ui gradient
for _, diff_data in links do
local old = diff_data[1]
local new = diff_data[2]
--Getting the new keypoint
local new_keypoint = ColorSequenceKeypoint.new(
old.Time + (new.Time - old.Time) * ease_time, --Lerping formula: A * (B - A) * t
old.Value:Lerp(new.Value, ease_time)
)
table.insert(step_keypoints, new_keypoint)
end
inst.Color = ColorSequence.new(step_keypoints)
end)
end
--Pause the tween. It can be resumed using <code>self:Play()</code>
function tweens:Pause(): ()
if self._tweening_connection then
self._tweening_connection:Disconnect()
end
self._playing = false
end
--Cancels the tween. The tween goes back to its initial state. It can be resumed using <code>self:Play()</code>
function tweens:Cancel(): ()
if self._tweening_connection then
self._tweening_connection:Disconnect()
end
self.Instance.Color = self._original_sequence
self._playing = false
end
--Destroys the tween and all its resources. This function automatically fires
--if another tween is created for the same UIGradient.
function tweens:Destroy(): ()
if self._tweening_connection then
self._tweening_connection:Disconnect()
end
local index = table.find(createds, self)
if index then
table.remove(createds, index)
end
setmetatable(self, nil)
self = nil
end
return m
Source code (ProgressionFormulas)
--Most of them are made by the roblox AI, i cant verify if it's correct mb guys
--I'll fix that once I'm less lazy, you can use "https://easings.net/" to easly get those formulas!
local formulas = {
Sine = {
In = function(x:number)
return 1 - math.cos((x * math.pi) / 2)
end,
Out = function(x:number)
return math.sin((x * math.pi) / 2);
end,
InOut = function(x:number)
return -(math.cos(math.pi * x) - 1) / 2
end,
},
Quad = {
In = function(x:number)
return x^2
end,
Out = function(x:number)
return x * (x - 2)
end,
InOut = function(x:number)
if x < 0.5 then
return 2 * x^2
else
return 2 * (2 - x) * x - 1
end
end,
},
Cubic = {
In = function(x:number)
return x^3
end,
Out = function(x:number)
return (x - 1)^3 + 1
end,
InOut = function(x:number)
if x < 0.5 then
return 4 * x^3
else
return 1 + 4 * (x - 1)^3
end
end,
},
Quart = {
In = function(x:number)
return x^4
end,
Out = function(x:number)
return 1 - (1 - x)^4
end,
InOut = function(x:number)
if x < 0.5 then
return 8 * x^4
else
return 1 - 8 * (x - 1)^4
end
end,
},
Quint = {
In = function(x:number)
return x^5
end,
Out = function(x:number)
return (x - 1)^5 + 1
end,
InOut = function(x:number)
if x < 0.5 then
return 16 * x^5
else
return 16 * (x - 1)^5 + 1
end
end,
},
Exponential = {
In = function(x:number)
if x == 0 then
return 0
else
return 2 ^ (10 * (x - 1))
end
end,
Out = function(x:number)
if x == 1 then
return 1
else
return 1 - 2 ^ (-10 * x)
end
end,
InOut = function(x:number)
if x == 0 or x == 1 then
return x
else
if x < 0.5 then
return 2 ^ (20 * x - 10) / 2
else
return 1 - 2 ^ (-20 * x + 10) / 2
end
end
end,
},
Elastic = {
In = function(x:number)
local c4 = (2 * math.pi) / 3
if x == 0 then
return 0
elseif x == 1 then
return 1
else
return -math.pow(2, 10 * x - 10) * math.sin((x * 10 - 10.75) * c4)
end
end,
Out = function(x:number)
local c4 = (2 * math.pi) / 3
if x == 0 then
return 0
elseif x == 1 then
return 1
else
return math.pow(2, -10 * x) * math.sin((x * 10 - 0.75) * c4) + 1
end
end,
InOut = function(x:number)
local c5 = (2 * math.pi) / 4.5
if x == 0 then
return 0
elseif x == 1 then
return 1
elseif x < 0.5 then
return -(math.pow(2, 20 * x - 10) * math.sin((20 * x - 11.125) * c5)) / 2
else
return (math.pow(2, -20 * x + 10) * math.sin((20 * x - 11.125) * c5)) / 2 + 1
end
end,
},
Bounce = {
In = function(x:number)
local n1 = 7.5625
local d1 = 2.75
if x < 1 / d1 then
return n1 * x * x
elseif x < 2 / d1 then
x = x - 1.5 / d1
return n1 * x * x + 0.75
elseif x < 2.5 / d1 then
x = x - 2.25 / d1
return n1 * x * x + 0.9375
else
x = x - 2.625 / d1
return n1 * x * x + 0.984375
end
end,
Out = function(x:number)
local function InBounce(y:number)
local n1 = 7.5625
local d1 = 2.75
if y < 1 / d1 then
return n1 * y * y
elseif y < 2 / d1 then
y = y - 1.5 / d1
return n1 * y * y + 0.75
elseif y < 2.5 / d1 then
y = y - 2.25 / d1
return n1 * y * y + 0.9375
else
y = y - 2.625 / d1
return n1 * y * y + 0.984375
end
end
return 1 - InBounce(1 - x)
end,
InOut = function(x:number)
local function OutBounce(x:number)
local function InBounce(y:number)
local n1 = 7.5625
local d1 = 2.75
if y < 1 / d1 then
return n1 * y * y
elseif y < 2 / d1 then
y = y - 1.5 / d1
return n1 * y * y + 0.75
elseif y < 2.5 / d1 then
y = y - 2.25 / d1
return n1 * y * y + 0.9375
else
y = y - 2.625 / d1
return n1 * y * y + 0.984375
end
end
return 1 - InBounce(1 - x)
end
if x < 0.5 then
return (1 - OutBounce(1 - 2 * x)) / 2
else
return (1 + OutBounce(2 * x - 1)) / 2
end
end,
},
Circular = {
In = function(x:number)
return -1 * (math.sqrt(1 - x * x) - 1)
end,
Out = function(x:number)
return math.sqrt(1 - math.pow(x - 1, 2));
end,
InOut = function(x:number)
if x < 0.5 then
return -math.sqrt(1 - x * x) / 2
else
return (math.sqrt(1 - (x - 1) * (x - 1)) - 1) / 2
end
end,
},
}
return formulas
(I hope the comments I’ve put are understandable
)
Let me know what you think ! ![]()