Anymate - Animate anything

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)
7 Likes

Hi, any vidros ? Or a .rbxl file to play around with ? I would like to check it out ! Thsnks

4 Likes

well, there’s a rbxm so :person_shrugging:
You can just remove it if you dont like it.

1 Like

just saying it is easy to open up the whole .rbxl , with examples and start playing… experimenting, expanding… either way… thanks, just another day in the hood