Skip to content

Guide Animation

MeowLynxSea edited this page Feb 19, 2026 · 5 revisions

Animation

A unified animation system providing preset animations, easing functions, constants, and orchestration capabilities for UI components.

Introduction

Animation brings user interfaces to life. It helps users understand state changes, provides visual feedback, and creates a more polished experience. This module provides tools for creating smooth, performant animations in your UI components.

Key Concepts

Before diving into the API, understand these fundamental concepts:

Progress (进度) Animation progress ranges from 0.0 (not started) to 1.0 (complete). Your animation functions receive this value and transform it into visual changes.

// At progress 0.0: element is invisible
// At progress 1.0: element is fully visible
element.opacity(progress);

Easing (缓动) Raw linear progress feels mechanical. Easing functions make animations feel natural by adjusting the timing curve:

Type Feel Use Case
ease_out_* Fast start, slow end Entrance animations
ease_in_* Slow start, fast end Exit animations
ease_in_out_* Slow start and end Equal emphasis
ease_out_back Overshoots target Attention-grabbing

Orchestration (编排) Complex UIs need multiple animations playing together. Orchestration lets you:

  • Run animations sequentially (one after another)
  • Run animations in parallel (at the same time)
  • Add delays for staggered effects

Quick Start

For most use cases, use the preset animations:

use yororen_ui::animation::{
    fade_slide_in,
    constants::duration::MENU_OPEN,
};

// Apply preset animation to a gpui element
div.with_animation(
    "menu-open",
    Animation::new(MENU_OPEN),
    fade_slide_in(MENU_OPEN),
)

Understanding the Animation Flow

Before exploring advanced topics, understand how animations work in gpui:

gpui Animation
    │
    ├── Duration (how long)
    ├── Easing function (timing curve)
    │
    └── Progress callback (your code)
            │
            └── Receives: progress: f32 (0.0 → 1.0)
            └── Returns: modified element

Basic Usage

1. Simple Fade Animation

use gpui::{Animation, AnimationExt};
use yororen_ui::animation::constants::duration::NORMAL;

div.with_animation(
    "fade-in",
    Animation::new(NORMAL),
    |this, t| this.opacity(t),  // 0.0 → 1.0
)

2. Using Preset Animations

use yororen_ui::animation::{
    Animation, AnimationExt,
    fade_slide_in,
    fade_slide_out,
    constants::duration::*,
};

// Menu open animation
menu.with_animation(
    "menu-open",
    Animation::new(MENU_OPEN),
    fade_slide_in(MENU_OPEN),  // fade + slide up
)

// Modal exit animation
modal.with_animation(
    "modal-close",
    Animation::new(MODAL_FADE_IN),
    fade_slide_out(MODAL_FADE_IN),
)

3. Loading Pulse

use yororen_ui::animation::{pulse, constants::duration::SLOW};

skeleton.with_animation(
    "pulse",
    Animation::new(SLOW).repeat(),
    pulse(SLOW),
)

Using Easing Safely with gpui

gpui's Animation::with_easing(...) expects the easing function to return values in the range [0.0, 1.0].

Some classic easings (notably back and elastic) overshoot by design. When using easing functions from yororen_ui::animation, prefer the *_clamped variants:

use gpui::{Animation, AnimationExt};
use yororen_ui::animation::{ease_out_elastic_clamped, constants::duration::FAST};

div.with_animation(
    "elastic",
    Animation::new(FAST).with_easing(ease_out_elastic_clamped),
    |this, t| this.opacity(t),
)

When to Use

Good fit:

  • Menu/dropdown open/close animations
  • Modal dialog transitions
  • Loading states (pulse, spin)
  • Hover/focus state transitions
  • Page/component entrance animations

Not a good fit:

  • Complex physics-based animations (use custom implementation)
  • Frame-by-frame procedural animations

Module Reference

Easing Functions (easing.rs)

Mathematical easing functions for controlling animation timing. All functions take a normalized time value (0.0 to 1.0) and return the eased progress value.

