[2.3.1] Cascade UI Animator - A Keyframe based UI Animator

Thanks! Hope the plugin and the runtime can be of help!

MAJOR UPDATE! v2.0.0 - Has breaking changes!

Please read the change log before updating!

Dual-Paradigm

The engine now supports two workflows, Service-Only and a Track-Based allowing for a high-level or low-level control depending on the prefered way of working.

Service-Only

Retains the same high-level API as the previous version. Functions like Play and Stop operate by accepting a GuiObject and a Config.

  • New Added Functions: Pause and Resume have been added directly to the service and allows pausing and resuming from paused state.
  • Play/Batch returns a proxy signal as previous version that fires the status strings: Completed, Looped, Cancelled.

Track-Based

Using CreateTrack or CreateBatchedTrack now generates a dedicated AnimTrack or BatchedAnimTrack object.

  • Control: The returned object provides direct access to the animation state. Methods such as :SetSpeed(), :Scrub(), or :Pause() can be called directly on the track instance.
    *Instead of firing strings the track objects exposes specific events as: Completed, Looped, Cancelled and MarkerReached.

Removed Functions

  • BindToEvent & GetRunner have been removed since they were redundant. Call play inside your own event connections
  • SetToStart has been removed since SetToTime can handle all cases.
  • SetLoop/Batch loop settings should only be handled with TrackOptions (Previously PlayOptions) by setting Loop=true or Loop = 4 (loop 4 time) or manipulating the track object itself.

New Functions

Function Status Description
SetFrame / Batch New Snaps a UI element to a specific frame number (calculated automatically using the FPS defined in the config).
Pause / Resume New Added to both the Service (global instance access) and the Track objects (local access).
ClearInstance / Batch Enhanced Resets cache values and halts active tracks and resets every single property defined in the animation configuration (e.g., Rotation, Size, Color) back to its cached base state.
Stop / StopBatch Updated Handles ResetToStart logic.
Suspend New A master switch to disconnect the service from RunService, effectively pausing the entire engine globally - used for Editor.

Lifecycle Examples

Service-Only

local MyFrame = script.Parent.Frame
local Config = require(config)

-- Play using the service. The service manages the track internally.
AnimationService.Play(MyFrame, Config, { Reset = true })

task.wait(1)
-- New Feature: Pause via the service without needing a track variable
AnimationService.Pause(MyFrame, Config)

task.wait(1)
-- Cleanup: Stops animations and resets ALL properties defined in 'Config' 
-- (e.g., Rotation, Size, Transparency) back to their original state.
AnimationService.ClearInstance(MyFrame)

Track-Based

local MyFrame = script.Parent.Frame
local Config = require(config)

-- Create the track manually
local track = AnimationService.CreateTrack(MyFrame, Config)

-- Connect to track events
local con = track.Completed:Connect(function()
    print("UI Transition Finished")
end)

con:Disconnect()

track:Play()

task.wait(0.5)
track:SetSpeed(2) -- Speed up the UI animation dynamically
track:Scrub(0)    -- Instant rewind to the start

-- Cleanup: The Service automatically detects destruction and performs cleanup
track:Destroy()

I’m really happy with this rewrite and I hope it will help you too!

And As always if you have any feedback or questions, let me know!

2 Likes

FastCatCatExcitedGIF
Nice update!!

1 Like

Thank you and thanks for the help testing before release! :folded_hands:t4:

2 Likes

Changelog

2.0.1

2026-02-09

Fixed:

  • Import bug where the FPS wasn’t loaded from the config.
1 Like

UI Rework Dev Update!

I need a couple of volunteers to test the new UI Rework! Please comment here or write me if you would like to test the update before release!

The only condition for testing is that you own a copy of the plugin!

Here a couple of images of how the new UI looks.



All help is appreciated, thanks!

2 Likes

roui3

also btw I like this project alot, the interface looks great, functionality looks great.

asdjnasdasdljkalkjkjladasdadasdasdsdaq

Next release is really close! The release will have a surprise as well! Probably this week or early next week!

2 Likes

Changelog

2.0.2

2026-02-24

