This is a module used to animate any Instances or tables. Roblox’s TweenService API isn’t great for playing simple, quick animations so I wrote my own animator module a few years ago. I recently rewrote it to be typed and now releasing it for anyone else to use. If you find any issues with this module, please let me know
Usage
-- Require the Anymate module
local Anymate = require(game.ReplicatedStorage.Anymate)
--[[
Use Anymate.Play to play an animation
An animation in anymate is just a table with some values
Here is a simple animation that turns the baseplate white over 1 second
]]
print(`Turning baseplate white`)
Anymate.Play({
Object = workspace.Baseplate,
Properties = {
{Key = "Color", Value = Color3.fromRGB(255, 255, 255)}
}
})
task.wait(1)
--[[
Use Anymate.PlaySync to play an animation and yield until the animation is complete
You can define an EasingStyle, EasingDirection, and Duration to change how the animation looks
]]
print(`Turning baseplate blue`)
Anymate.PlaySync({
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Quint,
EasingDirection = Enum.EasingDirection.Out,
Duration = 2.5,
Properties = {
{Key = "Color", Value = Color3.fromRGB(0, 0, 255)}
}
})
--[[
You can set the animation ID to stop any other animations with this same ID from playing
When you fire Anymate.Play, any animations with the same ID will automatically stop playing
]]
print(`Turning baseplate white again`)
Anymate.Play({
ID = `BaseplateAnimation`,
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Quint,
EasingDirection = Enum.EasingDirection.Out,
Duration = 2.5,
Properties = {
{Key = "Color", Value = Color3.fromRGB(255, 255, 255)}
}
})
task.delay(1, print, `SIKE turning it red`)
-- This will interrupt the previous animation after 1 second and turn the baseplate red
task.delay(1, Anymate.Play, {
ID = `BaseplateAnimation`,
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Sine,
EasingDirection = Enum.EasingDirection.In,
Duration = 1,
Properties = {
{Key = "Color", Value = Color3.fromRGB(255, 0, 0)}
}
})
--[[
You can define a BeforeStep, OnStep, and OnStop callback in the animation
The BeforeStep callback will fire before the properties are updated every frame
The OnStep callback will fire after the propertes are updated every frame
The OnStop callback will fire when the animation stops playing, including when it's interrupted
The second argument passed to the OnStop callback is a boolean which tells you if the animation was interrupted
]]
task.delay(3, Anymate.Play, {
ID = `BaseplateAnimation`,
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Elastic,
EasingDirection = Enum.EasingDirection.InOut,
Duration = 1.5,
Properties = {
{Key = `Color`, Value = Color3.fromRGB(0, 255, 0)},
},
BeforeStep = function(Animation, AnimationTime)
print(`(A) Firing BeforeStep callback`, Animation, AnimationTime)
end,
OnStep = function(Animation, AnimationTime)
print(`(A) Firing OnStep callback`, Animation, AnimationTime)
end,
OnStop = function(Animation, DidComplete)
print(`(A) Firing OnStop callback`, Animation, DidComplete)
end,
})
-- Interrupt the previous animation
task.delay(3.5, Anymate.Play, {
ID = `BaseplateAnimation`,
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Elastic,
EasingDirection = Enum.EasingDirection.InOut,
Duration = 2,
Properties = {
{Key = `Color`, Value = Color3.fromRGB(70, 70, 70)},
{Key = `Transparency`, Value = 1},
},
BeforeStep = function(Animation, AnimationTime)
print(`(B) Firing BeforeStep callback`, Animation, AnimationTime)
end,
OnStep = function(Animation, AnimationTime)
print(`(B) Firing OnStep callback`, Animation, AnimationTime)
end,
OnStop = function(Animation, DidComplete)
print(`(B) Firing OnStop callback`, Animation, DidComplete)
end,
})
--[[
The Object, EasingStyle, EasingDirection, and Duration can be overridden per property
]]
Anymate.PlaySync({
ID = `BaseplateAnimation`,
Object = workspace.Baseplate,
EasingStyle = Enum.EasingStyle.Quint,
EasingDirection = Enum.EasingDirection.Out,
Duration = 2.5,
Properties = {
{Key = "Color", Value = Color3.fromRGB(255, 255, 255)},
{Key = "Color", Value = Color3.fromRGB(0, 255, 255), Object = workspace.Baseplate2, Duration = 0.5},
{Key = "Color", Value = Color3.fromRGB(0, 255, 0), Object = workspace.Baseplate3, EasingStyle = Enum.EasingStyle.Circular},
},
})
Get it from the creator store
Source
Anymate.rbxm (7.2 KB)
export type Animation = {
ID: any?, -- The ID of the animation. Used to stop multiple animations with the same ID from playing at the same time
Object: Instance | {[any]: any}?, -- The object to animate
EasingStyle: Enum.EasingStyle | string, -- The easing style to use on the animation
EasingDirection: Enum.EasingDirection | string, -- The easing direction to use on the animation
Duration: number, -- How long the animation will take to complete
-- Array of properties to update on this animation object, or the object defined in the property data
Properties: {{
Key: string, -- The name of the property to update
Value: any, -- The value to updat the property to
Object: Instance | {[any]: any}?, -- [OPTIONAL] The object to apply the value to. Will override the object defined in animation
EasingStyle: Enum.EasingStyle | string?, -- [OPTIONAL] The easing style to use on the animation. Will override the object defined in animation
EasingDirection: Enum.EasingDirection | string, -- [OPTIONAL] The easing direction to use on the animation. Will override the object defined in animation
Duration: number?, -- How long the animation will take to complete. Animation will stop after longest defined duration
}},
-- Async callback fired before animation properties are updated every frame
BeforeStep: (Animation: Animation, AnimationTime: number) -> (),
-- Async callback fired after animation properties are updated every frame
OnStep: (Animation: Animation, AnimationTime: number) -> (),
-- Callback fired when animation stops playing
OnStop: (Animation: Animation, DidComplete: boolean) -> (),
}
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Anymate = {
ActiveAnimations = {} :: {[any]: Animation},
}
-- Cache for warning when invalid types are lerped
local InvalidTypeCache = {}
local function ClearInvalidTypeCache(Type)
InvalidTypeCache[Type] = nil
end
local BuiltInLerp = {"CFrame", "Vector2", "Vector3", "Color3", "UDim2"}
-- Function to lerp any value from start to end. If lerping unsupported value, will return start value if delta < 0.5, otherwise returns endvalue
-- Delta should be between 0 and 1
function Anymate.Lerp<T>(StartValue: T, EndValue: T, Delta: number): T
-- Validate delta
if (typeof(Delta) ~= "number") then
warn(`Failed to lerp '{StartValue}' to '{EndValue}', delta '{Delta}' must be a number`)
return StartValue
end
-- Validate type
local Type = typeof(StartValue)
if (Type ~= typeof(EndValue)) then
return if (Delta < 0.5) then StartValue else EndValue
end
-- Lerp
if (table.find(BuiltInLerp, Type)) then
return StartValue:Lerp(EndValue, Delta)
elseif (Type == "number") then
return StartValue + ((EndValue - StartValue) * Delta)
elseif (Type == "string") then
if (EndValue:match(`^{StartValue}`)) then
return EndValue:sub(#StartValue, Anymate.Lerp(#StartValue, #EndValue, Delta))
else
local TotalLen = #StartValue + #EndValue
local ClearRatio = TotalLen > 0 and (#StartValue / TotalLen) or 0
if (Delta < ClearRatio) then
return StartValue:sub(0, Anymate.Lerp(#StartValue, 0, Delta / ClearRatio))
else
return EndValue:sub(0, Anymate.Lerp(0, #EndValue, (Delta - ClearRatio) / (#EndValue / TotalLen)))
end
end
elseif (Type == "UDim") then -- Why no built in lerp for UDim?
return UDim.new(
Anymate.Lerp(StartValue.Scale, EndValue.Scale, Delta),
Anymate.Lerp(StartValue.Offset, EndValue.Offset, Delta)
)
end
-- Warn if an invalid type
if (not InvalidTypeCache[Type]) then
InvalidTypeCache[Type] = true
warn(`Type '{Type}' cannot be smoothly tweened`)
task.delay(1, ClearInvalidTypeCache, Type)
end
return if (Delta < 0.5) then StartValue else EndValue
end
-- Get animation playing with this ID
function Anymate.Get(ID: any): Animation?
return Anymate.ActiveAnimations[ID] or nil
end
-- Create a base animation object
function Anymate.Create(Animation: Animation): Animation
if (not Animation) then
Animation = {}
end
return Animation
end
local function EndAnimation(Animation: Animation, DidComplete: boolean)
-- Disconnect
if (Animation.UpdateConnection) then
Animation.UpdateConnection:Disconnect()
Animation.UpdateConnection = nil
end
-- Clear
if (Animation.ID and Anymate.ActiveAnimations[Animation.ID] == Animation) then
Anymate.ActiveAnimations[Animation.ID] = nil
end
-- Stop playing
Animation.IsPlaying = nil
-- Resume yielding threads
local YieldingThreads = Animation.YieldingThreads
if (YieldingThreads) then
Animation.YieldingThreads = nil
for _, Thread in YieldingThreads do
task.spawn(Thread, Animation, DidComplete)
end
table.clear(YieldingThreads)
end
-- Fire on stop
if (Animation.OnStop) then
local Success, Response = pcall(Animation.OnStop, Animation, DidComplete)
if (not Success) then
warn(`Failed to fire OnStop on animation {Animation.ID}, {Response}`)
end
end
end
-- Play the animation
function Anymate.Play(Animation: Animation): Animation
if (Animation.ID) then
-- Stop any animation currently playing with this ID
Anymate.Stop(Animation.ID)
-- Mark as active
Anymate.ActiveAnimations[Animation.ID] = Animation
end
-- Get start values
Animation.StartTime = os.clock()
Animation.IsPlaying = true
if (Animation.Properties) then
for _, PropertyData in Animation.Properties do
local Object = PropertyData.Object or Animation.Object
PropertyData.StartValue = Object[PropertyData.Key]
end
end
-- Safeguard to make sure last one is disconnected
if (Animation.UpdateConnection) then
Animation.UpdateConnection:Disconnect()
Animation.UpdateConnection = nil
end
-- Start updater
Animation.UpdateConnection = RunService.PreRender:Connect(function(DeltaTime)
-- Get animation time
local Now = os.clock()
local AnimationTime = math.max(0, Now - Animation.StartTime)
-- Fire before step
if (Animation.BeforeStep) then
task.spawn(Animation.BeforeStep, Animation, AnimationTime)
end
-- Update properties
local ArePropertiesAnimating = false
if (Animation.Properties) then
for _, PropertyData in Animation.Properties do
-- Get scaled time
local Duration = PropertyData.Duration or Animation.Duration or 1
local ScaledTime = if (Duration <= 0) then 1 else math.max(0, AnimationTime / Duration)
-- Animate scaled time
local EasingStyle = PropertyData.EasingStyle or Animation.EasingStyle
if (EasingStyle) then
local EasingDirection = PropertyData.EasingDirection or Animation.EasingDirection or Enum.EasingDirection.Out
ScaledTime = TweenService:GetValue(ScaledTime, EasingStyle, EasingDirection)
end
-- Update object
local Object = PropertyData.Object or Animation.Object
Object[PropertyData.Key] = Anymate.Lerp(PropertyData.StartValue, PropertyData.Value, math.clamp(ScaledTime, 0, 1))
-- Dont stop if not finished animating
if (AnimationTime < Duration) then
ArePropertiesAnimating = true
end
end
end
-- Fire on step
if (Animation.OnStep) then
task.spawn(Animation.OnStep, Animation, AnimationTime)
end
-- Stop if no properties animating
if (not ArePropertiesAnimating) then
EndAnimation(Animation, true)
end
end)
return Animation
end
-- Play an animation and yield until its complete
function Anymate.PlaySync(Animation: Animation): Animation
-- Add to array of yielding threads
if (not Animation.YieldingThreads) then
Animation.YieldingThreads = {}
end
table.insert(Animation.YieldingThreads, coroutine.running())
-- Play the animation
Anymate.Play(Animation)
-- Yield until its done
coroutine.yield()
end
-- Stop any animation playing with the passed ID
function Anymate.Stop(ID: any): Animation?
local Animation = Anymate.ActiveAnimations[ID]
if (Animation) then
EndAnimation(Animation, false)
return Animation
end
end
return Anymate
Example 1
local Anymate = require(game.ReplicatedStorage.Anymate)
local SpawnLocation = workspace:WaitForChild("SpawnLocation")
Anymate.Play({
ID = `Test`,
Object = SpawnLocation,
EasingStyle = Enum.EasingStyle.Quad,
EasingDirection = Enum.EasingDirection.Out,
Duration = 5,
Properties = {
{Key = `Color`, Value = Color3.fromRGB(255, 69, 69)},
{Key = `Transparency`, Value = 1},
},
BeforeStep = function(Animation, AnimationTime)
print(`Before step 1`)
end,
OnStep = function(Animation, AnimationTime)
print(`Step 1`)
end,
OnStop = function(Animation, DidComplete)
print(`Stop 1`, Animation, DidComplete)
end,
})
task.delay(2, Anymate.Play, {
ID = `Test`,
Object = SpawnLocation,
EasingStyle = Enum.EasingStyle.Quad,
EasingDirection = Enum.EasingDirection.Out,
Duration = 2.1,
Properties = {
{Key = `Color`, Value = Color3.fromRGB(0, 255, 0)},
{Key = `Transparency`, Value = 0},
{Key = "CFrame", Value = SpawnLocation.CFrame * CFrame.fromEulerAnglesYXZ(0, math.rad(45), 0)},
},
BeforeStep = function(Animation, AnimationTime)
print(`Before step 2`)
end,
OnStep = function(Animation, AnimationTime)
print(`Step 2`)
end,
OnStop = function(Animation, DidComplete)
print(`Stop 2`, Animation, DidComplete)
end,
})
Example 2
local Anymate = require(game.ReplicatedStorage.Anymate)
Anymate.Play({
Object = {String = `Welcome!`},
EasingStyle = Enum.EasingStyle.Quint,
EasingDirection = Enum.EasingDirection.Out,
Duration = 0.5,
Properties = {
{Key = "String", Value = "I hope you enjoy your time playing the game!"}
},
OnStep = function(Animation)
print(Animation.Object.String)
end,
})
Example 3
local Anymate = require(game.ReplicatedStorage.Anymate)
local CoinDisplayUI = {
DisplayValue = 0,
}
-- Create reusable animation for the CoinDisplayUI object
local DisplayValueAnimation = Anymate.Create({
ID = `UpdateCoinDisplay`,
Object = CoinDisplayUI,
EasingStyle = Enum.EasingStyle.Quint,
EasingDirection = Enum.EasingDirection.Out,
Duration = 0.5,
Properties = {
{Key = "DisplayValue", Value = 500}
},
OnStep = function()
print(`Coins: {math.round(CoinDisplayUI.DisplayValue)}`)
-- TextLabel.Text = `Coins: {CoinDisplayUI.DisplayValue}`
end,
})
-- Play animation whenever player's Coins attribute changes
local Player = game.Players.LocalPlayer
local function UpdateCoinDisplay()
-- Update target value of the animation to current Coins value
DisplayValueAnimation.Properties[1].Value = Player:GetAttribute("Coins") or 0
-- Play animation
Anymate.Play(DisplayValueAnimation)
end
Player:GetAttributeChangedSignal("Coins"):Connect(UpdateCoinDisplay)