← Examples

Animation

This example demonstrates animation capabilities in GPUI: basic animations with with_animation, easing functions, transformations, repeating and duration controls.

Run

cargo run --example animation

Category

learn

Source

View on GitHub →

Source Code

//! Animation Example
//!
//! This example demonstrates animation capabilities in GPUI:
//!
//! 1. Basic animations with `with_animation`
//! 2. Easing functions - ease_in_out, bounce, linear
//! 3. Transformations - rotate, scale, translate
//! 4. Repeating and duration controls

#[path = "../prelude.rs"]
mod example_prelude;

use std::time::Duration;

use anyhow::Result;
use gpui::{
    Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Colors, Context, Hsla,
    SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out,
    linear, percentage, prelude::*, px, size as gpui_size, svg,
};

struct Assets {}

impl AssetSource for Assets {
    fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
        std::fs::read(path)
            .map(Into::into)
            .map_err(Into::into)
            .map(Some)
    }

    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
        Ok(std::fs::read_dir(path)?
            .filter_map(|entry| {
                Some(SharedString::from(
                    entry.ok()?.path().to_string_lossy().into_owned(),
                ))
            })
            .collect::<Vec<_>>())
    }
}

const ARROW_CIRCLE_SVG: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/examples/legacy/image/arrow_circle.svg"
);

struct AnimationExample;

impl Render for AnimationExample {
    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .id("main")
            .size_full()
            .p_6()
            .bg(colors.background)
            .overflow_scroll()
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_6()
                    .max_w(px(600.))
                    .child(
                        div()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .child(
                                div()
                                    .text_xl()
                                    .font_weight(gpui::FontWeight::BOLD)
                                    .text_color(colors.text)
                                    .child("Animation Patterns"),
                            )
                            .child(
                                div()
                                    .text_sm()
                                    .text_color(colors.text_muted)
                                    .child("Animations, easing, and transformations in GPUI"),
                            ),
                    )
                    .child(section(
                        &colors,
                        "Rotation Animation",
                        rotation_example(&colors),
                    ))
                    .child(section(&colors, "Bounce Easing", bounce_example(&colors)))
                    .child(section(&colors, "Scale Animation", scale_example(&colors)))
                    .child(section(
                        &colors,
                        "Combined Animations",
                        combined_example(&colors),
                    )),
            )
    }
}

fn rotation_example(colors: &Colors) -> impl IntoElement {
    let text_muted = colors.text_muted;
    let accent = colors.accent;

    div()
        .flex()
        .flex_col()
        .gap_3()
        .child(
            div()
                .text_xs()
                .text_color(text_muted)
                .child("Continuous rotation with ease_in_out easing"),
        )
        .child(
            div().flex().items_center().justify_center().h_24().child(
                svg()
                    .size_16()
                    .overflow_hidden()
                    .path(ARROW_CIRCLE_SVG)
                    .text_color(accent)
                    .with_animation(
                        "rotation",
                        Animation::new(Duration::from_secs(2))
                            .repeat()
                            .with_easing(ease_in_out),
                        |svg, delta| {
                            svg.with_transformation(Transformation::rotate(percentage(delta)))
                        },
                    ),
            ),
        )
}

fn bounce_example(colors: &Colors) -> impl IntoElement {
    let text_muted = colors.text_muted;
    let success = colors.success;

    div()
        .flex()
        .flex_col()
        .gap_3()
        .child(
            div()
                .text_xs()
                .text_color(text_muted)
                .child("Bouncing rotation with bounce(ease_in_out)"),
        )
        .child(
            div().flex().items_center().justify_center().h_24().child(
                svg()
                    .size_16()
                    .overflow_hidden()
                    .path(ARROW_CIRCLE_SVG)
                    .text_color(success)
                    .with_animation(
                        "bounce_rotation",
                        Animation::new(Duration::from_secs(2))
                            .repeat()
                            .with_easing(bounce(ease_in_out)),
                        |svg, delta| {
                            svg.with_transformation(Transformation::rotate(percentage(delta)))
                        },
                    ),
            ),
        )
}

fn scale_example(colors: &Colors) -> impl IntoElement {
    let text_muted = colors.text_muted;
    let warning = colors.warning;

    div()
        .flex()
        .flex_col()
        .gap_3()
        .child(
            div()
                .text_xs()
                .text_color(text_muted)
                .child("Scale pulsing with linear easing"),
        )
        .child(
            div().flex().items_center().justify_center().h_24().child(
                svg()
                    .size_16()
                    .overflow_hidden()
                    .path(ARROW_CIRCLE_SVG)
                    .text_color(warning)
                    .with_animation(
                        "scale",
                        Animation::new(Duration::from_millis(1500))
                            .repeat()
                            .with_easing(bounce(linear)),
                        |svg, delta| {
                            let scale = 0.8 + (delta * 0.4);
                            svg.with_transformation(Transformation::scale(gpui_size(scale, scale)))
                        },
                    ),
            ),
        )
}

fn combined_example(colors: &Colors) -> impl IntoElement {
    let text_muted = colors.text_muted;
    let error = colors.error;

    div()
        .flex()
        .flex_col()
        .gap_3()
        .child(
            div()
                .text_xs()
                .text_color(text_muted)
                .child("Rotation + scale combined"),
        )
        .child(
            div().flex().items_center().justify_center().h_24().child(
                svg()
                    .size_16()
                    .overflow_hidden()
                    .path(ARROW_CIRCLE_SVG)
                    .text_color(error)
                    .with_animation(
                        "combined",
                        Animation::new(Duration::from_secs(3))
                            .repeat()
                            .with_easing(ease_in_out),
                        |svg, delta| {
                            let scale = 0.7 + (delta * 0.6);
                            svg.with_transformation(
                                Transformation::rotate(percentage(delta))
                                    .with_scaling(gpui_size(scale, scale)),
                            )
                        },
                    ),
            ),
        )
}

fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement {
    let surface: Hsla = colors.surface.into();

    div()
        .flex()
        .flex_col()
        .gap_2()
        .p_4()
        .bg(surface.opacity(0.5))
        .rounded_lg()
        .child(
            div()
                .text_sm()
                .font_weight(gpui::FontWeight::SEMIBOLD)
                .text_color(colors.text)
                .child(title),
        )
        .child(content)
}

fn main() {
    Application::new()
        .with_assets(Assets {})
        .run(|cx: &mut App| {
            let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx);
            cx.open_window(
                WindowOptions {
                    window_bounds: Some(WindowBounds::Windowed(bounds)),
                    ..Default::default()
                },
                |_, cx| cx.new(|_| AnimationExample),
            )
            .expect("Failed to open window");

            example_prelude::init_example(cx, "Animation");
        });
}