4 UIGradient Animations (Including Rainbow!)

Return

Hey y’all!

Before we start on this wonderful tutorial, I just wanted to say...


Fun fact: tomorrow’s Cinco de Mayo, what a wonderful collection of special days!

Today’s obviously Star Wars Day, the famous date May the Fourth is here!

Anyways, this tutorial is dedicated to teaching you about some satisfying gradient animations (among which is a rainbow all the at the end, don’t skip there!).

For all of these animations, we’ll be tweening the gradient’s offset via the TweenService.

I apologize, in advance, for the low GIF FPS.


Shine

  1. Set up your Color Sequence with the start and finish with the primary color (pink) and the very middle with a lighter counterpart of the primary color (light pink). In addition, set the rotation to something slanted to make the shine look better, something like 45 degrees is perfect.

In a Local Script, we’ll tween the offset to make it look like the middle light part is going from left to right.

  1. Start off by declaring some variables.
local button = script.Parent
local gradient = button.UIGradient
local ts = game:GetService("TweenService") 
local ti = TweenInfo.new(1, Enum.EasingStyle.Circular, Enum.EasingDirection.Out)
local offset1 = {Offset = Vector2.new(1, 0)}
local create = ts:Create(gradient, ti, offset1)
local startingPos = Vector2.new(-1, 0) --start on the right, tween to the left so it looks like the shine went from left to right
local addWait = 2.5 --the amount of seconds between each couplet of shines
  1. Add a recursive function after resetting the offset of the gradient:
gradient.Offset = startingPos

local function animate()
	
	create:Play()
	create.Completed:Wait() --wait for tween to stop
	gradient.Offset = startingPos --reset offset
	create:Play() --play again (I did this only 2 times per "couplet", you can do it more times if you want)
	create.Completed:Wait()
	gradient.Offset = startingPos
	wait(addWait) --wait some bit before the next couplet
	animate() --call itself to make this into a loop
	
end

animate() --but we still need to initially call it

And that is it! The shine gradient animations is compelted! That was the easiest of them all.


Hover


Because the GIF’s so laggy, I’ll explain what’s going on here. So, when I hover over a button the solid color button switches to a gradient. Upon leaving, it tweens to the opposite side to a different color. So, the button is now a new color. And it tweens back after hovering over and then leaving the button.

  1. Just create Color Sequence with at least 3 different colors (2 is fine, but it’s better with 3). I did yellow, green, blue.

  2. Again, declare our variable.

