diff --git a/Docs/FadeUi.md b/Docs/FadeUi.md new file mode 100644 index 0000000..1f9b356 --- /dev/null +++ b/Docs/FadeUi.md @@ -0,0 +1,47 @@ +# FadeUi + +Fade UI elements in and out using `TweenService`. Automatically traverses all descendant `GuiBase` elements and tweens their transparency properties. + +## Methods + +### `FadeOut` + +`FadeOut(ui: GuiBase, transparencyInfo: TweenInfo, waitForEnd: boolean)` → `void` + +Fades out all descendant `GuiBase` elements of the given UI container by tweening their transparency properties to `1`. + +Handles the following instance types: + +- `TextLabel`, `TextButton`, `TextBox`: tweens `BackgroundTransparency` and `TextTransparency` +- `ImageLabel`, `ImageButton`: tweens `BackgroundTransparency` and `ImageTransparency` +- `Frame`: tweens `BackgroundTransparency` + +#### Parameters + +| Name | Type | Required | +| ---------------- | ----------- | -------- | +| ui | `GuiBase` | Yes | +| transparencyInfo | `TweenInfo` | Yes | +| waitForEnd | `boolean` | Yes | + +--- + +### `FadeIn` + +`FadeIn(ui: GuiBase, transparencyInfo: TweenInfo, waitForEnd: boolean)` → `void` + +Fades in all descendant `GuiBase` elements of the given UI container by tweening their transparency properties to `0`. + +Handles the following instance types: + +- `TextLabel`, `TextButton`, `TextBox`: tweens `BackgroundTransparency` and `TextTransparency` +- `ImageLabel`, `ImageButton`: tweens `BackgroundTransparency` and `ImageTransparency` +- `Frame`: tweens `BackgroundTransparency` + +#### Parameters + +| Name | Type | Required | +| ---------------- | ----------- | -------- | +| ui | `GuiBase` | Yes | +| transparencyInfo | `TweenInfo` | Yes | +| waitForEnd | `boolean` | Yes | diff --git a/Docs/TweenSequence.md b/Docs/TweenSequence.md new file mode 100644 index 0000000..9bc9160 --- /dev/null +++ b/Docs/TweenSequence.md @@ -0,0 +1,92 @@ +# TweenSequence + +Play a sequence of tweens, waits, parallel tween groups, and callbacks with support for pausing and resuming mid-sequence. + +## Types + +### `TweenSequenceType` + +``` +{ number | Tween | {Tween} | () -> () } +``` + +A sequence is an ordered array of items. Each item is processed one at a time (except parallel groups), in the order it appears. Supported item types: + +| Type | Behaviour | +| ------------- | ----------------------------------------------------------------------------------------------- | +| `number` | Waits for that many seconds before advancing. | +| `Tween` | Plays the tween and waits for it to complete before advancing. | +| `{Tween}` | Plays all tweens in parallel. Waits for the longest one to complete before advancing. | +| `() -> ()` | Calls the function immediately and advances. No waiting. | + +--- + +## Constructor + +### `Create` + +`Create(sequence: TweenSequenceType)` → `TweenSequence` + +Creates a new `TweenSequence` from the provided sequence. Does not begin playback automatically. + +#### Parameters + +| Name | Type | Required | +| -------- | -------------------- | -------- | +| sequence | `TweenSequenceType` | Yes | + +#### Returns + +| Type | +| --------------- | +| `TweenSequence` | + +#### Example + +```lua +local seq = TweenSequence.Create({ + tweenA, -- plays tweenA, waits for completion + 1.5, -- waits 1.5 seconds + { tweenB, tweenC }, -- plays tweenB and tweenC in parallel, waits for longest + function() print("done!") end, +}) +seq:Play() +``` + +--- + +## Properties + +### `Playing` + +`Playing: boolean` + +`true` while the sequence is actively running. Set to `false` when the sequence completes or is paused. + +--- + +## Methods + +### `Play` + +`Play()` → `void` + +Plays the sequence from the beginning. Each item in the sequence is processed in order. + +--- + +### `Pause` + +`Pause()` → `void` + +Pauses the sequence at the current step. Any actively playing `Tween` instances at that step are also paused. The elapsed time within the current step is retained for use by [`Resume`](#resume). + +--- + +### `Resume` + +`Resume()` → `void` + +Resumes the sequence from where it was paused. Completes the remaining duration of the current step (accounting for elapsed time) before advancing to subsequent steps. + +Does nothing if the sequence is already playing or has not been started. diff --git a/Modules/FadeUi.luau b/Modules/FadeUi.luau new file mode 100644 index 0000000..39b1c2f --- /dev/null +++ b/Modules/FadeUi.luau @@ -0,0 +1,55 @@ +--!optimize 2 +--!strict +local module = {} + +local TweenService = game:GetService("TweenService") + +-- Fades out all descendant GuiBase elements of the given UI container. +-- Tweens BackgroundTransparency, TextTransparency, and ImageTransparency to 1. +-- Optionally waits for the tween to finish before returning. +function module.FadeOut(ui: GuiBase, transparencyInfo: TweenInfo, waitForEnd: boolean) + local uisToFade = {} + for _, v in ipairs(ui:GetDescendants()) do + if v:IsA("GuiBase") then + table.insert(uisToFade, v) + end + end + for _, v in ipairs(uisToFade) do + if v:IsA("TextLabel") or v:IsA("TextButton") or v:IsA("TextBox") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 1, TextTransparency = 1 }):Play() + elseif v:IsA("ImageLabel") or v:IsA("ImageButton") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 1, ImageTransparency = 1 }):Play() + elseif v:IsA("Frame") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 1 }):Play() + end + end + if waitForEnd == true then + task.wait(transparencyInfo.Time) + end +end + +-- Fades in all descendant GuiBase elements of the given UI container. +-- Tweens BackgroundTransparency, TextTransparency, and ImageTransparency to 0. +-- Optionally waits for the tween to finish before returning. +function module.FadeIn(ui: GuiBase, transparencyInfo: TweenInfo, waitForEnd: boolean) + local uisToFade = {} + for _, v in ipairs(ui:GetDescendants()) do + if v:IsA("GuiBase") then + table.insert(uisToFade, v) + end + end + for _, v in ipairs(uisToFade) do + if v:IsA("TextLabel") or v:IsA("TextButton") or v:IsA("TextBox") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 0, TextTransparency = 0 }):Play() + elseif v:IsA("ImageLabel") or v:IsA("ImageButton") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 0, ImageTransparency = 0 }):Play() + elseif v:IsA("Frame") then + TweenService:Create(v, transparencyInfo, { BackgroundTransparency = 0 }):Play() + end + end + if waitForEnd == true then + task.wait(transparencyInfo.Time) + end +end + +return module diff --git a/Modules/TweenSequence.luau b/Modules/TweenSequence.luau new file mode 100644 index 0000000..9733e31 --- /dev/null +++ b/Modules/TweenSequence.luau @@ -0,0 +1,201 @@ +--!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, +}