Easing Types

Linear

No easing, constant speed.

Function Description
ease_linear No easing, linear progression
Quadratic (Power 2)
Function Description
ease_in_quad Starts slow, accelerates
ease_out_quad Starts fast, decelerates
ease_in_out_quad Slow start and end, fast middle
Cubic (Power 3)
Function Description
ease_in_cubic Starts slow, accelerates
ease_out_cubic Starts fast, decelerates
ease_in_out_cubic Slow start and end, fast middle
Quartic (Power 4)
Function Description
ease_in_quart Starts slow, accelerates
ease_out_quart Starts fast, decelerates
ease_in_out_quart Slow start and end, fast middle
Quintic (Power 5)
Function Description
ease_in_quint Starts slow, accelerates
ease_out_quint Starts fast, decelerates
ease_in_out_quint Slow start and end, fast middle
Sine
Function Description
ease_in_sine Starts slow, accelerates
ease_out_sine Starts fast, decelerates
ease_in_out_sine Slow start and end, fast middle
Exponential
Function Description
ease_in_expo Very slow start, accelerates sharply
ease_out_expo Fast start, decelerates sharply
ease_in_out_expo Very slow start and end, fast middle
Circular
Function Description
ease_in_circ Starts slow, accelerates
ease_out_circ Starts fast, decelerates
ease_in_out_circ Slow start and end, fast middle
Back (Overshoot)

Creates a slight overshoot effect before settling.

Function Description
ease_out_back Overshoots target, then settles
ease_in_back Starts from overshoot
ease_in_out_back Overshoots at both ends

gpui-safe variants (clamped to [0, 1]):

Function Description
ease_out_back_clamped Clamped to [0, 1]
ease_in_back_clamped Clamped to [0, 1]
ease_in_out_back_clamped Clamped to [0, 1]
Elastic

Creates a spring-like bouncing effect.

Function Description
ease_in_elastic Bounces at the start
ease_out_elastic Bounces at the end
ease_in_out_elastic Bounces at both ends

gpui-safe variants:

Function Description
ease_out_elastic_clamped Clamped to [0, 1]
ease_in_elastic_clamped Clamped to [0, 1]
ease_in_out_elastic_clamped Clamped to [0, 1]
Bounce

Creates a bouncing effect like a ball hitting the ground.

Function Description
ease_in_bounce Bounces at the start
ease_out_bounce Bounces at the end
ease_in_out_bounce Bounces at both ends

gpui-safe variants:

Function Description
ease_out_bounce_clamped Clamped to [0, 1]
ease_in_bounce_clamped Clamped to [0, 1]
ease_in_out_bounce_clamped Clamped to [0, 1]
Basic Easing (Matching gpui)

Simple quadratic easing functions that match gpui's built-in easing.

Function Description
ease_in Quadratic ease in
ease_out Quadratic ease out
ease_in_out Quadratic ease in-out
Type Alias
pub type EasingFn = fn(f32) -> f32;

Preset Animations (preset.rs)

Ready-to-use animation functions that can be applied directly to gpui elements.

Animation Types (for reference)

These types are used by preset animations:

pub enum AnimationType {
    FadeIn,
    FadeOut,
    SlideIn(SlideDirection),
    SlideOut(SlideDirection),
    ScaleIn,
    ScaleOut,
    BounceIn,
    BounceOut,
    ElasticIn,
    ElasticOut,
    FadeSlideIn(SlideDirection),
    FadeScaleIn,
}

pub enum SlideDirection {
    Left,
    Right,
    Up,
    Down,
}

Preset Functions

