├── .DS_Store ├── .github └── workflows │ ├── gh_pages.yml │ ├── publish.yml │ └── release-plz.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ROADMAP.md ├── blog.md ├── docs ├── .gitignore ├── Cargo.toml ├── Dioxus.toml ├── README.md ├── assets │ ├── favicon.ico │ ├── header.svg │ └── main.css ├── input.css ├── justfile ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── animations.rs │ │ ├── code_block.rs │ │ ├── extras.rs │ │ ├── footer.rs │ │ ├── guide_navigation.rs │ │ ├── mod.rs │ │ ├── navbar.rs │ │ ├── page_not_found.rs │ │ └── page_transition.rs │ ├── examples │ │ └── keyframe_animation.rs │ ├── lib.rs │ ├── main.rs │ ├── old_showcase │ │ ├── components │ │ │ ├── animated_counter.rs │ │ │ ├── animated_flower.rs │ │ │ ├── animated_menu_item.rs │ │ │ ├── bouncing_text.rs │ │ │ ├── card_3d_flip.rs │ │ │ ├── cube_animation.rs │ │ │ ├── interactive_cube.rs │ │ │ ├── mod.rs │ │ │ ├── morphing_shape.rs │ │ │ ├── navbar.rs │ │ │ ├── path_animation.rs │ │ │ ├── progress_bar.rs │ │ │ ├── pulse_effect.rs │ │ │ ├── rotating_button.rs │ │ │ ├── transform_animation.rs │ │ │ ├── typewriter_effect.rs │ │ │ └── value_animation.rs │ │ ├── mod.rs │ │ └── showcase_component.rs │ ├── pages │ │ ├── basic_guide.rs │ │ ├── blog │ │ │ ├── index.rs │ │ │ └── mod.rs │ │ ├── complex_guide.rs │ │ ├── custom_guide.rs │ │ ├── docs │ │ │ ├── index.rs │ │ │ └── mod.rs │ │ ├── home │ │ │ ├── index.rs │ │ │ └── mod.rs │ │ ├── intermediate_guide.rs │ │ └── mod.rs │ └── utils │ │ ├── mod.rs │ │ └── router.rs └── tailwind.config.js ├── example.gif ├── justfile ├── packages └── dioxus-motion-transitions-macro │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── lib.rs ├── release-plz.toml └── src ├── animations ├── colors.rs ├── mod.rs ├── platform.rs ├── spring.rs ├── transform.rs ├── tween.rs └── utils.rs ├── keyframes.rs ├── lib.rs ├── manager.rs ├── motion.rs ├── sequence.rs ├── tests ├── helpers.rs ├── mod.rs └── motion.rs └── transitions ├── mod.rs ├── page_transitions.rs └── utils.rs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wheregmis/dioxus-motion/1061379d48fb24213090274737c22883b2a04d92/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/gh_pages.yml: -------------------------------------------------------------------------------- 1 | # name: github pages 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - main 7 | 8 | # jobs: 9 | # build-deploy: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions-rs/toolchain@v1 13 | # with: 14 | # profile: minimal 15 | # toolchain: stable 16 | # override: true 17 | # target: wasm32-unknown-unknown 18 | # - uses: Swatinem/rust-cache@v1 19 | # - uses: ilammy/setup-nasm@v1 20 | # - uses: taiki-e/install-action@cargo-binstall 21 | # - name: Install dioxus-cli 22 | # run: cargo binstall -y dioxus-cli@0.6.3 --force 23 | # - uses: actions/checkout@v2 24 | 25 | # - name: Build 26 | # run: | 27 | # cd example_projects 28 | # $HOME/.cargo/bin/dx bundle --release --platform web 29 | 30 | # - name: Deploy 🚀 31 | # uses: JamesIves/github-pages-deploy-action@v4.2.3 32 | # with: 33 | # branch: gh-pages 34 | # folder: target/dx/example_projects/release/web/public 35 | # target-folder: docs 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # name: Publish to Cargo 2 | 3 | # on: 4 | # push: 5 | # branches: [ main ] 6 | 7 | # jobs: 8 | # publish: 9 | # runs-on: ubuntu-latest 10 | 11 | # name: 'publish' 12 | 13 | # # Reference your environment variables 14 | # environment: cargo 15 | 16 | # steps: 17 | # - uses: actions/checkout@master 18 | 19 | # # Use caching to speed up your build 20 | # - name: Cache publish-action bin 21 | # id: cache-publish-action 22 | # uses: actions/cache@v3 23 | # env: 24 | # cache-name: cache-publish-action 25 | # with: 26 | # path: ~/.cargo 27 | # key: ${{ runner.os }}-build-${{ env.cache-name }}-v0.2.0 28 | 29 | # # install publish-action by cargo in github action 30 | # - name: Install publish-action 31 | # if: steps.cache-publish-action.outputs.cache-hit != 'true' 32 | # run: 33 | # cargo install publish-action --version=0.2.0 34 | 35 | # - name: Run publish-action 36 | # id: publish-action 37 | # run: 38 | # publish-action 39 | # env: 40 | # # This can help you tagging the github repository 41 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | # # This can help you publish to crates.io 43 | # CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | # Release unpublished packages. 14 | release-plz-release: 15 | name: Release-plz release 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Install Rust toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: Run release-plz 27 | uses: release-plz/action@v0.5 28 | with: 29 | command: release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 33 | 34 | # Create a PR with the new versions and changelog, preparing the next release. 35 | release-plz-pr: 36 | name: Release-plz PR 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: write 40 | pull-requests: write 41 | concurrency: 42 | group: release-plz-${{ github.ref }} 43 | cancel-in-progress: false 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v4 47 | with: 48 | fetch-depth: 0 49 | - name: Install Rust toolchain 50 | uses: dtolnay/rust-toolchain@stable 51 | - name: Run release-plz 52 | uses: release-plz/action@v0.5 53 | with: 54 | command: release-pr 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | example_projects/target 3 | router_test/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | ### Fixes: 10 | - Layout not being shown when animating in the case of nested Layouts 11 | - Nested Layout fully fixed 12 | ### Changes: 13 | - Few code refactoring 14 | - BREAKING: `KeyframeAnimation::add_keyframe` now returns a `Result`, not `Self`. Chaining requires `.and_then(...).unwrap()` or error handling. All documentation and guides updated to reflect this. 15 | 16 | ## [0.3.1] - 2024-02-08 17 | - Rerelease 18 | 19 | ## [0.3.0] - 2024-02-08 20 | ### New Features 21 | - Added initial support for page transitions (Special thanks to Marc and Evan) 22 | ### Bug Fixes or Enhancements 23 | - Support dioxus 0.6.3 24 | ### Changes 25 | - Most of the things should be on the prelude, so if you face any erros while migrating, just import prelude::*. 26 | 27 | ## [0.2.3] - 2024-01-23 28 | ### Dioxus Version Bump 29 | - updated to dioxus v0.6.2 30 | - minor fixes 31 | 32 | ## [0.2.2] - 2024-01-17 33 | ### Performance Improvements 34 | - Resource optimization for web 35 | 36 | ## [0.2.1] - 2024-01-11 37 | ### Performance Improvements 38 | - Smoothness Optimization 39 | ### New Features 40 | - Animation Sequence 41 | 42 | ## [0.2.0] - 2024-01-05 43 | ### Breaking Changes 44 | - Replaced `use_value_animation` and `use_transform_animation` with `use_motion` hook 45 | - Removed old animation configuration system 46 | - Updated Transform property names for consistency 47 | - Changed spring physics default parameters 48 | - Removed deprecated animation methods 49 | 50 | ### New Features 51 | - Added Color animation support 52 | - Introduced new `AnimationConfig` API 53 | - Added support for animation delays 54 | - Implemented loop modes (Infinite, Times) 55 | - Added new spring physics configuration 56 | - Improved cross-platform performance 57 | - Added new examples and documentation 58 | 59 | ### Performance Improvements 60 | - Optimized animation frame handling 61 | - Reduced CPU usage on desktop platforms 62 | - Improved interpolation calculations 63 | - Better memory management 64 | - Enhanced cleanup on unmount 65 | 66 | ### Bug Fixes 67 | - Fixed color interpolation for decreasing values 68 | - Corrected spring physics calculations 69 | - Fixed desktop platform timing issues 70 | - Resolved memory leaks in animation loops 71 | - Fixed transform rotation interpolation 72 | 73 | ## 🆕 What's New in v0.2.0 74 | 75 | ### New Animation API 76 | - Unified animation hook `use_animation` 77 | - Simplified configuration 78 | - Enhanced type safety 79 | - Better performance 80 | 81 | ### Color Animations 82 | ```rust 83 | let color = use_motion(Color::from_rgba(59, 130, 246, 255)); 84 | color.animate_to( 85 | Color::from_rgba(168, 85, 247, 255), 86 | AnimationConfig::new(AnimationMode::Spring(Spring::default())) 87 | ); 88 | ``` 89 | ### Animation Delays & Loops 90 | ```rust 91 | AnimationConfig::new(mode) 92 | .with_delay(Duration::from_secs(1)) 93 | .with_loop(LoopMode::Times(3)) 94 | ``` 95 | 96 | ## [0.1.4] - 2024-12-28 97 | ### Changes 98 | - Update dependencies and remove unused UUID references 99 | - Stop animations on component drop for improved resource management 100 | - Refactor delay function to improve animation frame handling 101 | - Optimize animation frame handling for smoother performance 102 | - Add Screen feature to web-sys and improve frame time calculation 103 | - Force target 90 FPS hardcoding for consistent performance 104 | 105 | ### Fixes 106 | - Remove Tailwind CDN dependency from Index.html 107 | - Remove Particle Effect temporarily for stability 108 | - Revert to initial implementation of delay function 109 | - Code cleanup and optimization 110 | 111 | ## [0.1.3] - 2024-12-27 112 | ### Changes 113 | - Adjust animation frame threshold for smoother performance 114 | 115 | ### Fixes 116 | - Fixed Desktop Platform (Seemed to be broken previously) 117 | 118 | ## [0.1.2] - 2024-12-27 119 | ### Changes 120 | - Example Overhaul 121 | 122 | ### Fixes 123 | - Fixed Desktop Platform (Seemed to be broken previously) 124 | 125 | ## [0.1.1] - 2024-12-27 126 | ### Changes 127 | - Update Readme 128 | 129 | ## [0.1.0] - 2024-12-27 130 | ### Changes 131 | - Initial Release 132 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-motion" 3 | description = "Animations library for Dioxus." 4 | version = "0.3.1" 5 | edition = "2024" 6 | license = "MIT" 7 | authors = ["Sabin Regmi "] 8 | readme = "./README.md" 9 | homepage = "https://wheregmis.github.io" 10 | repository = "https://github.com/wheregmis/dioxus-motion.git" 11 | keywords = ["dioxus", "animations"] 12 | categories = ["graphics", "gui"] 13 | 14 | [dependencies] 15 | easer = "0.3.0" 16 | futures-util = { version = "0.3.31", default-features = false } 17 | instant = { version = "0.1.13", optional = true } 18 | wasm-bindgen = { version = "0.2.100", optional = true, default-features = false } 19 | web-sys = { version = "0.3.77", optional = true, default-features = false, features = [ 20 | "Window", 21 | "Performance", 22 | ] } 23 | futures-channel = { version = "0.3.31", default-features = false } 24 | # For desktop platforms 25 | tokio = { version = "1.43.0", optional = true, default-features = false } 26 | # For transitions 27 | dioxus-motion-transitions-macro = { path = "packages/dioxus-motion-transitions-macro", version = "0.1.0", optional = true } 28 | dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", branch = "main", features = [ 29 | "router", 30 | ] } 31 | smallvec = "1.14.0" 32 | spin_sleep = "1.3.1" 33 | tracing = "0.1.41" 34 | thiserror = "2.0.12" 35 | 36 | [features] 37 | default = ["web"] 38 | web = ["wasm-bindgen", "web-sys", "instant/wasm-bindgen"] 39 | desktop = ["tokio", "instant"] 40 | transitions = ["dioxus-motion-transitions-macro"] 41 | 42 | 43 | [profile] 44 | 45 | [profile.wasm-dev] 46 | inherits = "dev" 47 | opt-level = 1 48 | 49 | [profile.server-dev] 50 | inherits = "dev" 51 | 52 | [profile.android-dev] 53 | inherits = "dev" 54 | 55 | [profile.dev] 56 | debug = 0 57 | opt-level = 0 58 | incremental = true 59 | overflow-checks = false 60 | lto = "thin" 61 | panic = "unwind" 62 | split-debuginfo = "unpacked" 63 | strip = "debuginfo" 64 | 65 | [profile.release] 66 | codegen-units = 1 # Allows LLVM to perform better optimization. 67 | lto = true # Enables link-time-optimizations. 68 | opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed. 69 | panic = "abort" # Higher performance by disabling panic handlers. 70 | strip = true # Ensures debug symbols are removed. 71 | 72 | [workspace] 73 | members = ["packages/dioxus-motion-transitions-macro", ".", "docs"] 74 | resolver = "3" 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 [your name] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Dioxus Animation Library" or "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dioxus Motion 🚀 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/wheregmis/dioxus-motion/blob/main/LICENSE) 4 | [![Crates.io](https://img.shields.io/crates/v/dioxus-motion.svg)](https://crates.io/crates/dioxus-motion) 5 | [![Docs](https://docs.rs/dioxus-motion/badge.svg)](https://docs.rs/dioxus-motion/0.1.4/dioxus_motion/) 6 | 7 | A lightweight, cross-platform animation library for Dioxus, designed to bring smooth, flexible animations to your Rust web, desktop, and mobile applications. 8 | 9 | ## ⚠️ Important Note 10 | 11 | This repository follows Dioxus's main branch for the latest features and improvements. For production use, we recommend using the stable version from [crates.io](https://crates.io/crates/dioxus-motion) instead of directly depending on the repository. 12 | 13 | ```toml 14 | # Recommended: Stable version from crates.io 15 | dioxus-motion = "0.3.1" 16 | 17 | # Development version: Follows Dioxus main branch 18 | dioxus-motion = { git = "https://github.com/wheregmis/dioxus-motion.git", branch = "main" } 19 | ``` 20 | 21 | ## 🎯 Live Examples 22 | 23 | 24 | 25 | Visit our [Example Website](https://wheregmis.github.io/dioxus-motion/) to see these animations in action: 26 | 27 | ## 🚀 Page Transitions 28 | 29 | ```rust 30 | use dioxus_motion::prelude::*; 31 | 32 | #[derive(Routable, Clone, Debug, PartialEq, MotionTransitions )] 33 | #[rustfmt::skip] 34 | enum Route { 35 | #[layout(NavBar)] 36 | #[route("/")] 37 | #[transition(Fade)] 38 | Home {}, 39 | #[route("/slide-left")] 40 | #[transition(ZoomIn)] 41 | SlideLeft {}, 42 | #[route("/slide-right")] 43 | SlideRight {}, 44 | #[route("/slide-up")] 45 | SlideUp {}, 46 | #[route("/slide-down")] 47 | SlideDown {}, 48 | #[route("/fade")] 49 | Fade {}, 50 | #[end_layout] 51 | #[route("/:..route")] 52 | PageNotFound { route: Vec }, 53 | } 54 | ``` 55 | 56 | And replace all your `Outlet:: {}` with `AnimatedOutlet:: {}` and place the layout containing OutletRouter on top with something like this 57 | 58 | ```rust 59 | #[component] 60 | fn NavBar() -> Element { 61 | rsx! { 62 | nav { id: "navbar take it", 63 | Link { to: Route::Home {}, "Home" } 64 | Link { to: Route::SlideLeft {}, "Blog" } 65 | } 66 | AnimatedOutlet:: {} 67 | } 68 | } 69 | ``` 70 | 71 | Each route can have its own transition effect: 72 | 73 | - `Fade`: Smooth opacity transition 74 | - `ZoomIn`: Scale and fade combination 75 | - `SlideLeft`: Horizontal slide animation 76 | - [And more!](https://github.com/wheregmis/dioxus-motion/blob/main/src/transitions/page_transitions.rs) 77 | - Also, add transitions feature to support page transitions. [Example](https://github.com/wheregmis/animated_router/blob/main/src/main.rs) which was translated from router [example](https://github.com/DioxusLabs/dioxus/blob/main/examples/router.rs) of Dioxus. More detailed guide will be updated soon. 78 | 79 | ### Quick Value Animation Example 80 | 81 | ```rust 82 | use dioxus_motion::prelude::*; 83 | 84 | #[component] 85 | fn PulseEffect() -> Element { 86 | let scale = use_motion(1.0f32); 87 | 88 | use_effect(move || { 89 | scale.animate_to( 90 | 1.2, 91 | AnimationConfig::new(AnimationMode::Spring(Spring { 92 | stiffness: 100.0, 93 | damping: 5.0, 94 | mass: 0.5, 95 | velocity: 1.0 96 | })) 97 | .with_loop(LoopMode::Infinite) 98 | ); 99 | }); 100 | 101 | rsx! { 102 | div { 103 | class: "w-20 h-20 bg-blue-500 rounded-full", 104 | style: "transform: scale({scale.get_value()})" 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | ### Animation Sequences Example 111 | 112 | Chain multiple animations together with different configurations: 113 | 114 | ```rust 115 | let scale = use_motion(1.0f32); 116 | 117 | // Create a bouncy sequence 118 | let sequence = AnimationSequence::new() 119 | .then( 120 | 1.2, // Scale up 121 | AnimationConfig::new(AnimationMode::Spring(Spring { 122 | stiffness: 400.0, 123 | damping: 10.0, 124 | mass: 1.0, 125 | velocity: 5.0, 126 | })) 127 | ) 128 | .then( 129 | 0.8, // Scale down 130 | AnimationConfig::new(AnimationMode::Spring(Spring { 131 | stiffness: 300.0, 132 | damping: 15.0, 133 | mass: 1.0, 134 | velocity: -2.0, 135 | })) 136 | ) 137 | .then( 138 | 1.0, // Return to original 139 | AnimationConfig::new(AnimationMode::Spring(Spring::default())) 140 | ); 141 | 142 | // Start the sequence 143 | scale.animate_sequence(sequence); 144 | // Each step in the sequence can have its own timing, easing, and spring physics configuration. Sequences can also be looped or chained with other animations. 145 | ``` 146 | 147 | ## ✨ Features 148 | 149 | - **Cross-Platform Support**: Works on web, desktop, and mobile 150 | - **Flexible Animation Configuration** 151 | - **Custom Easing Functions** 152 | - **Modular Feature Setup** 153 | - **Simple, Intuitive API** 154 | - **Page Transitions** 155 | 156 | ## 🛠 Installation 157 | 158 | Add to your `Cargo.toml`: 159 | 160 | ```toml 161 | [dependencies] 162 | dioxus-motion = { version = "0.3.0", optional = true, default-features = false } 163 | 164 | [features] 165 | default = ["web"] 166 | web = ["dioxus/web", "dioxus-motion/web"] 167 | desktop = ["dioxus/desktop", "dioxus-motion/desktop"] 168 | mobile = ["dioxus/mobile", "dioxus-motion/desktop"] 169 | ``` 170 | 171 | If you want to use page transiton dependency will look like, 172 | 173 | ```toml 174 | [dependencies] 175 | dioxus-motion = { version = "0.3.0", optional = true, default-features = false } 176 | 177 | [features] 178 | default = ["web"] 179 | web = ["dioxus/web", "dioxus-motion/web", "dioxus-motion/transitions"] 180 | desktop = [ 181 | "dioxus/desktop", 182 | "dioxus-motion/desktop", 183 | "dioxus-motion/transitions", 184 | ] 185 | mobile = ["dioxus/mobile", "dioxus-motion/desktop", "dioxus-motion/transitions"] 186 | ``` 187 | 188 | ## 🌐 Platform Support 189 | 190 | Choose the right feature for your platform: 191 | 192 | - `web`: For web applications using WASM 193 | - `desktop`: For desktop and mobile applications 194 | - `default`: Web support (if no feature specified) 195 | 196 | ## 🚀 Quick Start 197 | 198 | ## 🔄 Migration Guide (v0.3.0) 199 | 200 | - No breaking changes to the existing APIs. Just minor exports might change so just import prelude::\* if anything breaks on import 201 | 202 | ```rust 203 | use dioxus_motion::prelude::*; 204 | ``` 205 | 206 | ## 🔄 Migration Guide (v0.2.0) 207 | 208 | ### Breaking Changes 209 | 210 | - Combined `use_value_animation` and `use_transform_animation` into `use_motion` 211 | - New animation configuration API 212 | - Updated spring physics parameters 213 | - Changed transform property names 214 | 215 | ### New Animation API 216 | 217 | ```rust 218 | use dioxus_motion::prelude::*; 219 | 220 | // Before (v0.1.x) 221 | let mut motion = use_value_animation(Motion::new(0.0).to(100.0)); 222 | 223 | // After (v0.2.x) 224 | let mut value = use_motion(0.0f32); 225 | value.animate_to( 226 | 100.0, 227 | AnimationConfig::new(AnimationMode::Tween(Tween { 228 | duration: Duration::from_secs(2), 229 | easing: easer::functions::Linear::ease_in_out, 230 | })) 231 | ); 232 | 233 | // Before (v0.1.x) 234 | let mut transform = use_transform_animation(Transform::default()); 235 | 236 | // After (v0.2.x) 237 | let mut transform = use_motion(Transform::default()); 238 | transform.animate_to( 239 | Transform::new(100.0, 0.0, 1.2, 45.0), 240 | AnimationConfig::new(AnimationMode::Spring(Spring { 241 | stiffness: 100.0, 242 | damping: 10.0, 243 | mass: 1.0, 244 | ..Default::default() 245 | })) 246 | ); 247 | ``` 248 | 249 | ### If you were using transform.get_style(), that function is removed to make the library more generic so I recommend building something like 250 | 251 | ```rust 252 | let transform = use_motion(Transform::default()); 253 | 254 | let transform_style = use_memo(move || { 255 | format!( 256 | "transform: translate({}px, {}px) scale({}) rotate({}deg);", 257 | transform.get_value().x, 258 | transform.get_value().y, 259 | transform.get_value().scale, 260 | transform.get_value().rotation * 180.0 / std::f32::consts::PI 261 | ) 262 | }); 263 | 264 | // and using the memo in the component 265 | rsx! { 266 | div { 267 | class: "...", 268 | style: "{transform_style.read()}", 269 | // ...rest of component... 270 | } 271 | } 272 | ``` 273 | 274 | ## 🆕 New Features 275 | 276 | ### Loop Modes 277 | 278 | ```rust 279 | .with_loop(LoopMode::Infinite) 280 | .with_loop(LoopMode::Times(3)) 281 | ``` 282 | 283 | ### Animation Delays 284 | 285 | ```rust 286 | .with_delay(Duration::from_secs(1)) 287 | ``` 288 | 289 | ### On Complete 290 | 291 | ```rust 292 | .with_on_complete(|| println!("Animation complete!")) 293 | ``` 294 | 295 | ## 🎓 Advanced Guide: Extending Animations 296 | 297 | ### Implementing the Animatable Trait 298 | 299 | [Cube Component Example](https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/cube_animation.rs) 300 | 301 | The `Animatable` trait allows you to animate any custom type. 302 | 303 | Defination of Animatable Trait 304 | 305 | ```rust 306 | pub trait Animatable: Copy + 'static { 307 | fn zero() -> Self; 308 | fn epsilon() -> f32; 309 | fn magnitude(&self) -> f32; 310 | fn scale(&self, factor: f32) -> Self; 311 | fn add(&self, other: &Self) -> Self; 312 | fn sub(&self, other: &Self) -> Self; 313 | fn interpolate(&self, target: &Self, t: f32) -> Self; 314 | } 315 | 316 | ``` 317 | 318 | Here's how to implement it: 319 | 320 | ### Custom Position Type 321 | 322 | ```rust 323 | #[derive(Debug, Copy, Clone)] 324 | struct Position { 325 | x: f32, 326 | y: f32, 327 | } 328 | 329 | impl Animatable for Position { 330 | fn zero() -> Self { 331 | Position { x: 0.0, y: 0.0 } 332 | } 333 | 334 | fn epsilon() -> f32 { 335 | 0.001 336 | } 337 | 338 | fn magnitude(&self) -> f32 { 339 | (self.x * self.x + self.y * self.y).sqrt() 340 | } 341 | 342 | fn scale(&self, factor: f32) -> Self { 343 | Position { 344 | x: self.x * factor, 345 | y: self.y * factor, 346 | } 347 | } 348 | 349 | fn add(&self, other: &Self) -> Self { 350 | Position { 351 | x: self.x + other.x, 352 | y: self.y + other.y, 353 | } 354 | } 355 | 356 | fn sub(&self, other: &Self) -> Self { 357 | Position { 358 | x: self.x - other.x, 359 | y: self.y - other.y, 360 | } 361 | } 362 | 363 | fn interpolate(&self, target: &Self, t: f32) -> Self { 364 | Position { 365 | x: self.x + (target.x - self.x) * t, 366 | y: self.y + (target.y - self.y) * t, 367 | } 368 | } 369 | } 370 | ``` 371 | 372 | ### Best Practices 373 | 374 | - Zero State: Implement zero() as your type's neutral state 375 | - Epsilon: Choose a small value (~0.001) for animation completion checks 376 | - Magnitude: Return the square root of sum of squares for vector types 377 | - Scale: Multiply all components by the factor 378 | - Add/Sub: Implement component-wise addition/subtraction 379 | - Interpolate: Use linear interpolation for smooth transitions 380 | 381 | ### Common Patterns 382 | 383 | #### Circular Values (e.g., angles) 384 | 385 | ```rust 386 | fn interpolate(&self, target: &Self, t: f32) -> Self { 387 | let mut diff = target.angle - self.angle; 388 | // Ensure shortest path 389 | if diff > PI { diff -= 2.0 * PI; } 390 | if diff < -PI { diff += 2.0 * PI; } 391 | Self { angle: self.angle + diff * t } 392 | } 393 | ``` 394 | 395 | #### Normalized Values (e.g., colors) 396 | 397 | ```rust 398 | fn scale(&self, factor: f32) -> Self { 399 | Self { 400 | value: (self.value * factor).clamp(0.0, 1.0) 401 | } 402 | } 403 | ``` 404 | 405 | ## 🌈 Supported Easing Functions 406 | 407 | Leverages the `easer` crate, supporting: 408 | 409 | - Linear 410 | - Quadratic 411 | - Cubic 412 | - Quartic 413 | - And more! 414 | 415 | ## 🤝 Contributing 416 | 417 | 1. Fork the repository 418 | 2. Create your feature branch 419 | 3. Commit changes 420 | 4. Push to the branch 421 | 5. Create a Pull Request 422 | 423 | ## 📄 License 424 | 425 | MIT License 426 | 427 | ## 🐞 Reporting Issues 428 | 429 | Please report issues on the GitHub repository with: 430 | 431 | - Detailed description 432 | - Minimal reproducible example 433 | - Platform and feature configuration used 434 | 435 | ## 🌟 Motivation 436 | 437 | Bringing elegant, performant motion animations to Rust's web and desktop ecosystems with minimal complexity. 438 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Dioxus Motion - Simple Animation Roadmap If i dont get bored? 2 | 3 | # Dioxus Motion - Implementation Progress 4 | 5 | ## Phase 1: Core Animation Primitives 6 | 7 | ### Animate Component 8 | 9 | - [x] Basic value transitions 10 | ```rust 11 | // Already implemented via: 12 | Motion::new(0.0) 13 | .to(100.0) 14 | .duration(Duration::from_millis(300)) 15 | ``` 16 | - [x] Duration control 17 | - [x] Easing functions 18 | - [x] Common animation properties 19 | - [x] x, y (transform) 20 | - [x] scale 21 | - [x] rotate 22 | - [x] opacity 23 | 24 | ### Basic Transitions 25 | 26 | - [x] Duration (implemented via `duration()`) 27 | - [x] Easing functions (implemented via `easing()`) 28 | - [x] Animation completion callbacks 29 | - [x] Delay 30 | - [x] Spring animations 31 | 32 | ## Phase 2: Animation State & Control 33 | 34 | ### State Management 35 | 36 | - [x] Animation state tracking (Idle, Running, Completed) 37 | - [x] Progress tracking 38 | - [x] Running state detection 39 | 40 | ### Controls 41 | 42 | - [x] Start animation -> Will always start from the initial value 43 | - [x] Stop -> Will stop on the stopped value 44 | - [x] Resume -> Will continue from the stopped value 45 | - [x] Reset animation -> Will reset back to initial value 46 | 47 | ## Current Implementation Strengths 48 | 49 | - ✅ Solid foundation for value animations 50 | - ✅ Cross-platform support (Web/Desktop) 51 | - ✅ Clean API design 52 | - ✅ Good state management 53 | - ✅ Performance consideration (frame rate control) 54 | 55 | ## Success Goals 56 | 57 | - Simple, intuitive API 58 | - Smooth animations 59 | - Small bundle size 60 | - Easy to understand documentation 61 | - Basic but powerful feature set 62 | -------------------------------------------------------------------------------- /blog.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Building dioxus-motion: A Physics-based Animation Library for Rust" 3 | date = "2024-03-15" 4 | description = "A deep dive into creating a modern animation library for Dioxus, bringing fluid, physics-based animations to Rust web development" 5 | image = "http://rustacean.net/assets/rustacean-flat-happy.png" 6 | tags = ["rust", "web-dev", "tutorial"] 7 | +++ 8 | 9 | # Introduction 10 | In the evolving landscape of Rust web development, Dioxus has emerged as a powerful framework for building user interfaces. However, one crucial piece was missing: a sophisticated animation system. While JavaScript ecosystems flourish with animation libraries like Framer Motion, the Rust community lacked an equivalent solution. This gap inspired the creation of dioxus-motion, a physics-based animation library that brings fluid, natural-feeling animations to Rust applications. 11 | 12 | ## The Journey to dioxus-motion 13 | My journey began when I was using Framer Motion alongside Dioxus. The experience was promising, but the integration felt incomplete. After searching for native Rust alternatives and finding limited options, I decided to build a solution that would feel natural in the Rust ecosystem while maintaining the intuitive API design that makes Framer Motion so popular. 14 | 15 | ## Core Design Principles 16 | The library was built with four fundamental principles: 17 | 18 | 1. **Rust-First Design**: Leveraging Rust's type system to provide compile-time safety for animations 19 | 2. **Zero-Cost Philosophy**: Implementing performant animations with minimal runtime overhead 20 | 3. **Universal Compatibility**: Supporting both web and native platforms seamlessly 21 | 4. **Developer Experience**: Creating an intuitive API that feels natural to Rust developers 22 | 23 | ## Technical Architecture 24 | dioxus-motion is built on two foundational animation concepts: 25 | 26 | - **Spring Physics**: Implementing Hooke's law for natural, physical animations 27 | - **Tweening System**: Supporting traditional keyframe animations with customizable easing 28 | 29 | ### Core Animation Example 30 | ```rust 31 | let mut position = use_motion(0.0f32); 32 | position.animate_to( 33 | 100.0, 34 | AnimationConfig::new(AnimationMode::Spring(Spring { 35 | stiffness: 100.0, 36 | damping: 10.0, 37 | mass: 1.0, 38 | velocity: 0.0, 39 | })) 40 | ); 41 | ``` 42 | 43 | ## Implementation Deep Dive 44 | 45 | ### Core Animation System 46 | The animation system was built on three key components: 47 | 48 | 1. **Motion Hook**: 49 | ```rust 50 | pub fn use_motion(initial: T) -> Motion { 51 | let state = use_signal(|| AnimationState::new(initial)); 52 | Motion::new(state) 53 | } 54 | ``` 55 | This hook manages the animation state and provides a clean API for components. 56 | 57 | 58 | 59 | 60 | ### Challenges & Solutions 61 | 62 | 1. **Browser Compatibility** 63 | - **Challenge**: Different browsers handle requestAnimationFrame differently 64 | - **Solution**: Created a platform-agnostic timing system with fallbacks 65 | 66 | 2. **State Management** 67 | - **Challenge**: Keeping animations smooth during React-style rerenders 68 | - **Solution**: Implemented a separate animation loop that runs independent of the render cycle 69 | 70 | 3. **Type System Integration** 71 | - **Challenge**: Making animations work with any type while maintaining type safety 72 | - **Solution**: Created the `Animatable` trait with safe defaults: 73 | ```rust 74 | pub trait Animatable: 'static + Copy + Send + Sync { 75 | fn zero() -> Self; 76 | fn epsilon() -> f32; 77 | fn magnitude(&self) -> f32; 78 | fn scale(&self, factor: f32) -> Self; 79 | fn add(&self, other: &Self) -> Self; 80 | fn sub(&self, other: &Self) -> Self; 81 | fn interpolate(&self, target: &Self, t: f32) -> Self; 82 | } 83 | ``` 84 | 85 | ## Showcase: Advanced Animation Examples 86 | 87 | ### 3D Cube Animation 88 | The library supports complex 3D transformations, as demonstrated by this rotating cube example: 89 | 90 | ```rust 91 | #[derive(Debug, Clone, Copy)] 92 | struct Transform3D { 93 | rotate_x: f32, 94 | rotate_y: f32, 95 | rotate_z: f32, 96 | translate_x: f32, 97 | translate_y: f32, 98 | scale: f32, 99 | } 100 | 101 | #[component] 102 | fn SwingingCube() -> Element { 103 | let mut transform = use_motion(Transform3D::zero()); 104 | 105 | // Animate the cube with spring physics 106 | transform.animate_to( 107 | Transform3D::new( 108 | PI / 3.0, // X rotation 109 | PI / 2.0, // Y rotation 110 | PI / 4.0, // Z rotation 111 | 2.0, // X translation 112 | -1.0, // Y translation 113 | 1.2, // Scale 114 | ), 115 | AnimationConfig::new(AnimationMode::Spring(Spring { 116 | stiffness: 35.0, 117 | damping: 5.0, 118 | mass: 1.0, 119 | velocity: 2.0, 120 | })) 121 | .with_loop(LoopMode::Infinite), 122 | ); 123 | // ...rest of the implementation 124 | } 125 | ``` 126 | 127 | ### Animated Flower 128 | Another example showcasing organic animations with multiple coordinated elements: 129 | 130 | ```rust 131 | #[derive(Debug, Clone, Copy)] 132 | struct PetalTransform { 133 | rotate: f32, 134 | scale: f32, 135 | translate_x: f32, 136 | translate_y: f32, 137 | } 138 | 139 | #[component] 140 | fn AnimatedFlower() -> Element { 141 | let mut petal_transform = use_motion(PetalTransform::zero()); 142 | let mut center_scale = use_motion(0.0f32); 143 | 144 | // Animate petals blooming 145 | petal_transform.animate_to( 146 | PetalTransform::new(PI / 4.0, 1.2, 3.0, 3.0), 147 | AnimationConfig::new(AnimationMode::Spring(Spring { 148 | stiffness: 60.0, 149 | damping: 8.0, 150 | mass: 0.5, 151 | velocity: 1.0, 152 | })) 153 | .with_loop(LoopMode::Infinite), 154 | ); 155 | // ...rest of the implementation 156 | } 157 | ``` 158 | 159 | ## Roadmap and Future Development 160 | 1. **Performance Enhancements** 161 | - Implementation of batch animation updates 162 | - WebAssembly-specific optimizations 163 | - Advanced caching strategies 164 | 165 | 2. **API Evolution** 166 | - Direct DOM style manipulation integration 167 | - Enhanced gesture support 168 | - Animation composition utilities 169 | 170 | ## Conclusion 171 | dioxus-motion represents a significant step forward for animation capabilities in the Rust ecosystem. While we've achieved our initial goals of creating a robust, physics-based animation system, this is just the beginning. We're excited to see how the community will use and enhance these tools to create more dynamic and engaging user interfaces. 172 | 173 | Ready to start animating? Visit our [GitHub repository](https://github.com/wheregmis/dioxus-motion) to contribute or try it out! -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | .DS_Store 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /docs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "docs" 3 | version = "0.1.0" 4 | authors = ["Sabin Regmi "] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", branch = "main", features = [ 11 | "router", 12 | ] } 13 | dioxus-motion = { path = "../", default-features = false, optional = true } 14 | easer = { version = "0.3.0", default-features = false } 15 | 16 | [features] 17 | default = ["web"] 18 | web = ["dioxus/web", "dioxus-motion/web", "dioxus-motion/transitions"] 19 | desktop = [ 20 | "dioxus/desktop", 21 | "dioxus-motion/desktop", 22 | "dioxus-motion/transitions", 23 | ] 24 | mobile = ["dioxus/mobile", "dioxus-motion/desktop", "dioxus-motion/transitions"] 25 | -------------------------------------------------------------------------------- /docs/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | [web.app] 4 | 5 | # HTML title tag content 6 | # HTML title tag content 7 | title = "Dioxus Motion 🚀" 8 | 9 | # base_path = "dioxus-motion" 10 | 11 | # include `assets` in web platform 12 | [web.resource] 13 | 14 | # Additional CSS style files 15 | style = [] 16 | 17 | # Additional JavaScript files 18 | script = [] 19 | 20 | [web.resource.dev] 21 | 22 | # Javascript code file 23 | # serve: [dev-server] only 24 | script = [] 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets. 4 | 5 | ### Tailwind 6 | 1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm 7 | 2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation 8 | 3. Run the following command in the root of the project to start the Tailwind CSS compiler: 9 | 10 | ```bash 11 | npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch 12 | ``` 13 | 14 | ### Serving Your App 15 | 16 | Run the following command in the root of your project to start developing with the default platform: 17 | 18 | ```bash 19 | dx serve 20 | ``` 21 | 22 | To run for a different platform, use the `--platform platform` flag. E.g. 23 | ```bash 24 | dx serve --platform desktop 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wheregmis/dioxus-motion/1061379d48fb24213090274737c22883b2a04d92/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/input.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @config './tailwind.config.js'; 4 | 5 | @theme { 6 | /* Typography */ 7 | --font-display: 'JetBrains Mono', monospace; 8 | --font-body: 'Inter', sans-serif; 9 | 10 | /* Base theme colors */ 11 | --color-background: oklch(0.04 0 0); 12 | /* #0A0F1A */ 13 | --color-background-light: oklch(1 0 0); 14 | /* #FFFFFF */ 15 | 16 | /* Dark theme colors */ 17 | --color-dark-50: oklch(0.1 0 0); 18 | --color-dark-100: oklch(0.15 0 0); 19 | --color-dark-200: oklch(0.2 0 0); 20 | --color-dark-300: oklch(0.25 0 0); 21 | --color-dark-400: oklch(0.3 0 0); 22 | 23 | /* Light theme colors */ 24 | --color-light-50: oklch(0.98 0 0); 25 | --color-light-100: oklch(0.95 0 0); 26 | --color-light-200: oklch(0.9 0 0); 27 | --color-light-300: oklch(0.85 0 0); 28 | --color-light-400: oklch(0.8 0 0); 29 | 30 | /* Primary colors - Rust orange */ 31 | --color-primary: oklch(0.75 0.15 30); 32 | /* #CD7F32 - Rust bronze */ 33 | --color-primary-light: oklch(0.8 0.15 30); 34 | /* #FFA07A - Light salmon */ 35 | --color-primary-dark: oklch(0.7 0.15 30); 36 | /* #B87333 - Bronze */ 37 | --color-primary-hover: oklch(0.85 0.15 30); 38 | 39 | /* Secondary colors - Rust gray */ 40 | --color-secondary: oklch(0.65 0.02 30); 41 | /* #8B4513 - Saddle brown */ 42 | --color-secondary-light: oklch(0.9 0.02 30); 43 | /* #DEB887 - Burly wood */ 44 | --color-secondary-dark: oklch(0.35 0.02 30); 45 | /* #A0522D - Sienna */ 46 | 47 | /* Accent colors */ 48 | --color-accent-rust: oklch(0.75 0.15 30); 49 | /* #CD7F32 - Rust bronze */ 50 | --color-accent-rust-hover: oklch(0.85 0.15 30); 51 | /* #FFA07A - Light salmon */ 52 | 53 | /* Surface colors */ 54 | --color-surface: oklch(0.25 0 0); 55 | /* #1F2937 */ 56 | --color-surface-light: oklch(0.35 0 0); 57 | /* #374151 */ 58 | --color-surface-dark: oklch(0.15 0 0); 59 | /* #111827 */ 60 | --color-surface-hover: oklch(0.15 0 0 / 0.7); 61 | /* #111827B3 */ 62 | 63 | /* Text colors */ 64 | --color-text-primary: oklch(1 0 0); 65 | /* #FFFFFF */ 66 | --color-text-secondary: oklch(0.85 0 0); 67 | /* #D1D5DB */ 68 | --color-text-muted: oklch(0.65 0 0); 69 | /* #9CA3AF */ 70 | 71 | /* Text colors for light mode */ 72 | --color-text-primary-light: oklch(0.15 0 0); 73 | --color-text-secondary-light: oklch(0.25 0 0); 74 | --color-text-muted-light: oklch(0.35 0 0); 75 | 76 | /* Transitions */ 77 | --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); 78 | --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); 79 | --ease-snappy: cubic-bezier(0.2, 0, 0, 1); 80 | --transition-fast: 150ms var(--ease-snappy); 81 | --transition-medium: 300ms var(--ease-smooth); 82 | --transition-spring: 500ms var(--ease-spring); 83 | 84 | /* Shadows */ 85 | --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 86 | --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 87 | --shadow-lg: 88 | 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 89 | --shadow-neumorphic: 90 | 20px 20px 60px #d9d9d9, -20px -20px 60px #ffffff, 91 | 0 4px 6px -1px rgba(255, 255, 255, 0.1), 92 | 0 2px 4px -1px rgba(255, 255, 255, 0.06), 93 | 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 94 | } 95 | 96 | /* 97 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 98 | so we've added these compatibility styles to make sure everything still 99 | looks the same as it did with Tailwind CSS v3. 100 | 101 | If we ever want to remove these styles, we need to add an explicit border 102 | color utility to any element that depends on these defaults. 103 | */ 104 | @layer base { 105 | *, 106 | ::after, 107 | ::before, 108 | ::backdrop, 109 | ::file-selector-button { 110 | border-color: var(--color-gray-200, currentcolor); 111 | } 112 | } 113 | 114 | /* Keyframes */ 115 | @keyframes down { 116 | 0% { 117 | transform: translateY(0%); 118 | } 119 | 120 | 100% { 121 | transform: translateY(calc(45vh - 8rem)); 122 | } 123 | } 124 | 125 | @keyframes shimmer { 126 | 0% { 127 | transform: translateX(-100%); 128 | } 129 | 130 | 100% { 131 | transform: translateX(100%); 132 | } 133 | } 134 | 135 | @keyframes float { 136 | 137 | 0%, 138 | 100% { 139 | transform: translateY(0); 140 | } 141 | 142 | 50% { 143 | transform: translateY(-10px); 144 | } 145 | } 146 | 147 | @keyframes glow { 148 | 149 | 0%, 150 | 100% { 151 | filter: brightness(1); 152 | } 153 | 154 | 50% { 155 | filter: brightness(1.2); 156 | } 157 | } 158 | 159 | /* Animation classes */ 160 | .animate-move-down { 161 | animation: down 3s linear infinite; 162 | } 163 | 164 | .animate-shimmer { 165 | animation: shimmer 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 166 | } 167 | 168 | .animate-float { 169 | animation: float 3s ease-in-out infinite; 170 | } 171 | 172 | .animate-glow { 173 | animation: glow 2s ease-in-out infinite; 174 | } 175 | 176 | /* Gradient backgrounds */ 177 | .bg-gradient-dark { 178 | background: linear-gradient(to right, 179 | var(--color-dark-50), 180 | var(--color-dark-100)); 181 | } 182 | 183 | .bg-gradient-light { 184 | background: linear-gradient(to right, 185 | var(--color-light-50), 186 | var(--color-light-100)); 187 | } 188 | 189 | .bg-gradient-primary { 190 | background: linear-gradient(to right, 191 | var(--color-primary), 192 | var(--color-primary-light)); 193 | } 194 | 195 | /* Dark mode utilities */ 196 | :root[class~="dark"] { 197 | --bg-primary: var(--color-dark-50); 198 | --text-primary: var(--color-text-primary); 199 | --text-secondary: var(--color-text-secondary); 200 | --text-muted: var(--color-text-muted); 201 | } 202 | 203 | :root:not([class~="dark"]) { 204 | --bg-primary: var(--color-light-50); 205 | --text-primary: var(--color-text-primary-light); 206 | --text-secondary: var(--color-text-secondary-light); 207 | --text-muted: var(--color-text-muted-light); 208 | } 209 | 210 | /* Component styles */ 211 | .card { 212 | @apply rounded-lg border transition-all duration-300 bg-transparent; 213 | border-color: color-mix(in oklab, var(--bg-primary) 80%, var(--text-primary)); 214 | color: var(--text-primary); 215 | } 216 | 217 | .btn { 218 | @apply px-4 py-2 rounded-lg font-medium transition-all duration-300; 219 | } 220 | 221 | .btn-primary { 222 | background-color: var(--color-primary); 223 | color: var(--color-dark-50); 224 | } 225 | 226 | .btn-primary:hover { 227 | background-color: var(--color-primary-hover); 228 | } 229 | 230 | .nav-link { 231 | @apply px-4 py-2 rounded-lg transition-all duration-300; 232 | color: var(--text-secondary); 233 | } 234 | 235 | .nav-link:hover { 236 | color: var(--text-primary); 237 | background-color: color-mix(in oklab, var(--bg-primary) 90%, var(--text-primary)); 238 | } 239 | 240 | /* Code block styles */ 241 | .code-block { 242 | @apply font-mono bg-dark-100 rounded-lg p-4; 243 | border: 1px solid var(--color-dark-300); 244 | position: relative; 245 | } 246 | 247 | .code-block::before { 248 | content: ""; 249 | position: absolute; 250 | top: 0; 251 | left: 0; 252 | width: 4px; 253 | height: 100%; 254 | background: var(--color-primary); 255 | border-radius: 2px; 256 | opacity: 0.5; 257 | } 258 | 259 | /* Logo animation - more subtle */ 260 | @keyframes gentle-float { 261 | 262 | 0%, 263 | 100% { 264 | transform: translateY(0); 265 | } 266 | 267 | 50% { 268 | transform: translateY(-5px); 269 | } 270 | } 271 | 272 | .logo-float { 273 | animation: gentle-float 3s ease-in-out infinite; 274 | } 275 | 276 | /* Animation keyframes */ 277 | @keyframes float { 278 | 279 | 0%, 280 | 100% { 281 | transform: translateY(0); 282 | } 283 | 284 | 50% { 285 | transform: translateY(-10px); 286 | } 287 | } 288 | 289 | @keyframes glow { 290 | 291 | 0%, 292 | 100% { 293 | filter: brightness(1); 294 | } 295 | 296 | 50% { 297 | filter: brightness(1.2); 298 | } 299 | } 300 | 301 | @keyframes performance-pulse { 302 | 303 | 0%, 304 | 100% { 305 | box-shadow: var(--shadow-glow-perf); 306 | transform: scale(1); 307 | } 308 | 309 | 50% { 310 | box-shadow: none; 311 | transform: scale(0.98); 312 | } 313 | } 314 | 315 | /* Animation classes */ 316 | .animate-float { 317 | animation: float 3s ease-in-out infinite; 318 | transform-style: preserve-3d; 319 | will-change: transform; 320 | } 321 | 322 | .animate-glow { 323 | animation: glow 2s ease-in-out infinite; 324 | transform-style: preserve-3d; 325 | will-change: transform; 326 | } 327 | 328 | .animate-performance { 329 | animation: performance-pulse 1.5s var(--ease-spring) infinite; 330 | transform-style: preserve-3d; 331 | will-change: transform; 332 | } 333 | 334 | /* Layout utilities */ 335 | .container-lg { 336 | @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8; 337 | } 338 | 339 | .container-md { 340 | @apply max-w-5xl mx-auto px-4 sm:px-6 lg:px-8; 341 | } 342 | 343 | /* Performance indicator styles */ 344 | .performance-badge { 345 | @apply inline-flex items-center px-3 py-1 rounded-full; 346 | background: var(--color-perf-300); 347 | color: var(--color-dark-100); 348 | font-size: 0.875rem; 349 | font-weight: 500; 350 | } 351 | 352 | /* Documentation specific styles */ 353 | .doc-section { 354 | @apply py-12 border-b border-dark-300; 355 | } 356 | 357 | .doc-heading { 358 | @apply text-2xl font-bold text-text-primary mb-4; 359 | } 360 | 361 | .doc-text { 362 | @apply text-text-secondary leading-relaxed; 363 | } 364 | 365 | /* Rust-themed decorative elements */ 366 | .rust-accent { 367 | position: relative; 368 | } 369 | 370 | .rust-accent::before { 371 | content: ""; 372 | position: absolute; 373 | top: 0; 374 | left: 0; 375 | width: 100%; 376 | height: 100%; 377 | background: linear-gradient(45deg, 378 | transparent 0%, 379 | var(--color-primary) 0.1%, 380 | transparent 0.2%); 381 | opacity: 0.1; 382 | pointer-events: none; 383 | } 384 | 385 | /* Rust-themed decorative elements */ 386 | .rust-border { 387 | border: 1px solid var(--color-primary); 388 | opacity: 0.1; 389 | } 390 | 391 | .rust-glow { 392 | box-shadow: 0 0 20px var(--color-primary); 393 | opacity: 0.1; 394 | } 395 | 396 | /* Animation keyframes */ 397 | @keyframes rust-pulse { 398 | 399 | 0%, 400 | 100% { 401 | box-shadow: 0 0 0 0 var(--color-primary); 402 | } 403 | 404 | 50% { 405 | box-shadow: 0 0 20px 0 var(--color-primary); 406 | } 407 | } 408 | 409 | .animate-rust-pulse { 410 | animation: rust-pulse 2s infinite; 411 | } 412 | -------------------------------------------------------------------------------- /docs/justfile: -------------------------------------------------------------------------------- 1 | css: 2 | npm run css -- --watch 3 | 4 | run: 5 | dx serve --platform web 6 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "css": "tailwindcss -i input.css -o ./assets/main.css" 4 | }, 5 | "dependencies": { 6 | "@tailwindcss/cli": "^4.1.4", 7 | "tailwindcss": "^4.1.4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/components/extras.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/src/components/footer.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | #[component] 4 | /// Renders a consistent footer component used across all pages. 5 | /// 6 | /// This footer includes: 7 | /// - A "Made with love" message 8 | /// - Copyright information 9 | /// - Social and documentation links 10 | /// 11 | /// # Examples 12 | /// 13 | /// ```rust 14 | /// use dioxus::prelude::*; 15 | /// 16 | /// fn app() -> Element { 17 | /// rsx! { 18 | /// // Your page content 19 | /// Footer {} 20 | /// } 21 | /// } 22 | /// ``` 23 | pub fn Footer() -> Element { 24 | rsx! { 25 | footer { class: "relative z-10 border-t border-primary/10 mt-auto py-8", 26 | div { class: "container mx-auto px-4", 27 | div { class: "flex flex-col items-center justify-center space-y-4 text-center", 28 | // Made with love 29 | p { class: "text-text-secondary", 30 | "Made with " 31 | span { class: "text-red-500 animate-pulse", "♥" } 32 | " using " 33 | a { 34 | href: "https://dioxuslabs.com", 35 | target: "_blank", 36 | class: "text-primary hover:text-primary/80 transition-colors", 37 | "Dioxus" 38 | } 39 | } 40 | // Copyright 41 | p { class: "text-text-muted text-sm", 42 | "© 2025 Dioxus Motion. All rights reserved." 43 | } 44 | // Links 45 | div { class: "flex items-center space-x-4 text-sm text-text-secondary", 46 | a { 47 | href: "https://github.com/wheregmis/dioxus-motion", 48 | target: "_blank", 49 | class: "hover:text-text-primary transition-colors", 50 | "GitHub" 51 | } 52 | span { "·" } 53 | a { 54 | href: "https://crates.io/crates/dioxus-motion", 55 | target: "_blank", 56 | class: "hover:text-text-primary transition-colors", 57 | "Crates.io" 58 | } 59 | span { "·" } 60 | a { 61 | href: "https://docs.rs/dioxus-motion", 62 | target: "_blank", 63 | class: "hover:text-text-primary transition-colors", 64 | "Documentation" 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/guide_navigation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::utils::router::Route; 4 | 5 | /// Represents a guide section with its route and title 6 | struct GuideSection { 7 | route: Route, 8 | title: &'static str, 9 | } 10 | 11 | /// The ordered list of guide sections for navigation 12 | fn guide_sections() -> Vec { 13 | vec![ 14 | GuideSection { 15 | route: Route::PageTransition {}, 16 | title: "Page Transitions", 17 | }, 18 | GuideSection { 19 | route: Route::BasicAnimationGuide {}, 20 | title: "Basic Animation Guide", 21 | }, 22 | GuideSection { 23 | route: Route::IntermediateAnimationGuide {}, 24 | title: "Intermediate Animation Guide", 25 | }, 26 | GuideSection { 27 | route: Route::ComplexAnimationGuide {}, 28 | title: "Complex Animation Guide", 29 | }, 30 | ] 31 | } 32 | 33 | #[component] 34 | /// Renders a navigation component for moving between guide sections. 35 | /// 36 | /// This component displays previous and next links based on the current route, 37 | /// allowing users to navigate sequentially through the documentation. 38 | /// It's designed to be responsive and mobile-friendly. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ```rust 43 | /// use dioxus::prelude::*; 44 | /// 45 | /// fn app() -> Element { 46 | /// rsx! { 47 | /// // Your page content 48 | /// GuideNavigation {} 49 | /// } 50 | /// } 51 | /// ``` 52 | pub fn GuideNavigation() -> Element { 53 | let current_route = use_route::(); 54 | let sections = guide_sections(); 55 | 56 | // Find the current section index 57 | let current_index = sections 58 | .iter() 59 | .position(|section| section.route == current_route) 60 | .unwrap_or(0); 61 | 62 | // Determine previous and next sections 63 | let prev_section = if current_index > 0 { 64 | Some(§ions[current_index - 1]) 65 | } else { 66 | None 67 | }; 68 | 69 | let next_section = if current_index < sections.len() - 1 { 70 | Some(§ions[current_index + 1]) 71 | } else { 72 | None 73 | }; 74 | 75 | rsx! { 76 | // Navigation container 77 | nav { 78 | class: "mt-12 pt-6 border-t border-primary/10", 79 | div { 80 | class: "flex flex-col sm:flex-row justify-between items-center gap-4", 81 | 82 | // Previous link 83 | div { 84 | class: "w-full sm:w-auto", 85 | if let Some(prev) = prev_section { 86 | Link { 87 | to: prev.route.clone(), 88 | class: "flex items-center justify-center sm:justify-start gap-2 w-full px-4 py-3 89 | bg-dark-200/50 backdrop-blur-xs rounded-lg 90 | border border-primary/10 hover:border-primary/20 91 | text-text-secondary hover:text-text-primary 92 | transition-all duration-300 group", 93 | 94 | // Arrow icon 95 | span { 96 | class: "text-primary transform transition-transform group-hover:-translate-x-1", 97 | "←" 98 | } 99 | 100 | // Label 101 | div { 102 | class: "flex flex-col", 103 | span { class: "text-xs text-text-muted", "Previous" } 104 | span { class: "text-sm font-medium", {prev.title} } 105 | } 106 | } 107 | } else { 108 | // Empty div to maintain layout when there's no previous link 109 | div { class: "hidden sm:block" } 110 | } 111 | } 112 | 113 | // Next link 114 | div { 115 | class: "w-full sm:w-auto", 116 | if let Some(next) = next_section { 117 | Link { 118 | to: next.route.clone(), 119 | class: "flex items-center justify-center sm:justify-end gap-2 w-full px-4 py-3 120 | bg-dark-200/50 backdrop-blur-xs rounded-lg 121 | border border-primary/10 hover:border-primary/20 122 | text-text-secondary hover:text-text-primary 123 | transition-all duration-300 group", 124 | 125 | // Label 126 | div { 127 | class: "flex flex-col items-end", 128 | span { class: "text-xs text-text-muted", "Next" } 129 | span { class: "text-sm font-medium", {next.title} } 130 | } 131 | 132 | // Arrow icon 133 | span { 134 | class: "text-primary transform transition-transform group-hover:translate-x-1", 135 | "→" 136 | } 137 | } 138 | } else { 139 | // Empty div to maintain layout when there's no next link 140 | div { class: "hidden sm:block" } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /docs/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod animations; 2 | pub mod code_block; 3 | pub mod extras; 4 | pub mod footer; 5 | pub mod guide_navigation; 6 | pub mod navbar; 7 | pub mod page_not_found; 8 | pub mod page_transition; 9 | -------------------------------------------------------------------------------- /docs/src/components/page_not_found.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | #[component] 4 | /// Renders a "Page not found" component that displays a message and the attempted navigation route. 5 | /// 6 | /// This component visually informs users that the requested page does not exist by presenting a heading, 7 | /// an apologetic message, and a formatted log of the navigation route that was attempted. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `route` - A vector of strings representing the segments of the attempted navigation path. 12 | /// 13 | /// # Returns 14 | /// 15 | /// Returns an `Element` representing the rendered "Page not found" user interface. 16 | /// 17 | /// # Examples 18 | /// 19 | /// ``` 20 | /// use dioxus::prelude::*; 21 | /// 22 | /// fn App(cx: Scope) -> Element { 23 | /// PageNotFound(vec!["nonexistent".to_string(), "path".to_string()]) 24 | /// } 25 | /// ``` 26 | pub fn PageNotFound(route: Vec) -> Element { 27 | rsx! { 28 | div { class: "max-w-4xl mx-auto px-6 py-12", 29 | h1 { class: "text-4xl font-bold text-gray-900 mb-4", "Page not found" } 30 | p { class: "text-gray-600 mb-4", 31 | "We are terribly sorry, but the page you requested doesn't exist." 32 | } 33 | pre { class: "bg-red-50 text-red-600 p-4 rounded-md font-mono text-sm", 34 | "log:\nattemped to navigate to: {route:?}" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/src/examples/keyframe_animation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[component] 5 | fn KeyframeExample() -> Element { 6 | let mut transform = use_motion(Transform::default()); 7 | 8 | let start = move |_| { 9 | let animation = KeyframeAnimation::new(Duration::from_secs(2)) 10 | .add_keyframe( 11 | Transform::new(0.0, 0.0, 1.0, 0.0), 12 | 0.0, 13 | Some(Box::new(easer::functions::Cubic::ease_in)), 14 | ) 15 | .add_keyframe( 16 | Transform::new(100.0, 0.0, 1.5, 45.0), 17 | 0.3, 18 | Some(Box::new(easer::functions::Elastic::ease_out)), 19 | ) 20 | .add_keyframe( 21 | Transform::new(100.0, 100.0, 0.8, 180.0), 22 | 0.7, 23 | Some(Box::new(easer::functions::Bounce::ease_out)), 24 | ) 25 | .add_keyframe( 26 | Transform::new(0.0, 0.0, 1.0, 360.0), 27 | 1.0, 28 | Some(Box::new(easer::functions::Back::ease_in_out)), 29 | ) 30 | .with_loop_mode(LoopMode::Alternate); 31 | 32 | transform.animate_keyframes(animation); 33 | }; 34 | 35 | rsx! { 36 | div { 37 | class: "demo-box", 38 | style: "transform: translate({}px, {}px) scale({}) rotate({}deg)", 39 | transform.get_value().x, 40 | transform.get_value().y, 41 | transform.get_value().scale, 42 | transform.get_value().rotation, 43 | onclick: start, 44 | "Click to animate" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod components; 2 | pub mod old_showcase; 3 | pub mod pages; 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /docs/src/main.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | pub mod components; 4 | pub mod old_showcase; 5 | pub mod pages; 6 | pub mod utils; 7 | 8 | use docs::utils::router::Route; 9 | 10 | const MAIN_CSS: Asset = asset!("/assets/main.css"); 11 | 12 | /// Launches the Dioxus web application. 13 | /// 14 | /// This function serves as the entry point of the application. It initializes the Dioxus framework 15 | /// with an HTML layout defined using the `rsx!` macro. The layout includes a head section that loads 16 | /// external fonts from Google Fonts and a local stylesheet via the `MAIN_CSS` asset, as well as a 17 | /// Router component parameterized with the `Route` type to handle navigation. 18 | /// 19 | /// # Examples 20 | /// 21 | /// ```no_run`` 22 | /// fn main() { 23 | /// dioxus::launch(|| { 24 | /// rsx! { 25 | /// head { 26 | /// link { 27 | /// rel: "stylesheet", 28 | /// href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap", 29 | /// } 30 | /// link { rel: "stylesheet", href: MAIN_CSS } 31 | /// } 32 | /// Router:: {} 33 | /// } 34 | /// }); 35 | /// } 36 | /// ``` 37 | fn main() { 38 | dioxus::launch(|| { 39 | rsx! { 40 | head { 41 | link { 42 | rel: "stylesheet", 43 | href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap", 44 | } 45 | link { rel: "stylesheet", href: MAIN_CSS } 46 | } 47 | Router:: {} 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/animated_counter.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | // An animated counter that shows basic motion and sequences 5 | #[component] 6 | pub fn AnimatedCounter() -> Element { 7 | let mut value = use_motion(0.0f32); 8 | let mut scale = use_motion(1.0f32); 9 | let mut count = use_signal(|| 0); 10 | 11 | let onclick = move |_| { 12 | let sequence = AnimationSequence::new().then( 13 | ((*count)() + 1) as f32 * 100.0, 14 | AnimationConfig::new(AnimationMode::Spring(Spring { 15 | stiffness: 180.0, 16 | damping: 12.0, 17 | mass: 1.0, 18 | velocity: 10.0, 19 | })), 20 | ); 21 | 22 | scale.animate_to( 23 | 1.2, 24 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 25 | ); 26 | value.animate_sequence(sequence); 27 | count.set((*count)() + 1); 28 | }; 29 | 30 | rsx! { 31 | div { class: "flex flex-col items-center gap-6 p-8 rounded-2xl backdrop-blur-xs", 32 | div { 33 | class: "relative text-5xl font-bold text-transparent bg-clip-text bg-linear-to-r from-blue-500 to-purple-500", 34 | style: "transform: translateY({value.get_value()}px) scale({scale.get_value()})", 35 | "Count: {count}" 36 | } 37 | button { 38 | class: "px-6 py-3 bg-linear-to-r from-blue-500 to-purple-500 text-white rounded-full font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-300", 39 | onclick, 40 | "Increment" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/animated_flower.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::{animations::utils::Animatable, prelude::*}; 3 | use std::f32::consts::PI; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct PetalTransform { 7 | rotate: f32, 8 | scale: f32, 9 | translate_x: f32, 10 | translate_y: f32, 11 | } 12 | 13 | impl PetalTransform { 14 | pub fn new(rotate: f32, scale: f32, translate_x: f32, translate_y: f32) -> Self { 15 | Self { 16 | rotate, 17 | scale, 18 | translate_x, 19 | translate_y, 20 | } 21 | } 22 | } 23 | 24 | impl Animatable for PetalTransform { 25 | fn zero() -> Self { 26 | Self::new(0.0, 0.0, 0.0, 0.0) 27 | } 28 | 29 | fn epsilon() -> f32 { 30 | 0.001 31 | } 32 | 33 | fn magnitude(&self) -> f32 { 34 | (self.rotate * self.rotate 35 | + self.scale * self.scale 36 | + self.translate_x * self.translate_x 37 | + self.translate_y * self.translate_y) 38 | .sqrt() 39 | } 40 | 41 | fn scale(&self, factor: f32) -> Self { 42 | Self::new( 43 | self.rotate * factor, 44 | self.scale * factor, 45 | self.translate_x * factor, 46 | self.translate_y * factor, 47 | ) 48 | } 49 | 50 | fn add(&self, other: &Self) -> Self { 51 | Self::new( 52 | self.rotate + other.rotate, 53 | self.scale + other.scale, 54 | self.translate_x + other.translate_x, 55 | self.translate_y + other.translate_y, 56 | ) 57 | } 58 | 59 | fn sub(&self, other: &Self) -> Self { 60 | Self::new( 61 | self.rotate - other.rotate, 62 | self.scale - other.scale, 63 | self.translate_x - other.translate_x, 64 | self.translate_y - other.translate_y, 65 | ) 66 | } 67 | 68 | fn interpolate(&self, target: &Self, t: f32) -> Self { 69 | Self::new( 70 | self.rotate + (target.rotate - self.rotate) * t, 71 | self.scale + (target.scale - self.scale) * t, 72 | self.translate_x + (target.translate_x - self.translate_x) * t, 73 | self.translate_y + (target.translate_y - self.translate_y) * t, 74 | ) 75 | } 76 | } 77 | 78 | #[component] 79 | pub fn AnimatedFlower() -> Element { 80 | let mut petal_transform = use_motion(PetalTransform::zero()); 81 | let mut leaf_transform = use_motion(PetalTransform::zero()); 82 | let mut center_scale = use_motion(1.0f32); // Start from 1.0 instead of 0.0 83 | let mut center_rotate = use_motion(0.0f32); 84 | let mut is_leaves_grown = use_signal_sync(|| false); 85 | let mut stem_length = use_motion(100.0f32); 86 | let mut stem_sway = use_motion(0.0f32); 87 | let mut glow_opacity = use_motion(0.0f32); 88 | 89 | let animate_leaves = move |_: Event| { 90 | // Enhanced stem animation with natural growth 91 | stem_length.animate_to( 92 | 0.0, 93 | AnimationConfig::new(AnimationMode::Spring(Spring { 94 | stiffness: 25.0, // Slower for more organic movement 95 | damping: 8.0, 96 | mass: 0.4, 97 | velocity: 0.5, 98 | })), 99 | ); 100 | 101 | // Add gentle stem sway 102 | stem_sway.animate_to( 103 | 5.0, 104 | AnimationConfig::new(AnimationMode::Spring(Spring { 105 | stiffness: 15.0, 106 | damping: 3.0, 107 | mass: 0.3, 108 | velocity: 0.0, 109 | })) 110 | .with_loop(LoopMode::Alternate), 111 | ); 112 | 113 | // Enhanced leaf growth animation 114 | leaf_transform.animate_to( 115 | PetalTransform::new( 116 | PI / 5.0, 117 | 1.2, // Slightly larger scale 118 | 2.0, // Add some x movement 119 | -22.0, // Higher up 120 | ), 121 | AnimationConfig::new(AnimationMode::Spring(Spring { 122 | stiffness: 35.0, 123 | damping: 6.0, 124 | mass: 0.4, 125 | velocity: 2.5, 126 | })) 127 | .with_on_complete(move || { 128 | is_leaves_grown.set(true); 129 | }), 130 | ); 131 | }; 132 | 133 | let mut animate_petals = move || { 134 | if *is_leaves_grown.read() { 135 | // More dynamic petal animation 136 | petal_transform.animate_to( 137 | PetalTransform::new(PI / 3.5, 1.3, 4.0, 4.0), 138 | AnimationConfig::new(AnimationMode::Spring(Spring { 139 | stiffness: 45.0, 140 | damping: 7.0, 141 | mass: 0.4, 142 | velocity: 1.5, 143 | })) 144 | .with_loop(LoopMode::Alternate), 145 | ); 146 | 147 | // Add rotation to center 148 | center_rotate.animate_to( 149 | 360.0, 150 | AnimationConfig::new(AnimationMode::Spring(Spring { 151 | stiffness: 20.0, 152 | damping: 5.0, 153 | mass: 0.3, 154 | velocity: 0.5, 155 | })) 156 | .with_loop(LoopMode::Infinite), 157 | ); 158 | 159 | // Modified center scaling animation 160 | center_scale.animate_to( 161 | 1.4, 162 | AnimationConfig::new(AnimationMode::Spring(Spring { 163 | stiffness: 60.0, // Reduced stiffness 164 | damping: 12.0, // Increased damping 165 | mass: 1.0, // Increased mass 166 | velocity: 0.0, // Start with zero velocity 167 | })) 168 | .with_loop(LoopMode::Alternate), 169 | ); 170 | 171 | // Add subtle glow effect 172 | glow_opacity.animate_to( 173 | 0.6, 174 | AnimationConfig::new(AnimationMode::Spring(Spring { 175 | stiffness: 40.0, 176 | damping: 6.0, 177 | mass: 0.5, 178 | velocity: 0.0, 179 | })) 180 | .with_loop(LoopMode::Alternate), 181 | ); 182 | } 183 | }; 184 | 185 | use_effect(move || { 186 | if *is_leaves_grown.read() { 187 | animate_petals(); 188 | } 189 | }); 190 | 191 | rsx! { 192 | div { class: "flex items-center justify-center p-8", 193 | // Add subtle glow behind the flower 194 | div { 195 | class: "absolute", 196 | style: "filter: blur(20px); opacity: {glow_opacity.get_value()}", 197 | svg { 198 | width: "300", 199 | height: "300", 200 | view_box: "-50 -50 100 100", 201 | circle { 202 | cx: "0", 203 | cy: "0", 204 | r: "30", 205 | fill: "url(#glow_gradient)", 206 | } 207 | } 208 | } 209 | svg { 210 | width: "300", 211 | height: "300", 212 | view_box: "-50 -50 100 100", 213 | onmounted: animate_leaves, 214 | 215 | // Enhanced leaves with gradient 216 | { 217 | (0..8) 218 | .map(|i| { 219 | rsx! { 220 | path { 221 | key: "leaf_{i}", 222 | d: "M 0 0 C 5 -3, 8 0, 5 5 C 8 0, 5 -3, 0 0", 223 | fill: "url(#leaf_gradient)", 224 | transform: "translate(0 {25.0 + leaf_transform.get_value().translate_y + (i as f32 * 5.0)}) 225 | rotate({-20.0 + (i as f32 * 15.0) + stem_sway.get_value()}) 226 | scale({leaf_transform.get_value().scale})", 227 | opacity: "0.95", 228 | style: "filter: drop-shadow(0 2px 3px rgba(0,0,0,0.2))", 229 | } 230 | } 231 | }) 232 | } 233 | 234 | // Enhanced stem with dynamic curve 235 | path { 236 | d: "M 0 25 C {-4.0 + stem_sway.get_value()} 20, {4.0 - stem_sway.get_value()} 15, {-2.0 + stem_sway.get_value()} 10 C {4.0 - stem_sway.get_value()} 5, {-4.0 + stem_sway.get_value()} 0, 0 -2", 237 | stroke: "#2F855A", 238 | stroke_width: "1.4", 239 | fill: "none", 240 | stroke_dasharray: "100", 241 | stroke_dashoffset: "{stem_length.get_value()}", 242 | style: "filter: drop-shadow(0 2px 2px rgba(0,0,0,0.1))", 243 | } 244 | 245 | // Enhanced center with rotation 246 | circle { 247 | cx: "0", 248 | cy: "0", 249 | r: "{(3.0 * center_scale.get_value()).max(0.1)}", // Added minimum radius 250 | fill: "url(#center_gradient)", 251 | transform: "rotate({center_rotate.get_value()})", 252 | style: "filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2))", 253 | } 254 | 255 | // Enhanced petals with gradients 256 | { 257 | (0..8) 258 | .map(|i| { 259 | let base_angle = (i as f32) * PI / 4.0; 260 | let transform_value = petal_transform.get_value(); 261 | let hue = 340.0 + (i as f32 * 8.0); 262 | rsx! { 263 | path { 264 | key: "petal_{i}", 265 | d: "M 0 -1 C 3 -6, 6 -8, 0 -14 C -6 -8, -3 -6, 0 -1", 266 | fill: "hsl({hue}, 85%, 75%)", 267 | transform: "translate({transform_value.translate_x} {transform_value.translate_y}) 268 | rotate({(base_angle + transform_value.rotate) * 180.0 / PI}) 269 | scale({transform_value.scale})", 270 | opacity: "0.9", 271 | style: "filter: drop-shadow(0 2px 3px rgba(0,0,0,0.15))", 272 | } 273 | } 274 | }) 275 | } 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/animated_menu_item.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | // An interactive menu item with smooth transitions 5 | #[component] 6 | pub fn AnimatedMenuItem(label: String) -> Element { 7 | let mut x_offset = use_motion(0.0f32); 8 | let mut scale = use_motion(1.0f32); 9 | let mut glow = use_motion(0.0f32); 10 | 11 | let onmouseenter = move |_| { 12 | x_offset.animate_to( 13 | 20.0, 14 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 15 | ); 16 | scale.animate_to( 17 | 1.1, 18 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 19 | ); 20 | glow.animate_to( 21 | 1.0, 22 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 23 | ); 24 | }; 25 | 26 | let onmouseleave = move |_| { 27 | x_offset.animate_to( 28 | 0.0, 29 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 30 | ); 31 | scale.animate_to( 32 | 1.0, 33 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 34 | ); 35 | glow.animate_to( 36 | 0.0, 37 | AnimationConfig::new(AnimationMode::Spring(Spring::default())), 38 | ); 39 | }; 40 | 41 | rsx! { 42 | div { 43 | class: "relative p-4 cursor-pointer bg-linear-to-r from-gray-800 to-gray-900 text-white rounded-xl overflow-hidden group", 44 | style: "transform: translateX({x_offset.get_value()}px) scale({scale.get_value()})", 45 | onmouseenter, 46 | onmouseleave, 47 | // Glow effect 48 | div { 49 | class: "absolute inset-0 bg-linear-to-r from-blue-500/30 to-purple-500/30 transition-opacity duration-300", 50 | style: "opacity: {glow.get_value()}", 51 | } 52 | // Content 53 | div { class: "relative z-10 flex items-center gap-2", 54 | span { class: "text-lg font-medium", "{label}" } 55 | span { class: "text-blue-400 group-hover:translate-x-1 transition-transform duration-300", 56 | "→" 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/bouncing_text.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | #[component] 6 | fn BouncingLetter(letter: char, delay: f32) -> Element { 7 | let mut transform = use_motion(Transform::identity()); 8 | 9 | use_effect(move || { 10 | let delay = Duration::from_secs_f32(delay); 11 | transform.animate_to( 12 | Transform { 13 | y: -30.0, 14 | scale: 1.5, 15 | rotation: 5.0 * (std::f32::consts::PI / 180.0), 16 | x: 0.0, 17 | }, 18 | AnimationConfig::new(AnimationMode::Tween(Tween { 19 | duration: Duration::from_secs(1), 20 | easing: easer::functions::Sine::ease_in_out, 21 | })) 22 | .with_loop(LoopMode::Infinite) 23 | .with_delay(delay), 24 | ); 25 | }); 26 | 27 | rsx! { 28 | span { 29 | class: "text-4xl font-bold text-indigo-600 inline-block origin-bottom 30 | transition-transform duration-300", 31 | style: "transform: translateY({transform.get_value().y}px) 32 | scale({transform.get_value().scale})", 33 | "{letter}" 34 | } 35 | } 36 | } 37 | 38 | #[component] 39 | pub fn BouncingText(text: String) -> Element { 40 | rsx! { 41 | div { class: "flex space-x-1", 42 | { 43 | text.chars() 44 | .enumerate() 45 | .map(|(i, char)| { 46 | rsx! { 47 | BouncingLetter { letter: char, delay: i as f32 * 0.1 } 48 | } 49 | }) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/card_3d_flip.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[component] 5 | pub fn Card3DFlip() -> Element { 6 | let mut transform = use_motion(Transform::identity()); 7 | let mut is_flipped = use_signal(|| false); 8 | 9 | let animate_flip = move |_| { 10 | if *is_flipped.read() { 11 | transform.animate_to( 12 | Transform::identity(), 13 | AnimationConfig::new(AnimationMode::Spring(Spring { 14 | stiffness: 200.0, // Increased for snappier response 15 | damping: 20.0, // Increased for less oscillation 16 | mass: 0.8, // Reduced for lighter feel 17 | velocity: 5.0, // Reduced for smoother start 18 | })), 19 | ); 20 | } else { 21 | transform.animate_to( 22 | Transform { 23 | rotation: 180.0, 24 | scale: 1.0, 25 | x: 0.0, 26 | y: 0.0, 27 | }, 28 | AnimationConfig::new(AnimationMode::Spring(Spring { 29 | stiffness: 200.0, // Increased for snappier response 30 | damping: 20.0, // Increased for less oscillation 31 | mass: 0.8, // Reduced for lighter feel 32 | velocity: 5.0, // Reduced for smoother start 33 | })), 34 | ); 35 | } 36 | is_flipped.toggle(); 37 | }; 38 | 39 | rsx! { 40 | div { class: "perspective-1000", 41 | div { 42 | class: "relative w-64 h-64 cursor-pointer", 43 | style: "transform-style: preserve-3d; 44 | transform: rotateY({transform.get_value().rotation}deg) 45 | scale({transform.get_value().scale});", 46 | onclick: animate_flip, 47 | 48 | // Front 49 | div { class: "absolute w-full h-full bg-linear-to-br from-cyan-400 50 | to-blue-500 rounded-xl p-6 text-white backface-hidden", 51 | div { class: "flex items-center justify-center h-full text-xl font-bold", 52 | "Front Side" 53 | } 54 | } 55 | 56 | // Back 57 | div { 58 | class: "absolute w-full h-full bg-linear-to-br from-purple-400 59 | to-pink-500 rounded-xl p-6 text-white backface-hidden", 60 | style: "transform: rotateY(180deg);", 61 | div { class: "flex items-center justify-center h-full text-xl font-bold", 62 | "Back Side" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/cube_animation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::{animations::utils::Animatable, prelude::*}; 3 | use std::f32::consts::PI; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct Transform3D { 7 | rotate_x: f32, 8 | rotate_y: f32, 9 | rotate_z: f32, 10 | translate_x: f32, 11 | translate_y: f32, 12 | scale: f32, 13 | } 14 | 15 | impl Transform3D { 16 | pub fn new( 17 | rotate_x: f32, 18 | rotate_y: f32, 19 | rotate_z: f32, 20 | translate_x: f32, 21 | translate_y: f32, 22 | scale: f32, 23 | ) -> Self { 24 | Self { 25 | rotate_x, 26 | rotate_y, 27 | rotate_z, 28 | translate_x, 29 | translate_y, 30 | scale, 31 | } 32 | } 33 | } 34 | 35 | impl Animatable for Transform3D { 36 | fn zero() -> Self { 37 | Self::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0) 38 | } 39 | 40 | fn epsilon() -> f32 { 41 | 0.001 42 | } 43 | 44 | fn magnitude(&self) -> f32 { 45 | (self.rotate_x * self.rotate_x 46 | + self.rotate_y * self.rotate_y 47 | + self.rotate_z * self.rotate_z 48 | + self.translate_x * self.translate_x 49 | + self.translate_y * self.translate_y 50 | + self.scale * self.scale) 51 | .sqrt() 52 | } 53 | 54 | fn scale(&self, factor: f32) -> Self { 55 | Self::new( 56 | self.rotate_x * factor, 57 | self.rotate_y * factor, 58 | self.rotate_z * factor, 59 | self.translate_x * factor, 60 | self.translate_y * factor, 61 | self.scale * factor, 62 | ) 63 | } 64 | 65 | fn add(&self, other: &Self) -> Self { 66 | Self::new( 67 | self.rotate_x + other.rotate_x, 68 | self.rotate_y + other.rotate_y, 69 | self.rotate_z + other.rotate_z, 70 | self.translate_x + other.translate_x, 71 | self.translate_y + other.translate_y, 72 | self.scale + other.scale, 73 | ) 74 | } 75 | 76 | fn sub(&self, other: &Self) -> Self { 77 | Self::new( 78 | self.rotate_x - other.rotate_x, 79 | self.rotate_y - other.rotate_y, 80 | self.rotate_z - other.rotate_z, 81 | self.translate_x - other.translate_x, 82 | self.translate_y - other.translate_y, 83 | self.scale - other.scale, 84 | ) 85 | } 86 | 87 | fn interpolate(&self, target: &Self, t: f32) -> Self { 88 | Self::new( 89 | self.rotate_x + (target.rotate_x - self.rotate_x) * t, 90 | self.rotate_y + (target.rotate_y - self.rotate_y) * t, 91 | self.rotate_z + (target.rotate_z - self.rotate_z) * t, 92 | self.translate_x + (target.translate_x - self.translate_x) * t, 93 | self.translate_y + (target.translate_y - self.translate_y) * t, 94 | self.scale + (target.scale - self.scale) * t, 95 | ) 96 | } 97 | } 98 | 99 | #[derive(Debug, Clone, Copy)] 100 | struct Point3D { 101 | x: f32, 102 | y: f32, 103 | z: f32, 104 | } 105 | 106 | impl Point3D { 107 | fn rotate_x(self, angle: f32) -> Self { 108 | Point3D { 109 | x: self.x, 110 | y: self.y * angle.cos() - self.z * angle.sin(), 111 | z: self.y * angle.sin() + self.z * angle.cos(), 112 | } 113 | } 114 | 115 | fn rotate_y(self, angle: f32) -> Self { 116 | Point3D { 117 | x: self.x * angle.cos() + self.z * angle.sin(), 118 | y: self.y, 119 | z: -self.x * angle.sin() + self.z * angle.cos(), 120 | } 121 | } 122 | 123 | fn rotate_z(self, angle: f32) -> Self { 124 | Point3D { 125 | x: self.x * angle.cos() - self.y * angle.sin(), 126 | y: self.x * angle.sin() + self.y * angle.cos(), 127 | z: self.z, 128 | } 129 | } 130 | 131 | fn translate(self, tx: f32, ty: f32) -> Self { 132 | Point3D { 133 | x: self.x + tx, 134 | y: self.y + ty, 135 | z: self.z, 136 | } 137 | } 138 | 139 | fn project(self, scale: f32) -> (f32, f32) { 140 | ( 141 | 100.0 + scale * self.x / (self.z + 4.0), 142 | 100.0 + scale * self.y / (self.z + 4.0), 143 | ) 144 | } 145 | } 146 | 147 | // Cube vertices and faces remain the same as in your original code 148 | const VERTICES: [Point3D; 8] = [ 149 | Point3D { 150 | x: -1.0, 151 | y: -1.0, 152 | z: -1.0, 153 | }, 154 | Point3D { 155 | x: 1.0, 156 | y: -1.0, 157 | z: -1.0, 158 | }, 159 | Point3D { 160 | x: 1.0, 161 | y: 1.0, 162 | z: -1.0, 163 | }, 164 | Point3D { 165 | x: -1.0, 166 | y: 1.0, 167 | z: -1.0, 168 | }, 169 | Point3D { 170 | x: -1.0, 171 | y: -1.0, 172 | z: 1.0, 173 | }, 174 | Point3D { 175 | x: 1.0, 176 | y: -1.0, 177 | z: 1.0, 178 | }, 179 | Point3D { 180 | x: 1.0, 181 | y: 1.0, 182 | z: 1.0, 183 | }, 184 | Point3D { 185 | x: -1.0, 186 | y: 1.0, 187 | z: 1.0, 188 | }, 189 | ]; 190 | 191 | const FACES: [[usize; 4]; 6] = [ 192 | [0, 1, 2, 3], // front 193 | [1, 5, 6, 2], // right 194 | [5, 4, 7, 6], // back 195 | [4, 0, 3, 7], // left 196 | [3, 2, 6, 7], // top 197 | [4, 5, 1, 0], // bottom 198 | ]; 199 | 200 | #[component] 201 | pub fn SwingingCube() -> Element { 202 | let mut transform = use_motion(Transform3D::zero()); 203 | let mut glow_scale = use_motion(1.0f32); 204 | let mut pulse_scale = use_motion(1.0f32); 205 | let mut highlight_opacity = use_motion(0.0f32); 206 | 207 | let animate = move |_| { 208 | // More dynamic cube animation 209 | transform.animate_to( 210 | Transform3D::new( 211 | PI / 2.5, // More dramatic X rotation 212 | PI, // Full Y rotation 213 | PI / 3.0, // Adjusted Z rotation 214 | 3.0, // Larger X translation 215 | -2.0, // Larger Y translation 216 | 1.4, // Larger scale 217 | ), 218 | AnimationConfig::new(AnimationMode::Spring(Spring { 219 | stiffness: 25.0, // Softer spring for smoother motion 220 | damping: 8.0, // Adjusted damping for better bounce 221 | mass: 1.2, // Increased mass for more weight 222 | velocity: 3.0, // Faster initial velocity 223 | })) 224 | .with_loop(LoopMode::Alternate), // Makes the animation go back and forth 225 | ); 226 | 227 | // Add glow and pulse animations 228 | glow_scale.animate_to( 229 | 1.3, 230 | AnimationConfig::new(AnimationMode::Spring(Spring { 231 | stiffness: 30.0, 232 | damping: 5.0, 233 | mass: 1.0, 234 | velocity: 0.0, 235 | })) 236 | .with_loop(LoopMode::Alternate), 237 | ); 238 | 239 | pulse_scale.animate_to( 240 | 1.2, 241 | AnimationConfig::new(AnimationMode::Spring(Spring { 242 | stiffness: 40.0, 243 | damping: 6.0, 244 | mass: 0.8, 245 | velocity: 0.0, 246 | })) 247 | .with_loop(LoopMode::Alternate), 248 | ); 249 | 250 | highlight_opacity.animate_to( 251 | 0.6, 252 | AnimationConfig::new(AnimationMode::Spring(Spring { 253 | stiffness: 35.0, 254 | damping: 7.0, 255 | mass: 0.5, 256 | velocity: 0.0, 257 | })) 258 | .with_loop(LoopMode::Alternate), 259 | ); 260 | }; 261 | 262 | let projected_vertices: Vec<(f32, f32)> = VERTICES 263 | .iter() 264 | .map(|v| { 265 | v.rotate_x(transform.get_value().rotate_x) 266 | .rotate_y(transform.get_value().rotate_y) 267 | .rotate_z(transform.get_value().rotate_z) 268 | .translate( 269 | transform.get_value().translate_x, 270 | transform.get_value().translate_y, 271 | ) 272 | .project(50.0 * transform.get_value().scale * pulse_scale.get_value()) 273 | }) 274 | .collect(); 275 | 276 | rsx! { 277 | div { class: "flex items-center justify-center p-8", 278 | svg { 279 | width: "400.0", 280 | height: "400.0", 281 | view_box: "-20.0 -20.0 240.0 240.0", // Adjusted viewBox for better centering 282 | onmounted: animate, 283 | defs { 284 | // Enhanced gradient with more colors 285 | linearGradient { 286 | id: "cube-gradient", 287 | x1: "0%", 288 | y1: "0%", 289 | x2: "100%", 290 | y2: "100%", 291 | stop { offset: "0%", style: "stop-color:#60a5fa" } 292 | stop { offset: "25%", style: "stop-color:#7c3aed" } 293 | stop { offset: "50%", style: "stop-color:#db2777" } 294 | stop { offset: "75%", style: "stop-color:#9333ea" } 295 | stop { offset: "100%", style: "stop-color:#3b82f6" } 296 | } 297 | // Enhanced glow filter 298 | filter { id: "glow", 299 | feGaussianBlur { 300 | "in": "SourceGraphic", 301 | std_deviation: "6.0", 302 | result: "blur-sm", 303 | } 304 | feColorMatrix { 305 | "in": "blur-sm", 306 | r#type: "matrix", 307 | values: "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -7", 308 | } 309 | } 310 | // Highlight gradient 311 | radialGradient { 312 | id: "highlight", 313 | cx: "50%", 314 | cy: "50%", 315 | r: "50%", 316 | stop { 317 | offset: "0%", 318 | style: "stop-color:rgba(255,255,255,0.8)", 319 | } 320 | stop { 321 | offset: "100%", 322 | style: "stop-color:rgba(255,255,255,0)", 323 | } 324 | } 325 | } 326 | // Background effects 327 | circle { 328 | cx: "100.0", 329 | cy: "100.0", 330 | r: "{40.0 * glow_scale.get_value()}", 331 | fill: "url(#cube-gradient)", 332 | filter: "url(#glow)", 333 | opacity: "0.4", 334 | } 335 | // Enhanced rope with double line 336 | path { 337 | d: "M 100 10 Q {projected_vertices[4].0} {projected_vertices[4].1 - 30.0} 338 | {projected_vertices[4].0} {projected_vertices[4].1}", 339 | stroke: "url(#cube-gradient)", 340 | stroke_width: "2", 341 | fill: "none", 342 | stroke_dasharray: "6,6", 343 | filter: "url(#glow)", 344 | } 345 | // Cube faces with enhanced effects 346 | { 347 | FACES 348 | .iter() 349 | .enumerate() 350 | .map(|(i, face)| { 351 | let path = format!( 352 | "M {} {} L {} {} L {} {} L {} {} Z", 353 | projected_vertices[face[0]].0, 354 | projected_vertices[face[0]].1, 355 | projected_vertices[face[1]].0, 356 | projected_vertices[face[1]].1, 357 | projected_vertices[face[2]].0, 358 | projected_vertices[face[2]].1, 359 | projected_vertices[face[3]].0, 360 | projected_vertices[face[3]].1, 361 | ); 362 | rsx! { 363 | g { key: "{i}", 364 | // Enhanced shadow 365 | path { 366 | d: "{path}", 367 | fill: "rgba(0,0,0,0.3)", 368 | transform: "translate(3.0 3.0)", 369 | filter: "url(#glow)", 370 | } 371 | // Main face with gradient and stroke 372 | path { 373 | d: "{path}", 374 | fill: "url(#cube-gradient)", 375 | stroke: "#ffffff", 376 | stroke_width: "1.0", 377 | opacity: "{0.8 + (i as f32 * 0.05)}", 378 | } 379 | // Highlight overlay 380 | path { 381 | d: "{path}", 382 | fill: "url(#highlight)", 383 | opacity: "{highlight_opacity.get_value()}", 384 | } 385 | } 386 | } 387 | }) 388 | } 389 | // Additional decorative elements 390 | circle { 391 | cx: "100.0", 392 | cy: "100.0", 393 | r: "80.0", 394 | fill: "none", 395 | stroke: "url(#cube-gradient)", 396 | stroke_width: "0.5", 397 | stroke_dasharray: "4,4", 398 | opacity: "0.3", 399 | } 400 | } 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/interactive_cube.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | const CONTAINER_SIZE: f32 = 200.0; // Increased size for better visibility 5 | const PERSPECTIVE: f32 = 800.0; // Increased perspective for more dramatic 3D effect 6 | 7 | #[component] 8 | pub fn InteractiveCube() -> Element { 9 | let mut rotation_x = use_motion(0.0f32); 10 | let mut rotation_y = use_motion(0.0f32); 11 | let mut rotation_z = use_motion(0.0f32); // Added Z rotation for more dynamics 12 | let mut scale = use_motion(1.0f32); 13 | let mut glow = use_motion(0.2f32); // Initial subtle glow 14 | let mut hover_lift = use_motion(0.0f32); // New hover effect 15 | 16 | let onclick = move |_e: Event| { 17 | // Enhanced spin animation 18 | let spin_sequence = AnimationSequence::new().then( 19 | rotation_y.get_value() + 360.0, 20 | AnimationConfig::new(AnimationMode::Spring(Spring { 21 | stiffness: 150.0, 22 | damping: 12.0, 23 | mass: 1.0, 24 | velocity: 25.0, 25 | })), 26 | ); 27 | 28 | // Enhanced bounce animation 29 | let bounce_sequence = AnimationSequence::new() 30 | .then( 31 | 1.3, // Bigger bounce 32 | AnimationConfig::new(AnimationMode::Spring(Spring { 33 | stiffness: 400.0, 34 | damping: 8.0, 35 | mass: 1.0, 36 | velocity: 8.0, 37 | })), 38 | ) 39 | .then( 40 | 1.0, 41 | AnimationConfig::new(AnimationMode::Spring(Spring { 42 | stiffness: 300.0, 43 | damping: 15.0, 44 | mass: 1.0, 45 | velocity: 0.0, 46 | })), 47 | ); 48 | 49 | // Z-axis wobble effect 50 | let wobble_sequence = AnimationSequence::new() 51 | .then( 52 | 15.0, 53 | AnimationConfig::new(AnimationMode::Spring(Spring { 54 | stiffness: 200.0, 55 | damping: 5.0, 56 | mass: 0.5, 57 | velocity: 10.0, 58 | })), 59 | ) 60 | .then( 61 | 0.0, 62 | AnimationConfig::new(AnimationMode::Spring(Spring { 63 | stiffness: 200.0, 64 | damping: 10.0, 65 | mass: 0.5, 66 | velocity: 0.0, 67 | })), 68 | ); 69 | 70 | scale.animate_sequence(bounce_sequence); 71 | rotation_y.animate_sequence(spin_sequence); 72 | rotation_z.animate_sequence(wobble_sequence); 73 | 74 | // Enhanced glow effect 75 | glow.animate_to( 76 | 1.0, 77 | AnimationConfig::new(AnimationMode::Spring(Spring { 78 | stiffness: 300.0, 79 | damping: 10.0, 80 | mass: 0.5, 81 | velocity: 5.0, 82 | })), 83 | ); 84 | 85 | // Reset glow after animation 86 | glow.animate_to( 87 | 0.2, 88 | AnimationConfig::new(AnimationMode::Spring(Spring::default())) 89 | .with_delay(std::time::Duration::from_millis(500)), 90 | ); 91 | }; 92 | 93 | let onmousemove = move |e: Event| { 94 | let rect = e.data().client_coordinates(); 95 | let x = (rect.x as f32 - CONTAINER_SIZE / 2.0) / (CONTAINER_SIZE / 2.0); 96 | let y = (rect.y as f32 - CONTAINER_SIZE / 2.0) / (CONTAINER_SIZE / 2.0); 97 | 98 | // Smoother rotation response 99 | rotation_x.animate_to( 100 | -y * 30.0, // Inverted for natural movement 101 | AnimationConfig::new(AnimationMode::Spring(Spring { 102 | stiffness: 150.0, 103 | damping: 15.0, 104 | mass: 0.8, 105 | velocity: 0.0, 106 | })), 107 | ); 108 | 109 | rotation_y.animate_to( 110 | x * 30.0, 111 | AnimationConfig::new(AnimationMode::Spring(Spring { 112 | stiffness: 150.0, 113 | damping: 15.0, 114 | mass: 0.8, 115 | velocity: 0.0, 116 | })), 117 | ); 118 | }; 119 | 120 | let onmouseenter = move |_| { 121 | hover_lift.animate_to( 122 | 20.0, 123 | AnimationConfig::new(AnimationMode::Spring(Spring { 124 | stiffness: 200.0, 125 | damping: 15.0, 126 | mass: 0.8, 127 | velocity: 0.0, 128 | })), 129 | ); 130 | }; 131 | 132 | let onmouseleave = move |_| { 133 | hover_lift.animate_to( 134 | 0.0, 135 | AnimationConfig::new(AnimationMode::Spring(Spring { 136 | stiffness: 200.0, 137 | damping: 15.0, 138 | mass: 0.8, 139 | velocity: 0.0, 140 | })), 141 | ); 142 | 143 | // Reset rotations 144 | rotation_x.animate_to( 145 | 0.0, 146 | AnimationConfig::new(AnimationMode::Spring(Spring { 147 | stiffness: 150.0, 148 | damping: 15.0, 149 | mass: 0.8, 150 | velocity: 0.0, 151 | })), 152 | ); 153 | 154 | rotation_y.animate_to( 155 | 0.0, 156 | AnimationConfig::new(AnimationMode::Spring(Spring { 157 | stiffness: 150.0, 158 | damping: 15.0, 159 | mass: 0.8, 160 | velocity: 0.0, 161 | })), 162 | ); 163 | }; 164 | 165 | rsx! { 166 | div { 167 | class: "relative cursor-pointer select-none", 168 | style: "width: {CONTAINER_SIZE}px; height: {CONTAINER_SIZE}px; perspective: {PERSPECTIVE}px", 169 | // Enhanced glow background 170 | div { 171 | class: "absolute inset-0 bg-linear-to-r from-blue-500/30 to-purple-500/30 blur-3xl -z-10 transition-all duration-300", 172 | style: "opacity: {glow.get_value()}; transform: scale({1.0 + glow.get_value() * 0.2})", 173 | } 174 | 175 | // Shadow 176 | div { 177 | class: "absolute bottom-0 left-1/2 -translate-x-1/2 bg-black/20 blur-xl rounded-full transition-all duration-300", 178 | style: "width: {CONTAINER_SIZE * 0.8}px; height: {CONTAINER_SIZE * 0.1}px; transform: translateY({20.0 + hover_lift.get_value()}px) scale({scale.get_value()}, 1.0)", 179 | } 180 | 181 | div { 182 | onclick, 183 | onmousemove, 184 | onmouseenter, 185 | onmouseleave, 186 | class: "relative w-full h-full items-center justify-center transform-style-3d transition-all duration-100", 187 | style: "transform: translateY(-{hover_lift.get_value()}px) rotateX({rotation_x.get_value()}deg) rotateY({rotation_y.get_value()}deg) rotateZ({rotation_z.get_value()}deg) scale({scale.get_value()})", 188 | // Front face with enhanced gradient 189 | div { 190 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-blue-500 to-blue-600 shadow-lg transform translate-z-[100px] opacity-90 hover:opacity-100 transition-all duration-300", 191 | style: "box-shadow: 0 0 30px rgba(59, 130, 246, 0.5)", 192 | "Front" 193 | } 194 | // Back face 195 | div { 196 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-purple-500 to-purple-600 shadow-lg transform -translate-z-[100px] rotate-y-180 opacity-90 hover:opacity-100 transition-all duration-300", 197 | style: "box-shadow: 0 0 30px rgba(147, 51, 234, 0.5)", 198 | "Back" 199 | } 200 | // Right face 201 | div { 202 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-pink-500 to-pink-600 shadow-lg transform translate-x-[100px] rotate-y-90 opacity-90 hover:opacity-100 transition-all duration-300", 203 | style: "box-shadow: 0 0 30px rgba(236, 72, 153, 0.5)", 204 | "Right" 205 | } 206 | // Left face 207 | div { 208 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-green-500 to-green-600 shadow-lg transform -translate-x-[100px] -rotate-y-90 opacity-90 hover:opacity-100 transition-all duration-300", 209 | style: "box-shadow: 0 0 30px rgba(34, 197, 94, 0.5)", 210 | "Left" 211 | } 212 | // Top face 213 | div { 214 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-yellow-400 to-yellow-500 shadow-lg transform translate-y-[-100px] rotate-x-90 opacity-90 hover:opacity-100 transition-all duration-300", 215 | style: "box-shadow: 0 0 30px rgba(234, 179, 8, 0.5)", 216 | "Top" 217 | } 218 | // Bottom face 219 | div { 220 | class: "absolute w-full h-full flex items-center justify-center text-2xl font-bold text-white bg-linear-to-br from-red-500 to-red-600 shadow-lg transform translate-y-[100px] -rotate-x-90 opacity-90 hover:opacity-100 transition-all duration-300", 221 | style: "box-shadow: 0 0 30px rgba(239, 68, 68, 0.5)", 222 | "Bottom" 223 | } 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod transform_animation; 2 | pub use transform_animation::TransformAnimationShowcase; 3 | 4 | pub mod value_animation; 5 | pub use value_animation::ValueAnimationShowcase; 6 | 7 | pub mod bouncing_text; 8 | pub use bouncing_text::BouncingText; 9 | 10 | pub mod typewriter_effect; 11 | pub use typewriter_effect::TypewriterEffect; 12 | 13 | pub mod pulse_effect; 14 | pub use pulse_effect::PulseEffect; 15 | 16 | pub mod morphing_shape; 17 | pub use morphing_shape::MorphingShape; 18 | 19 | pub mod card_3d_flip; 20 | pub use card_3d_flip::Card3DFlip; 21 | 22 | pub mod path_animation; 23 | pub use path_animation::PathAnimation; 24 | 25 | pub mod progress_bar; 26 | pub use progress_bar::ProgressBar; 27 | 28 | pub mod interactive_cube; 29 | pub use interactive_cube::InteractiveCube; 30 | 31 | pub mod animated_menu_item; 32 | pub use animated_menu_item::AnimatedMenuItem; 33 | 34 | pub mod rotating_button; 35 | pub use rotating_button::RotatingButton; 36 | 37 | pub mod animated_flower; 38 | pub use animated_flower::AnimatedFlower; 39 | 40 | pub mod animated_counter; 41 | pub use animated_counter::AnimatedCounter; 42 | 43 | pub mod cube_animation; 44 | pub use cube_animation::SwingingCube; 45 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/morphing_shape.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[derive(PartialEq, Copy, Clone)] 5 | struct ShapeConfig { 6 | path: &'static str, 7 | rotation: f32, 8 | scale: f32, 9 | color_from: &'static str, 10 | color_to: &'static str, 11 | } 12 | 13 | #[component] 14 | pub fn MorphingShape(shapes: Vec<&'static str>, duration: f32) -> Element { 15 | let mut current_shape = use_signal(|| 0); 16 | let mut transform = use_motion(Transform::identity()); 17 | let mut scale_pulse = use_motion(1.0f32); 18 | 19 | let shape_configs = [ 20 | ShapeConfig { 21 | path: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)", // Diamond 22 | rotation: 0.0, 23 | scale: 1.0, 24 | color_from: "blue-200", // Ice blue to aqua for diamond's crystalline look 25 | color_to: "cyan-200", 26 | }, 27 | ShapeConfig { 28 | path: "polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%)", // Hexagon 29 | rotation: 180.0, 30 | scale: 1.2, 31 | color_from: "amber-200", // Honey colors for hexagon (beehive inspired) 32 | color_to: "yellow-200", 33 | }, 34 | ShapeConfig { 35 | path: "circle(50% at 50% 50%)", // Circle 36 | rotation: 360.0, 37 | scale: 0.9, 38 | color_from: "rose-200", // Soft pinks for smooth circular form 39 | color_to: "pink-200", 40 | }, 41 | ShapeConfig { 42 | path: "polygon(0% 15%, 15% 15%, 15% 0%, 85% 0%, 85% 15%, 100% 15%, 100% 85%, 85% 85%, 85% 100%, 15% 100%, 15% 85%, 0% 85%)", // Cross 43 | rotation: 45.0, 44 | scale: 1.1, 45 | color_from: "emerald-200", // Nature-inspired greens for the cross 46 | color_to: "lime-200", 47 | }, 48 | ShapeConfig { 49 | path: "polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)", // Pentagon 50 | rotation: 270.0, 51 | scale: 1.15, 52 | color_from: "violet-200", // Royal purple tones for pentagon 53 | color_to: "purple-200", 54 | }, 55 | ShapeConfig { 56 | path: "polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%)", // Octagon 57 | rotation: 90.0, 58 | scale: 1.05, 59 | color_from: "orange-200", // Warm sunset colors for octagon 60 | color_to: "red-200", 61 | }, 62 | ShapeConfig { 63 | path: "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)", // Star 64 | rotation: 135.0, 65 | scale: 1.25, 66 | color_from: "sky-200", // Sky and sea colors for star 67 | color_to: "indigo-200", 68 | }, 69 | ]; 70 | 71 | use_effect(move || { 72 | // Main rotation and scale animation 73 | transform.animate_to( 74 | Transform { 75 | rotation: 360.0, 76 | scale: 1.2, 77 | x: 0.0, 78 | y: 0.0, 79 | }, 80 | AnimationConfig::new(AnimationMode::Spring(Spring { 81 | stiffness: 35.0, // Reduced for more fluid motion 82 | damping: 5.0, // Lower damping for organic movement 83 | mass: 0.6, // Lighter mass for faster response 84 | velocity: 0.8, // Increased initial velocity 85 | })) 86 | .with_loop(LoopMode::Infinite), 87 | ); 88 | 89 | // Additional scale pulse animation 90 | scale_pulse.animate_to( 91 | 1.15, 92 | AnimationConfig::new(AnimationMode::Spring(Spring { 93 | stiffness: 25.0, 94 | damping: 3.0, 95 | mass: 0.5, 96 | velocity: 0.5, 97 | })) 98 | .with_loop(LoopMode::Infinite), 99 | ); 100 | 101 | // Shape transition loop 102 | spawn(async move { 103 | loop { 104 | Time::delay(Duration::from_secs_f32(duration)).await; 105 | let next = (*current_shape.read() + 1) % shape_configs.len(); 106 | current_shape.set(next); 107 | } 108 | }); 109 | }); 110 | 111 | let current_config = &shape_configs[*current_shape.read()]; 112 | 113 | rsx! { 114 | div { class: "w-32 h-32 relative transition-all duration-300", 115 | div { 116 | class: "absolute inset-0 rounded-lg shadow-lg backdrop-blur-xs", 117 | class: "absolute inset-0 bg-linear-to-r from-pink-500 to-orange-500 118 | hover:from-purple-500 hover:to-blue-500 rounded-lg", 119 | style: "clip-path: {current_config.path}; 120 | transform: rotate({transform.get_value().rotation}deg) 121 | scale({transform.get_value().scale * scale_pulse.get_value()}); 122 | transition: clip-path 0.8s cubic-bezier(0.4, 0, 0.2, 1); 123 | filter: brightness(1.2) contrast(1.1) saturate(1.2);", 124 | // Lighter inner glow effect 125 | div { 126 | class: "absolute inset-0 bg-white/30 rounded-lg", 127 | style: "mix-blend-mode: soft-light;", 128 | } 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/navbar.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[component] 5 | pub fn Navbar() -> Element { 6 | let mut transform = use_motion(Transform::new(0.0, -100.0, 1.0, 0.0)); 7 | 8 | use_effect(move || { 9 | // Animate transform with spring physics 10 | transform.animate_to( 11 | Transform::new(0.0, 0.0, 1.0, 0.0), 12 | AnimationConfig::new(AnimationMode::Spring(Spring { 13 | stiffness: 100.0, 14 | damping: 20.0, 15 | mass: 1.0, 16 | velocity: 10.0, 17 | })), 18 | ); 19 | }); 20 | 21 | use_drop(move || { 22 | transform.stop(); 23 | }); 24 | 25 | rsx! { 26 | nav { 27 | class: "fixed top-0 left-0 right-0 backdrop-blur-md 28 | shadow-lg shadow-black/5 dark:shadow-white/5 29 | border-b border-gray-200/20 30 | transition-all duration-300 z-50 31 | hover:shadow-xl", 32 | style: "transform: translate({}px, {}px) scale({}) rotate({}deg); transform-style: preserve-3d; will-change: transform;", 33 | transform.get_value().x, 34 | transform.get_value().y, 35 | transform.get_value().scale, 36 | transform.get_value().rotation, 37 | div { class: "max-w-6xl mx-auto px-4", 38 | div { class: "flex justify-between items-center h-28", 39 | // Logo 40 | div { class: "flex items-center space-x-4", 41 | div { class: "text-3xl font-bold bg-linear-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent", 42 | "Dioxus Motion" 43 | } 44 | } 45 | 46 | // Social Links 47 | div { class: "flex items-center space-x-4", 48 | a { 49 | class: " hover:text-blue-500 transition-colors", 50 | href: "https://github.com/wheregmis/dioxus-motion", 51 | target: "_blank", 52 | "GitHub" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/path_animation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | #[component] 6 | pub fn PathAnimation(path: &'static str, duration: f32) -> Element { 7 | let mut dash_offset = use_motion(1000.0f32); 8 | 9 | use_effect(move || { 10 | dash_offset.animate_to( 11 | 0.0, 12 | AnimationConfig::new(AnimationMode::Tween(Tween { 13 | duration: Duration::from_secs_f32(duration), 14 | easing: easer::functions::Cubic::ease_in_out, 15 | })) 16 | .with_loop(LoopMode::Infinite), 17 | ); 18 | }); 19 | 20 | rsx! { 21 | div { class: "w-full h-48 flex items-center justify-center rounded-xl", 22 | svg { class: "w-full h-full", view_box: "0 0 200 200", 23 | path { 24 | d: "{path}", 25 | fill: "none", 26 | stroke: "url(#gradient)", 27 | stroke_width: "4", 28 | stroke_dasharray: "1000", 29 | style: "stroke-dashoffset: {dash_offset.get_value()}; 30 | transition: stroke-dashoffset 0.1s linear;", 31 | } 32 | defs { 33 | linearGradient { 34 | id: "gradient", 35 | x1: "0%", 36 | y1: "0%", 37 | x2: "100%", 38 | y2: "0%", 39 | stop { offset: "0%", style: "stop-color: #3B82F6;" } 40 | stop { offset: "100%", style: "stop-color: #8B5CF6;" } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/progress_bar.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | #[component] 6 | pub fn ProgressBar(title: &'static str) -> Element { 7 | let mut progress = use_motion(0.0f32); 8 | 9 | use_effect(move || { 10 | progress.animate_to( 11 | 100.0, 12 | AnimationConfig::new(AnimationMode::Tween(Tween { 13 | duration: Duration::from_secs(5), 14 | easing: easer::functions::Sine::ease_in_out, 15 | })) 16 | .with_loop(LoopMode::Infinite), 17 | ); 18 | }); 19 | 20 | rsx! { 21 | div { class: "w-full p-6 rounded-xl shadow-lg", 22 | // Title and percentage display 23 | div { class: "flex justify-between items-center mb-4", 24 | span { class: "text-lg font-semibold", "{title}" } 25 | span { class: "text-sm font-medium text-blue-600", "{progress.get_value() as i32}%" } 26 | } 27 | 28 | // Progress bar container 29 | div { class: "relative w-full h-4 bg-gray-100 rounded-full overflow-hidden", 30 | // Progress fill 31 | div { 32 | class: "absolute top-0 left-0 h-full bg-linear-to-r from-blue-500 to-purple-600 33 | rounded-full transition-all duration-300 ease-out", 34 | style: "width: {progress.get_value()}%", 35 | } 36 | // Shimmer effect 37 | div { 38 | class: "absolute top-0 left-0 w-full h-full bg-linear-to-r from-transparent 39 | via-white/30 to-transparent animate-shimmer", 40 | style: "background-size: 200% 100%", 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/pulse_effect.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[component] 5 | pub fn PulseEffect(color: &'static str, size: &'static str) -> Element { 6 | let mut scale = use_motion(1.0f32); 7 | let mut opacity = use_motion(0.8f32); 8 | 9 | use_effect(move || { 10 | // Main pulse animation 11 | scale.animate_to( 12 | 1.2, 13 | AnimationConfig::new(AnimationMode::Spring(Spring { 14 | stiffness: 100.0, 15 | damping: 5.0, 16 | mass: 0.5, 17 | ..Default::default() 18 | })) 19 | .with_loop(LoopMode::Infinite), 20 | ); 21 | 22 | // Fade animation 23 | opacity.animate_to( 24 | 0.2, 25 | AnimationConfig::new(AnimationMode::Spring(Spring { 26 | stiffness: 80.0, 27 | damping: 10.0, 28 | mass: 0.5, 29 | ..Default::default() 30 | })) 31 | .with_loop(LoopMode::Infinite), 32 | ); 33 | }); 34 | 35 | rsx! { 36 | div { class: "relative flex items-center justify-center", 37 | // Main circle 38 | div { 39 | class: "{size} {color} rounded-full transition-all", 40 | style: "transform: scale({scale.get_value()}); opacity: {opacity.get_value()}", 41 | } 42 | // Background pulse rings 43 | div { class: "absolute inset-0", 44 | { 45 | (0..3) 46 | .map(|i| { 47 | let delay = i as f32 * 0.2; 48 | rsx! { 49 | div { 50 | class: "{size} {color} rounded-full absolute inset-0 animate-ping", 51 | style: "animation-delay: {delay}s; opacity: 0.3", 52 | } 53 | } 54 | }) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/rotating_button.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | // A playful button that bounces on click 6 | #[component] 7 | pub fn RotatingButton() -> Element { 8 | let mut scale = use_motion(1.0f32); 9 | let mut rotation = use_motion(0.0f32); 10 | let mut glow = use_motion(0.0f32); 11 | 12 | let onclick = move |_| { 13 | // Optimized scale sequence with better physics and smoother transitions 14 | let scale_sequence = AnimationSequence::new() 15 | .then( 16 | 1.15, // Reduced maximum scale for snappier feel 17 | AnimationConfig::new(AnimationMode::Spring(Spring { 18 | stiffness: 500.0, // Increased stiffness for faster response 19 | damping: 15.0, // Balanced damping for controlled bounce 20 | mass: 0.8, // Lighter mass for quicker movement 21 | velocity: 8.0, // Increased initial velocity 22 | })), 23 | ) 24 | .then( 25 | 0.9, // Subtle scale down 26 | AnimationConfig::new(AnimationMode::Spring(Spring { 27 | stiffness: 400.0, 28 | damping: 12.0, 29 | mass: 0.6, 30 | velocity: -4.0, // Negative velocity for natural rebound 31 | })), 32 | ) 33 | .then( 34 | 1.0, // Return to original size 35 | AnimationConfig::new(AnimationMode::Spring(Spring { 36 | stiffness: 350.0, 37 | damping: 20.0, // Higher damping for smooth finish 38 | mass: 0.7, 39 | velocity: 0.0, 40 | })), 41 | ); 42 | 43 | // Optimized rotation with smoother easing 44 | let rotation_sequence = AnimationSequence::new().then( 45 | 360.0, 46 | AnimationConfig::new(AnimationMode::Tween(Tween { 47 | duration: Duration::from_millis(800), // Faster rotation 48 | easing: easer::functions::Expo::ease_out, // Smoother deceleration 49 | })), 50 | ); 51 | 52 | // Quick glow effect 53 | glow.animate_to( 54 | 1.0, 55 | AnimationConfig::new(AnimationMode::Spring(Spring { 56 | stiffness: 450.0, 57 | damping: 15.0, 58 | mass: 0.5, 59 | velocity: 10.0, 60 | })), 61 | ); 62 | 63 | scale.animate_sequence(scale_sequence); 64 | rotation.animate_sequence(rotation_sequence); 65 | }; 66 | 67 | rsx! { 68 | button { 69 | class: "relative px-8 py-4 bg-linear-to-r from-purple-500 to-pink-500 70 | text-white rounded-xl font-bold text-lg overflow-hidden 71 | transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/20", 72 | style: "transform: scale({scale.get_value()}) rotate({rotation.get_value()}deg)", 73 | onclick, 74 | // Enhanced glow effect 75 | div { 76 | class: "absolute inset-0 bg-white/30 blur-xl", 77 | style: "opacity: {glow.get_value()}", 78 | } 79 | "Click me!" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/transform_animation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | #[component] 5 | pub fn TransformAnimationShowcase() -> Element { 6 | let mut transform = use_motion(Transform::identity()); 7 | 8 | let animate_hover = move |_| { 9 | transform.animate_to( 10 | Transform::new( 11 | 0.0, // x 12 | -20.0, // y 13 | 1.1, // scale 14 | 5.0 * (std::f32::consts::PI / 180.0), // rotation in radians 15 | ), 16 | AnimationConfig::new(AnimationMode::Spring(Spring { 17 | stiffness: 180.0, // Softer spring 18 | damping: 12.0, // Less damping for bounce 19 | mass: 1.0, 20 | ..Default::default() 21 | })), 22 | ); 23 | }; 24 | 25 | let animate_reset = move |_| { 26 | transform.animate_to( 27 | Transform::identity(), 28 | AnimationConfig::new(AnimationMode::Spring(Spring { 29 | stiffness: 200.0, 30 | damping: 20.0, 31 | mass: 1.0, 32 | ..Default::default() 33 | })), 34 | ); 35 | }; 36 | 37 | let transform_style = use_memo(move || { 38 | format!( 39 | "transform: translate({}px, {}px) scale({}) rotate({}deg); transform-style: preserve-3d; will-change: transform;", 40 | transform.get_value().x, 41 | transform.get_value().y, 42 | transform.get_value().scale, 43 | transform.get_value().rotation * 180.0 / std::f32::consts::PI 44 | ) 45 | }); 46 | 47 | let glow_style = use_memo(move || { 48 | format!( 49 | "transform: translate({}px, {}px) scale(1.2); opacity: {};", 50 | transform.get_value().x, 51 | transform.get_value().y, 52 | if transform.get_value().y < 0.0 { 53 | 0.6 54 | } else { 55 | 0.0 56 | } 57 | ) 58 | }); 59 | 60 | rsx! { 61 | div { class: "h-[400px] flex items-center justify-center p-4", 62 | div { 63 | class: "relative group cursor-pointer", 64 | onmouseenter: animate_hover, 65 | onmouseleave: animate_reset, 66 | // Main card - reduced from w-64/h-64 to w-48/h-48 67 | div { 68 | class: "w-36 h-36 bg-linear-to-tr from-emerald-400 to-cyan-400 rounded-xl shadow-xl", 69 | style: "{transform_style.read()}", 70 | div { class: "h-full w-full flex flex-col items-center justify-center text-white", 71 | span { class: "text-xl font-bold mb-1", "Hover Me!" } 72 | span { class: "text-xs opacity-75", "Spring Animation" } 73 | } 74 | } 75 | // Glow effect - scaled proportionally 76 | div { 77 | class: "absolute inset-0 bg-linear-to-tr from-emerald-400/30 to-cyan-400/30 78 | rounded-2xl blur-lg -z-10", 79 | style: "{glow_style.read()}", 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/typewriter_effect.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | #[component] 6 | pub fn TypewriterEffect(text: &'static str) -> Element { 7 | let mut char_count = use_motion(0.0f32); 8 | let mut cursor_opacity = use_motion(1.0f32); 9 | let text_len = text.len() as f32; 10 | 11 | use_effect(move || { 12 | // Start typing animation 13 | char_count.animate_to( 14 | text_len, 15 | AnimationConfig::new(AnimationMode::Tween(Tween { 16 | duration: Duration::from_secs_f32(text_len * 0.1), // 0.1s per character 17 | easing: easer::functions::Linear::ease_in_out, 18 | })) 19 | .with_loop(LoopMode::Infinite), 20 | ); 21 | 22 | // Start cursor blink 23 | cursor_opacity.animate_to( 24 | 0.0, 25 | AnimationConfig::new(AnimationMode::Tween(Tween { 26 | duration: Duration::from_secs(1), 27 | easing: easer::functions::Linear::ease_in_out, 28 | })) 29 | .with_loop(LoopMode::Infinite), 30 | ); 31 | }); 32 | 33 | let visible_text = text 34 | .chars() 35 | .take(char_count.get_value() as usize) 36 | .collect::(); 37 | 38 | rsx! { 39 | div { class: "relative font-mono text-2xl text-blue-500", 40 | // Text container 41 | span { "{visible_text}" } 42 | // Cursor 43 | span { 44 | class: "absolute right-0 top-0", 45 | style: "opacity: {cursor_opacity.get_value()};", 46 | "|" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/src/old_showcase/components/value_animation.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | use easer::functions::Easing; 4 | 5 | #[component] 6 | pub fn ValueAnimationShowcase() -> Element { 7 | let mut value = use_motion(0.0f32); 8 | 9 | let start_animation = move |_| { 10 | value.animate_to( 11 | 100.0, 12 | AnimationConfig::new(AnimationMode::Tween(Tween { 13 | duration: Duration::from_secs(10), 14 | easing: easer::functions::Sine::ease_in_out, 15 | })), 16 | ); 17 | }; 18 | 19 | let reset_animation = move |_| { 20 | value.animate_to( 21 | 0.0, 22 | AnimationConfig::new(AnimationMode::Tween(Tween { 23 | duration: Duration::from_secs(3), 24 | easing: easer::functions::Sine::ease_out, 25 | })), 26 | ); 27 | }; 28 | 29 | rsx! { 30 | div { class: "h-[400px] flex items-center justify-center", 31 | div { class: "flex flex-col items-center justify-center p-6 bg-linear-to-br from-blue-500 to-purple-600 rounded-xl shadow-lg", 32 | // Counter with smaller font 33 | div { class: "text-4xl font-bold text-white mb-3", "{value.get_value() as i32}%" } 34 | 35 | // Smaller progress circle 36 | div { 37 | class: "relative w-24 h-24", 38 | style: "background: conic-gradient(from 0deg, #ffffff {value.get_value()}%, transparent 0)", 39 | div { class: "absolute inset-2 bg-blue-600 rounded-full" } 40 | } 41 | 42 | // Compact buttons 43 | div { class: "flex gap-2 mt-4", 44 | button { 45 | class: "px-4 py-1.5 bg-white text-blue-600 rounded-full font-semibold 46 | hover:bg-opacity-90 transition-all text-sm flex items-center gap-2", 47 | onclick: start_animation, 48 | "Start" 49 | } 50 | button { 51 | class: "px-4 py-1.5 bg-white text-blue-600 rounded-full font-semibold 52 | hover:bg-opacity-90 transition-all text-sm flex items-center gap-2", 53 | onclick: reset_animation, 54 | "Reset" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/src/old_showcase/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod components; 2 | pub mod showcase_component; 3 | -------------------------------------------------------------------------------- /docs/src/old_showcase/showcase_component.rs: -------------------------------------------------------------------------------- 1 | use crate::components::footer::Footer; 2 | use crate::old_showcase::components::{ 3 | AnimatedCounter, AnimatedFlower, AnimatedMenuItem, BouncingText, Card3DFlip, InteractiveCube, 4 | MorphingShape, PathAnimation, ProgressBar, PulseEffect, RotatingButton, SwingingCube, 5 | TransformAnimationShowcase, TypewriterEffect, ValueAnimationShowcase, 6 | }; 7 | 8 | use dioxus::prelude::*; 9 | 10 | #[component] 11 | pub fn ShowcaseGallery() -> Element { 12 | rsx! { 13 | div { class: "flex flex-col min-h-screen relative bg-gradient-dark", 14 | div { class: "grow mt-16", 15 | div { class: "container-lg mx-auto px-8 py-12 pt-20", 16 | // div { 17 | // h2 { class: "text-xl font-display font-bold text-text-primary", 18 | // "Animated Counter" 19 | // } 20 | // AnimatedCounter {} 21 | // } 22 | div { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8", 23 | // Update each card with our theme 24 | for (title , component , url) in showcase_items() { 25 | ShowcaseCard { title, url, component } 26 | } 27 | } 28 | } 29 | } 30 | 31 | // Footer 32 | Footer {} 33 | } 34 | } 35 | } 36 | 37 | #[component] 38 | fn ShowcaseCard(title: String, url: String, component: Element) -> Element { 39 | rsx! { 40 | div { 41 | class: "group relative flex flex-col items-start justify-between h-[400px]", 42 | class: "rounded-xl border border-surface-light/10 backdrop-blur-xs", 43 | class: "bg-surface/30 hover:bg-surface-light/10", 44 | class: "transition-all duration-500 ease-out", 45 | // Gradient glow effect on hover 46 | div { 47 | class: "absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100", 48 | class: "bg-linear-to-r from-primary/20 to-accent-purple/20", 49 | class: "transition-opacity duration-500 ease-out -z-10", 50 | } 51 | // Title section 52 | h3 { 53 | class: "text-lg font-display font-semibold text-text-primary mb-4 w-full p-4", 54 | class: "border-b border-surface-light/10", 55 | "{title}" 56 | } 57 | // Component showcase area 58 | div { 59 | class: "grow w-full flex items-center justify-center p-4 overflow-hidden", 60 | class: "group-hover:scale-105 transition-transform duration-500 ease-out", 61 | style: "max-height: 280px;", 62 | {component} 63 | } 64 | // Button section with gradient border 65 | div { class: "w-full p-4 border-t border-surface-light/10", 66 | ViewCodeButton { url } 67 | } 68 | } 69 | } 70 | } 71 | 72 | #[component] 73 | fn ViewCodeButton(url: String) -> Element { 74 | rsx! { 75 | a { 76 | class: "group relative inline-flex items-center gap-2 w-full", 77 | class: "px-4 py-2 rounded-lg font-medium", 78 | class: "bg-surface hover:bg-surface-light/20", 79 | class: "text-text-primary hover:text-primary-light", 80 | class: "transition-all duration-300 ease-out", 81 | href: "{url}", 82 | target: "_blank", 83 | // Button content 84 | span { class: "transition-transform duration-300 group-hover:translate-x-1", 85 | "View Example Code" 86 | } 87 | // Arrow icon with animation 88 | span { 89 | class: "text-xs transition-all duration-300", 90 | class: "transform group-hover:translate-x-1 group-hover:text-accent-purple", 91 | "→" 92 | } 93 | } 94 | } 95 | } 96 | 97 | // Helper function to organize showcase items 98 | fn showcase_items() -> Vec<(&'static str, Element, &'static str)> { 99 | vec![ 100 | ( 101 | "Cube Animation", 102 | rsx!(SwingingCube {}), 103 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/cube_animation.rs", 104 | ), 105 | ( 106 | "Flower Animation", 107 | rsx!(AnimatedFlower {}), 108 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/animated_flower.rs", 109 | ), 110 | ( 111 | "Morphing Shape", 112 | rsx!(MorphingShape { 113 | shapes: vec!["square", "triangle"], 114 | duration: 3.0 115 | }), 116 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/morphing_shape.rs", 117 | ), 118 | ( 119 | "Interactive Cube", 120 | rsx!(InteractiveCube {}), 121 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/interactive_cube.rs", 122 | ), 123 | ( 124 | "Value Animation", 125 | rsx!(ValueAnimationShowcase {}), 126 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/value_animation.rs", 127 | ), 128 | ( 129 | "Transform Animation", 130 | rsx!(TransformAnimationShowcase {}), 131 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/transform_animation.rs", 132 | ), 133 | ( 134 | "Animated Menu Bar", 135 | rsx!( 136 | section { class: "", 137 | p { class: "text-gray-600", "Shows smooth transitions on hover" } 138 | div { class: "space-y-2", 139 | AnimatedMenuItem { label: "Home" } 140 | AnimatedMenuItem { label: "About" } 141 | AnimatedMenuItem { label: "Contact" } 142 | } 143 | } 144 | ), 145 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/animated_menu_item.rs", 146 | ), 147 | ( 148 | "Rotating Button", 149 | rsx!(RotatingButton {}), 150 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/rotating_button.rs", 151 | ), 152 | ( 153 | "Progress Animation", 154 | rsx!(ProgressBar { 155 | title: "Loading..." 156 | }), 157 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/progress_bar.rs", 158 | ), 159 | ( 160 | "Bouncing Text", 161 | rsx!(BouncingText { 162 | text: "Dioxus Motion" 163 | }), 164 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/bouncing_text.rs", 165 | ), 166 | ( 167 | "Path Animation", 168 | rsx!(PathAnimation { 169 | path: "M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80", 170 | duration: 5.0 171 | }), 172 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/path_animation.rs", 173 | ), 174 | ( 175 | "Pulse Effect", 176 | rsx!(PulseEffect { 177 | color: "bg-blue-500", 178 | size: "w-16 h-16" 179 | }), 180 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/pulse_effect.rs", 181 | ), 182 | ( 183 | "3D Card Flip", 184 | rsx!(Card3DFlip {}), 185 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/card_3d_flip.rs", 186 | ), 187 | ( 188 | "Typewriter Effect", 189 | rsx!(TypewriterEffect { 190 | text: "Hello, Dioxus Motion" 191 | }), 192 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/typewriter_effect.rs", 193 | ), 194 | ( 195 | "Counter Animation", 196 | rsx!(AnimatedCounter {}), 197 | "https://github.com/wheregmis/dioxus-motion/blob/main/docs/src/old_showcase/components/animated_counter.rs", 198 | ), 199 | ] 200 | } 201 | -------------------------------------------------------------------------------- /docs/src/pages/blog/index.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | #[component] 4 | /// Renders a Dioxus blog component with a welcoming header. 5 | /// 6 | /// This component returns an `Element` containing a styled container with an `h1` 7 | /// element that displays the message "Welcome to the Dioxus Blog!". 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```rust 12 | /// use dioxus::prelude::*; 13 | /// 14 | /// // Create the blog component element 15 | /// let blog_element: Element = Blog(); 16 | /// // The returned element can be integrated into a Dioxus application view. 17 | /// ``` 18 | pub fn Blog() -> Element { 19 | rsx! { 20 | div { class: "max-w-4xl mx-auto px-6 py-12", 21 | h1 { class: "text-4xl font-bold text-gray-900 mb-4", "Welcome to the Dioxus Blog!" } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/pages/blog/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | -------------------------------------------------------------------------------- /docs/src/pages/docs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | -------------------------------------------------------------------------------- /docs/src/pages/home/index.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | use crate::components::footer::Footer; 5 | use crate::old_showcase::components::{AnimatedFlower, SwingingCube, TransformAnimationShowcase}; 6 | use crate::utils::router::Route; 7 | 8 | #[component] 9 | /// Renders the main landing page of the application. 10 | /// 11 | /// This component initializes animated states for opacity, scale, and vertical positioning of key elements. 12 | /// On mount, it triggers staggered spring and tween animations that animate the hero section, titles, and feature overlays, 13 | /// creating a dynamic and engaging home page layout. 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// use dioxus::prelude::*; 19 | /// // Adjust the import path below according to your project setup. 20 | /// use your_crate::Home; 21 | /// 22 | /// fn main() { 23 | /// dioxus::web::launch(Home); 24 | /// } 25 | /// ``` 26 | pub fn Home() -> Element { 27 | let hero_opacity = use_motion(1.0f32); // Changed from 0.0 to 1.0 28 | let mut demo_scale = use_motion(1.0f32); 29 | // Remove these animations since we don't want them 30 | // let mut title_y = use_motion(-20.0f32); 31 | // let mut subtitle_y = use_motion(20.0f32); 32 | 33 | use_effect(move || { 34 | { 35 | // Remove title and subtitle animations 36 | demo_scale.animate_to( 37 | 1.1, 38 | AnimationConfig::new(AnimationMode::Spring(Spring { 39 | stiffness: 100.0, 40 | damping: 10.0, 41 | mass: 1.0, 42 | velocity: 0.0, 43 | })) 44 | .with_loop(LoopMode::Infinite), 45 | ); 46 | } 47 | }); 48 | 49 | rsx! { 50 | section { 51 | class: "min-h-screen bg-gradient-dark relative overflow-hidden flex flex-col", 52 | style: "opacity: {hero_opacity.get_value()}", 53 | 54 | // Animated background elements 55 | div { class: "absolute inset-0 overflow-hidden", 56 | div { class: "absolute -top-1/2 -left-1/2 w-full h-full bg-primary/5 rounded-full blur-3xl" } 57 | div { class: "absolute -bottom-1/2 -right-1/2 w-full h-full bg-secondary/5 rounded-full blur-3xl" } 58 | } 59 | 60 | // Content overlay 61 | div { class: "relative z-10 flex-1", 62 | // Main content 63 | div { class: "container mx-auto px-4 pt-8", 64 | // Hero section with animations in a row 65 | div { class: "flex flex-col lg:flex-row items-center justify-between gap-8 mb-12", 66 | // Left side - Simple animation 67 | div { class: "w-full lg:w-1/3", 68 | div { class: "flex flex-col items-center gap-4", 69 | TransformAnimationShowcase {} 70 | div { class: "text-center", 71 | span { class: "inline-block text-lg font-medium bg-clip-text text-transparent 72 | bg-linear-to-r from-text-secondary/70 to-text-secondary/40 73 | tracking-wide transform -rotate-12", 74 | "From Simple" 75 | } 76 | div { class: "mt-2 text-sm text-text-muted", 77 | "Basic Transformations" 78 | } 79 | } 80 | } 81 | } 82 | 83 | // Center content - Flower 84 | div { class: "w-full lg:w-1/3", 85 | div { class: "flex flex-col items-center gap-4", 86 | AnimatedFlower {} 87 | div { class: "text-center", 88 | span { class: "inline-block text-lg font-medium bg-clip-text text-transparent 89 | bg-linear-to-r from-text-secondary/70 to-text-secondary/40 90 | tracking-wide", 91 | "" 92 | } 93 | div { class: "mt-2 text-sm text-text-muted", "Complex Animations" } 94 | } 95 | } 96 | } 97 | 98 | // Right side - Advanced animation 99 | div { class: "w-full lg:w-1/3", 100 | div { class: "flex flex-col items-center gap-4", 101 | SwingingCube {} 102 | div { class: "text-center", 103 | span { class: "inline-block text-lg font-medium bg-clip-text text-transparent 104 | bg-linear-to-r from-text-secondary/70 to-text-secondary/40 105 | tracking-wide transform rotate-12", 106 | "To Advanced" 107 | } 108 | div { class: "mt-2 text-sm text-text-muted", 109 | "Custom Transformations" 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Title and CTA section 117 | div { class: "text-center max-w-4xl mx-auto", 118 | h1 { class: "text-4xl md:text-5xl lg:text-6xl font-bold mb-4", 119 | // Remove the transform style 120 | span { class: "text-gradient-primary", "Dioxus Motion" } 121 | } 122 | p { class: "text-lg md:text-xl text-text-secondary mb-8", 123 | // Remove the transform style 124 | "Simple and powerful animations for your Dioxus applications" 125 | } 126 | 127 | // CTA buttons 128 | div { class: "flex flex-col sm:flex-row justify-center gap-4", 129 | Link { 130 | to: Route::DocsLanding {}, 131 | class: "px-8 py-3 bg-primary/90 backdrop-blur-xs text-dark-50 rounded-xl 132 | font-semibold transition-all duration-300 hover:scale-105 133 | shadow-lg shadow-primary/20 hover:shadow-primary/30", 134 | "Get Started →" 135 | } 136 | a { 137 | href: "https://github.com/wheregmis/dioxus-motion", 138 | target: "_blank", 139 | class: "px-8 py-3 bg-dark-200/50 backdrop-blur-xs text-white/90 rounded-xl 140 | font-semibold transition-all duration-300 hover:scale-105 141 | border border-white/10 hover:border-white/20", 142 | "Explore Examples" 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | // Features section 150 | section { class: "container mx-auto px-4 py-20 pb-4 relative z-10", 151 | h2 { class: "text-3xl font-bold text-center mb-12 text-gradient-primary", 152 | "Features" 153 | } 154 | div { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-5xl mx-auto", 155 | FeatureCard { 156 | title: "Spring Physics", 157 | description: "Natural animations with customizable spring parameters", 158 | icon: "🌊", 159 | } 160 | FeatureCard { 161 | title: "Easy to Use", 162 | description: "Simple API with powerful configuration options", 163 | icon: "🎯", 164 | } 165 | FeatureCard { 166 | title: "Cross Platform", 167 | description: "Works on Web, Desktop, and Mobile", 168 | icon: "🌐", 169 | } 170 | FeatureCard { 171 | title: "Page Transitions", 172 | description: "Smooth animations for route changes", 173 | icon: "🔄", 174 | } 175 | } 176 | } 177 | 178 | // Footer 179 | Footer {} 180 | } 181 | } 182 | } 183 | 184 | #[component] 185 | /// Renders an animated feature card with a specified icon, title, and description. 186 | /// 187 | /// This component displays a card that animates on hover by scaling up and shifting slightly upward, 188 | /// then reverting to its original state when the mouse leaves. The animations are achieved using spring 189 | /// dynamics to ensure smooth transitions. 190 | /// 191 | /// # Arguments 192 | /// 193 | /// * `title` - The title text displayed on the card. 194 | /// * `description` - A brief description of the feature. 195 | /// * `icon` - A static string representing the feature's icon (e.g., an emoji). 196 | /// 197 | /// # Returns 198 | /// 199 | /// A Dioxus `Element` representing the rendered feature card. 200 | /// 201 | /// # Examples 202 | /// 203 | /// ``` 204 | /// use dioxus::prelude::*; 205 | /// 206 | /// fn app(cx: Scope) -> Element { 207 | /// cx.render(rsx! { 208 | /// FeatureCard("Efficiency", "Boosts performance significantly.", "⚡") 209 | /// }) 210 | /// } 211 | /// ``` 212 | fn FeatureCard(title: &'static str, description: &'static str, icon: &'static str) -> Element { 213 | let mut card_scale = use_motion(1.0f32); 214 | let mut card_y = use_motion(0.0f32); 215 | 216 | rsx! { 217 | div { 218 | class: "p-6 rounded-xl bg-dark-200/50 backdrop-blur-xs 219 | border border-primary/10 transition-all duration-300 220 | hover:border-primary/20", 221 | style: "transform: translateY({card_y.get_value()}px) scale({card_scale.get_value()})", 222 | onmouseenter: move |_| { 223 | card_scale 224 | .animate_to( 225 | 1.05, 226 | AnimationConfig::new( 227 | AnimationMode::Spring(Spring { 228 | stiffness: 300.0, 229 | damping: 20.0, 230 | mass: 1.0, 231 | velocity: 0.0, 232 | }), 233 | ), 234 | ); 235 | card_y 236 | .animate_to( 237 | -5.0, 238 | AnimationConfig::new( 239 | AnimationMode::Spring(Spring { 240 | stiffness: 300.0, 241 | damping: 20.0, 242 | mass: 1.0, 243 | velocity: 0.0, 244 | }), 245 | ), 246 | ); 247 | }, 248 | onmouseleave: move |_| { 249 | card_scale 250 | .animate_to( 251 | 1.0, 252 | AnimationConfig::new( 253 | AnimationMode::Spring(Spring { 254 | stiffness: 300.0, 255 | damping: 20.0, 256 | mass: 1.0, 257 | velocity: 0.0, 258 | }), 259 | ), 260 | ); 261 | card_y 262 | .animate_to( 263 | 0.0, 264 | AnimationConfig::new( 265 | AnimationMode::Spring(Spring { 266 | stiffness: 300.0, 267 | damping: 20.0, 268 | mass: 1.0, 269 | velocity: 0.0, 270 | }), 271 | ), 272 | ); 273 | }, 274 | div { class: "flex items-center gap-3 mb-4", 275 | span { class: "text-2xl", {icon} } 276 | h3 { class: "text-xl font-medium text-text-primary", {title} } 277 | } 278 | p { class: "text-text-secondary leading-relaxed", {description} } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /docs/src/pages/home/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | -------------------------------------------------------------------------------- /docs/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basic_guide; 2 | pub mod blog; 3 | pub mod complex_guide; 4 | pub mod docs; 5 | pub mod home; 6 | pub mod intermediate_guide; 7 | -------------------------------------------------------------------------------- /docs/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod router; 2 | -------------------------------------------------------------------------------- /docs/src/utils/router.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | use dioxus_motion::prelude::*; 3 | 4 | use crate::components::navbar::NavBar; 5 | use crate::components::page_not_found::PageNotFound; 6 | use crate::components::page_transition::PageTransition; 7 | use crate::old_showcase::showcase_component::ShowcaseGallery; 8 | use crate::pages::basic_guide::BasicAnimationGuide; 9 | use crate::pages::blog::index::Blog; 10 | use crate::pages::complex_guide::ComplexAnimationGuide; 11 | use crate::pages::docs::index::Docs; 12 | use crate::pages::docs::index::DocsLanding; 13 | use crate::pages::home::index::Home; 14 | use crate::pages::intermediate_guide::IntermediateAnimationGuide; 15 | 16 | // Turn off rustfmt since we're doing layouts and routes in the same enum 17 | #[derive(Routable, Clone, Debug, PartialEq, MotionTransitions)] 18 | #[rustfmt::skip] 19 | #[allow(clippy::empty_line_after_outer_attr)] 20 | pub enum Route { 21 | // Wrap Home in a Navbar Layout 22 | #[layout(NavBar)] 23 | // The default route is always "/" unless otherwise specified 24 | #[route("/")] 25 | #[transition(Fade)] 26 | Home {}, 27 | 28 | // Wrap the next routes in a layout and a nest 29 | #[nest("/docs")] 30 | #[layout(Docs)] 31 | // At "/blog", we want to show a list of blog posts 32 | #[route("/")] 33 | #[transition(SlideLeft)] 34 | DocsLanding {}, 35 | 36 | #[route("/transitions")] 37 | #[transition(SlideLeft)] 38 | PageTransition {}, 39 | 40 | #[route("/basic_guide")] 41 | #[transition(SlideLeft)] 42 | BasicAnimationGuide {}, 43 | 44 | #[route("/intermediate_guide")] 45 | #[transition(SlideLeft)] 46 | IntermediateAnimationGuide {}, 47 | 48 | #[route("/complex_guide")] 49 | #[transition(SlideLeft)] 50 | ComplexAnimationGuide {}, 51 | 52 | 53 | // // At "/blog/:name", we want to show a specific blog post, using the name slug 54 | // #[route("/animations")] 55 | // #[transition(SlideLeft)] 56 | // Animations {}, 57 | 58 | 59 | 60 | // We need to end the blog layout and nest 61 | // Note we don't need either - we could've just done `/blog/` and `/blog/:name` without nesting, 62 | // but it's a bit cleaner this way 63 | #[end_layout] 64 | #[end_nest] 65 | 66 | #[route("/blog")] 67 | #[transition(SlideDown)] 68 | Blog {}, 69 | 70 | #[route("/old_showcase")] 71 | #[transition(Fade)] 72 | ShowcaseGallery {}, 73 | 74 | 75 | // And the regular page layout 76 | #[end_layout] 77 | 78 | // Finally, we need to handle the 404 page 79 | #[route("/:..route")] 80 | PageNotFound { 81 | route: Vec, 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'selector', 4 | mode: "all", 5 | content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], 6 | theme: { 7 | extend: { 8 | keyframes: { 9 | down: { 10 | '0%': { transform: 'translateY(0%)' }, 11 | '100%': { transform: 'translateY(calc(45vh - 8rem))' } 12 | }, 13 | shimmer: { 14 | '0%': { 15 | transform: 'translateX(-100%)' 16 | }, 17 | '100%': { 18 | transform: 'translateX(100%)' 19 | } 20 | } 21 | }, 22 | animation: { 23 | 'move-down': 'down 3s linear infinite', 24 | 'shimmer': 'shimmer 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' 25 | }, 26 | boxShadow: { 27 | neumorphic: '20px 20px 60px #d9d9d9, -20px -20px 60px #ffffff, 0 4px 6px -1px rgba(255, 255, 255, 0.1), 0 2px 4px -1px rgba(255, 255, 255, 0.06), 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 28 | }, 29 | backgroundImage: theme => ({ 30 | 'gradient-bg': 'linear-gradient(to right, #1A202C, black)', // bg-gray-950 is #1F2937 31 | }), 32 | colors: { 33 | 'background': { 34 | DEFAULT: '#121212', 35 | 'light': '#FFFFFF' 36 | }, 37 | 'primary': { 38 | DEFAULT: '#BB86FC', 39 | 'light': '#60a5fa', 40 | 'dark': '#3b82f6', 41 | 'hover': '#147197253' 42 | }, 43 | 'secondary': { 44 | DEFAULT: '#03A9F4', 45 | 'light': '#e5e7eb', 46 | 'dark': '#374151' 47 | }, 48 | 'accent': { 49 | 'purple': { 50 | DEFAULT: '#C084FC', 51 | 'hover': '#D8B4FE' 52 | } 53 | }, 54 | 'surface': { 55 | DEFAULT: '#1F2937', 56 | 'light': '#374151', 57 | 'dark': '#111827', 58 | 'hover': '#111827B3' 59 | }, 60 | 'text': { 61 | 'primary': '#FFFFFF', 62 | 'secondary': '#D1D5DB', 63 | 'muted': '#9CA3AF' 64 | } 65 | } 66 | }, 67 | }, 68 | plugins: [], 69 | }; 70 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wheregmis/dioxus-motion/1061379d48fb24213090274737c22883b2a04d92/example.gif -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | remove_deps: 2 | cargo machete --with-metadata 3 | 4 | check_timing: 5 | cargo build --timings 6 | 7 | remove_unused_features: 8 | cargo features prune 9 | -------------------------------------------------------------------------------- /packages/dioxus-motion-transitions-macro/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "proc-macro2" 7 | version = "1.0.93" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 10 | dependencies = [ 11 | "unicode-ident", 12 | ] 13 | 14 | [[package]] 15 | name = "quote" 16 | version = "1.0.38" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 19 | dependencies = [ 20 | "proc-macro2", 21 | ] 22 | 23 | [[package]] 24 | name = "route_transitions" 25 | version = "0.1.0" 26 | dependencies = [ 27 | "proc-macro2", 28 | "quote", 29 | "syn", 30 | ] 31 | 32 | [[package]] 33 | name = "syn" 34 | version = "2.0.98" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 37 | dependencies = [ 38 | "proc-macro2", 39 | "quote", 40 | "unicode-ident", 41 | ] 42 | 43 | [[package]] 44 | name = "unicode-ident" 45 | version = "1.0.16" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 48 | -------------------------------------------------------------------------------- /packages/dioxus-motion-transitions-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-motion-transitions-macro" 3 | version = "0.1.0" 4 | edition = "2024" 5 | description = "Page transition support for dioxus-motion" 6 | license = "MIT" 7 | authors = ["Sabin Regmi "] 8 | repository = "https://github.com/wheregmis/dioxus-motion" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "2.0.100", features = [ 15 | "derive", 16 | "parsing", 17 | "proc-macro", 18 | ], default-features = false } 19 | quote = { version = "1.0.40", default-features = false } 20 | proc-macro2 = { version = "1.0.94", default-features = false } 21 | -------------------------------------------------------------------------------- /packages/dioxus-motion-transitions-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{Attribute, Data, DataEnum, DeriveInput, Fields, Meta, parse_macro_input}; 4 | 5 | fn get_transition_from_attrs(attrs: &[Attribute]) -> Option { 6 | attrs 7 | .iter() 8 | .find(|attr| attr.path().is_ident("transition")) 9 | .and_then(|attr| { 10 | if let Ok(Meta::Path(path)) = attr.parse_args::() { 11 | path.get_ident().map(|ident| ident.to_string()) 12 | } else { 13 | None 14 | } 15 | }) 16 | } 17 | 18 | // Helper to extract layout nesting information from enum variants 19 | fn get_layout_depth(variants: &[&syn::Variant]) -> Vec<(syn::Ident, usize)> { 20 | let mut layout_depth = Vec::new(); 21 | let mut current_depth = 0; 22 | 23 | for variant in variants { 24 | // Check if this variant has a layout attribute 25 | if variant 26 | .attrs 27 | .iter() 28 | .any(|attr| attr.path().is_ident("layout")) 29 | { 30 | current_depth += 1; 31 | } 32 | 33 | // Check if this variant ends a layout 34 | if variant 35 | .attrs 36 | .iter() 37 | .any(|attr| attr.path().is_ident("end_layout")) 38 | && current_depth > 0 39 | { 40 | current_depth -= 1; 41 | } 42 | 43 | // Associate current depth with this variant 44 | layout_depth.push((variant.ident.clone(), current_depth)); 45 | } 46 | 47 | layout_depth 48 | } 49 | 50 | #[proc_macro_derive(MotionTransitions, attributes(transition, layout, end_layout))] 51 | pub fn derive_route_transitions(input: TokenStream) -> TokenStream { 52 | let input = parse_macro_input!(input as DeriveInput); 53 | let name = &input.ident; 54 | let variants = match input.data { 55 | Data::Enum(DataEnum { variants, .. }) => variants, 56 | _ => panic!("MotionTransitions can only be derived for enums"), 57 | }; 58 | 59 | let component_match_arms = variants.iter().map(|variant| { 60 | let variant_ident = &variant.ident; 61 | let component_name = &variant.ident; 62 | 63 | match &variant.fields { 64 | Fields::Named(fields) => { 65 | let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); 66 | quote! { 67 | Self::#variant_ident { #(#field_names,)* } => { 68 | rsx! { #component_name { #(#field_names: #field_names.clone(),)* } } 69 | } 70 | } 71 | } 72 | Fields::Unnamed(_) => { 73 | quote! { Self::#variant_ident(..) => rsx! { #component_name {} } } 74 | } 75 | Fields::Unit => { 76 | quote! { Self::#variant_ident {} => rsx! { #component_name {} } } 77 | } 78 | } 79 | }); 80 | 81 | let transition_match_arms = variants.iter().map(|variant| { 82 | let variant_ident = &variant.ident; 83 | let transition = get_transition_from_attrs(&variant.attrs) 84 | .map(|t| format_ident!("{}", t)) 85 | .unwrap_or(format_ident!("Fade")); 86 | 87 | match &variant.fields { 88 | Fields::Named(fields) => { 89 | let field_patterns = fields.named.iter().map(|f| { 90 | let name = &f.ident; 91 | quote! { #name: _ } 92 | }); 93 | quote! { 94 | Self::#variant_ident { #(#field_patterns,)* } => TransitionVariant::#transition 95 | } 96 | } 97 | Fields::Unnamed(_) => { 98 | quote! { Self::#variant_ident(..) => TransitionVariant::#transition } 99 | } 100 | Fields::Unit => { 101 | quote! { Self::#variant_ident {} => TransitionVariant::#transition } 102 | } 103 | } 104 | }); 105 | 106 | // Generate layout depth match arms 107 | let layout_depths = get_layout_depth(&variants.iter().collect::>()); 108 | let layout_depth_match_arms = 109 | layout_depths.iter().map(|(variant_ident, depth)| { 110 | match &variants 111 | .iter() 112 | .find(|v| &v.ident == variant_ident) 113 | .unwrap() 114 | .fields 115 | { 116 | Fields::Named(fields) => { 117 | let field_patterns = fields.named.iter().map(|f| { 118 | let name = &f.ident; 119 | quote! { #name: _ } 120 | }); 121 | quote! { 122 | Self::#variant_ident { #(#field_patterns,)* } => #depth 123 | } 124 | } 125 | Fields::Unnamed(_) => { 126 | quote! { Self::#variant_ident(..) => #depth } 127 | } 128 | Fields::Unit => { 129 | quote! { Self::#variant_ident {} => #depth } 130 | } 131 | } 132 | }); 133 | 134 | let expanded = quote! { 135 | impl AnimatableRoute for #name { 136 | fn get_transition(&self) -> TransitionVariant { 137 | match self { 138 | #(#transition_match_arms,)* 139 | _ => TransitionVariant::Fade, 140 | } 141 | } 142 | 143 | fn get_component(&self) -> Element { 144 | match self { 145 | #(#component_match_arms,)* 146 | } 147 | } 148 | 149 | // New method to get layout depth 150 | fn get_layout_depth(&self) -> usize { 151 | match self { 152 | #(#layout_depth_match_arms,)* 153 | _ => 0, 154 | } 155 | } 156 | } 157 | }; 158 | 159 | TokenStream::from(expanded) 160 | } 161 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | git_release_enable = false 3 | 4 | git_release_body = """ 5 | {{ changelog }} 6 | {% if remote.contributors %} 7 | ### Contributors 8 | {% for contributor in remote.contributors %} 9 | * @{{ contributor.username }} 10 | {% endfor %} 11 | {% endif %} 12 | """ 13 | 14 | [[package]] 15 | name = "dioxus-motion" 16 | changelog_path = "./CHANGELOG.md" 17 | git_release_enable = true 18 | 19 | [[package]] 20 | name = "dioxus-motion-transitions-macro" 21 | changelog_include = [] 22 | changelog_path = "./CHANGELOG.md" 23 | git_release_enable = true 24 | -------------------------------------------------------------------------------- /src/animations/colors.rs: -------------------------------------------------------------------------------- 1 | //! Color module for animation support 2 | //! 3 | //! Provides RGBA color representation and animation interpolation. 4 | //! Supports both normalized (0.0-1.0) and byte (0-255) color values. 5 | 6 | use crate::animations::utils::Animatable; 7 | 8 | /// Represents an RGBA color with normalized components 9 | /// 10 | /// Each component (r,g,b,a) is stored as a float between 0.0 and 1.0 11 | #[derive(Debug, Copy, Clone, PartialEq)] 12 | pub struct Color { 13 | /// Red component (0.0-1.0) 14 | pub r: f32, 15 | /// Green component (0.0-1.0) 16 | pub g: f32, 17 | /// Blue component (0.0-1.0) 18 | pub b: f32, 19 | /// Alpha component (0.0-1.0) 20 | pub a: f32, 21 | } 22 | 23 | impl Color { 24 | /// Creates a new color with normalized components 25 | /// 26 | /// # Examples 27 | /// ``` 28 | /// use dioxus_motion::prelude::Color; 29 | /// let color = Color::new(1.0, 0.5, 0.0, 1.0); // Orange color 30 | /// ``` 31 | pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { 32 | Self { 33 | r: r.clamp(0.0, 1.0), 34 | g: g.clamp(0.0, 1.0), 35 | b: b.clamp(0.0, 1.0), 36 | a: a.clamp(0.0, 1.0), 37 | } 38 | } 39 | 40 | /// Creates a color from 8-bit RGBA values 41 | /// 42 | /// # Examples 43 | /// ``` 44 | /// use dioxus_motion::prelude::Color; 45 | /// let color = Color::from_rgba(255, 128, 0, 255); // Orange color 46 | /// ``` 47 | pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self { 48 | Color::new( 49 | r as f32 / 255.0, 50 | g as f32 / 255.0, 51 | b as f32 / 255.0, 52 | a as f32 / 255.0, 53 | ) 54 | } 55 | 56 | /// Converts color to 8-bit RGBA values 57 | /// 58 | /// # Returns 59 | /// Tuple of (r,g,b,a) with values from 0-255 60 | pub fn to_rgba(&self) -> (u8, u8, u8, u8) { 61 | ( 62 | (self.r * 255.0 + 0.5) as u8, 63 | (self.g * 255.0 + 0.5) as u8, 64 | (self.b * 255.0 + 0.5) as u8, 65 | (self.a * 255.0 + 0.5) as u8, 66 | ) 67 | } 68 | } 69 | 70 | /// Implementation of animation interpolation for Color 71 | impl Animatable for Color { 72 | /// Creates a fully transparent black color 73 | fn zero() -> Self { 74 | Color::new(0.0, 0.0, 0.0, 0.0) 75 | } 76 | 77 | /// Minimum difference between color components 78 | fn epsilon() -> f32 { 79 | 0.00001 // Increased precision for smoother transitions 80 | } 81 | 82 | /// Calculates color vector magnitude 83 | fn magnitude(&self) -> f32 { 84 | // Weighted magnitude calculation for better precision 85 | let r_diff = self.r; 86 | let g_diff = self.g; 87 | let b_diff = self.b; 88 | let a_diff = self.a; 89 | 90 | (r_diff * r_diff + g_diff * g_diff + b_diff * b_diff + a_diff * a_diff).sqrt() 91 | } 92 | 93 | /// Scales color components by a factor 94 | fn scale(&self, factor: f32) -> Self { 95 | Color::new( 96 | self.r * factor, 97 | self.g * factor, 98 | self.b * factor, 99 | self.a * factor, 100 | ) 101 | } 102 | 103 | /// Adds two colors component-wise 104 | fn add(&self, other: &Self) -> Self { 105 | Color::new( 106 | self.r + other.r, 107 | self.g + other.g, 108 | self.b + other.b, 109 | self.a + other.a, 110 | ) 111 | } 112 | 113 | /// Subtracts two colors component-wise 114 | fn sub(&self, other: &Self) -> Self { 115 | Color::new( 116 | self.r - other.r, 117 | self.g - other.g, 118 | self.b - other.b, 119 | self.a - other.a, 120 | ) 121 | } 122 | 123 | /// Linearly interpolates between two colors 124 | /// 125 | /// # Parameters 126 | /// * `target` - Target color to interpolate towards 127 | /// * `t` - Interpolation factor (0.0-1.0) 128 | fn interpolate(&self, target: &Self, t: f32) -> Self { 129 | let t = t.clamp(0.0, 1.0); 130 | 131 | // Direct linear interpolation that works for both increasing and decreasing values 132 | let r = self.r * (1.0 - t) + target.r * t; 133 | let g = self.g * (1.0 - t) + target.g * t; 134 | let b = self.b * (1.0 - t) + target.b * t; 135 | let a = self.a * (1.0 - t) + target.a * t; 136 | 137 | Color::new(r, g, b, a) 138 | } 139 | } 140 | 141 | #[cfg(test)] 142 | mod tests { 143 | use super::*; 144 | 145 | #[test] 146 | fn test_color_new() { 147 | let color = Color::new(1.0, 0.5, 0.0, 1.0); 148 | assert_eq!(color.r, 1.0); 149 | assert_eq!(color.g, 0.5); 150 | assert_eq!(color.b, 0.0); 151 | assert_eq!(color.a, 1.0); 152 | } 153 | 154 | #[test] 155 | fn test_color_from_rgba() { 156 | let color = Color::from_rgba(255, 128, 0, 255); 157 | assert!((color.r - 1.0).abs() < f32::EPSILON); 158 | assert!((color.g - 0.5019608).abs() < 0.000001); 159 | assert!((color.b - 0.0).abs() < f32::EPSILON); 160 | assert!((color.a - 1.0).abs() < f32::EPSILON); 161 | } 162 | 163 | #[test] 164 | fn test_color_lerp() { 165 | let start = Color::new(0.0, 0.0, 0.0, 1.0); 166 | let end = Color::new(1.0, 1.0, 1.0, 1.0); 167 | let mid = start.interpolate(&end, 0.5); 168 | 169 | assert!((mid.r - 0.5).abs() < f32::EPSILON); 170 | assert!((mid.g - 0.5).abs() < f32::EPSILON); 171 | assert!((mid.b - 0.5).abs() < f32::EPSILON); 172 | assert!((mid.a - 1.0).abs() < f32::EPSILON); 173 | } 174 | 175 | #[test] 176 | fn test_color_to_rgba() { 177 | let color = Color::new(1.0, 0.5, 0.0, 1.0); 178 | let (r, g, b, a) = color.to_rgba(); 179 | assert_eq!(r, 255); 180 | assert_eq!(g, 128); 181 | assert_eq!(b, 0); 182 | assert_eq!(a, 255); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/animations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod colors; 2 | pub mod platform; 3 | pub mod spring; 4 | pub mod transform; 5 | pub mod tween; 6 | pub mod utils; 7 | -------------------------------------------------------------------------------- /src/animations/platform.rs: -------------------------------------------------------------------------------- 1 | //! Platform abstraction for time-related functionality 2 | //! 3 | //! Provides cross-platform timing operations for animations. 4 | //! Supports both web (WASM) and native platforms. 5 | 6 | use instant::{Duration, Instant}; 7 | use std::future::Future; 8 | 9 | /// Provides platform-agnostic timing operations 10 | /// 11 | /// Abstracts timing functionality across different platforms, 12 | /// ensuring consistent animation behavior in both web and native environments. 13 | pub trait TimeProvider { 14 | /// Returns the current instant 15 | fn now() -> Instant; 16 | 17 | /// Creates a future that completes after the specified duration 18 | fn delay(duration: Duration) -> impl Future; 19 | } 20 | 21 | /// Default time provider implementation for motion animations 22 | /// 23 | /// Implements platform-specific timing operations: 24 | /// - For web: Uses requestAnimationFrame or setTimeout 25 | /// - For native: Uses tokio's sleep 26 | #[derive(Debug, Clone, Copy)] 27 | pub struct MotionTime; 28 | 29 | impl TimeProvider for MotionTime { 30 | fn now() -> Instant { 31 | Instant::now() 32 | } 33 | 34 | /// Creates a delay future using platform-specific implementations 35 | /// 36 | /// # Web 37 | /// Uses requestAnimationFrame for short delays (<16ms) 38 | /// Uses setTimeout for longer delays 39 | /// 40 | /// # Native 41 | /// Uses tokio::time::sleep 42 | #[cfg(feature = "web")] 43 | fn delay(_duration: Duration) -> impl Future { 44 | use futures_util::FutureExt; 45 | use wasm_bindgen::prelude::*; 46 | use web_sys::window; 47 | 48 | const RAF_THRESHOLD_MS: u8 = 16; 49 | 50 | let (sender, receiver) = futures_channel::oneshot::channel::<()>(); 51 | 52 | if let Some(window) = window() { 53 | let performance = window.performance().expect("Performance API not available"); 54 | let start_time = performance.now(); 55 | 56 | let cb = Closure::once(move || { 57 | let current_time = performance.now(); 58 | let elapsed = current_time - start_time; 59 | 60 | // Only complete if we've waited the full duration 61 | if elapsed >= _duration.as_millis() as f64 { 62 | let _ = sender.send(()); 63 | } 64 | }); 65 | 66 | // Cache the callback reference 67 | let cb_ref = cb.as_ref().unchecked_ref(); 68 | 69 | // Choose timing method based on duration 70 | if _duration.as_millis() < RAF_THRESHOLD_MS as u128 { 71 | window 72 | .request_animation_frame(cb_ref) 73 | .expect("Failed to request animation frame"); 74 | } else { 75 | window 76 | .set_timeout_with_callback_and_timeout_and_arguments_0( 77 | cb_ref, 78 | _duration.as_millis() as i32, 79 | ) 80 | .expect("Failed to set timeout"); 81 | } 82 | 83 | cb.forget(); 84 | } 85 | 86 | receiver.map(|_| ()) 87 | } 88 | 89 | #[cfg(not(feature = "web"))] 90 | fn delay(duration: Duration) -> impl Future { 91 | Box::pin(async move { 92 | let start = std::time::Instant::now(); 93 | 94 | tokio::time::sleep(duration).await; 95 | 96 | // High precision timing for desktop 97 | let remaining = duration.saturating_sub(start.elapsed()); 98 | if remaining.subsec_micros() > 0 { 99 | spin_sleep::sleep(remaining); 100 | } 101 | }) 102 | } 103 | } 104 | 105 | /// Type alias for the default time provider 106 | pub type Time = MotionTime; 107 | -------------------------------------------------------------------------------- /src/animations/spring.rs: -------------------------------------------------------------------------------- 1 | //! Spring physics implementation for animations 2 | //! 3 | //! Provides a physical spring model for smooth, natural-looking animations. 4 | //! Based on Hooke's law with damping for realistic motion. 5 | 6 | /// Configuration for spring-based animations 7 | /// 8 | /// Uses a mass-spring-damper system to create natural motion. 9 | /// 10 | /// # Examples 11 | /// ```rust 12 | /// use dioxus_motion::prelude::Spring; 13 | /// let spring = Spring { 14 | /// stiffness: 100.0, // Higher values = faster snap 15 | /// damping: 10.0, // Higher values = less bounce 16 | /// mass: 1.0, // Higher values = more inertia 17 | /// velocity: 0.0, // Initial velocity 18 | /// }; 19 | /// ``` 20 | #[derive(Debug, Clone, Copy, PartialEq)] 21 | pub struct Spring { 22 | /// Spring stiffness constant (default: 100.0) 23 | /// Higher values make the spring stronger and faster 24 | pub stiffness: f32, 25 | 26 | /// Damping coefficient (default: 10.0) 27 | /// Higher values reduce oscillation 28 | pub damping: f32, 29 | 30 | /// Mass of the object (default: 1.0) 31 | /// Higher values increase inertia 32 | pub mass: f32, 33 | 34 | /// Initial velocity (default: 0.0) 35 | /// Can be set for pre-existing motion 36 | pub velocity: f32, 37 | } 38 | 39 | /// Default spring configuration for general-purpose animations 40 | impl Default for Spring { 41 | fn default() -> Self { 42 | Self { 43 | stiffness: 100.0, 44 | damping: 10.0, 45 | mass: 1.0, 46 | velocity: 0.0, 47 | } 48 | } 49 | } 50 | 51 | /// Represents the current state of a spring animation 52 | /// 53 | /// Used to track whether the spring is still moving or has settled 54 | #[derive(Debug, Copy, Clone, PartialEq)] 55 | pub enum SpringState { 56 | /// Spring is still in motion 57 | Active, 58 | /// Spring has settled to its target position 59 | Completed, 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn test_spring_default() { 68 | let spring = Spring::default(); 69 | assert_eq!(spring.stiffness, 100.0); 70 | assert_eq!(spring.damping, 10.0); 71 | assert_eq!(spring.mass, 1.0); 72 | assert_eq!(spring.velocity, 0.0); 73 | } 74 | 75 | #[test] 76 | fn test_spring_custom() { 77 | let spring = Spring { 78 | stiffness: 200.0, 79 | damping: 20.0, 80 | mass: 2.0, 81 | velocity: 5.0, 82 | }; 83 | 84 | assert_eq!(spring.stiffness, 200.0); 85 | assert_eq!(spring.damping, 20.0); 86 | assert_eq!(spring.mass, 2.0); 87 | assert_eq!(spring.velocity, 5.0); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/animations/transform.rs: -------------------------------------------------------------------------------- 1 | //! Transform module for 2D transformations 2 | //! 3 | //! Provides a Transform type that can be animated, supporting: 4 | //! - Translation (x, y) 5 | //! - Scale 6 | //! - Rotation 7 | //! 8 | //! Uses radians for rotation and supports smooth interpolation. 9 | 10 | use crate::Animatable; 11 | 12 | /// Represents a 2D transformation with translation, scale, and rotation 13 | /// 14 | /// # Examples 15 | /// ```rust 16 | /// use dioxus_motion::prelude::Transform; 17 | /// use std::f32::consts::PI; 18 | /// let transform = Transform::new(100.0, 50.0, 1.5, PI/4.0); 19 | /// ``` 20 | #[derive(Debug, Copy, Clone, PartialEq)] 21 | pub struct Transform { 22 | /// X translation component 23 | pub x: f32, 24 | /// Y translation component 25 | pub y: f32, 26 | /// Uniform scale factor 27 | pub scale: f32, 28 | /// Rotation in radians 29 | pub rotation: f32, 30 | } 31 | 32 | impl Transform { 33 | /// Creates a new transform with specified parameters 34 | pub fn new(x: f32, y: f32, scale: f32, rotation: f32) -> Self { 35 | Self { 36 | x, 37 | y, 38 | scale, 39 | rotation, 40 | } 41 | } 42 | 43 | /// Creates an identity transform (no transformation) 44 | pub fn identity() -> Self { 45 | Self { 46 | x: 0.0, 47 | y: 0.0, 48 | scale: 1.0, 49 | rotation: 0.0, 50 | } 51 | } 52 | } 53 | 54 | /// Implementation of Animatable for f32 primitive type 55 | /// Enables direct animation of float values 56 | impl Animatable for f32 { 57 | fn zero() -> Self { 58 | 0.0 59 | } 60 | 61 | fn epsilon() -> f32 { 62 | 0.001 63 | } 64 | 65 | fn magnitude(&self) -> f32 { 66 | self.abs() 67 | } 68 | 69 | fn scale(&self, factor: f32) -> Self { 70 | self * factor 71 | } 72 | 73 | fn add(&self, other: &Self) -> Self { 74 | self + other 75 | } 76 | 77 | fn sub(&self, other: &Self) -> Self { 78 | self - other 79 | } 80 | 81 | fn interpolate(&self, target: &Self, t: f32) -> Self { 82 | self + (target - self) * t 83 | } 84 | } 85 | 86 | /// Implementation of Animatable for Transform 87 | /// Provides smooth interpolation between transform states 88 | impl Animatable for Transform { 89 | /// Creates a zero transform (all components 0) 90 | fn zero() -> Self { 91 | Transform::new(0.0, 0.0, 0.0, 0.0) 92 | } 93 | 94 | /// Minimum meaningful difference between transforms 95 | fn epsilon() -> f32 { 96 | 0.001 97 | } 98 | 99 | /// Calculates the magnitude of the transform 100 | fn magnitude(&self) -> f32 { 101 | (self.x * self.x 102 | + self.y * self.y 103 | + self.scale * self.scale 104 | + self.rotation * self.rotation) 105 | .sqrt() 106 | } 107 | 108 | /// Scales all transform components by a factor 109 | fn scale(&self, factor: f32) -> Self { 110 | Transform::new( 111 | self.x * factor, 112 | self.y * factor, 113 | self.scale * factor, 114 | self.rotation * factor, 115 | ) 116 | } 117 | 118 | /// Adds two transforms component-wise 119 | fn add(&self, other: &Self) -> Self { 120 | Transform::new( 121 | self.x + other.x, 122 | self.y + other.y, 123 | self.scale + other.scale, 124 | self.rotation + other.rotation, 125 | ) 126 | } 127 | 128 | /// Subtracts two transforms component-wise 129 | fn sub(&self, other: &Self) -> Self { 130 | Transform::new( 131 | self.x - other.x, 132 | self.y - other.y, 133 | self.scale - other.scale, 134 | self.rotation - other.rotation, 135 | ) 136 | } 137 | 138 | /// Interpolates between two transforms 139 | /// Handles rotation specially to ensure shortest path 140 | fn interpolate(&self, target: &Self, t: f32) -> Self { 141 | // Special handling for rotation to ensure shortest path 142 | let mut rotation_diff = target.rotation - self.rotation; 143 | if rotation_diff > std::f32::consts::PI { 144 | rotation_diff -= 2.0 * std::f32::consts::PI; 145 | } else if rotation_diff < -std::f32::consts::PI { 146 | rotation_diff += 2.0 * std::f32::consts::PI; 147 | } 148 | 149 | Transform::new( 150 | self.x + (target.x - self.x) * t, 151 | self.y + (target.y - self.y) * t, 152 | self.scale + (target.scale - self.scale) * t, 153 | self.rotation + rotation_diff * t, 154 | ) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | use std::f32::consts::PI; 162 | 163 | #[test] 164 | fn test_transform_new() { 165 | let transform = Transform::new(100.0, 50.0, 1.5, PI / 4.0); 166 | assert_eq!(transform.x, 100.0); 167 | assert_eq!(transform.y, 50.0); 168 | assert_eq!(transform.scale, 1.5); 169 | assert!((transform.rotation - PI / 4.0).abs() < f32::EPSILON); 170 | } 171 | 172 | #[test] 173 | fn test_transform_default() { 174 | let transform = Transform::identity(); 175 | assert_eq!(transform.x, 0.0); 176 | assert_eq!(transform.y, 0.0); 177 | assert_eq!(transform.scale, 1.0); 178 | assert_eq!(transform.rotation, 0.0); 179 | } 180 | 181 | #[test] 182 | fn test_transform_lerp() { 183 | let start = Transform::new(0.0, 0.0, 1.0, 0.0); 184 | let end = Transform::new(100.0, 100.0, 2.0, PI); 185 | let mid = start.interpolate(&end, 0.5); 186 | 187 | assert_eq!(mid.x, 50.0); 188 | assert_eq!(mid.y, 50.0); 189 | assert_eq!(mid.scale, 1.5); 190 | assert!((mid.rotation - PI / 2.0).abs() < f32::EPSILON); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/animations/tween.rs: -------------------------------------------------------------------------------- 1 | //! Tween animation module 2 | //! 3 | //! Provides time-based animation with customizable easing functions. 4 | //! Supports duration and interpolation control for smooth animations. 5 | 6 | use easer::functions::{Easing, Linear}; 7 | pub use instant::Duration; 8 | 9 | /// Configuration for tween-based animations 10 | /// 11 | /// # Examples 12 | /// ```rust 13 | /// use dioxus_motion::Duration; 14 | /// use dioxus_motion::prelude::Tween; 15 | /// use easer::functions::Easing; 16 | /// let tween = Tween::new(Duration::from_secs(1)) 17 | /// .with_easing(easer::functions::Cubic::ease_in_out); 18 | /// ``` 19 | #[derive(Debug, Clone, Copy, PartialEq)] 20 | pub struct Tween { 21 | /// Duration of the animation 22 | pub duration: Duration, 23 | /// Easing function for interpolation 24 | pub easing: fn(f32, f32, f32, f32) -> f32, 25 | } 26 | 27 | /// Default tween configuration with 300ms duration and linear easing 28 | impl Default for Tween { 29 | fn default() -> Self { 30 | Self { 31 | duration: Duration::from_millis(300), 32 | easing: Linear::ease_in_out, 33 | } 34 | } 35 | } 36 | 37 | impl Tween { 38 | /// Creates a new tween with specified duration and linear easing 39 | pub fn new(duration: Duration) -> Self { 40 | Self { 41 | duration, 42 | easing: Linear::ease_in_out, 43 | } 44 | } 45 | 46 | /// Sets the easing function for the animation 47 | /// 48 | /// # Arguments 49 | /// * `easing` - Function that takes (t, b, c, d) and returns interpolated value 50 | pub fn with_easing(mut self, easing: fn(f32, f32, f32, f32) -> f32) -> Self { 51 | self.easing = easing; 52 | self 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use easer::functions::{Cubic, Easing}; 60 | 61 | #[test] 62 | fn test_tween_new() { 63 | let tween = Tween { 64 | duration: Duration::from_secs(1), 65 | easing: Cubic::ease_in_out, 66 | }; 67 | 68 | assert_eq!(tween.duration, Duration::from_secs(1)); 69 | } 70 | 71 | #[test] 72 | fn test_tween_interpolation() { 73 | let tween = Tween { 74 | duration: Duration::from_secs(1), 75 | easing: Linear::ease_in_out, 76 | }; 77 | 78 | // Test midpoint 79 | let progress = 0.5; 80 | let result = (tween.easing)(progress, 0.0, 1.0, 1.0); 81 | assert!((result - 0.5).abs() < f32::EPSILON); 82 | 83 | // Test start 84 | let result = (tween.easing)(0.0, 0.0, 1.0, 1.0); 85 | assert!((result - 0.0).abs() < f32::EPSILON); 86 | 87 | // Test end 88 | let result = (tween.easing)(1.0, 0.0, 1.0, 1.0); 89 | assert!((result - 1.0).abs() < f32::EPSILON); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/animations/utils.rs: -------------------------------------------------------------------------------- 1 | //! Animation module providing core animation functionality 2 | //! 3 | //! This module contains traits and types for implementing animations in Dioxus Motion. 4 | //! It provides support for both tweening and spring-based animations with configurable 5 | //! parameters. 6 | 7 | use std::sync::{Arc, Mutex}; 8 | 9 | use crate::animations::{spring::Spring, tween::Tween}; 10 | use instant::Duration; 11 | 12 | /// A trait for types that can be animated 13 | /// 14 | /// Types implementing this trait can be used with both tween and spring animations. 15 | /// The trait provides basic mathematical operations needed for interpolation and 16 | /// physics calculations. 17 | pub trait Animatable: Copy + 'static { 18 | /// Creates a zero value for the type 19 | fn zero() -> Self; 20 | 21 | /// Returns the smallest meaningful difference between values 22 | fn epsilon() -> f32; 23 | 24 | /// Calculates the magnitude/length of the value 25 | fn magnitude(&self) -> f32; 26 | 27 | /// Scales the value by a factor 28 | fn scale(&self, factor: f32) -> Self; 29 | 30 | /// Adds another value 31 | fn add(&self, other: &Self) -> Self; 32 | 33 | /// Subtracts another value 34 | fn sub(&self, other: &Self) -> Self; 35 | 36 | /// Interpolates between self and target using t (0.0 to 1.0) 37 | fn interpolate(&self, target: &Self, t: f32) -> Self; 38 | } 39 | 40 | /// Defines the type of animation to be used 41 | #[derive(Debug, Clone, Copy, PartialEq)] 42 | pub enum AnimationMode { 43 | /// Tween animation with duration and easing 44 | Tween(Tween), 45 | /// Physics-based spring animation 46 | Spring(Spring), 47 | } 48 | 49 | impl Default for AnimationMode { 50 | fn default() -> Self { 51 | Self::Tween(Tween::default()) 52 | } 53 | } 54 | 55 | /// Defines how the animation should loop 56 | #[derive(Debug, Clone, Copy, PartialEq)] 57 | pub enum LoopMode { 58 | /// Play animation once 59 | None, 60 | /// Loop animation indefinitely 61 | Infinite, 62 | /// Loop animation a specific number of times 63 | Times(u8), 64 | /// Loop animation back and forth indefinitely 65 | Alternate, 66 | /// Loop animation back and forth a specific number of times 67 | AlternateTimes(u8), 68 | } 69 | 70 | impl Default for LoopMode { 71 | fn default() -> Self { 72 | Self::None 73 | } 74 | } 75 | 76 | pub type OnComplete = Arc>; 77 | /// Configuration for an animation 78 | #[derive(Clone, Default)] 79 | pub struct AnimationConfig { 80 | /// The type of animation (Tween or Spring) 81 | pub mode: AnimationMode, 82 | /// How the animation should loop 83 | pub loop_mode: Option, 84 | /// Delay before animation starts 85 | pub delay: Duration, 86 | /// Callback when animation completes 87 | pub on_complete: Option>>, 88 | } 89 | 90 | impl AnimationConfig { 91 | /// Creates a new animation configuration with specified mode 92 | pub fn new(mode: AnimationMode) -> Self { 93 | Self { 94 | mode, 95 | loop_mode: None, 96 | delay: Duration::default(), 97 | on_complete: None, 98 | } 99 | } 100 | 101 | /// Sets the loop mode for the animation 102 | pub fn with_loop(mut self, loop_mode: LoopMode) -> Self { 103 | self.loop_mode = Some(loop_mode); 104 | self 105 | } 106 | 107 | /// Sets a delay before the animation starts 108 | pub fn with_delay(mut self, delay: Duration) -> Self { 109 | self.delay = delay; 110 | self 111 | } 112 | 113 | /// Sets a callback to be called when animation completes 114 | pub fn with_on_complete(mut self, f: F) -> Self 115 | where 116 | F: FnMut() + Send + 'static, 117 | { 118 | self.on_complete = Some(Arc::new(Mutex::new(f))); 119 | self 120 | } 121 | 122 | /// Gets the total duration of the animation 123 | pub fn get_duration(&self) -> Duration { 124 | match &self.mode { 125 | AnimationMode::Spring(_) => { 126 | // Springs don't have a fixed duration, estimate based on typical settling time 127 | Duration::from_secs_f32(1.0) // You might want to adjust this based on spring parameters 128 | } 129 | AnimationMode::Tween(tween) => { 130 | let base_duration = tween.duration; 131 | match self.loop_mode { 132 | Some(LoopMode::Infinite) => Duration::from_secs(f32::INFINITY as u64), 133 | Some(LoopMode::Times(count)) => base_duration * count.into(), 134 | Some(LoopMode::Alternate) => Duration::from_secs(f32::INFINITY as u64), 135 | Some(LoopMode::AlternateTimes(count)) => base_duration * (count * 2).into(), 136 | Some(LoopMode::None) | None => base_duration, 137 | } 138 | } 139 | } 140 | } 141 | 142 | /// Execute the completion callback if it exists 143 | pub fn execute_completion(&mut self) { 144 | if let Some(on_complete) = &self.on_complete { 145 | if let Ok(mut callback) = on_complete.lock() { 146 | callback(); 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/keyframes.rs: -------------------------------------------------------------------------------- 1 | use crate::Duration; 2 | use crate::animations::utils::Animatable; 3 | use tracing::error; 4 | 5 | pub type EasingFn = fn(f32, f32, f32, f32) -> f32; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum KeyframeError { 9 | #[error("Failed to compare keyframe offsets (possible NaN value)")] 10 | InvalidOffset, 11 | } 12 | 13 | #[derive(Clone)] 14 | pub struct Keyframe { 15 | pub value: T, 16 | pub offset: f32, 17 | pub easing: Option, 18 | } 19 | 20 | #[derive(Clone)] 21 | pub struct KeyframeAnimation { 22 | pub keyframes: Vec>, 23 | pub duration: Duration, 24 | } 25 | 26 | impl KeyframeAnimation { 27 | pub fn new(duration: Duration) -> Self { 28 | Self { 29 | keyframes: Vec::new(), 30 | duration, 31 | } 32 | } 33 | 34 | pub fn add_keyframe( 35 | mut self, 36 | value: T, 37 | offset: f32, 38 | easing: Option, 39 | ) -> Result { 40 | self.keyframes.push(Keyframe { 41 | value, 42 | offset: offset.clamp(0.0, 1.0), 43 | easing, 44 | }); 45 | self.keyframes.sort_by(|a, b| { 46 | a.offset.partial_cmp(&b.offset).map_or_else( 47 | || { 48 | error!( 49 | "Failed to compare keyframe offsets: {} vs {}", 50 | a.offset, b.offset 51 | ); 52 | std::cmp::Ordering::Equal 53 | }, 54 | |ordering| ordering, 55 | ) 56 | }); 57 | if self.keyframes.iter().any(|k| k.offset.is_nan()) { 58 | error!("Keyframe sorting failed: InvalidOffset (NaN offset)"); 59 | return Err(KeyframeError::InvalidOffset); 60 | } 61 | Ok(self) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Dioxus Motion - Animation library for Dioxus 2 | //! 3 | //! Provides smooth animations for web and native applications built with Dioxus. 4 | //! Supports both spring physics and tween-based animations with configurable parameters. 5 | //! 6 | //! # Features 7 | //! - Spring physics animations 8 | //! - Tween animations with custom easing 9 | //! - Color interpolation 10 | //! - Transform animations 11 | //! - Configurable animation loops 12 | //! - Animation sequences 13 | //! 14 | //! # Example 15 | //! ```rust,no_run 16 | //! use dioxus_motion::prelude::*; 17 | //! 18 | //! let mut value = use_motion(0.0f32); 19 | //! value.animate_to(100.0, AnimationConfig::new(AnimationMode::Spring(Spring::default()))); 20 | //! ``` 21 | 22 | #![deny(clippy::unwrap_used)] 23 | #![deny(clippy::panic)] 24 | #![deny(unused_variables)] 25 | #![deny(unused_must_use)] 26 | #![deny(unsafe_code)] // Prevent unsafe blocks 27 | #![deny(clippy::unwrap_in_result)] // No unwrap() on Result 28 | // #![deny(clippy::indexing_slicing)] // Prevent unchecked indexing 29 | #![deny(rustdoc::broken_intra_doc_links)] // Check doc links 30 | // #![deny(clippy::arithmetic_side_effects)] // Check for integer overflow 31 | #![deny(clippy::modulo_arithmetic)] // Check modulo operations 32 | #![deny(clippy::option_if_let_else)] // Prefer map/and_then 33 | #![deny(clippy::option_if_let_else)] // Prefer map/and_then 34 | 35 | use std::cell::RefCell; 36 | 37 | use animations::utils::Animatable; 38 | use dioxus::prelude::*; 39 | pub use instant::Duration; 40 | 41 | pub mod animations; 42 | pub mod keyframes; 43 | pub mod manager; 44 | pub mod motion; 45 | pub mod sequence; 46 | pub mod transitions; 47 | 48 | #[cfg(feature = "transitions")] 49 | pub use dioxus_motion_transitions_macro; 50 | 51 | pub use animations::platform::{MotionTime, TimeProvider}; 52 | use animations::spring::SpringState; 53 | use prelude::Transform; 54 | 55 | pub use keyframes::{Keyframe, KeyframeAnimation}; 56 | pub use manager::AnimationManager; 57 | 58 | use motion::Motion; 59 | 60 | // Re-exports 61 | pub mod prelude { 62 | pub use crate::animations::utils::{AnimationConfig, AnimationMode, LoopMode}; 63 | pub use crate::animations::{ 64 | colors::Color, spring::Spring, transform::Transform, tween::Tween, 65 | }; 66 | #[cfg(feature = "transitions")] 67 | pub use crate::dioxus_motion_transitions_macro::MotionTransitions; 68 | pub use crate::sequence::AnimationSequence; 69 | #[cfg(feature = "transitions")] 70 | pub use crate::transitions::page_transitions::{AnimatableRoute, AnimatedOutlet}; 71 | #[cfg(feature = "transitions")] 72 | pub use crate::transitions::utils::TransitionVariant; 73 | pub use crate::{AnimationManager, Duration, Time, TimeProvider, use_motion}; 74 | } 75 | 76 | pub type Time = MotionTime; 77 | 78 | /// Creates an animation manager that continuously updates a motion state. 79 | /// 80 | /// This function initializes a motion state with the provided initial value and spawns an asynchronous loop 81 | /// that updates the animation state based on the elapsed time between frames. When the animation is running, 82 | /// it updates the state using the calculated time delta and dynamically adjusts the update interval to optimize CPU usage; 83 | /// when the animation is inactive, it waits longer before polling again. 84 | /// 85 | /// # Example 86 | /// 87 | /// ```no_run 88 | /// use dioxus_motion::prelude::*; 89 | /// use dioxus::prelude::*; 90 | /// 91 | /// fn app() -> Element { 92 | /// let mut value = use_motion(0.0f32); 93 | /// 94 | /// // Animate to 100 with spring physics 95 | /// value.animate_to( 96 | /// 100.0, 97 | /// AnimationConfig::new(AnimationMode::Spring(Spring::default())) 98 | /// ); 99 | /// 100 | /// rsx! { 101 | /// div { 102 | /// style: "transform: translateY({value.get_value()}px)", 103 | /// "Animated content" 104 | /// } 105 | /// } 106 | /// } 107 | /// ``` 108 | pub fn use_motion(initial: T) -> impl AnimationManager { 109 | let mut state = use_signal(|| Motion::new(initial)); 110 | 111 | #[cfg(feature = "web")] 112 | let idle_poll_rate = Duration::from_millis(100); 113 | 114 | #[cfg(not(feature = "web"))] 115 | let idle_poll_rate = Duration::from_millis(33); 116 | 117 | use_effect(move || { 118 | // This executes after rendering is complete 119 | spawn(async move { 120 | let mut last_frame = Time::now(); 121 | let mut _running_frames = 0u32; 122 | 123 | loop { 124 | let now = Time::now(); 125 | let dt = (now.duration_since(last_frame).as_secs_f32()).min(0.1); 126 | last_frame = now; 127 | 128 | // Only check if running first, then write to the signal 129 | if (*state.peek()).is_running() { 130 | _running_frames += 1; 131 | (*state.write()).update(dt); 132 | 133 | #[cfg(feature = "web")] 134 | // Adaptive frame rate 135 | let delay = match dt { 136 | x if x < 0.008 => Duration::from_millis(8), // ~120fps 137 | x if x < 0.016 => Duration::from_millis(16), // ~60fps 138 | _ => Duration::from_millis(32), // ~30fps 139 | }; 140 | 141 | #[cfg(not(feature = "web"))] 142 | let delay = match _running_frames { 143 | // Higher frame rate for the first ~200 frames for smooth starts 144 | 0..=200 => Duration::from_micros(8333), // ~120fps 145 | _ => match dt { 146 | x if x < 0.005 => Duration::from_millis(8), // ~120fps 147 | x if x < 0.011 => Duration::from_millis(16), // ~60fps 148 | _ => Duration::from_millis(33), // ~30fps 149 | }, 150 | }; 151 | 152 | Time::delay(delay).await; 153 | } else { 154 | _running_frames = 0; 155 | Time::delay(idle_poll_rate).await; 156 | } 157 | } 158 | }); 159 | }); 160 | 161 | state 162 | } 163 | 164 | // Reuse allocations for common operations 165 | thread_local! { 166 | static TRANSFORM_BUFFER: RefCell> = RefCell::new(Vec::with_capacity(32)); 167 | static SPRING_BUFFER: RefCell> = RefCell::new(Vec::with_capacity(16)); 168 | } 169 | -------------------------------------------------------------------------------- /src/manager.rs: -------------------------------------------------------------------------------- 1 | use crate::Duration; 2 | use crate::animations::utils::Animatable; 3 | use crate::keyframes::KeyframeAnimation; 4 | use crate::motion::Motion; 5 | use crate::prelude::AnimationConfig; 6 | use crate::sequence::AnimationSequence; 7 | use dioxus::prelude::{Readable, Signal, Writable}; 8 | use std::sync::Arc; 9 | 10 | pub trait AnimationManager: Clone + Copy { 11 | fn new(initial: T) -> Self; 12 | fn animate_to(&mut self, target: T, config: AnimationConfig); 13 | fn animate_sequence(&mut self, sequence: AnimationSequence); 14 | fn animate_keyframes(&mut self, animation: KeyframeAnimation); 15 | fn update(&mut self, dt: f32) -> bool; 16 | fn get_value(&self) -> T; 17 | fn is_running(&self) -> bool; 18 | fn reset(&mut self); 19 | fn stop(&mut self); 20 | fn delay(&mut self, duration: Duration); 21 | } 22 | 23 | impl AnimationManager for Signal> { 24 | fn new(initial: T) -> Self { 25 | Signal::new(Motion::new(initial)) 26 | } 27 | 28 | fn animate_to(&mut self, target: T, config: AnimationConfig) { 29 | (*self.write()).animate_to(target, config); 30 | } 31 | 32 | fn animate_sequence(&mut self, sequence: AnimationSequence) { 33 | if let Some(first_step) = sequence.steps.first() { 34 | let mut state = self.write(); 35 | (*state).animate_to(first_step.target, (*first_step.config).clone()); 36 | state.sequence = Some(sequence.into()); 37 | } 38 | } 39 | 40 | fn animate_keyframes(&mut self, animation: KeyframeAnimation) { 41 | (*self.write()).animate_keyframes(animation); 42 | } 43 | 44 | fn update(&mut self, dt: f32) -> bool { 45 | (*self.write()).update(dt) 46 | } 47 | 48 | fn get_value(&self) -> T { 49 | (*self.read()).get_value() 50 | } 51 | 52 | fn is_running(&self) -> bool { 53 | (*self.read()).is_running() 54 | } 55 | 56 | fn reset(&mut self) { 57 | (*self.write()).reset(); 58 | } 59 | 60 | #[track_caller] 61 | fn stop(&mut self) { 62 | (*self.write()).stop(); 63 | } 64 | 65 | fn delay(&mut self, duration: Duration) { 66 | let mut state = self.write(); 67 | let mut config = state.config.as_ref().clone(); 68 | config.delay = duration; 69 | state.config = Arc::new(config); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/sequence.rs: -------------------------------------------------------------------------------- 1 | //! AnimationSequence - Animation step sequences 2 | 3 | use crate::animations::utils::Animatable; 4 | use crate::prelude::AnimationConfig; 5 | use smallvec::SmallVec; 6 | use std::sync::Arc; 7 | 8 | #[derive(Clone)] 9 | pub struct AnimationStep { 10 | pub target: T, 11 | pub config: Arc, 12 | pub predicted_next: Option, 13 | } 14 | 15 | pub type AnimationSteps = SmallVec<[AnimationStep; 8]>; 16 | 17 | pub struct AnimationSequence { 18 | pub steps: AnimationSteps, 19 | pub current_step: u8, 20 | pub on_complete: Option>, 21 | pub capacity_hint: u8, 22 | } 23 | 24 | impl Clone for AnimationSequence { 25 | fn clone(&self) -> Self { 26 | Self { 27 | steps: self.steps.clone(), 28 | current_step: self.current_step, 29 | on_complete: None, 30 | capacity_hint: self.capacity_hint, 31 | } 32 | } 33 | } 34 | 35 | impl AnimationSequence { 36 | pub fn new() -> Self { 37 | Self::default() 38 | } 39 | 40 | pub fn with_capacity(capacity: u8) -> Self { 41 | Self { 42 | steps: SmallVec::with_capacity(capacity as usize), 43 | current_step: 0, 44 | on_complete: None, 45 | capacity_hint: capacity, 46 | } 47 | } 48 | 49 | pub fn reserve(&mut self, additional: u8) { 50 | self.steps.reserve(additional as usize); 51 | } 52 | 53 | pub fn then(mut self, target: T, config: AnimationConfig) -> Self { 54 | let predicted_next = self 55 | .steps 56 | .last() 57 | .map(|last_step| last_step.target.interpolate(&target, 0.5)); 58 | 59 | self.steps.push(AnimationStep { 60 | target, 61 | config: Arc::new(config), 62 | predicted_next, 63 | }); 64 | self 65 | } 66 | 67 | pub fn on_complete(mut self, f: F) -> Self { 68 | self.on_complete = Some(Box::new(f)); 69 | self 70 | } 71 | } 72 | 73 | impl Default for AnimationSequence { 74 | fn default() -> Self { 75 | Self { 76 | steps: AnimationSteps::new(), 77 | current_step: 0, 78 | on_complete: None, 79 | capacity_hint: 0, 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tests/helpers.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/transitions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod page_transitions; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/transitions/page_transitions.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use dioxus::prelude::*; 4 | 5 | use crate::{ 6 | AnimationManager, 7 | prelude::{AnimationConfig, AnimationMode, Spring}, 8 | use_motion, 9 | }; 10 | 11 | use super::utils::TransitionVariant; 12 | 13 | #[derive(Clone)] 14 | pub enum AnimatedRouterContext { 15 | /// Transition from one route to another. 16 | FromTo(R, R), 17 | /// Settled in a route. 18 | In(R), 19 | } 20 | 21 | impl AnimatedRouterContext { 22 | /// Get the current destination route. 23 | pub fn target_route(&self) -> &R { 24 | match self { 25 | Self::FromTo(_, to) => to, 26 | Self::In(to) => to, 27 | } 28 | } 29 | 30 | /// Update the destination route. 31 | pub fn set_target_route(&mut self, to: R) { 32 | match self { 33 | Self::FromTo(old_from, old_to) => { 34 | *old_from = old_to.clone(); 35 | *old_to = to 36 | } 37 | Self::In(old_to) => *self = Self::FromTo(old_to.clone(), to), 38 | } 39 | } 40 | 41 | /// After the transition animation has finished, make the outlet only render the destination route. 42 | pub fn settle(&mut self) { 43 | if let Self::FromTo(_, to) = self { 44 | *self = Self::In(to.clone()) 45 | } 46 | } 47 | } 48 | 49 | #[component] 50 | /// Renders an outlet that supports animated transitions between routes. 51 | /// 52 | /// This function sets up a routing context and monitors changes in the current route to 53 | /// determine when an animated transition should occur. When a transition is detected and 54 | /// the layout depth or route conditions are met, it renders a transition component; otherwise, 55 | /// it renders a standard outlet. 56 | /// ``` 57 | pub fn AnimatedOutlet() -> Element { 58 | let route = use_route::(); 59 | // Create router context only if we're the root AnimatedOutlet 60 | let mut prev_route = use_signal(|| AnimatedRouterContext::In(route.clone())); 61 | use_context_provider(move || prev_route); 62 | 63 | use_effect(move || { 64 | if prev_route.peek().target_route() != &use_route::() { 65 | prev_route 66 | .write() 67 | .set_target_route(use_route::().clone()); 68 | } 69 | }); 70 | 71 | let outlet: OutletContext = use_outlet_context(); 72 | 73 | let from_route: Option<(R, R)> = match prev_route() { 74 | AnimatedRouterContext::FromTo(from, to) => Some((from, to)), 75 | _ => None, 76 | }; 77 | 78 | if let Some((from, to)) = from_route { 79 | // Get the layout depth of both the previous and current routes 80 | let from_depth = from.get_layout_depth(); 81 | let to_depth = to.get_layout_depth(); 82 | 83 | // Get the current level of nesting in the outlet 84 | let current_level = outlet.level(); 85 | 86 | // Determine if the transition involves the root route (depth 1) 87 | let involves_root = from_depth == 1 || to_depth == 1; 88 | 89 | // Check if the depth hasn't changed and the outlet level matches 90 | let is_same_depth_and_matching_level = from_depth == to_depth && current_level == to_depth; 91 | 92 | // If we're transitioning from/to root, or the outlet is at the same depth, 93 | // render the animated transition between routes 94 | if involves_root || is_same_depth_and_matching_level { 95 | return rsx! { 96 | FromRouteToCurrent:: { 97 | route_type: PhantomData, 98 | from: from.clone(), 99 | to: to.clone(), 100 | } 101 | }; 102 | } else { 103 | return rsx! { 104 | Outlet:: {} 105 | }; 106 | } 107 | } else { 108 | return rsx! { 109 | Outlet:: {} 110 | }; 111 | } 112 | } 113 | 114 | pub trait AnimatableRoute: Routable + Clone + PartialEq { 115 | fn get_transition(&self) -> TransitionVariant; 116 | fn get_component(&self) -> Element; 117 | fn get_layout_depth(&self) -> usize; 118 | } 119 | 120 | /// Shortcut to get access to the [AnimatedRouterContext]. 121 | pub fn use_animated_router() -> Signal> { 122 | use_context() 123 | } 124 | 125 | #[component] 126 | fn FromRouteToCurrent(route_type: PhantomData, from: R, to: R) -> Element { 127 | let mut animated_router = use_animated_router::(); 128 | let config = to.get_transition().get_config(); 129 | let mut from_transform = use_motion(config.exit_start); 130 | let mut to_transform = use_motion(config.enter_start); 131 | let mut from_opacity = use_motion(1.0f32); 132 | let mut to_opacity = use_motion(0.0f32); 133 | 134 | use_effect(move || { 135 | let spring = Spring { 136 | stiffness: 160.0, // Reduced from 180.0 for less aggressive movement 137 | damping: 25.0, // Increased from 12.0 for faster settling 138 | mass: 1.5, // Slightly increased for more "weight" 139 | velocity: 10.0, // Keep at 0 for predictable start 140 | }; 141 | 142 | // Animate FROM route 143 | from_transform.animate_to( 144 | config.exit_end, 145 | AnimationConfig::new(AnimationMode::Spring(spring)), 146 | ); 147 | 148 | // Animate TO route 149 | to_transform.animate_to( 150 | config.enter_end, 151 | AnimationConfig::new(AnimationMode::Spring(spring)), 152 | ); 153 | 154 | // Fade out old route 155 | from_opacity.animate_to(0.0, AnimationConfig::new(AnimationMode::Spring(spring))); 156 | to_opacity.animate_to(1.0, AnimationConfig::new(AnimationMode::Spring(spring))); 157 | }); 158 | 159 | use_effect(move || { 160 | if !from_transform.is_running() 161 | && !to_transform.is_running() 162 | && !from_opacity.is_running() 163 | && !to_opacity.is_running() 164 | { 165 | animated_router.write().settle(); 166 | } 167 | }); 168 | 169 | rsx! { 170 | div { 171 | class: "route-container", 172 | style: "position: relative; overflow-visible;", 173 | div { 174 | class: "route-content from", 175 | style: " 176 | transform: translate3d({from_transform.get_value().x}%, {from_transform.get_value().y}%, 0) 177 | scale({from_transform.get_value().scale}); 178 | opacity: {from_opacity.get_value()}; 179 | will-change: transform, opacity; 180 | backface-visibility: hidden; 181 | -webkit-backface-visibility: hidden; 182 | ", 183 | {from.render(from.get_layout_depth() + 1)} 184 | } 185 | div { 186 | class: "route-content to", 187 | style: " 188 | transform: translate3d({to_transform.get_value().x}%, {to_transform.get_value().y}%, 0) 189 | scale({to_transform.get_value().scale}); 190 | opacity: {to_opacity.get_value()}; 191 | will-change: transform, opacity; 192 | backface-visibility: hidden; 193 | -webkit-backface-visibility: hidden; 194 | ", 195 | Outlet:: {} 196 | } 197 | } 198 | } 199 | } 200 | --------------------------------------------------------------------------------