Fixed:

  • Import bug were loading animations could fail when having the FPS property.

I promised animation events early this year… And.

Change log v2.1.0

  • Features a fully reworked UI with a lot of quality of life keybinds and a design that will make working with the plugin easier. Go through the keybinds to see all the available actions.
  • Animation Events - Right now you can use the datatypes - String, number and boolean. You can toggle the events track inside of the Animation Settings popup.
  • Themes - you can cycle between - Dark,. Blue, Light and Orange.
  • Keybinds - Can be set to new input, can be set with modifiers and are saved between sessions.

A big thanks to @winpol who helped with testing during the test phase!

Enjoy!

Cascade Animation Events — Usage Examples

This guide below is a bit of a repeat from the v2.0.0 post but I figure it might be needed. the guide shows how to use Animation Events (markers) with Cascade’s runtime engine.
Events are fired during playback when the animation reaches a specific frame, letting you
trigger sounds, particles, logic, or any game behaviour in sync with your animations.


Setup

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AnimationService = require(ReplicatedStorage:WaitForChild("CascadeRuntimeEngine").AnimationService)
local MyAnim = require(ReplicatedStorage:WaitForChild("CascadeAnimations").MyAnimation)

1. Service-Only Approach — AnimationService.Play()

Play an animation on a single instance. Returns a proxy signal — a single
signal that fires for every event type. The first argument is always the event
type string ("Completed", "Looped", "Cancelled", or "MarkerReached"),
followed by any type-specific arguments.

local myFrame = script.Parent.MyFrame

local signal = AnimationService.Play(myFrame, MyAnim)

signal:Connect(function(eventType, ...)
    if eventType == "MarkerReached" then
        local name, eventData = ...
        if name == "PlaySound" then
            local soundId = eventData.Data
            local sound = Instance.new("Sound")
            sound.SoundId = soundId
            sound.Parent = myFrame
            sound:Play()
            sound.Ended:Once(function() sound:Destroy() end)

        elseif name == "ToggleVisibility" then
            myFrame.Visible = not myFrame.Visible
        end

    elseif eventType == "Completed" then
        print("Animation finished!")

    elseif eventType == "Looped" then
        print("Animation looped!")
    end
end)

With Options

local signal = AnimationService.Play(myFrame, MyAnim, {
    Loop = true,
    Delay = 0.5,
    Reset = true,
})

signal:Connect(function(eventType, ...)
    if eventType == "MarkerReached" then
        local name, eventData = ...
        if name == "LoopPulse" then
            print("Pulse!")
        end

    elseif eventType == "Looped" then
        print("Animation looped!")
    end
end)

2. Service-Only Approach — AnimationService.PlayBatch()

Play the same animation on multiple instances at once. Same proxy signal pattern
as Play(), but MarkerReached events include a fourth value: the specific
instance that triggered the event.

local buttons = {
    script.Parent.Button1,
    script.Parent.Button2,
    script.Parent.Button3,
}

local signal = AnimationService.PlayBatch(buttons, MyAnim, {
    Stagger = 0.1,
})

signal:Connect(function(eventType, ...)
    if eventType == "MarkerReached" then
        local name, eventData, instance = ...
        if name == "Highlight" then
            print(instance.Name .. " reached the Highlight marker")
            instance.BackgroundColor3 = Color3.fromRGB(255, 200, 0)
        end

    elseif eventType == "Completed" then
        print("All instances finished!")
    end
end)

3. Track-Based Approach — AnimationService.CreateTrack()

Create a track for full playback control (play, pause, resume, scrub, speed).
Unlike Play()/PlayBatch(), tracks expose separate signals for each
event type: Completed, Looped, Cancelled, and MarkerReached.

local myFrame = script.Parent.MyFrame

local track = AnimationService.CreateTrack(myFrame, MyAnim, {
    Loop = 3,
    Reset = true,
})

track.MarkerReached:Connect(function(name, eventData)
    if name == "Damage" then
        local amount = tonumber(eventData.Data) or 10
        print("Deal " .. amount .. " damage!")
    elseif name == "ScreenShake" then
        shakeCamera(0.2)
    end
end)