Function Signature Description
fade_slide_in (duration: Duration) Fade + slide up (opacity 0→1, translate 10px→0)
fade_slide_out (duration: Duration) Reverse of fade_slide_in
pulse (duration: Duration) Loading pulse (opacity 0.55→0.95)
fade_slide_in_left (distance: Pixels) Slide from left with fade
fade_slide_in_right (distance: Pixels) Slide from right with fade
fade_slide_in_up (distance: Pixels) Slide from top with fade
fade_slide_in_down (distance: Pixels) Slide from bottom with fade
fade_slide_in_from (direction, distance: Pixels) Slide in from any direction
fade_slide_out_to (direction, distance: Pixels) Slide out to any direction
bounce_out_to (direction, distance: Pixels) Bounce out to a direction
fade_scale_in (duration: Duration) Fade + scale in
fade_scale_out (duration: Duration) Fade + scale out
elastic_scale_in (duration: Duration) Elastic scale in
elastic_scale_out (duration: Duration) Elastic scale out

Fade Structs

For more control over easing:

use yororen_ui::animation::{FadeIn, FadeOut};

// Custom easing
let apply_fn = FadeIn::new().apply(duration, my_easing_function);

// Default easing
element = FadeIn::new().apply_default(element, progress);

Duration Constants (preset module)

use yororen_ui::animation::preset::preset_duration;

preset_duration::VERY_FAST  // 100ms
preset_duration::FAST      // 150ms
preset_duration::NORMAL    // 200ms
preset_duration::SLOW      // 300ms
preset_duration::VERY_SLOW // 400ms
preset_duration::INSTANT   // 0ms

Animation Configuration (config.rs)

Configuration and state tracking for animations.

AnimationConfig

Configurable animation settings:

pub struct AnimationConfig {
    pub duration: Duration,
    pub easing: EasingFn,
    pub delay: Duration,
    pub repeat: bool,
    pub reverse: bool,
}

Builder pattern:

use yororen_ui::animation::AnimationConfig;
use std::time::Duration;

let config = AnimationConfig::new()
    .with_duration(Duration::from_millis(300))
    .with_easing(ease_out_cubic)
    .with_delay(Duration::from_millis(100))
    .with_repeat()
    .with_reverse(); // yoyo effect

// Convert to gpui Animation
let animation = config.to_gpui_animation();

AnimationState

Track animation progress and status:

pub struct AnimationState {
    pub progress: f32,      // 0.0 to 1.0
    pub is_running: bool,
    pub is_paused: bool,
}

let mut state = AnimationState::new();
state.update(0.5);  // Set progress to 50%
state.reset();      // Reset to initial state

Animation Constants (constants.rs)

Timing constants for consistent animation timing across the library.

Cursor

Constant Duration
CURSOR_BLINK_INTERVAL 500ms

Menu / Dropdown

Constant Duration
duration::MENU_OPEN 160ms
duration::MENU_OPEN_FAST 100ms
duration::MENU_OPEN_SLOW 250ms

Navigator / Slider

Constant Duration
duration::NAVIGATOR_SLIDER 200ms
duration::TAB_SWITCH 150ms

Loading / Skeleton

Constant Duration
duration::SKELETON_PULSE_1 1100ms
duration::SKELETON_PULSE_2 1200ms

Progress / Spinner

Constant Duration
duration::PROGRESS_SPINNER 850ms
duration::PROGRESS_CIRCLE 900ms
duration::PROGRESS_BAR 1500ms

Modal / Dialog

Constant Duration
duration::MODAL_FADE_IN 200ms
duration::MODAL_SLIDE_UP 250ms

Tooltip

Constant Duration
duration::TOOLTIP_SHOW 150ms
duration::TOOLTIP_HIDE 100ms

General Purpose

Constant Duration
duration::VERY_FAST 100ms
duration::FAST 150ms
duration::NORMAL 200ms
duration::SLOW 300ms
duration::VERY_SLOW 400ms

Timing Helpers (timing.rs)

Utilities for calculating progress from elapsed time. Useful when you need manual control over animation timing.

clamp01

Clamp a value to [0.0, 1.0]:

use yororen_ui::animation::timing::clamp01;

