General purpose modules for Roblox development. [Read-only Codeberg mirror]
1--!optimize 2
2--!strict
3local RunService = game:GetService("RunService")
4
5type TweenSequenceItem = number | Tween | { Tween } | () -> ()
6export type TweenSequenceType = { TweenSequenceItem }
7
8type CurrentState = {
9 thread: thread?,
10 index: number,
11 elapsed: number,
12 heartbeat: RBXScriptConnection?,
13 activeTweenIndices: { number },
14}
15
16type TweenSequenceImpl = {
17 __index: TweenSequenceImpl,
18 __tostring: (self: TweenSequence) -> string,
19 new: (sequence: TweenSequenceType) -> TweenSequence,
20 _connectHeartbeat: (self: TweenSequence) -> (),
21 _disconnectHeartbeat: (self: TweenSequence) -> (),
22 _runItem: (self: TweenSequence, item: TweenSequenceItem) -> (),
23 _resumeItem: (self: TweenSequence, item: TweenSequenceItem) -> (),
24 Play: (self: TweenSequence) -> (),
25 Pause: (self: TweenSequence) -> (),
26 Resume: (self: TweenSequence) -> (),
27}
28
29export type TweenSequence = typeof(setmetatable(
30 {} :: {
31 _sequence: TweenSequenceType,
32 _current: CurrentState,
33 Playing: boolean,
34 },
35 {} :: TweenSequenceImpl
36 ))
37
38local TweenSequence = {} :: TweenSequenceImpl
39TweenSequence.__index = TweenSequence
40
41-- Creates a new <code>TweenSequence</code> from a sequence of items.
42-- Items can be <code>number</code> (wait), <code>Tween</code>, <code>{Tween}</code> (parallel), or <code>() -> ()</code> (callback).
43function TweenSequence.new(sequence: TweenSequenceType): TweenSequence
44 local self = setmetatable({}, TweenSequence)
45 self._sequence = sequence
46 self.Playing = false
47 self._current = {
48 thread = nil,
49 index = 0,
50 elapsed = 0,
51 heartbeat = nil,
52 activeTweenIndices = {},
53 }
54 return self
55end
56
57function TweenSequence:_connectHeartbeat()
58 self._current.heartbeat = RunService.Heartbeat:Connect(function(dt: number)
59 self._current.elapsed += dt
60 end)
61end
62
63function TweenSequence:_disconnectHeartbeat()
64 if self._current.heartbeat then
65 self._current.heartbeat:Disconnect()
66 self._current.heartbeat = nil
67 end
68end
69
70function TweenSequence:_runItem(item: TweenSequenceItem)
71 if typeof(item) == "number" then
72 task.wait(item)
73 elseif typeof(item) == "table" then
74 self._current.activeTweenIndices = {}
75 for tweenIndex, tween in item do
76 tween:Play()
77 table.insert(self._current.activeTweenIndices, tweenIndex)
78 tween.Completed:Connect(function()
79 local idx = table.find(self._current.activeTweenIndices, tweenIndex)
80 if idx then
81 table.remove(self._current.activeTweenIndices, idx)
82 end
83 end)
84 end
85 local longest: Tween = item[1]
86 for _, tween in item do
87 if tween.TweenInfo.Time > longest.TweenInfo.Time then
88 longest = tween
89 end
90 end
91 longest.Completed:Wait()
92 elseif typeof(item) == "function" then
93 item()
94 else
95 item:Play()
96 item.Completed:Wait()
97 end
98end
99
100function TweenSequence:_resumeItem(item: TweenSequenceItem)
101 if typeof(item) == "number" then
102 local remaining = item - self._current.elapsed
103 if remaining > 0 then
104 task.wait(remaining)
105 end
106 elseif typeof(item) == "table" then
107 for _, tweenIndex in self._current.activeTweenIndices do
108 item[tweenIndex]:Play()
109 end
110 if #self._current.activeTweenIndices > 0 then
111 local longest: Tween = item[self._current.activeTweenIndices[1]]
112 for _, tweenIndex in self._current.activeTweenIndices do
113 local tween: Tween = item[tweenIndex]
114 if tween.TweenInfo.Time > longest.TweenInfo.Time then
115 longest = tween
116 end
117 end
118 longest.Completed:Wait()
119 end
120 else
121 local tween = item :: Tween
122 tween:Play()
123 tween.Completed:Wait()
124 end
125end
126
127-- Plays the sequence from the beginning.
128-- Sets <code>Playing</code> to <code>true</code> for the duration of playback.
129function TweenSequence:Play()
130 self._current.thread = task.spawn(function()
131 self.Playing = true
132 for index, item in self._sequence do
133 self._current.index = index
134 self._current.elapsed = 0
135 self._current.activeTweenIndices = {}
136 self:_connectHeartbeat()
137 self:_runItem(item)
138 self:_disconnectHeartbeat()
139 end
140 self.Playing = false
141 end)
142end
143
144-- Pauses the sequence at the current step.
145-- Pauses any actively playing <code>Tween</code> instances at that step.
146function TweenSequence:Pause()
147 self:_disconnectHeartbeat()
148 if self._current.thread then
149 task.cancel(self._current.thread)
150 self._current.thread = nil
151 end
152 local item = self._sequence[self._current.index]
153 if item then
154 if typeof(item) == "table" then
155 for _, tween in item do
156 if tween.PlaybackState == Enum.PlaybackState.Playing then
157 tween:Pause()
158 end
159 end
160 elseif typeof(item) ~= "number" and typeof(item) ~= "function" then
161 item:Pause()
162 end
163 end
164 self.Playing = false
165end
166
167-- Resumes the sequence from where it was paused.
168-- Completes the remaining duration of the current step before advancing.
169function TweenSequence:Resume()
170 if self._current.index == 0 or self.Playing then
171 return
172 end
173 self._current.thread = task.spawn(function()
174 self.Playing = true
175 local startIndex = self._current.index
176 self:_connectHeartbeat()
177 self:_resumeItem(self._sequence[startIndex])
178 self:_disconnectHeartbeat()
179 for index = startIndex + 1, #self._sequence do
180 self._current.index = index
181 self._current.elapsed = 0
182 self._current.activeTweenIndices = {}
183 self:_connectHeartbeat()
184 self:_runItem(self._sequence[index])
185 self:_disconnectHeartbeat()
186 end
187 self.Playing = false
188 end)
189end
190
191function TweenSequence:__tostring(): string
192 return ("TweenSequence(Playing=%s, step=%d/%d)"):format(
193 tostring(self.Playing),
194 self._current.index,
195 #self._sequence
196 )
197end
198
199return {
200 Create = TweenSequence.new,
201}