track.Completed:Connect(function()
    print("Track finished all loops")
    track:Destroy()
end)

track.Looped:Connect(function()
    print("Loop iteration complete")
end)

track:Play()

Track Control

track:Pause()
task.wait(1)
track:Play()

track:SetSpeed(2)
track:SetSpeed(0.5)
track:SetSpeed(-1)

track:Scrub(1.5)

track:Stop()

track:Destroy()

4. Track-Based Batch — AnimationService.CreateBatchedTrack()

Full track control over multiple instances with staggered playback.
MarkerReached receives a third argument: the instance that triggered the event.

local cards = {
    script.Parent.Card1,
    script.Parent.Card2,
    script.Parent.Card3,
    script.Parent.Card4,
}

local track = AnimationService.CreateBatchedTrack(cards, MyAnim, {
    Stagger = 0.15,
    Loop = false,
    Reset = true,
})

track.MarkerReached:Connect(function(name, eventData, instance)
    if name == "FlipSound" then
        playSound("rbxassetid://123456", instance)
    elseif name == "Glow" then
        applyGlow(instance)
    end
end)

track.Completed:Connect(function()
    print("All cards finished animating")
    track:Destroy()
end)

track:Play()

5. Event Data Types

Events support three data types set in the editor. The eventData table
contains Name, Time, DataType, and Data.

String Events

-- Track-based (separate signal)
track.MarkerReached:Connect(function(name, eventData)
    -- eventData.DataType == "String"
    -- eventData.Data is a string
    if name == "PlaySound" then
        local soundId = eventData.Data -- e.g. "rbxassetid://123456"
        playSound(soundId)
    end
end)

-- Service-only (proxy signal)
signal:Connect(function(eventType, ...)
    if eventType == "MarkerReached" then
        local name, eventData = ...
        if name == "PlaySound" then
            playSound(eventData.Data)
        end
    end
end)

Number Events

track.MarkerReached:Connect(function(name, eventData)
    -- eventData.DataType == "Number"
    -- eventData.Data is a number
    if name == "SetAlpha" then
        local alpha = eventData.Data -- e.g. 0.5
        myFrame.BackgroundTransparency = alpha
    end
end)

Boolean Events

track.MarkerReached:Connect(function(name, eventData)
    -- eventData.DataType == "Boolean"
    -- eventData.Data is a boolean
    if name == "ToggleHUD" then
        local show = eventData.Data -- true or false
        hudFrame.Visible = show
    end
end)

6. Example Use — UI Intro Sequence

A complete example using track-based batch: an intro animation that fades in
cards one by one, plays sounds at key moments, and enables interaction when done.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AnimationService = require(ReplicatedStorage.CascadeRuntimeEngine.AnimationService)
local IntroAnim = require(ReplicatedStorage.CascadeAnimations.IntroSequence)

local menuFrame = script.Parent.MenuFrame
local cards = menuFrame.CardContainer:GetChildren()

for _, card in cards do
    card.Active = false
    card.Interactable = false
end

local track = AnimationService.CreateBatchedTrack(cards, IntroAnim, {
    Stagger = 0.12,
    Reset = false,
})

track.MarkerReached:Connect(function(name, eventData, instance)
    if name == "Whoosh" then
        local sound = Instance.new("Sound")
        sound.SoundId = eventData.Data
        sound.PlaybackSpeed = 0.9 + math.random() * 0.2
        sound.Parent = instance
        sound:Play()
        sound.Ended:Once(function() sound:Destroy() end)

    elseif name == "EnableInteraction" then
        instance.Active = true
        instance.Interactable = true
    end
end)

track.Completed:Connect(function()
    print("Intro complete — menu is ready")
    track:Destroy()
end)

track:Play()
1 Like

Patch for the UI!

Partly because of a bug where keyframes were disappearing, the further down the hierarchy they were and partly the expand/collapse on each track made it impossible to work with larger hierarchies for a cutscene or some larger intro animation.

Changelog

v2.1.1

  • Split hierarchy browser and property tracklist into separate panels

  • Hierarchy viewer is toggleable from the tracklist header

  • Tracklist now only shows animated properties, grouped by object

  • Fixed timeline grid clipping bug where keyframes disappeared on scroll