let result = clamp01(1.5);  // Returns 1.0
let result = clamp01(-0.5); // Returns 0.0
let result = clamp01(0.5);  // Returns 0.5

progress_from_elapsed

Convert elapsed time to progress:

use yororen_ui::animation::timing::progress_from_elapsed;
use std::time::Duration;

let elapsed = Duration::from_millis(100);
let duration = Duration::from_millis(200);
let progress = progress_from_elapsed(elapsed, duration);  // 0.5

// Returns 1.0 if duration is zero
let progress = progress_from_elapsed(Duration::ZERO, Duration::ZERO);  // 1.0

sequence_progress

Convert overall progress to per-item progress for sequential animations:

use yororen_ui::animation::timing::sequence_progress;
use std::time::Duration;

let durations = &[
    Duration::from_millis(100),
    Duration::from_millis(200),
    Duration::from_millis(100),
]; // Total: 400ms

// At 25% overall progress (100ms elapsed)
let (index, progress) = sequence_progress(durations, 0.25);
// index=0, progress=1.0 (first item done)

// At 50% overall progress (200ms elapsed)
let (index, progress) = sequence_progress(durations, 0.5);
// index=1, progress=0.5 (second item half done)

// At 75% overall progress (300ms elapsed)
let (index, progress) = sequence_progress(durations, 0.75);
// index=2, progress=0.0 (third item just started)

parallel_progress

Convert overall progress to per-item progress for parallel animations:

use yororen_ui::animation::timing::parallel_progress;
use std::time::Duration;

let durations = &[
    Duration::from_millis(100),  // Short
    Duration::from_millis(200),  // Long
];

// At 50% overall progress
// Short item (index 0): full duration is 200ms, so 100ms/100ms = 1.0
let progress = parallel_progress(durations, 0.5, 0);  // 1.0

// Long item (index 1): 100ms/200ms = 0.5
let progress = parallel_progress(durations, 0.5, 1);  // 0.5

Orchestration (orchestrator.rs)

Orchestration is for complex animations where you need multiple tracks playing together with precise timing control.

When to Use Orchestration

Use Orchestration when:

  • You need multiple properties animating on one element
  • You need precise control over timing (delays, overlapping)
  • You're building complex transitions (modals, complex state changes)

For simple single-property animations, use the simpler with_animation API.

Core Concepts

Step (步骤) A step is a time slice in your animation timeline. Each step can contain one or more tracks.

Track (轨道) A track is a single animation within a step. Each track has its own duration and optional delay.

TrackId (轨道标识符) A handle to reference a specific track when querying its progress.

Simple Sequential Animation

use gpui::AnimationExt;
use yororen_ui::animation::{Orchestration, constants::duration};

// Create orchestration with one track
let (orch, fade_track) = Orchestration::new()
    .then(duration::MODAL_FADE_IN);

// Compile to gpui animations
let animations = orch.compile();

// Apply to element
div.with_animations(
    "modal-enter",
    animations,
    move |this, step_ix, delta| {
        let progress = orch.track_progress(step_ix, delta, fade_track);
        this.opacity(progress)
    },
)

Parallel Animation

Run multiple tracks simultaneously within the same step:

use gpui::AnimationExt;
use yororen_ui::animation::{Orchestration, constants::duration};

let (orch, tracks) = Orchestration::new()
    .with_parallel([
        duration::FAST,  // Track 0: 150ms
        duration::SLOW,  // Track 1: 300ms
    ]);

let track_a = tracks[0];
let track_b = tracks[1];

div.with_animations(
    "parallel",
    orch.compile(),
    move |this, step_ix, delta| {
        let a = orch.track_progress(step_ix, delta, track_a);
        let b = orch.track_progress(step_ix, delta, track_b);
        this.opacity(a).mt(gpui::px(10.0 * (1.0 - b)))
    },
);

Delayed Tracks

Add delay to individual tracks within a parallel step:

use std::time::Duration;
use gpui::AnimationExt;
use yororen_ui::animation::{Orchestration, constants::duration};

