Cascade UI Animator
A high-performance animation engine and visual editor for Roblox UI. Cascade lets you animate any UI property with keyframes, easing curves, and sub-property control — then play those animations at runtime with a single function call.
Architecture
Cascade uses a dual-paradigm design:
Service Mode
Fire-and-forget. Call AnimationService.Play() and the engine handles everything. Best for hovers, clicks, simple transitions.
Track Mode
Full manual control. Call AnimationService.CreateTrack() to get an AnimTrack object you can play, pause, scrub, and speed-shift. Best for cutscenes, complex HUDs, synced sequences.
Quick Start
-- Require the service from your saved animation module
local AnimationService = require(game.ReplicatedStorage.CascadeAnimations.AnimationService)
-- Load an animation config (exported from the editor)
local HoverAnim = require(game.ReplicatedStorage.CascadeAnimations.MyHoverAnim)
-- Service mode: fire and forget
local signal = AnimationService.Play(button, HoverAnim, { Reset = true })
signal:Connect(function(event, ...)
if event == "Completed" then
print("Animation finished!")
elseif event == "MarkerReached" then
local name, data = ...
print("Event:", name, data)
end
end)
-- Track mode: manual control
local track = AnimationService.CreateTrack(button, HoverAnim)
track.MarkerReached:Connect(function(name, data)
print("Marker:", name)
end)
track:Play()
task.wait(0.3)
track:Pause()
track:Scrub(0) -- jump to start
How It Works
Cascade blends multiple animations on the same instance additively — a hover, a click, and a shake can all run simultaneously without flickering or overwriting each other. Use Reset = true to automatically restore properties to their original values when an animation ends.
AnimationService
The top-level module. Manages the global render loop, active tracks, preprocessing, and provides both fire-and-forget and track-based APIs.
local AnimationService = require(path.to.AnimationService)
Methods
Plays an animation on a single instance. If a track for the same instance+config already exists, it reuses it (resetting time). Returns a Signal proxy that fires "Completed", "Looped", "Cancelled", or "MarkerReached".
| Parameter | Type | Description |
|---|---|---|
| instance | Instance | The root GuiObject to animate |
| config | AnimationConfig | ProcessedAnimation | Animation data (raw or preprocessed) |
| options | TrackOptions? | Optional playback flags |
local signal = AnimationService.Play(button, HoverAnim, {
Reset = true, -- restore properties when done
Loop = true, -- loop forever
Reverse = false, -- play forward
})
signal:Connect(function(event, ...)
if event == "Completed" then
print("Done!")
elseif event == "MarkerReached" then
local name, data = ...
print("Hit marker:", name)
end
end)
Plays the same animation on multiple instances using a single BatchedAnimTrack. Supports Stagger for cascading delays between items.
local buttons = { btn1, btn2, btn3, btn4 }
local signal = AnimationService.PlayBatch(buttons, FadeIn, {
Stagger = 0.05, -- 50ms delay between each item
Reset = true,
})
signal:Connect(function(event, ...)
if event == "MarkerReached" then
local name, data, instance = ...
print(instance.Name, "hit marker:", name)
end
end)
Creates an AnimTrack for manual control. Does not auto-play. You must call :Play() on the returned track.
local track = AnimationService.CreateTrack(panel, SlideIn)
track.Completed:Connect(function()
print("Slide finished")
end)
track:Play()
Creates a BatchedAnimTrack for manual control over a batch of instances. Does not auto-play.
local track = AnimationService.CreateBatchedTrack(listItems, RevealAnim, {
Stagger = 0.03,
})
track:Play()
Pauses a running animation on the given instance without needing the track reference.
Resumes a paused animation on the given instance.
Batch equivalents of Pause and Resume.
Snaps the instance to the exact state at the given time (in seconds). Creates a track internally if needed, then suspends it. Useful for previewing a specific point in the animation.
Snaps to a specific frame number. Converts to time using the config's FPS.
-- Show exactly what frame 10 looks like
AnimationService.SetFrame(button, ClickAnim, 10)
Batch equivalents of SetTime and SetFrame.
Stops all active tracks on the instance. Fires each track's Cancelled signal.
Stops all batched tracks containing any of the given instances.
Stops all tracks on the instance, restores all cached base-state properties (if Reset was enabled), and removes the instance from the base cache. Use this for full cleanup.
-- Player closed a menu, clean everything up
AnimationService.ClearInstance(menuFrame)
Batch equivalent of ClearInstance.
Pre-parses an AnimationConfig into an optimized format. Results are cached per config table reference. Calling Play or CreateTrack does this automatically, but for large batches (100+ items), preprocessing once upfront avoids redundant work.
local processed = AnimationService.Preprocess(ListItemAnim)
-- Now use the processed result for many instances
for _, item in ipairs(items) do
AnimationService.Play(item, processed, { Stagger = 0.02 })
end
Clears the internal preprocessed config cache. Call this if you dynamically modify animation configs at runtime.
Disconnects the global PreRender connection, pausing the entire engine. Use for menu screens or when animations aren't needed.
Suspends the engine, destroys all active tracks, and clears all caches. Full teardown.
TrackOptions
The optional options table accepted by Play, PlayBatch, CreateTrack, and CreateBatchedTrack.
| Option | Type | Default | Description |
|---|---|---|---|
| Reset | boolean | false | When the track is cleaned up or removed by ClearInstance, all properties touched by the animation are restored to the values they had before playback started. Use this for effects that should leave no trace. |
| ResetToStart | boolean | false | When :Stop() is called, the track scrubs to time 0 before stopping. The instance snaps to its frame-0 state rather than freezing at whatever time it was at. |
| Reverse | boolean | false | Plays the animation backwards (speed is set to -1). The track starts at the end and moves toward frame 0. Ideal for "undo" transitions like mouse-leave after a hover. |
| Loop | boolean | number | config value | Overrides the loop setting baked into the animation config. Pass true for infinite looping, false to play once, or a number (e.g. 3) to loop exactly that many times. |
| Delay | number | 0 | Seconds to wait after :Play() before the animation actually begins. The track is in a "waiting" state during the delay — it won't evaluate or modify properties until the delay expires. |
| Stagger | number | 0 | BatchedAnimTrack only. Delay in seconds between each instance's start time. With 4 instances and Stagger = 0.05, the 4th instance starts 150ms after the 1st. The total duration extends by (count - 1) * stagger. |
| DeleteOnComplete | boolean | false | Automatically calls :Destroy() on the track when it finishes. Useful for one-shot animations where you don't need the track reference afterwards. Prevents track accumulation. |
-- Common patterns:
-- Hover: forward on enter, reverse on leave
AnimationService.Play(btn, HoverAnim)
AnimationService.Play(btn, HoverAnim, { Reverse = true })
-- One-shot notification that cleans itself up
AnimationService.Play(popup, FadeIn, { DeleteOnComplete = true })
-- Looping idle animation with restore on cleanup
AnimationService.Play(icon, PulseAnim, { Loop = true, Reset = true })
-- Staggered list reveal
AnimationService.PlayBatch(items, SlideIn, {
Stagger = 0.04,
Delay = 0.2,
})
AnimTrack
Returned by AnimationService.CreateTrack(). Represents a single animation bound to a single instance. Gives you frame-level control over playback.
Properties
| Property | Type | Description |
|---|---|---|
| Instance | Instance | The root instance being animated |
| State | "Playing" | "Paused" | "Suspended" | Current playback state |
| CurrentTime | number | Current playback position in seconds |
| Speed | number | Playback speed multiplier (negative = reverse) |
| Duration | number | Total length of the animation in seconds |
| AutoDestroy | boolean | If true, track self-destructs on completion |
| IsFinished | boolean | True when animation has reached the end |
| IsDestroyed | boolean | True after Destroy() has been called |
Methods
Starts or resumes playback from the current time. Respects the Delay option on first play.
Freezes playback at the current time. The track stays active and can be resumed with :Play().
Stops playback, resets time to 0, and fires the Cancelled signal. If ResetToStart was set, scrubs to frame 0 first. If AutoDestroy is true, the track self-destructs.
Jumps to a specific time (clamped to 0..Duration). Works while playing, paused, or suspended. While suspended, a scrub triggers a single evaluation pass so the instance updates visually.
-- Scrub to the halfway point
track:Scrub(track.Duration / 2)
-- Scrub to a specific frame (at 30 FPS)
track:Scrub(15 / 30)
Changes playback speed. Use -1 for reverse, 0.5 for half speed, 2 for double speed.
track:SetSpeed(-1) -- play in reverse
track:SetSpeed(0.5) -- slow motion
Changes the loop behavior. Pass true for infinite loop, false to stop looping, or a number for a specific loop count.
Marks the track for removal from the engine. Destroys all signal connections. The engine cleans it up on the next render frame.
Events
Fires when the animation reaches the end (or the start, if playing in reverse) and has no remaining loops.
Fires each time the animation wraps around for another loop cycle.
Fires when :Stop() is called manually.
Fires when playback crosses an animation event marker. The callback receives the event name and the full event data table.
track.MarkerReached:Connect(function(name, data)
if name == "PlaySound" then
SoundService:PlayLocalSound(data.Data)
end
end)
Full Example
local AnimationService = require(game.ReplicatedStorage.CascadeAnimations.AnimationService)
local SlideAnim = require(game.ReplicatedStorage.CascadeAnimations.SlideIn)
local track = AnimationService.CreateTrack(menuFrame, SlideAnim, {
Delay = 0.2,
DeleteOnComplete = true,
})
track.Completed:Connect(function()
print("Menu is now visible")
end)
track:Play()
BatchedAnimTrack
Returned by AnimationService.CreateBatchedTrack(). Animates the same animation across multiple instances with a single track, supporting staggered timing.
Properties
| Property | Type | Description |
|---|---|---|
| Instances | {Instance} | Array of root instances being animated |
| Stagger | number | Delay in seconds between each instance's start time |
| State | "Playing" | "Paused" | "Suspended" | Current playback state |
| CurrentTime | number | Global playback time (including stagger) |
| Speed | number | Playback speed multiplier |
| Duration | number | Total duration = animation length + (instanceCount - 1) * stagger |
Methods
BatchedAnimTrack shares the same method interface as AnimTrack:
| Method | Description |
|---|---|
| :Play() | Starts or resumes playback |
| :Pause() | Freezes at current time |
| :Stop() | Stops and fires Cancelled |
| :Scrub(time) | Jumps to a global time position |
| :SetSpeed(speed) | Changes playback speed |
| :SetLoop(loop) | Changes loop behavior |
| :SetStagger(seconds) | Changes stagger delay and recalculates duration |
| :Destroy() | Marks for cleanup |
Events
Same as AnimTrack: .Completed, .Looped, .Cancelled, .MarkerReached.
For MarkerReached, the callback receives a third argument: the specific Instance that triggered the event.
track.MarkerReached:Connect(function(name, data, instance)
-- 'instance' tells you which item in the batch hit the marker
print(instance.Name, "reached marker:", name)
end)
Stagger Example
-- Stagger a list of items for a cascading reveal
local items = scrollFrame:GetChildren()
local track = AnimationService.CreateBatchedTrack(items, FadeIn, {
Stagger = 0.04, -- 40ms between each
Loop = false,
Reset = true,
})
track.Completed:Connect(function()
print("All items revealed")
end)
track:Play()
Signal
A lightweight custom event class used throughout Cascade. Follows standard Roblox signal conventions. Shipped as part of the AnimationService module.
Creating a Signal
local Signal = require(path.to.AnimationService.Signal)
local onHealthChanged = Signal.new()
Methods
Subscribes a callback. Returns a Connection object with a Disconnect() method and a Connected property.
local connection = onHealthChanged:Connect(function(newHealth)
print("Health:", newHealth)
end)
-- Later:
connection.Disconnect() -- stop listening
Invokes all connected callbacks with the given arguments. Each callback runs in its own thread via task.spawn.
onHealthChanged:Fire(75)
Yields the current thread until the signal is fired. Returns whatever arguments were passed to :Fire().
local status = track.Completed:Wait()
print("Track completed")
Disconnects all listeners and marks them as disconnected. The signal becomes inert.
Types & Config
Reference for the data structures used by the animation engine.
AnimationConfig
The primary data format. This is what the editor exports when you save an animation.
| Field | Type | Description |
|---|---|---|
| Length | number | Total duration in seconds |
| Loop | boolean | number? | Loop forever (true), a specific count, or don't loop (false) |
| Delay | number? | Delay before playback starts (seconds) |
| Tracks | {[string]: {Keyframe}} | Map of track path to keyframe array |
| Settings | {[string]: any}? | Per-track settings (e.g. IsRelative) |
| Events | {AnimationEvent}? | Timeline event markers |
Keyframe
A single point in a track's timeline.
| Field | Type | Description |
|---|---|---|
| Time | number | Position in seconds |
| Value | any | The property value at this point |
| Easing | EasingStyle? | Interpolation curve (e.g. Enum.EasingStyle.Quad) |
| Direction | EasingDirection? | Easing direction (In, Out, InOut) |
TrackOptions
Optional flags passed to Play, CreateTrack, etc.
| Field | Type | Description |
|---|---|---|
| Delay | number? | Seconds to wait before starting |
| Loop | boolean | number? | Override the config's loop setting |
| Stagger | number? | Delay between batch instances (BatchedAnimTrack only) |
| Reset | boolean? | Restore properties to pre-animation values on cleanup |
| ResetToStart | boolean? | Scrub to frame 0 on Stop |
| Reverse | boolean? | Play the animation backwards |
| DeleteOnComplete | boolean? | Auto-destroy the track when it finishes |
AnimationEvent
A marker placed on the timeline that fires MarkerReached during playback.
| Field | Type | Description |
|---|---|---|
| Time | number | Position in seconds |
| Name | string | Identifier for the event |
| DataType | string? | Optional type hint for the data payload |
| Data | any? | Arbitrary data attached to the event |
Track Paths
Track keys in the Tracks table use slash-delimited paths. The last segment is the property name. Sub-properties use dot notation.
-- Direct property on root
"Transparency"
-- Child object's property
"ContentFrame/Transparency"
-- Deeply nested
"LeftPanel/Content/TitleArea/Tagline/TextTransparency"
-- Sub-property (axis isolation)
"ContentFrame/Size.X.Scale"
"ContentFrame/Position.Y.Offset"
The engine resolves instance paths via FindFirstChild from the root. Every sibling must have a unique name.
Core Concepts
Before using the editor, understand these fundamental concepts.
What is a Keyframe?
A keyframe is a snapshot of a property's value at a specific point in time. The animation engine interpolates between keyframes to produce smooth motion.
For example, to fade a frame from fully visible to invisible:
- Keyframe at Frame 0:
Transparency = 0(fully visible) - Keyframe at Frame 30:
Transparency = 1(invisible)
The engine fills in every frame between 0 and 30 automatically based on the easing curve.
FPS (Frames Per Second)
FPS defines the time resolution of your animation. It determines how many frames fit into one second.
- 60 FPS — each frame = 1/60th of a second (~16.7ms). Smooth and precise.
- 30 FPS — each frame = 1/30th of a second (~33.3ms). Standard for UI.
- 20 FPS — each frame = 1/20th of a second (50ms). Good for simple transitions.
FPS is a timeline grid setting, not a runtime cap. The engine evaluates at Roblox's native framerate and interpolates between your keyframes regardless of FPS.
Total Frames
TotalFrames defines how many frames long your animation is. Combined with FPS, it determines the duration:
Duration = TotalFrames / FPS
Example: 60 frames at 30 FPS = 2.0 seconds
When you change FPS in Animation Settings, TotalFrames stays the same and the duration adjusts. When you change TotalFrames, the duration adjusts too.
Easing
Each keyframe has an easing style and direction that controls how the value transitions to the next keyframe.
Cascade supports all Roblox EasingStyle values: Linear, Quad, Cubic, Quart, Quint, Sine, Exponential, Circular, Elastic, Back, Bounce.
Directions: In (slow start), Out (slow end), InOut (slow both ends).
Relative Mode
Normally, keyframes store absolute values (e.g. Size = {0.5, 0, 0.3, 0}). In Relative Mode, keyframes store deltas (offsets from the instance's current value).
This is powerful for reusable effects. A "hover grow" animation that adds +10px to Size works on any button regardless of its base size.
How it works: The engine captures the property's value before the animation starts (the "base state"), then adds the keyframe delta on top. Multiple relative animations stack additively.
Sub-Property Animation
Most editors lock entire properties like Size or Position. Cascade lets you animate individual axes independently:
Size.X.Scale— only the X scalePosition.Y.Offset— only the Y offsetBackgroundColor3.R— only the red channel
This enables effects that would be impossible otherwise, like growing width while shrinking height independently with different easing curves.
Animation Events
Events are markers placed on the timeline that fire callbacks during playback. Use them to sync sounds, trigger particle effects, or run game logic at precise moments in your animation.
Events fire via the MarkerReached signal on AnimTrack/BatchedAnimTrack. Each event has a Name and optional Data payload. Events work correctly in all playback modes: forward, reverse, looping, and staggered batches.
Service Mode (Play / PlayBatch)
The signal returned by .Play() and .PlayBatch() wraps all events into a single callback. The first argument is the event type string.
local signal = AnimationService.Play(button, MyAnim)
signal:Connect(function(event, ...)
if event == "MarkerReached" then
local name, data = ...
if name == "PlaySound" then
SoundService:PlayLocalSound(data.Data)
end
elseif event == "Completed" then
print("Done!")
end
end)
Track Mode (CreateTrack / CreateBatchedTrack)
Track objects expose .MarkerReached as a dedicated signal. The callback receives the event name and data directly.
local track = AnimationService.CreateTrack(button, MyAnim)
track.MarkerReached:Connect(function(name, data)
if name == "PlaySound" then
SoundService:PlayLocalSound(data.Data)
end
end)
track:Play()
Batched Track Events
For BatchedAnimTrack, the callback receives a third argument: the specific instance that triggered the event. With stagger, each instance fires its events independently after its stagger delay elapses.
local track = AnimationService.CreateBatchedTrack(items, RevealAnim, {
Stagger = 0.04,
})
track.MarkerReached:Connect(function(name, data, instance)
print(instance.Name, "reached:", name)
end)
track:Play()
Editor Features
The Cascade editor is a dockable Roblox Studio plugin. Here's what it can do.
Hierarchy Browser
The left panel shows a tree view of the selected instance and all its descendants. Click any object in the hierarchy to select it in the Studio Explorer. The hierarchy panel is toggleable from the tracklist header.
Property Tracklist
The right side of the tracklist shows all animated properties, grouped by object. Each row represents one track (one animatable property). Properties are added via the Property Popup.
Timeline Grid
The main workspace. Keyframes appear as diamonds on track rows aligned to the frame grid. You can:
- Click a keyframe to select it
- Right-click a keyframe to open the keyframe editor
- Drag keyframes to retime them
- Box-select to select multiple keyframes across tracks
- Scroll to move through time, Ctrl+Scroll to zoom
- Middle-mouse drag to pan
- Use the Add Keyframe keybind (0 or +) to insert a keyframe at the current frame
Auto-Record
When recording mode is active (the record button is toggled on), any property change you make in the Studio viewport or Properties panel is automatically captured as a keyframe at the current frame position. This lets you "act out" animations naturally.
Keyframe Editor
Right-click a keyframe (or press the keybind) to open the keyframe editor popup. Here you can:
- Edit the exact value with type-aware inputs
- Change the easing style and direction
Relative Mode Toggle
Each track row has a Δ button. Click it to toggle relative mode for that specific track. When active, the badge turns accent-colored and all keyframes on that track are treated as deltas from the base state.
Smart Inspector
The value input fields accept loose syntax. For example, you can type {0.5, 10} for a UDim value and the parser will format it correctly. Color values accept 0-255 RGB.
Clipboard & Undo
Full non-destructive editing with undo/redo history. Copy, cut, and paste keyframes across tracks. Multi-select keyframes with box selection and move them as a group.
Animation Settings
Open via the toolbar or keybind. Configure:
- FPS — frame rate (affects grid density and time conversion)
- Total Frames — animation length in frames
- Loop — whether the animation loops during preview
- Events — enable/disable the event track row
The computed length in seconds is displayed as a read-only label.
Save & Load
Animations are serialized to ModuleScripts inside ReplicatedStorage.CascadeAnimations. The save/open dialogs let you name, browse, and overwrite animations. The exported format is a plain Luau table that you require() at runtime.
Themes
The editor ships with multiple themes: Dark, Light, Blue, and Orange. Themes are managed by the ThemeManager and affect all UI components.
Customizable Keybinds
Every action can be rebound in the keybind settings. Bindings persist across Studio sessions via plugin:SetSetting(). You can assign multiple keys to the same action, add modifier keys (Shift, Ctrl, Alt), and reset to defaults.
Keybinds
Default keyboard shortcuts. All keybinds are customizable in the editor's settings panel.
Playback
| Action | Default Keybind | Description |
|---|---|---|
| Play / Pause | Space | Toggle animation playback |
| Next Frame | K or → | Step one frame forward |
| Previous Frame | J or ← | Step one frame backward |
| Go to Start | H | Jump to the first frame |
| Go to End | L | Jump to the last keyframe |
Editing
| Action | Default Keybind | Description |
|---|---|---|
| Toggle Record | R | Toggle recording mode |
| Add Keyframe | 0 or + | Add keyframe at current frame |
| Delete Keyframe | Shift+D or Backspace | Delete selected keyframes |
| Reset Object | G | Reset object to initial state |
Clipboard
| Action | Default Keybind | Description |
|---|---|---|
| Undo | Shift+Z | Undo last action |
| Redo | Shift+Y | Redo last action |
| Copy | Shift+C | Copy selection |
| Cut | Shift+X | Cut selection |
| Paste | Shift+V | Paste selection |
Popups
| Action | Default Keybind | Description |
|---|---|---|
| Open Property Popup | 5 | Open the property manager |
| Open Keyframe Editor | 6 or Right-click | Open keyframe editor |
| Open Animation Settings | 7 | Open animation settings popup |
Navigation
| Action | Default | Description |
|---|---|---|
| Pan Timeline | Middle Mouse drag | Pan the timeline view |
| Zoom Timeline | Ctrl+Scroll | Zoom in/out on the timeline |
Best Practices
Practical tips for getting the most out of Cascade.
Naming & Hierarchy
DO: Use unique sibling names
The engine resolves paths via FindFirstChild. If two children share a name, the wrong one may be animated.
DON'T: Rename instances after animating them
Track paths are baked into the animation data. Renaming an instance will break path resolution. If you must rename, re-save the animation.
DO: Keep hierarchies stable
Moving an animated child to a different parent will break its track path. Design your UI structure before animating.
Service vs. Track Mode
DO: Use Service mode for simple interactions
AnimationService.Play() handles track reuse, signal proxying, and cleanup automatically. Use it for hovers, clicks, and basic transitions where you don't need frame-level control.
DO: Use Track mode for complex sequences
If you need to scrub, change speed dynamically, or coordinate multiple tracks, use CreateTrack(). Store the track reference for ongoing control.
DON'T: Create tracks in loops without cleanup
Every CreateTrack() call adds a track to the engine. If you create tracks in a loop (e.g., per-frame), they'll accumulate. Use DeleteOnComplete = true or call :Destroy() when done.
Performance
DO: Use Preprocess for large batches
If you're animating 100+ instances with the same config, call AnimationService.Preprocess(config) once and pass the result. This avoids re-parsing the config for each instance.
DO: Use PlayBatch with Stagger for lists
Instead of calling Play() in a loop with task.wait(), use PlayBatch() with a Stagger option. It's a single track internally, meaning one evaluation pass instead of N.
TIP: ClearInstance for full cleanup
When a UI element is being removed or completely reset, call ClearInstance() rather than just Stop(). This restores properties AND frees the base-state cache.
Relative Mode
DO: Use Relative mode for reusable effects
Hover effects, click punches, shake animations — anything that should work regardless of the element's current size/position. Define keyframes as deltas and the engine adds them on top of the current state.
DON'T: Mix Absolute and Relative on the same property
If two animations affect the same property, and one is Absolute while the other is Relative, the Absolute one will overwrite the base value. Keep effects on the same property in the same mode.
DO: Reverse the animation on leave
For hover effects, play the animation forward on enter and play it again with { Reverse = true } on leave. This produces a smooth transition back to the original state instead of a hard snap.
button.MouseEnter:Connect(function()
AnimationService.Play(button, HoverAnim)
end)
button.MouseLeave:Connect(function()
AnimationService.Play(button, HoverAnim, { Reverse = true })
end)
Editor Workflow
DO: Set FPS and TotalFrames before animating
Decide your frame rate and animation length upfront. Changing FPS later re-maps keyframe times, which may shift them off clean frame boundaries.
DO: Use Auto-Record for exploratory work
Toggle record mode on, scrub to a frame, and adjust properties in the viewport. Keyframes are created automatically. Great for "feeling out" an animation.
TIP: Use sub-property tracks for independent axes
Need width to ease with Quad but height with Bounce? Add separate tracks for Size.X and Size.Y with different easing curves.
TIP: Save frequently
Animations are stored as ModuleScripts in ReplicatedStorage. Save often — the editor doesn't auto-save, and Studio crashes happen.