Let me know if you have any feedback on this!

1 Like

Changelog

I was a bit trigger happy yesterday, a couple of more fixes!

v2.1.2

  • Fixed animation not playing past a certain frame when FPS < 60 (duration was capped at LengthSeconds instead of actual animation length. Plugin only, no effects on AnimationService)

  • Fixed track ordering so parent objects always appear above their children

  • Fixed box selection not aligning with the new tracklist row layout

  • Clicking an object in the hierarchy viewer now selects it in the Explorer

  • Changing FPS no longer changes TotalFrames — length in seconds adjusts instead

  • Property popup filter auto-focuses on open for faster searching

  • Fixed so Length is updated in the config when changing FPS or TotalFrames

New UI looking pretty good! It’s getting more intuitive.

1 Like

I’m glad to hear that! :smile: Have you tried the new additions to the API?

Changelog

Re-install the runtime to get the fix for relative tracks!

v2.1.3

  • Fixed a crash when playing animations with relative mode enabled on isolated axis tracks (e.g. animating only Position.X.Scale). Relative tracks now correctly offset from the instance’s base value without affecting untouched axes.

  • Fixed the Open dialog not listing animations stored inside subfolders of CascadeAnimations. All animations are now shown recursively with their folder path (e.g. UI/FadeIn).

  • Save and Save As now support subfolder paths — entering UI/FadeIn as the animation name will create the UI folder automatically if it doesn’t exist.

2 Likes

Changelog

Re-install the runtime to get the new fixes for animation events!

v2.1.4

  • Events now fire correctly on the first and last frame of every animation
  • Events work in both forward and reverse play
  • Events fire at the correct time per-instance in staggered batch animations
  • Looping animations fire events on every loop pass, not just the first

Changelog

Re-install the runtime to get the new fixes type annotations

v2.1.5

  • Fixed the type annotations on Play and PlayBatch functions. Now they have the correct class and gives auto-complete.

Also… a teaser from my upcoming tutorials on creating UI Animations…

1 Like

Changelog

Re-install the runtime to getthe fixes for Axis Isolation!

v2.1.6

  • Fixed axis isolation locking non-animated axes to their capture-time values — animating Position.X no longer prevents moving the object on Y.

Just posted a tutorial series on animating game UI with the plugin — two episodes up so far covering the basics and building a full main menu. More on the way.

1 Like

Change log

Re-install the runtime to get Step easing support!

v2.2.0

Visual overhaul

  • Redesigned panel layout with clear borders between hierarchy, tracklist, and timeline

  • New alternating row colors for better readability across all themes

  • Smoother hover and selection transitions on track rows

  • Refined toolbar with matching header color and active button highlights

  • Selected keyframes now have a subtle glow halo

  • Timeline ruler shows more frame numbers at different zoom levels

  • Polished “Select an object” start screen

New features

  • Step easing — new “Step” option in the easing picker. Holds the previous value and snaps instantly at the keyframe, like a hold/constant keyframe. Works in forward play, reverse, and preview scrubbing. (Suggested by @winpol)

  • Track visibility toggle — eye icon on each track row to hide/show individual tracks from preview and playback without deleting them

  • Hierarchy visibility toggle — toggle switch on each object in the hierarchy to collapse all of its tracks from the timeline view

  • Name animation on creation — prompted to name your animation when starting or creating new, shown in the toolbar

  • Unsaved changes indicator — asterisk (*) next to the animation name when there are unsaved edits

  • Version number — fetched from the marketplace, displayed in the License popup and stamped on the installed runtime

  • Smart runtime replacement — installing the runtime now checks the existing version and prompts before replacing

Improvements

  • Saving is now allowed with no tracks (empty/template animations)

  • Removed unnecessary metadata (CreatedAt) from saved animation files

  • Default values (Delay=0, Loop=false) are no longer written to saved files

  • Fixed hierarchy viewer child indentation — leaf and branch children at the same depth now align correctly

As always, let me know if you have any feedback on the update!

Also a reminder that I have released a couple of tutorials for the plugin!

1 Like