Cool new more advanced tool came out, use this! Tween Anything!
Features:
All tween styles and directions,
Tweens to the nearest timestamp value but doesn’t change the original timestamp
Tweens color, size, and transparency
Cons:
Script yields to function to cancel this you can just use: task.spawn()
task.spawn documentation
Instead of just calling the function
Main Script Here v
Script
local function lerp(a, b, t)
return a + (b - a) * t
end
function lerpColor3(a, b, t)
local lerpedColor = Color3.new(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t
)
return lerpedColor
end
local function tweenSequence(sequence, targetSequence, smoothness, timeTaken, objectToUpdate, propertyName, easeStyle, easeDirection)
local sequenceType = false
if typeof(sequence) == "NumberSequence" then
sequenceType = "NumberSequence"
elseif typeof(sequence) == "ColorSequence" then
sequenceType = "ColorSequence"
end
assert(smoothness and type(smoothness) == "number" and smoothness > 0, "Invalid smoothness")
assert(timeTaken and type(timeTaken) == "number" and timeTaken > 0, "Invalid timeTaken")
assert(type(objectToUpdate[propertyName]) == "userdata" , "Invalid objectToUpdate")
assert(propertyName and type(propertyName) == "string", "Invalid propertyName")
if sequenceType == "NumberSequence" then
local keypoints = sequence.Keypoints
local targetKeypoints = targetSequence.Keypoints
local originalTimes = {}
local originalValues = {}
local originalEnvelopes = {}
for _, keypoint in ipairs(keypoints) do
table.insert(originalTimes, keypoint.Time)
table.insert(originalValues, keypoint.Value)
table.insert(originalEnvelopes, keypoint.Envelope)
end
local function updateNumberSequence(progress)
local newKeypoints = {}
for i, originalTime in ipairs(originalTimes) do
local closestTargetKeypointIndex = 1
local closestTimeDifference = math.abs(originalTime - targetKeypoints[1].Time)
for j, targetKeypoint in ipairs(targetKeypoints) do
local timeDifference = math.abs(originalTime - targetKeypoint.Time)
if timeDifference < closestTimeDifference then
closestTimeDifference = timeDifference
closestTargetKeypointIndex = j
end
end
local targetValue = targetKeypoints[closestTargetKeypointIndex].Value
local targetEnvelope = targetKeypoints[closestTargetKeypointIndex].Envelope
local newValue = lerp(originalValues[i], targetValue, progress)
local newEnvelope = lerp(originalEnvelopes[i], targetEnvelope, progress)
table.insert(newKeypoints, NumberSequenceKeypoint.new(originalTime, newValue, newEnvelope))
end
return NumberSequence.new(newKeypoints)
end
for t = 0, 1, 1 / (smoothness * timeTaken) do
local ta = game.TweenService:GetValue(t, easeStyle, easeDirection)
local newNumberSequence = updateNumberSequence(ta)
objectToUpdate[propertyName] = newNumberSequence
task.wait(1 / smoothness)
end
local newKeypoints = {}
for i, originalTime in ipairs(originalTimes) do
local closestTargetKeypointIndex = 1
local closestTimeDifference = math.abs(originalTime - targetKeypoints[1].Time)
for j, targetKeypoint in ipairs(targetKeypoints) do
local timeDifference = math.abs(originalTime - targetKeypoint.Time)
if timeDifference < closestTimeDifference then
closestTimeDifference = timeDifference
closestTargetKeypointIndex = j
end
end
local targetValue = targetKeypoints[closestTargetKeypointIndex].Value
local targetEnvelope = targetKeypoints[closestTargetKeypointIndex].Envelope
table.insert(newKeypoints, NumberSequenceKeypoint.new(originalTime, targetValue, targetValue))
end
objectToUpdate[propertyName] = NumberSequence.new(newKeypoints)
elseif sequenceType == "ColorSequence" then
local keypoints = sequence.Keypoints
local targetKeypoints = targetSequence.Keypoints
local originalTimes = {}
local originalValues = {}
for _, keypoint in ipairs(keypoints) do
table.insert(originalTimes, keypoint.Time)
table.insert(originalValues, keypoint.Value)
end
local function updateColorSequence(progress)
local newKeypoints = {}
for i, originalTime in ipairs(originalTimes) do
local closestTargetKeypointIndex = 1
local closestTimeDifference = math.abs(originalTime - targetKeypoints[1].Time)
for j, targetKeypoint in ipairs(targetKeypoints) do
local timeDifference = math.abs(originalTime - targetKeypoint.Time)
if timeDifference < closestTimeDifference then
closestTimeDifference = timeDifference
closestTargetKeypointIndex = j
end
end
local targetValue = targetKeypoints[closestTargetKeypointIndex].Value
local newValue = lerpColor3(originalValues[i], targetValue, progress)
table.insert(newKeypoints, ColorSequenceKeypoint.new(originalTime, newValue))
end
return ColorSequence.new(newKeypoints)
end
for t = 0, 1, 1 / (smoothness * timeTaken) do
local ta = game.TweenService:GetValue(t, easeStyle, easeDirection)
local newColorSequence = updateColorSequence(ta)
objectToUpdate[propertyName] = newColorSequence
task.wait(1 / smoothness)
end
local newKeypoints = {}
for i, originalTime in ipairs(originalTimes) do
local closestTargetKeypointIndex = 1
local closestTimeDifference = math.abs(originalTime - targetKeypoints[1].Time)
for j, targetKeypoint in ipairs(targetKeypoints) do
local timeDifference = math.abs(originalTime - targetKeypoint.Time)
if timeDifference < closestTimeDifference then
closestTimeDifference = timeDifference
closestTargetKeypointIndex = j
end
end
local targetValue = targetKeypoints[closestTargetKeypointIndex].Value
table.insert(newKeypoints, ColorSequenceKeypoint.new(originalTime, targetValue, targetValue))
end
objectToUpdate[propertyName] = ColorSequence.new(newKeypoints)
end
end
-- fancy changeable values down here
local objectToUpdate = game.Workspace.Partt.ParticleEmitter
local attributeTweened = "Transparency"
local sequence = objectToUpdate[attributeTweened] -- don't change
local targetSequence = NumberSequence.new({
NumberSequenceKeypoint.new(0,0,0),
NumberSequenceKeypoint.new(1,0,0),
})
--[[ for color
local targetSequence = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1,0,0)),
ColorSequenceKeypoint.new(1, Color3.new(0,0,1))
})
]]
local smoothness = 50 -- How smooth the tweening less smoothing = more choppy
local timeTaken = 5 -- In seconds
local easeStyle = Enum.EasingStyle.Exponential
local easeDirection = Enum.EasingDirection.Out
wait(3)
print('started')
task.spawn(tweenSequence, sequence, targetSequence, smoothness, timeTaken, objectToUpdate, attributeTweened, easeStyle, easeDirection)
Video Demonstration
ExponentialOut 5-second tween
Looks lame but it’s never been done before so it’s cool I guess…
code used for video
local objectToUpdate = game.Workspace.Part.ParticleEmitter
local attributeTweened = "Transparency"
local sequence = objectToUpdate[attributeTweened]
local targetSequence = NumberSequence.new({
NumberSequenceKeypoint.new(0,0,0),
NumberSequenceKeypoint.new(1,0,0),
})
local smoothness = 50 -- How smooth the tweening less smoothing = more choppy
local timeTaken = 5 -- In seconds
local easeStyle = Enum.EasingStyle.Exponential
local easeDirection = Enum.EasingDirection.Out
wait(3)
print('started')
task.spawn(tweenSequence, sequence, targetSequence, smoothness, timeTaken, objectToUpdate, attributeTweened, easeStyle, easeDirection)
wait(1)
targetSequence = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1,0,0)),
ColorSequenceKeypoint.new(1, Color3.new(0,0,1))
})
attributeTweened = "Color"
sequence = objectToUpdate[attributeTweened] -- always make sure to re-evaluate this after changing attributeTweened
task.spawn(tweenSequence, sequence, targetSequence, smoothness, timeTaken, objectToUpdate, attributeTweened, easeStyle, easeDirection)
Credits:
@Katrist: Small script timing optimization
@InKrnl: Module script version (I don’t know if it works because I forgot how to module)
Features: Info
Module Script
-- SERVICES --
local TS = game:GetService("TweenService")
-- AUXILIARY FUNCTIONS --
local function lerp(a, b, t)
return a + (b - a) * t
end
local function interpolateKeypoints(kp1, kp2, t)
return NumberSequenceKeypoint.new(
lerp(kp1.Time, kp2.Time, t),
lerp(kp1.Value, kp2.Value, t),
lerp(kp1.Envelope, kp2.Envelope, t)
)
end
local function getNewValues(progress: number, originalSequence : NumberSequence, targetSequence: NumberSequence)
local newKeypoints = {}
local originalKeypoints = originalSequence.Keypoints
local targetKeypoints = targetSequence.Keypoints
local originalNumKeypoints = #originalKeypoints
local targetNumKeypoints = #targetKeypoints
for i = 1, math.max(originalNumKeypoints, targetNumKeypoints) do
local originalIndex = math.floor((i - 1) * (originalNumKeypoints - 1) / (targetNumKeypoints - 1)) + 1
local targetIndex = i
if originalIndex < 1 then
originalIndex = 1
elseif originalIndex > originalNumKeypoints then
originalIndex = originalNumKeypoints
end
local originalKeypoint = originalKeypoints[originalIndex]
local targetKeypoint = targetKeypoints[targetIndex]
local t = progress
local newTime = lerp(originalKeypoint.Time, targetKeypoint.Time, t)
local newValue = lerp(originalKeypoint.Value, targetKeypoint.Value, t)
local newEnvelope = lerp(originalKeypoint.Envelope, targetKeypoint.Envelope, t)
table.insert(newKeypoints, NumberSequenceKeypoint.new(newTime, newValue, newEnvelope))
end
return NumberSequence.new(newKeypoints)
end
-- TYPES --
type TweenObject = {
PlaybackState : Enum.PlaybackState,
Cancel : (TweenObject) -> TweenObject,
Play : (TweenObject) -> TweenObject,
Pause : (TweenObject) -> TweenObject,
Destroy : (TweenObject) -> nil,
Completed : RBXScriptSignal,
Paused : RBXScriptSignal,
}
-- MAIN --
local Tween = {}
Tween.__index = Tween
function Tween.new(Object : NumberSequence | ParticleEmitter | Beam, TargetSequence : NumberSequence, Info : TweenInfo, PropertyName : string?) : TweenObject
local ObjectType : 'NumberSequence' | 'Instance' = 'NumberSequence'
if not Info or typeof(Info) ~= 'TweenInfo' then warn("TweenInfo can't be NIL!") return end
if typeof(Object) ~= 'NumberSequence' then
if typeof(Object) ~= 'Instance' or not Object:IsA("ParticleEmitter") and not Object:IsA("Beam") then warn("Invalid object") return end
if not PropertyName or typeof(PropertyName) ~= 'string' then warn("Invalid property name: ", PropertyName) return end
ObjectType = "Instance" -- If the property does not exist in the instance, this will likely error
if typeof(Object[PropertyName]) ~= 'NumberSequence' then warn("Invalid property type: ", typeof(Object)) return end
end
local self = setmetatable({}, Tween)
self.PlaybackState = Enum.PlaybackState.Begin
local completedEvent = Instance.new("BindableEvent")
local pausedEvent = Instance.new("BindableEvent")
self.Completed = completedEvent.Event
self.Paused = pausedEvent.Event
local startTick = tick()
local endTime = startTick + Info.Time
local pauseTime = tick()
local activeTask : thread? = nil
local originalValues = {}
for i, keypoint : NumberSequenceKeypoint in ipairs(ObjectType == 'NumberSequence' and Object.Keypoints or Object[PropertyName].Keypoints) do
table.insert(originalValues, keypoint)
end
local originalSequenceCopy = NumberSequence.new(originalValues)
function self:Cancel()
if not self.PlaybackState or self.PlaybackState == Enum.PlaybackState.Completed or self.PlaybackState == Enum.PlaybackState.Cancelled then return self end
self.PlaybackState = Enum.PlaybackState.Cancelled
return self
end
function self:Destroy()
self:Cancel()
if activeTask then
task.cancel(activeTask)
activeTask = nil
end
pausedEvent:Destroy()
completedEvent:Destroy()
self.Completed = nil
self.Paused = nil
return self
end
function self:Play()
if not self.PlaybackState or self.PlaybackState == Enum.PlaybackState.Completed or self.PlaybackState == Enum.PlaybackState.Cancelled then warn(1) return self end
if self.PlaybackState == Enum.PlaybackState.Paused then
local currentTime = tick()
local timePaused = currentTime - pauseTime
startTick = startTick + timePaused
endTime = endTime + timePaused
elseif self.PlaybackState == Enum.PlaybackState.Begin then
startTick = tick()
endTime = startTick + Info.Time
end
self.PlaybackState = Enum.PlaybackState.Playing
if not activeTask then
activeTask = task.spawn(function()
while tick() < endTime do
if tick() >= endTime then break end
if self.PlaybackState == Enum.PlaybackState.Cancelled then break end
if self.PlaybackState == Enum.PlaybackState.Paused then
task.wait()
continue
end
local timePassed = tick() - startTick
local currentProgress = math.clamp(timePassed / Info.Time, 0, 1)
currentProgress = :GetValue(currentProgress, Info.EasingStyle, Info.EasingDirection)
local newSequence = getNewValues(currentProgress, originalSequenceCopy, TargetSequence)
if ObjectType == 'NumberSequence' then
Object = newSequence
else
Object[PropertyName] = newSequence
end
task.wait()
end
if self.PlaybackState == Enum.PlaybackState.Playing or self.PlaybackState == Enum.PlaybackState.Cancelled then
self.PlaybackState = Enum.PlaybackState.Completed
completedEvent:Fire()
end
end)
end
return self
end
function self:Pause()
if self.PlaybackState == Enum.PlaybackState.Playing then
self.PlaybackState = Enum.PlaybackState.Paused
pauseTime = tick()
pausedEvent:Fire()
end
return self
end
return self
end
return Tween
Dear ROBLOX,
Please add this as a normal feature instead of these ~150 lines of code.