Dioxus Motion

repository·main·Indexed 41 days ago

https://github.com/wheregmis/dioxus-motion

A lightweight, cross-platform animation library for the Dioxus framework providing smooth animations for web, desktop, and mobile applications. It features a Rust API for animating values via the use_motion hook, spring physics and tweening systems, and the MotionTransitions derive macro for implementing route transitions with AnimatedOutlet. Version 0.3.6.

Tokens
21.3K
Snippets
67
Records
83
Agent score
51%

What's inside Dioxus Motion

  1. Use PopLayout for instant sibling reflow

    main

    When using PresenceMode::PopLayout, the exiting child is removed from the normal layout flow immediately. This allows siblings to move to their new positions without waiting for the exiting child's animation to finish.

    Behavioral Details:

    • The exiting node is visually 'pinned' at its last-known measured position.
    • On web platforms, this uses DOM measurement to calculate absolute coordinates relative to the offset parent.
    • It is designed to avoid the visual truncation caused by animating a wrapper to height: 0 or using overflow: hidden.
    • Note: Transformed ancestors can affect the layout positioning of popped elements.
  2. How the core animation system works

    main

    The dioxus-motion animation system is built around two primary animation models: Spring Physics (using Hooke's law for natural movement) and a Tweening System (for traditional keyframe animations with easing).

    To manage animation state within a Dioxus component, you use the use_motion hook. This hook returns a Motion<T> object that manages an internal AnimationState and provides an API to trigger animations. The animation loop runs independently of the Dioxus render cycle to ensure smooth motion even during component re-renders.

    // The core hook pattern
    let mut position = use_motion(0.0f32);
  3. Understand AnimatePresence and its PresenceModes

    main

    The AnimatePresence component manages the lifecycle of its children, allowing them to perform exit animations before being removed from the DOM. It supports three distinct modes that change how children are handled during removal:

    • PresenceMode::Sync: The default mode. Exiting children keep their measured layout slot for the duration of their exit animation, allowing surrounding layout to close smoothly after the animation completes.
    • PresenceMode::Wait: Used when you want to ensure an exiting child has finished its animation before the next element enters or the component unmounts.
    • PresenceMode::PopLayout: Removes the exiting child from the normal document flow immediately (effectively using absolute positioning) so that siblings can reflow to their new positions instantly. This prevents the 'jumping' effect during exit animations. On web, this relies on DOM measurement.

    To manage presence state within a component, you can use the use_is_present or use_presence hooks.

  4. Understand AnimatePresence modes: sync, wait, and popLayout

    main

    When children are removed, AnimatePresence manages how they are rendered during the exit phase using different modes:

    • sync: Merges present and exiting records by their last known order. This prevents list items from jumping positions when an item exits.
    • wait: Renders only exiting records while exits are pending. Once all exits are complete, it renders the new incoming present entries.
    • popLayout: Similar to sync, but attempts to remove exiting records from the normal layout flow (using absolute positioning) where the platform supports it, preventing exiting elements from taking up space in the layout.
  5. Implement the Animatable trait for custom types

    main

    To animate custom data structures, implement the Animatable trait. Your type must also implement Copy, Default, and the standard arithmetic operator traits: Add, Sub, and Mul<f32>.

    Required Methods:

    • interpolate(&self, target: &Self, t: f32) -> Self: Defines how to transition between two values at time t (0.0 to 1.0).
    • magnitude(&self) -> f32: Returns the magnitude of the value (e.g., the length of a vector).

    Best Practices:

    • Circular Values (Angles): In interpolate, ensure you calculate the shortest path between angles to avoid spinning the long way around.
    • Normalized Values (Colors): Use .clamp(0.0, 1.0) in your scaling logic to keep values within valid bounds.
    • Precision: You can override the default epsilon (0.01) using .with_epsilon().
    #[derive(Debug, Copy, Clone, PartialEq, Default)]
    struct Position {
        x: f32,
        y: f32,
    }
    
    // 1. Implement standard operators
    impl std::ops::Add for Position {
        type Output = Self;
        fn add(self, other: Self) -> Self { Self { x: self.x + other.x, y: self.y + other.y } }
    }
    
    impl std::ops::Sub for Position {
        type Output = Self;
        fn sub(self, other: Self) -> Self { Self { x: self.x - other.x, y: self.y - other.y } }
    }
    
    impl std::ops::Mul<f32> for Position {
        type Output = Self;
        fn mul(self, factor: f32) -> Self { Self { x: self.x * factor, y: self.y * factor } }
    }
    
    // 2. Implement Animatable
    impl Animatable for Position {
        fn interpolate(&self, target: &Self, t: f32) -> Self {
            *self + (*target - *self) * t
        }
        
        fn magnitude(&self) -> f32 {
            (self.x * self.x + self.y * self.y).sqrt()
        }
    }
  6. Requirements for AnimatePresence children

    main

    For AnimatePresence to correctly track and animate elements, children must follow these rules:

    1. Must have unique keys: AnimatePresence uses keys to identify which elements are being added, removed, or reordered. Duplicate keys will be rejected.
    2. Missing keys: Siblings managed by presence must have keys; otherwise, the presence logic cannot reliably track them.
    3. RSX Compatibility: You can use standard RSX children. AnimatePresence will automatically flatten fragments and treat a single keyed VNode as a presence child.
  7. Control animation playback state

    main

    Dioxus Motion provides mechanisms to manage the lifecycle and playback of an animation. The system tracks states such as Idle, Running, and Completed.

    Available controls include:

    • Start: Begins the animation from its initial value.
    • Stop: Halts the animation at its current value.
    • Resume: Continues the animation from the value where it was stopped.
    • Reset: Returns the animation to its initial value.
  8. Set up the Dioxus Motion documentation site

    main

    The documentation site is a standalone Dioxus application located in the docs/ directory. To run it locally, you must install Node.js dependencies, compile the Tailwind CSS assets, and serve the app using the Dioxus CLI.

    Prerequisites:

    • Rust toolchain with cargo
    • Dioxus CLI (dx)
    • Node.js and npm
    # 1. Install frontend dependencies
    cd docs
    npm install
    
    # 2. Build CSS
    npm run css
    
    # 3. Run the docs app
    dx serve --platform web
  9. Verify the documentation site build

    main

    To ensure the documentation site is correctly aligned with the local workspace dependency and the current library API (especially after changing prose or examples), run a check on the docs package with the web feature enabled.

    cargo check -p docs --features web
  10. Migrate to use_motion with Send + 'static bounds

    main

    In recent versions, use_motion<T> requires that the type T implements Send + 'static.

    • Standard types: f32, Transform, and Color already satisfy these bounds.
    • Custom types: Ensure all fields are Send + 'static. If you need shared ownership, use Arc<T> instead of Rc<T>.
    • Sharing handles: To share a single motion handle across multiple components, wrap the result of use_motion in an Arc (e.g., let shared_handle = Arc::new(use_motion(0.0f32));).
    // ❌ This won't compile - Rc<T> is not Send
    // let bad_motion = use_motion(std::rc::Rc::new(0.0f32));
    
    // ✅ Use Arc<T> instead for shared ownership
    #[derive(Copy, Clone, Default)]
    struct SharedValue { value: f32 }
    
    // ... implement Add, Sub, Mul, and Animatable for SharedValue ...
    
    let shared_motion = use_motion(SharedValue { value: 0.0 });
    
    // ✅ Alternative: Use Arc to share the motion handle itself
    let shared_motion_handle = std::sync::Arc::new(use_motion(0.0f32));
    let motion_clone = shared_motion_handle.clone();
  11. Implement Page Transitions with AnimatedOutlet

    main

    You can implement smooth route transitions by using the MotionTransitions derive macro on your Routable enum and replacing the standard Outlet with AnimatedOutlet.

    1. Derive MotionTransitions on your Route enum.
    2. Use the #[transition(Effect)] attribute on specific routes to define their animation style (e.g., Fade, ZoomIn, SlideLeft).
    3. In your layout component, replace Outlet::<Route> {} with AnimatedOutlet::<Route> {}.
    use dioxus_motion::prelude::*;
    
    #[derive(Routable, Clone, Debug, PartialEq, MotionTransitions)]
    #[rustfmt::skip]
    enum Route {
        #[layout(NavBar)]
            #[route("/")]
            #[transition(Fade)]
            Home {},
            #[route("/slide-left")]
            #[transition(ZoomIn)]
            SlideLeft {},
            // ... other routes
        #[end_layout]
        #[route("/:..route")]
        PageNotFound { route: Vec<String> },
    }
    
    #[component]
    fn NavBar() -> Element {
        rsx! {
            nav {
                Link { to: Route::Home {}, "Home" }
                Link { to: Route::SlideLeft {}, "Blog" }
            }
            AnimatedOutlet::<Route> {}
        }
    }
  12. Implement manual exit animations with use_presence_motion_completion

    main

    If you are not using the high-level PresenceMotion component and instead want to manually control an exit animation using use_motion, you must use use_presence_motion_completion to ensure the AnimatePresence parent doesn't remove the component before the animation finishes.

    This is necessary because direct Dioxus callbacks are not Send + 'static, and this helper acts as a presence-safe adapter for raw use_motion exits.

    Workflow:

    1. Use use_presence() to detect if the component is being removed (!presence.is_present).
    2. Trigger your exit animation (e.g., opacity.animate_to(0.0, ...)).
    3. Pass the motion handle and the exit trigger state to use_presence_motion_completion.
    #[component]
    fn Toast(message: String) -> Element {
        let presence = use_presence();
        let mut opacity = use_motion(1.0);
        let mut exit_started = use_signal(|| false);
    
        use_effect(move || {
            if !presence.is_present {
                exit_started.set(true);
                opacity.animate_to(
                    0.0,
                    AnimationConfig::new(AnimationMode::Tween(Tween::default())),
                );
            }
        });
        use_presence_motion_completion(opacity, exit_started());
    
        rsx! {
            div {
                style: "opacity: {opacity.get_value()}",
                "{message}"
            }
        }
    }