--!strict local TweenService = game:GetService("TweenService") export type Kind = "Fade" | "Slide" | "Wipe" | "Circle" | "Bars" export type State = "Idle" | "Covering" | "Covered" | "Revealing" | "Destroyed" export type Callback = () -> () export type PresetOptions = { Kind: Kind?, Duration: number?, Hold: number?, Style: Enum.EasingStyle?, Color: Color3?, Rotation: number?, Softness: number?, Count: number?, Stagger: number?, } export type Preset = { Kind: Kind, Duration: number, Hold: number, Style: Enum.EasingStyle, Color: Color3, Rotation: number, Softness: number, Count: number, Stagger: number, } export type PresetName = "Fade" | "FadeWhite" | "Flash" | "SlideLeft" | "SlideRight" | "SlideUp" | "SlideDown" | "WipeRight" | "WipeDown" | "SoftWipe" | "Circle" | "Bars" | "Blinds" type Data = { State: State, Host: GuiBase2d, Settings: Preset, Root: Frame, Panels: { Frame }, Gradient: UIGradient?, Running: { Tween }, Thread: thread?, Connections: { RBXScriptConnection }, } local Transition = {} Transition.__index = Transition export type Transition = typeof(setmetatable({} :: Data, Transition)) const Middle = UDim2.fromScale(0.5, 0.5) const Black = Color3.new(0, 0, 0) const White = Color3.new(1, 1, 1) local function Freeze(Value: T): T return (table.freeze :: any)(Value) end local function MakePreset(Options: PresetOptions): Preset local Settings: Preset = { Kind = Options.Kind or ("Fade" :: Kind), Duration = math.max(Options.Duration or 0.5, 0), Hold = math.max(Options.Hold or 0.15, 0), Style = Options.Style or Enum.EasingStyle.Quad, Color = Options.Color or Black, Rotation = Options.Rotation or 0, Softness = math.clamp(Options.Softness or 0.1, 0.001, 0.49), Count = math.max(math.floor(Options.Count or 8), 1), Stagger = math.clamp(Options.Stagger or 0.4, 0, 0.9), } return Freeze(Settings) end local function DirectionFrom(Rotation: number): Vector2 local Angle = math.rad(Rotation) return Vector2.new(math.round(math.cos(Angle) * 1e3) / 1e3, math.round(math.sin(Angle) * 1e3) / 1e3) end local function EdgeFade(Softness: number, Covering: boolean): NumberSequence local From = if Covering then 0 else 1 local To = 1 - From return NumberSequence.new({ NumberSequenceKeypoint.new(0, From), NumberSequenceKeypoint.new(0.5 - Softness, From), NumberSequenceKeypoint.new(0.5 + Softness, To), NumberSequenceKeypoint.new(1, To), }) end local function NewRoot(Host: GuiBase2d): Frame local Root = Instance.new("Frame") Root.Name = "Transition" Root.BackgroundTransparency = 1 Root.BorderSizePixel = 0 Root.ClipsDescendants = true Root.Size = UDim2.fromScale(1, 1) Root.ZIndex = 100 Root.Active = true Root.Visible = false Root.Parent = Host return Root end local function NewPanel(Root: Frame, Color: Color3): Frame local Panel = Instance.new("Frame") Panel.Name = "Panel" Panel.BackgroundColor3 = Color Panel.BorderSizePixel = 0 Panel.AnchorPoint = Vector2.new(0.5, 0.5) Panel.Position = Middle Panel.Size = UDim2.fromScale(1, 1) Panel.Parent = Root return Panel end local function BuildPanels(Root: Frame, Settings: Preset): ({ Frame }, UIGradient?) if Settings.Kind == "Bars" then local Bars = {} :: { Frame } for _ = 1, Settings.Count do table.insert(Bars, NewPanel(Root, Settings.Color)) end return Bars, nil end local Panel = NewPanel(Root, Settings.Color) if Settings.Kind == "Circle" then local Corner = Instance.new("UICorner") Corner.CornerRadius = UDim.new(0.5, 0) Corner.Parent = Panel elseif Settings.Kind == "Wipe" then local Gradient = Instance.new("UIGradient") Gradient.Rotation = Settings.Rotation Gradient.Parent = Panel return { Panel }, Gradient end return { Panel }, nil end local function TweensFor(self: Transition, Covering: boolean): { Tween } local Settings = self.Settings local Easing = if Covering then Enum.EasingDirection.Out else Enum.EasingDirection.In local function TweenTo(Target: Instance, Goal: { [string]: any }, Time: number?, Delay: number?): Tween local Info = TweenInfo.new(Time or Settings.Duration, Settings.Style, Easing, 0, false, Delay or 0) return TweenService:Create(Target, Info, Goal) end local Panel = self.Panels[1] if Settings.Kind == "Fade" then if Covering then Panel.BackgroundTransparency = 1 end return { TweenTo(Panel, { BackgroundTransparency = if Covering then 0 else 1 }) } elseif Settings.Kind == "Slide" then local Dir = DirectionFrom(Settings.Rotation) if Covering then Panel.Position = UDim2.fromScale(0.5 - Dir.X, 0.5 - Dir.Y) end local Goal = if Covering then Middle else UDim2.fromScale(0.5 + Dir.X, 0.5 + Dir.Y) return { TweenTo(Panel, { Position = Goal }) } elseif Settings.Kind == "Wipe" then local Gradient = self.Gradient assert(Gradient, "Wipe transition missing the gradient") local Travel = DirectionFrom(Settings.Rotation) * (0.5 + Settings.Softness) Gradient.Transparency = EdgeFade(Settings.Softness, Covering) Gradient.Offset = -Travel return { TweenTo(Gradient, { Offset = Travel }) } elseif Settings.Kind == "Circle" then local Diameter = if Covering then self.Root.AbsoluteSize.Magnitude + 4 else 0 if Covering then Panel.Size = UDim2.fromOffset(0, 0) end return { TweenTo(Panel, { Size = UDim2.fromOffset(Diameter, Diameter) }) } end local Count = #self.Panels local BarTime = Settings.Duration * (1 - Settings.Stagger) local Gap = if Count > 1 then Settings.Duration * Settings.Stagger / (Count - 1) else 0 local FromSide = if DirectionFrom(Settings.Rotation).X < 0 then 1 else 0 local Anchor = if Covering then FromSide else 1 - FromSide local Fill = if Covering then 1 else 0 local Tweens = {} :: { Tween } for Index, Bar in self.Panels do Bar.AnchorPoint = Vector2.new(Anchor, 0) Bar.Position = UDim2.fromScale(Anchor, (Index - 1) / Count) Bar.Size = UDim2.new(1 - Fill, 0, 1 / Count, 1) table.insert(Tweens, TweenTo(Bar, { Size = UDim2.new(Fill, 0, 1 / Count, 1) }, BarTime, Gap * (Index - 1))) end return Tweens end local function PlayAndWait(self: Transition, Tweens: { Tween }) self.Running = Tweens for _, Tween in Tweens do Tween:Play() end local Slowest = Tweens[#Tweens] if Slowest and Slowest.PlaybackState ~= Enum.PlaybackState.Completed then Slowest.Completed:Wait() end for _, Tween in Tweens do Tween:Destroy() end table.clear(Tweens) end local function SafeCall(Callback: Callback?, Name: string) if not Callback then return end local Ok, Err = pcall(Callback) if not Ok then warn(string.format("%s callback failed: %s", Name, tostring(Err))) end end function Transition.new(Host: GuiBase2d, Options: PresetOptions?): Transition assert(typeof(Host) == "Instance" and Host:IsA("GuiBase2d"), "Transition wants GuiBase2d type host") local Settings = MakePreset(Options or {}) local Root = NewRoot(Host) local Panels, Gradient = BuildPanels(Root, Settings) local self = setmetatable({ State = "Idle", Host = Host, Settings = Settings, Root = Root, Panels = Panels, Gradient = Gradient, Running = {}, Thread = nil, Connections = {}, } :: Data, Transition) table.insert(self.Connections, Host.Destroying:Connect(function() self:Destroy() end)) return self end const Presets: { [PresetName]: Preset } = { Fade = MakePreset({ Kind = "Fade", Duration = 0.5, Hold = 0.2 }), FadeWhite = MakePreset({ Kind = "Fade", Duration = 0.4, Hold = 0.1, Color = White }), Flash = MakePreset({ Kind = "Fade", Duration = 0.12, Hold = 0.05, Style = Enum.EasingStyle.Linear, Color = White }), SlideLeft = MakePreset({ Kind = "Slide", Duration = 0.6, Style = Enum.EasingStyle.Quint, Rotation = 180 }), SlideRight = MakePreset({ Kind = "Slide", Duration = 0.6, Style = Enum.EasingStyle.Quint, Rotation = 0 }), SlideUp = MakePreset({ Kind = "Slide", Duration = 0.6, Style = Enum.EasingStyle.Quint, Rotation = 270 }), SlideDown = MakePreset({ Kind = "Slide", Duration = 0.6, Style = Enum.EasingStyle.Quint, Rotation = 90 }), WipeRight = MakePreset({ Kind = "Wipe", Duration = 0.7, Style = Enum.EasingStyle.Sine, Rotation = 0, Softness = 0.08 }), WipeDown = MakePreset({ Kind = "Wipe", Duration = 0.7, Style = Enum.EasingStyle.Sine, Rotation = 90, Softness = 0.08 }), SoftWipe = MakePreset({ Kind = "Wipe", Duration = 1, Style = Enum.EasingStyle.Sine, Rotation = 0, Softness = 0.3 }), Circle = MakePreset({ Kind = "Circle", Duration = 0.6, Style = Enum.EasingStyle.Quad }), Bars = MakePreset({ Kind = "Bars", Duration = 0.7, Style = Enum.EasingStyle.Quart, Count = 6, Stagger = 0.4 }), Blinds = MakePreset({ Kind = "Bars", Duration = 0.8, Style = Enum.EasingStyle.Sine, Count = 14, Stagger = 0.6, Rotation = 180 }), } table.freeze(Presets) Transition.Presets = Presets function Transition.FromPreset(Host: GuiBase2d, Name: PresetName): Transition local Settings = Presets[Name] :: PresetOptions assert(Settings, "Unknown preset") return Transition.new(Host, Settings) end function Transition.GetRoot(self: Transition): Frame return self.Root end function Transition.GetState(self: Transition): State return self.State end function Transition.IsBusy(self: Transition): boolean return self.State ~= "Idle" end function Transition.Cover(self: Transition) if self.State ~= "Idle" then return end self.State = "Covering" self.Root.Visible = true PlayAndWait(self, TweensFor(self, true)) if self.State == "Covering" then self.State = "Covered" end end function Transition.Reveal(self: Transition) if self.State ~= "Covered" then return end self.State = "Revealing" PlayAndWait(self, TweensFor(self, false)) if self.State == "Revealing" then self.State = "Idle" self.Root.Visible = false end end function Transition.Play(self: Transition, OnCovered: Callback?, OnFinished: Callback?): boolean if self.State ~= "Idle" then return false end self.Thread = task.spawn(function() self:Cover() SafeCall(OnCovered, "OnCovered") if self.State ~= "Covered" then return end task.wait(self.Settings.Hold) self:Reveal() self.Thread = nil if self.State == "Idle" then SafeCall(OnFinished, "OnFinished") end end) return true end function Transition.Destroy(self: Transition) if self.State == "Destroyed" then return end self.State = "Destroyed" local Thread = self.Thread self.Thread = nil if Thread and Thread ~= coroutine.running() then task.cancel(Thread) end for _, Tween in self.Running do Tween:Cancel() Tween:Destroy() end for _, Connection in self.Connections do Connection:Disconnect() end table.clear(self.Connections) self.Root:Destroy() end return Transition