// Two tracks starting at different times
let (orch, tracks) = Orchestration::new().with_parallel_delayed([
    (duration::FAST, Duration::ZERO),      // Starts immediately
    (duration::FAST, Duration::from_millis(50)),  // Starts 50ms later
]);

Mixed Sequential and Parallel

Combine multiple steps for complex timelines:

use gpui::AnimationExt;
use yororen_ui::animation::{Orchestration, constants::duration};

let (orch, fade) = Orchestration::new()
    .then(duration::MODAL_FADE_IN);  // Step 1: fade in

let (orch, (slide_a, slide_b)) = orch
    .with_parallel([                 // Step 2: slide in two directions
        duration::MODAL_SLIDE_UP,
        duration::MODAL_SLIDE_UP,
    ]);

div.with_animations(
    "complex",
    orch.compile(),
    move |this, step_ix, delta| {
        let fade_p = orch.track_progress(step_ix, delta, fade);
        let a_p = orch.track_progress(step_ix, delta, slide_a);
        let b_p = orch.track_progress(step_ix, delta, slide_b);
        this.opacity(fade_p)
            .mt(gpui::px(12.0 * (1.0 - a_p)))
            .ml(gpui::px(8.0 * (1.0 - b_p)))
    },
);

API Reference

Method Description
Orchestration::new() Create empty orchestration
.then(duration) Add sequential step, returns (Orchestration, TrackId)
.then_delayed(duration, delay) Add sequential step with delay
.with_parallel([durations...]) Add parallel step, returns (Orchestration, Vec<TrackId>)
.with_parallel_delayed([(duration, delay)...]) Add parallel step with per-track delays
.delay(duration) Add delay-only step
.compile() Convert to Vec<gpui::Animation>
.track_progress(step_index, delta, track_id) Get progress for a specific track

Legacy API (Backward Compatible)

These APIs are maintained for compatibility but prefer Orchestration for new code.

AnimationSequence

Run animations one after another:

use yororen_ui::animation::{AnimationSequence, sequence};
use yororen_ui::animation::constants::duration::*;

// Builder pattern
let seq = AnimationSequence::new()
    .then(MODAL_FADE_IN)
    .then(MODAL_SLIDE_UP);

// Or use convenience function
let seq = sequence(&[MODAL_FADE_IN, MODAL_SLIDE_UP]);

// Query sequence info
let total = seq.total_duration();
let count = seq.len();

// Calculate progress
let (anim_index, anim_progress) = seq.calculate_progress(0.5);

AnimationParallel

Run animations simultaneously:

use yororen_ui::animation::{AnimationParallel, parallel};
use yororen_ui::animation::constants::duration::*;

// Builder pattern
let par = AnimationParallel::new()
    .with(FAST)
    .with(FAST);

// Or use convenience function
let par = parallel(&[FAST, FAST]);

// Query parallel info
let max_duration = par.max_duration();
let count = par.len();

// Calculate progress for specific animation
let progress = par.calculate_progress(0.5, 0);

Staggered Animations

Create staggered delays for list animations:

use yororen_ui::animation::Staggered;
use std::time::Duration;

let base_duration = Duration::from_millis(200);
let delay = Duration::from_millis(50);

// Create staggered delays for 5 items
let delays: Vec<Duration> = base_duration.stagger(5, delay);
// Result: [200ms, 250ms, 300ms, 350ms, 400ms]

Helper Traits and Functions (helpers.rs)

AnimateExt Trait

Extension trait for animating gpui elements directly:

use yororen_ui::animation::AnimateExt;

// Fade animation
div.animate_fade("fade-id", Duration::from_millis(200), 0.5);

// Slide animation
div.animate_slide(
    "slide-id",
    SlideDirection::Up,
    gpui::px(10.0),
    Duration::from_millis(200),
    0.5,
);

// Scale animation
div.animate_scale("scale-id", Duration::from_millis(200), 0.5);

Interpolation Helpers

