--!optimize 2
--!strict
local RunService = game:GetService("RunService")
type TweenSequenceItem = number | Tween | { Tween } | () -> ()
export type TweenSequenceType = { TweenSequenceItem }
type CurrentState = {
thread: thread?,
index: number,
elapsed: number,
heartbeat: RBXScriptConnection?,
activeTweenIndices: { number },
}
type TweenSequenceImpl = {
__index: TweenSequenceImpl,
__tostring: (self: TweenSequence) -> string,
new: (sequence: TweenSequenceType) -> TweenSequence,
_connectHeartbeat: (self: TweenSequence) -> (),
_disconnectHeartbeat: (self: TweenSequence) -> (),
_runItem: (self: TweenSequence, item: TweenSequenceItem) -> (),
_resumeItem: (self: TweenSequence, item: TweenSequenceItem) -> (),
Play: (self: TweenSequence) -> (),
Pause: (self: TweenSequence) -> (),
Resume: (self: TweenSequence) -> (),
}
export type TweenSequence = typeof(setmetatable(
{} :: {
_sequence: TweenSequenceType,
_current: CurrentState,
Playing: boolean,
},
{} :: TweenSequenceImpl
))
local TweenSequence = {} :: TweenSequenceImpl
TweenSequence.__index = TweenSequence
-- Creates a new TweenSequence from a sequence of items.
-- Items can be number (wait), Tween, {Tween} (parallel), or () -> () (callback).
function TweenSequence.new(sequence: TweenSequenceType): TweenSequence
local self = setmetatable({}, TweenSequence)
self._sequence = sequence
self.Playing = false
self._current = {
thread = nil,
index = 0,
elapsed = 0,
heartbeat = nil,
activeTweenIndices = {},
}
return self
end
function TweenSequence:_connectHeartbeat()
self._current.heartbeat = RunService.Heartbeat:Connect(function(dt: number)
self._current.elapsed += dt
end)
end
function TweenSequence:_disconnectHeartbeat()
if self._current.heartbeat then
self._current.heartbeat:Disconnect()
self._current.heartbeat = nil
end
end
function TweenSequence:_runItem(item: TweenSequenceItem)
if typeof(item) == "number" then
task.wait(item)
elseif typeof(item) == "table" then
self._current.activeTweenIndices = {}
for tweenIndex, tween in item do
tween:Play()
table.insert(self._current.activeTweenIndices, tweenIndex)
tween.Completed:Connect(function()
local idx = table.find(self._current.activeTweenIndices, tweenIndex)
if idx then
table.remove(self._current.activeTweenIndices, idx)
end
end)
end
local longest: Tween = item[1]
for _, tween in item do
if tween.TweenInfo.Time > longest.TweenInfo.Time then
longest = tween
end
end
longest.Completed:Wait()
elseif typeof(item) == "function" then
item()
else
item:Play()
item.Completed:Wait()
end
end
function TweenSequence:_resumeItem(item: TweenSequenceItem)
if typeof(item) == "number" then
local remaining = item - self._current.elapsed
if remaining > 0 then
task.wait(remaining)
end
elseif typeof(item) == "table" then
for _, tweenIndex in self._current.activeTweenIndices do
item[tweenIndex]:Play()
end
if #self._current.activeTweenIndices > 0 then
local longest: Tween = item[self._current.activeTweenIndices[1]]
for _, tweenIndex in self._current.activeTweenIndices do
local tween: Tween = item[tweenIndex]
if tween.TweenInfo.Time > longest.TweenInfo.Time then
longest = tween
end
end
longest.Completed:Wait()
end
else
local tween = item :: Tween
tween:Play()
tween.Completed:Wait()
end
end
-- Plays the sequence from the beginning.
-- Sets Playing to true for the duration of playback.
function TweenSequence:Play()
self._current.thread = task.spawn(function()
self.Playing = true
for index, item in self._sequence do
self._current.index = index
self._current.elapsed = 0
self._current.activeTweenIndices = {}
self:_connectHeartbeat()
self:_runItem(item)
self:_disconnectHeartbeat()
end
self.Playing = false
end)
end
-- Pauses the sequence at the current step.
-- Pauses any actively playing Tween instances at that step.
function TweenSequence:Pause()
self:_disconnectHeartbeat()
if self._current.thread then
task.cancel(self._current.thread)
self._current.thread = nil
end
local item = self._sequence[self._current.index]
if item then
if typeof(item) == "table" then
for _, tween in item do
if tween.PlaybackState == Enum.PlaybackState.Playing then
tween:Pause()
end
end
elseif typeof(item) ~= "number" and typeof(item) ~= "function" then
item:Pause()
end
end
self.Playing = false
end
-- Resumes the sequence from where it was paused.
-- Completes the remaining duration of the current step before advancing.
function TweenSequence:Resume()
if self._current.index == 0 or self.Playing then
return
end
self._current.thread = task.spawn(function()
self.Playing = true
local startIndex = self._current.index
self:_connectHeartbeat()
self:_resumeItem(self._sequence[startIndex])
self:_disconnectHeartbeat()
for index = startIndex + 1, #self._sequence do
self._current.index = index
self._current.elapsed = 0
self._current.activeTweenIndices = {}
self:_connectHeartbeat()
self:_runItem(self._sequence[index])
self:_disconnectHeartbeat()
end
self.Playing = false
end)
end
function TweenSequence:__tostring(): string
return ("TweenSequence(Playing=%s, step=%d/%d)"):format(
tostring(self.Playing),
self._current.index,
#self._sequence
)
end
return {
Create = TweenSequence.new,
}