-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Animation
A unified animation system providing preset animations, easing functions, constants, and orchestration capabilities for UI components.
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.
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
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),
)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
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
)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),
)use yororen_ui::animation::{pulse, constants::duration::SLOW};
skeleton.with_animation(
"pulse",
Animation::new(SLOW).repeat(),
pulse(SLOW),
)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),
)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
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.
No easing, constant speed.
| Function | Description |
|---|---|
ease_linear |
No easing, linear progression |
| Function | Description |
|---|---|
ease_in_quad |
Starts slow, accelerates |
ease_out_quad |
Starts fast, decelerates |
ease_in_out_quad |
Slow start and end, fast middle |
| Function | Description |
|---|---|
ease_in_cubic |
Starts slow, accelerates |
ease_out_cubic |
Starts fast, decelerates |
ease_in_out_cubic |
Slow start and end, fast middle |
| Function | Description |
|---|---|
ease_in_quart |
Starts slow, accelerates |
ease_out_quart |
Starts fast, decelerates |
ease_in_out_quart |
Slow start and end, fast middle |
| Function | Description |
|---|---|
ease_in_quint |
Starts slow, accelerates |
ease_out_quint |
Starts fast, decelerates |
ease_in_out_quint |
Slow start and end, fast middle |
| Function | Description |
|---|---|
ease_in_sine |
Starts slow, accelerates |
ease_out_sine |
Starts fast, decelerates |
ease_in_out_sine |
Slow start and end, fast middle |
| 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 |
| Function | Description |
|---|---|
ease_in_circ |
Starts slow, accelerates |
ease_out_circ |
Starts fast, decelerates |
ease_in_out_circ |
Slow start and end, fast middle |
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]
|
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]
|
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]
|
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 |
pub type EasingFn = fn(f32) -> f32;Ready-to-use animation functions that can be applied directly to gpui elements.
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,
}| 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 |
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);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 // 0msConfiguration and state tracking for animations.
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();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 stateTiming constants for consistent animation timing across the library.
| Constant | Duration |
|---|---|
CURSOR_BLINK_INTERVAL |
500ms |
| Constant | Duration |
|---|---|
duration::MENU_OPEN |
160ms |
duration::MENU_OPEN_FAST |
100ms |
duration::MENU_OPEN_SLOW |
250ms |
| Constant | Duration |
|---|---|
duration::NAVIGATOR_SLIDER |
200ms |
duration::TAB_SWITCH |
150ms |
| Constant | Duration |
|---|---|
duration::SKELETON_PULSE_1 |
1100ms |
duration::SKELETON_PULSE_2 |
1200ms |
| Constant | Duration |
|---|---|
duration::PROGRESS_SPINNER |
850ms |
duration::PROGRESS_CIRCLE |
900ms |
duration::PROGRESS_BAR |
1500ms |
| Constant | Duration |
|---|---|
duration::MODAL_FADE_IN |
200ms |
duration::MODAL_SLIDE_UP |
250ms |
| Constant | Duration |
|---|---|
duration::TOOLTIP_SHOW |
150ms |
duration::TOOLTIP_HIDE |
100ms |
| Constant | Duration |
|---|---|
duration::VERY_FAST |
100ms |
duration::FAST |
150ms |
duration::NORMAL |
200ms |
duration::SLOW |
300ms |
duration::VERY_SLOW |
400ms |
Utilities for calculating progress from elapsed time. Useful when you need manual control over animation timing.
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.5Convert 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.0Convert 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)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.5Orchestration is for complex animations where you need multiple tracks playing together with precise timing control.
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.
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.
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)
},
)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)))
},
);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
]);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)))
},
);| 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 |
These APIs are maintained for compatibility but prefer Orchestration for new code.
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);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);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]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);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.0Linear 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);Create animation IDs for stateful animations:
use yororen_ui::animation::animation_id;
let id = animation_id("menu", OpenState::Open);
// Result: "menu:Open"Stick to the defined constants for consistency:
// Good
use yororen_ui::animation::constants::duration::MENU_OPEN;
// Avoid
let duration = Duration::from_millis(160);| 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
|
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
Some users prefer reduced motion. Check for user preferences before applying animations.
Before (old):
use crate::constants::animation::MENU_OPEN;After:
use yororen_ui::animation::constants::duration::MENU_OPEN;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),
)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),
)
}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),
);
}
}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)))
},
)
}Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.