lerp

Linear interpolation between two values:

use yororen_ui::animation::lerp;

let start = 0.0;
let end = 100.0;
let value = lerp(start, end, 0.5);  // 50.0
lerp_color

Linear interpolation between two colors:

use yororen_ui::animation::lerp_color;
use gpui::Hsla;

let start = Hsla { h: 0.0, s: 1.0, l: 0.5, a: 1.0 };
let end = Hsla { h: 0.33, s: 1.0, l: 0.5, a: 1.0 };
let color = lerp_color(start, end, 0.5);
animation_id

Create animation IDs for stateful animations:

use yororen_ui::animation::animation_id;

let id = animation_id("menu", OpenState::Open);
// Result: "menu:Open"

Best Practices

1. Use Preset Durations

Stick to the defined constants for consistency:

// Good
use yororen_ui::animation::constants::duration::MENU_OPEN;

// Avoid
let duration = Duration::from_millis(160);

2. Choose Appropriate Easing

Animation Type Recommended Easing
Entrance animations ease_out_*
Exit animations ease_in_*
Equal emphasis ease_in_out_*
Loading/continuous ease_linear or ease_in_out
Attention-grabbing ease_out_back or ease_out_elastic

3. Keep Animations Short

Most UI animations should be 100-300ms:

  • Very fast (100ms): Hover states, tooltips
  • Fast (150ms): Simple toggles, tab switches
  • Normal (200ms): Most transitions, fades
  • Slow (300ms): Complex animations, modals

4. Consider Accessibility

Some users prefer reduced motion. Check for user preferences before applying animations.


Migration Guide

Import Path Changes

Before (old):

use crate::constants::animation::MENU_OPEN;

After:

use yororen_ui::animation::constants::duration::MENU_OPEN;

Using with gpui's with_animation

use yororen_ui::animation::{
    Animation, AnimationExt,
    fade_slide_in,
    constants::duration::MENU_OPEN,
};

menu.with_animation(
    "popover-menu",
    Animation::new(MENU_OPEN),
    fade_slide_in(MENU_OPEN),
)

Complete Examples

Example 1: Simple Menu Animation

use yororen_ui::animation::{
    Animation, AnimationExt,
    fade_slide_in,
    constants::duration::*,
};

fn animate_menu_open(menu: gpui::Div) -> gpui::Div {
    menu.with_animation(
        "menu-open",
        Animation::new(MENU_OPEN).with_easing(ease_out_quint),
        fade_slide_in(MENU_OPEN),
    )
}

Example 2: Staggered List Animation

use yororen_ui::animation::{Staggered, Animation, AnimationExt};
use std::time::Duration;

fn animate_list_items(items: &[gpui::Div]) {
    let delays = FAST.stagger(items.len(), Duration::from_millis(30));

    for (item, delay) in items.iter().zip(delays) {
        item.with_animation(
            "list-item",
            Animation::new(delay),
            fade_slide_in(delay),
        );
    }
}

Example 3: Complex Modal Animation

use gpui::AnimationExt;
use yororen_ui::animation::{
    Orchestration,
    constants::duration::*,
};

fn animate_modal_enter(modal: gpui::Div) -> gpui::Div {
    // Build orchestration: fade in → slide up + scale
    let (orch, fade) = Orchestration::new().then(duration::MODAL_FADE_IN);
    let (orch, (slide, scale)) = orch.with_parallel([
        duration::MODAL_SLIDE_UP,
        duration::NORMAL,
    ]);

    modal.with_animations(
        "modal-enter",
        orch.compile(),
        move |this, step_ix, delta| {
            let fade_p = orch.track_progress(step_ix, delta, fade);
            let slide_p = orch.track_progress(step_ix, delta, slide);
            let scale_p = orch.track_progress(step_ix, delta, scale);

            this.opacity(fade_p)
                .mt(gpui::px(24.0 * (1.0 - slide_p)))
        },
    )
}

Clone this wiki locally