local button = script.Parent
local gradient = button.UIGradient
local ts = game:GetService("TweenService")
local ti = TweenInfo.new(1, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
local s, kpt, ran = ColorSequence.new, ColorSequenceKeypoint.new, math.random
local offset1 = {Offset = Vector2.new(0, 0)} --hover in, so that the gradient is visible
local offset2 = {Offset = Vector2.new(-1, 0)} --hover out, but to one side
local offset3 = {Offset = Vector2.new(1, 0)} --hover out, but to another side
local create1 = ts:Create(gradient, ti, offset1)
local create2 = ts:Create(gradient, ti, offset2)
local create3 = ts:Create(gradient, ti, offset3)
local startPos = Vector2.new(-1, 0) --start off in a solid color
local hover = 1 --used to indicate which offset value it will tween to to get a different color upon the mouse leaving the button

--[[

We'll use modulus with the "hover" variable to check if the number is even or odd, 
if even then tween to one side, else tween to the other

]]
  1. Add in the functions connected to the MouseEnter and MouseLeave events.
button.MouseEnter:Connect(function()
	
	create1:Play() --tween to the middle/the area with the gradient
	
end)

button.MouseLeave:Connect(function()
	
--[[if even, then tween to the right, else, tween to the left]]

	if hover %  2 == 0 then create2:Play() else create3:Play() end
	
	hover = hover + 1 --increase hover to switch hover out colors
	
end)

And we’re done with this 2nd easiest gradient animation!


Hover Stay

  1. For this animation, you literally just need two colors in the Color Sequence.

  2. Again, declare some variables, first:

local button = script.Parent
local gradient = button.UIGradient
local ts = game:GetService("TweenService")
local ti = TweenInfo.new(2.5, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local offset1 = {Offset = Vector2.new(-1, 0)}
local create1 = ts:Create(gradient, ti, offset1)
local startPos = Vector2.new(1, 0)
local rot = 180 --starting gradient rotation

This is why we’re using the gradient’s rotation: when we stay hovering over the button, the gradient will eventually move to a solid color. And that’s the moment when we flip the rotation, either 180 or 0, so that the color we just passed to reach this solid color will be right after. Then, we repeat this with the other color.

  1. Reset the rotation and offset and create the hover in function:
gradient.Offset = startPos
gradient.Rotation = 0

button.MouseEnter:Connect(function()
	
	button.BorderSizePixel = 2 --just to show that the mouse is currently in
	create1:Play()
	
end)
  1. Now, we need to create the function that actually runs this animation when the previous tween has been completed:
local function completed()
		
    --border size pixel comming in handy because we only animate the gradient when the mouse is hovering over

	if button.BorderSizePixel == 2 and rot == 0 then
		
		gradient.Rotation = 180
		gradient.Offset = startPos
		create1:Play()	
		
	elseif button.BorderSizePixel == 2 and rot == 180 then --this will be run first
		
		gradient.Rotation = 0
		gradient.Offset = startPos
		create1:Play()
		
	end
		
end
  1. We need to run the function above initially with an event and also need to end off by including the MouseLeave event:
create1.Completed:Connect(function() 
	
    --flip rotation
	if rot == 0 then rot = 180 elseif rot == 180 then rot = 0 end
	completed()
	
end)

button.MouseLeave:Connect(function()
	
	button.BorderSizePixel = 3 --some different value to indicate that button is not being hovered over
	create1:Pause() --pause the tween right there, and it will be played from there when tween:Play() is called
	
end)

Finally, we’re done with this part. Now, onto…

RAINBOW!

There are several ways to create a rainbow, but after a lot of experimenting, I finally reached the result that was the most fluent and where the colors blended together very well.

Plan of Action:

We’ll do the rotation flipping as we did with the hover stay animation. There are going to be 3 key points, so it will be a little more difficult to code than 2 key points, but the result is better! We need to include many if statements because we’ll be using a table with random color values, but indexing with a number greater than the number of elements spits out an error. Believe me, I tried to optimize the script as much as possible, but this is the shortest I got it to. Also, the main reason it’s long is that it’s a little repetitive (which I also tried to solve, but I’m unsuccessful), which is why I’ll leave out comments for the later parts. With that being said, let’s begin:

  1. Of course, start with a gradient consisting of 3 key points.

  2. Variable declaration:

local button = script.Parent
local gradient = button.UIGradient
local ts = game:GetService("TweenService")
local ti = TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local offset = {Offset = Vector2.new(1, 0)}
local create = ts:Create(gradient, ti, offset)
local startingPos = Vector2.new(-1, 0)
local list = {} --list of random colors (we'll be generating them shortly after)
local s, kpt = ColorSequence.new, ColorSequenceKeypoint.new
local counter = 0 --count the last table index we just indexed/last gradient color reference
local status = "down" --[[there will be two groups of if statements (one above and one below). 
It glitches out some times and runs the same group multiple times, so we need this. ]] 
  1. Initialization (random colors creation, setup of the gradient, etc.):
gradient.Offset = startingPos --reset the offset of the gradient

local function rainbowColors()
	
    --[[HSV uses values 0-1, but we'll use until 255 and divide later to 
    better control the colors.]]

	local sat, val = 255, 255 
	
	for i = 1, 15 do --15 is a multiple of 255
		
		local hue = i * 17 --255/15 = 17
		table.insert(list, Color3.fromHSV(hue / 255, sat / 255, val / 255)) --divide by 255 to be in range of 0-1
		
	end
	
end

rainbowColors() --add to the list table

--set up the first gradient 
gradient.Color = s({

	kpt(0, list[#list]),
	kpt(0.5, list[#list - 1]),
	kpt(1, list[#list - 2])

})

counter = #list --max indexed is #list, which is 10 in this instance
  1. Now, onto the repetitive, but rather a simple part:
local function animate()
	
	create:Play()
	create.Completed:Wait() --wait for tween completion
	gradient.Offset = startingPos 
	gradient.Rotation = 180 --flip time!
	
    --[[#list - 1 because we have 3 key points, 1 will be preserved from 
    the previous tween, while the other 2 are new. We need to make 
    sure that indexing something beyond #list doesn't happen as it will 
    throw an error. In this instance, it will be 9, 10, and instead of it looking
    for 11, it will go back to 1.]]
	if counter == #list - 1 and status == "down" then
		
		gradient.Color = s({
	
			kpt(0, gradient.Color.Keypoints[1].Value), --preserve previous color, which we'll be able to see
			kpt(0.5, list[#list]), --change this color behind the scenes!
			kpt(1, list[1]) --change this color behind the scenes!
	
		})
		
		counter = 1 --last index is 1 i.e. list[1]
		status = "up" --the upper section already ran, time for the lower!
		
	elseif counter == #list and status == "down" then --if the current counter is exactly 10 (in this instance), then it will go back to 1 and 2
		
		gradient.Color = s({
	
			kpt(0, gradient.Color.Keypoints[1].Value),
			kpt(0.5, list[1]),
			kpt(1, list[2])
	
		})
	
		counter = 2
		status = "up"
		
	elseif counter <= #list - 2 and status == "down" then  --in all other cases, when couter is 1-8
		
		gradient.Color = s({
	
			kpt(0, gradient.Color.Keypoints[1].Value),
			kpt(0.5, list[counter + 1]), --one color over
			kpt(1, list[counter + 2]) --two colors over
	
		})
		
		counter = counter + 2
		status = "up"
	
	end
	
	create:Play()
	create.Completed:Wait()
	gradient.Offset = startingPos
	gradient.Rotation = 0 --flip time again!
	
	if counter == #list - 1 and status == "up" then --same as before, really, but instead of "down", it's "up", since the upper section already ran
		
		gradient.Color = s({
		    
            --descending order because now it's rotation 0
			kpt(0, list[1]), --1
			kpt(0.5, list[#list]), --10
			kpt(1, gradient.Color.Keypoints[3].Value) --put this at #3 because we just flipped rotation, so this color will be at the opposite side
				
		})
		
		counter = 1
		status = "down" --below section already ran, back to the top!
		
	 elseif counter == #list and status == "up" then
		
		gradient.Color = s({
		
			kpt(0, list[2]), --2
			kpt(0.5, list[1]), --1
			kpt(1, gradient.Color.Keypoints[3].Value) --10
				
		})
		
		counter = 2
		status = "down"
	
	elseif counter <= #list - 2 and status == "up" then --in all other cases like before
		
		gradient.Color = s({
		
			kpt(0, list[counter + 2]), 
			kpt(0.5, list[counter + 1]), 
			kpt(1, gradient.Color.Keypoints[3].Value) 
				
		})
		
		counter = counter + 2
		status = "down"
	
	end
	
	animate() --call the function inside of itself, so that it runs indefinitely
	
end

animate() --call the function initially 

Congrats! You just coded one of the most difficult UIGradient animations. If you have any tips to shortern my code, then feel free to share via DM.


Closing Remarks

Download the .RBXM file that is a StarterGui of all the gradient animations discussed in this tutorial and upload it under ScreenGui. Note: code is uncommented.

GradientAnimations.rbxm (8.7 KB)

Please give your feedback, too:

From a scale of 1-10, how useful is this to you and your game/future game? 1 being you’re never going to use these, 10 being you’re 100% thinking about doing so! All of these animations are meant to be applied to GUI buttons to emphasize and style them.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

Did you learn anything new? Any new animation, any new way of coding, even the Tween Service, anything.

  • Yes
  • No

0 voters


May the Fourth be with you,
and have a colorful day!

92 Likes

Wow, really nice tutorial!
I hope this will be helping other developers who want their game to have a good GUI.
thanks for the help we really appreciated your work :+1:

2 Likes

This is really cool especially since it’s something sort of new to roblox; gradients. You did an amazing job on animations (smooth) and making this open sourced.

I don’t know if this is me or applies to a lot of users; gradients don’t fit “my theme” of user interface. They just don’t look right having them as a button because of the way they look.

2 Likes

This is a really good tutorial

3 Likes

Thank you for this tutorial on gradients! Helped me out a lot.

1 Like

This is so useful! Thank you for this I’ll be using it for my future games.

1 Like

This is a great guide! Learned a lot from it.

Although I think there’s a much nicer way to do rainbows (it does require a loop rather than TweenService). For those interested, you can read more about it on this site explaining rainbows and some colour theory behind them, as well as how to implement them (it’s in JavaScript, not Lua). The code carries over quite easily and it’s pretty easy to follow along in case you don’t know JS.

The overhead is fairly simple, if you have a UIGradient all you need to do is the following:

local RS = game:GetService("RunService")

local rainbow = script.Parent  -- GUI object
local grad = rainbow.UIGradient

local counter = 0       -- phase shift 
local w = math.pi / 12  -- frequency 
local CS = {}           -- colorsequence table
local num = 15 			-- number of colorsequence points (maxed at 15 or 16 I believe)
local frames = 0		-- frame counter, used to buffer if you want lower framerate updates

RS.Heartbeat:Connect(function()
	if math.fmod(frames, 2) == 0 then
		for i = 0, num do
			local c = Color3.fromRGB(127 * math.sin(w*i + counter) + 128, 127 * math.sin(w*i + 2 * math.pi/3 + counter) + 128, 127*math.sin(w*i + 4*math.pi/3 + counter) + 128)
			table.insert(CS, i+1, ColorSequenceKeypoint.new(i/num, c))
		end
		grad.Color = ColorSequence.new(CS)
		CS = {}
		counter = counter + math.pi/40
		if (counter >= math.pi * 2) then
			counter = 0
		end
	end
	if frames >= 1000 then
		frames = 0
	end
	frames = frames + 1
end)

It’s different than how a TweenService implementation would look like, but in a way the update loop every frame is in itself a custom Tweening (it’s linear anyways, so TweenService doesn’t really add functionality). It’s much shorter this way, and if you wanted to get a hover effect that changes directions, all you need to do is set up a state system (again, different from TweenService - instead of hovering calling a certain Tween into action, we make a custom state system using booleans or by changing the variables directly if they’re in the scope). This would involve something like:

rainbow.MouseEnter:Connect(function()
    w = -math.pi/12
end)
rainbow.MouseLeave:Connect(function()
    w = math.pi/12
end)

And thus you get the same hover effect. I think it’s a lot cleaner for how little code is required (thanks to sinusoidals). I still think the Tweening method is more elegant, maybe I’ll look into optimizing that so it takes less code to write. Although I’m thinking it’s probably within reach since TweenService has a sine easing style…

Looks like I have some work to do.

5 Likes

Today I learned we need a lot more 10 bit displays.

2 Likes

Will we be expecting a module with alot of UIgradient animations?

2 Likes

Interesting idea. I did think of this before but I thought that there were so many possibilities and ways to customize them that having preset ones in a module. Plus, these animations are mostly easy and they’re in a tutorial so everyone should be able to know how to create them. However, I suppose that I can maybe create such a module.

2 Likes

Don’t mind if i do. Yoink! Thanks for that

1 Like

Wow, this is amazing! Wasn’t looking for this, but now I’ll be sure to use this for future projects!

1 Like

At first, I would like to thank you for this awesome idea. This is really cool and might help me earn x2.

I’m facing a problem with HoverStay. The script is same as yours, but-

create1.Completed:Connect(function() 
	if rot == 0 then rot = 180 elseif rot == 180 then rot = 0 end
	completed()	-- Here, the completed word is underlined and it says that "W001: Unknow global 'completed' "
end)

It works when it’s hovered for the first time, but after it stops - it doesn’t work anymore.

Would be really helpful if you could help. Anyways, if anyone wanna see it live in a game, play this game or watch a GIF at gyazo

I also made a HoverShine effect.

1 Like

Cool tutorial! I learned a few things from this!

1 Like

I really loved @tralalah’s code, but for some reason it was taking up about 0.3% in the script performance analyzer for a single instance every few hundred milliseconds. I decided to pre-cache all of the ColorSequenceKeypoints, since they repeat, and minimize the actual loop that runs during the heartbeat. I also added some more comments to help those who are more beginners understand what is going on. I am going to expand this to be a general “rainbowifier” where you can inject any UIGradient into the process. It should be pretty trivial to make all gradients animate the same or set a random offset and have them all be different. I will post it to this thread when I finish it.

local RS = game:GetService("RunService")

local rainbow = script.Parent  -- GUI object
local grad = rainbow.UIGradient

local counter = 0       -- phase shift {def = 0}
local w = math.pi / 6  -- frequency [the lower the number the tighter the rainbow] {def = 12}
local CS = {}           -- colorsequence table
local num = 15 			-- number of colorsequence points (maxed at 16) [provides a more granular rainbow] {def = 15}
local frames = 0		-- frame counter, used to buffer if you want lower framerate updates {def = 0}

local count = 0
local cskCache = {}

while true do
	-- build a ColorSequenceKeyPoint 
	for i = 0, num do
		local c = Color3.fromRGB(127 * math.sin(w*i + counter) + 128, 127 * math.sin(w*i + 2 * math.pi/3 + counter) + 128, 127*math.sin(w*i + 4*math.pi/3 + counter) + 128)
		table.insert(CS, i+1, ColorSequenceKeypoint.new(i/num, c))
	end
	local newCS = ColorSequence.new(CS)
	
	-- build the cache	
	if #cskCache > 0 then
		if newCS == cskCache[1] then
			CS = {}
			break
		end
	end
	table.insert(cskCache, newCS)
	
	-- clear out the CS table	
	CS = {}
	
	-- this counter effectively sets the total cache to 80 ColorSequences (countStart = 0, countIncrement = math.pi/40, countMax = 2*math.pi)
	counter = counter + math.pi/40
	if (counter >= math.pi * 2) then counter = 0 end	
end

local finalCacheCt = #cskCache
local rotation = 1

RS.Heartbeat:Connect(function()	
	if math.fmod(frames, 2) == 0 then
		-- set the new gradient.
		grad.Color = cskCache[rotation]		
		-- reset rotation once we have iterated through all the frames		
		if rotation >= #cskCache then rotation = 0 end
		-- move to the next frame
		rotation = rotation + 1				
	end
	-- controls when the frame fires based off the fmod() above. We reset to prevent the number from endlessly increasing
	if frames >= 1000 then frames = 0 end
	frames = frames + 1
end)
2 Likes

The loop itself is pretty heavy, calling table.insert and resetting the UIGradient by calling a new ColorSequence every 2 frames will take its toll. Thanks for cleaning up the code and expanding on it :slight_smile:


There is one improvement I made in the meantime that I think is quite important for those who don’t want to cache (idk, maybe for memory constraints or something) - reset the color sequence table CS = {} before the for i = 0, num do loop when you are running this on a RunService event. Sometimes if the loop is running behind, a new event will fire before the previous one is finished (which means, CS = {} might not be called before it starts adding new elements to the table). Calling it at the start of the loop is safer and doesn’t change anything performance wise (in theory).

1 Like

If you guys can’t find the animation you want here, I really recommend this plugin. Great tutorial though! :grinning: