// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "imgui.h" #include #include namespace dragonx { namespace ui { namespace material { // ============================================================================ // Material Design Motion System // ============================================================================ // Based on https://m2.material.io/design/motion/speed.html // and https://m2.material.io/design/motion/customization.html // // Material motion uses specific easing curves and durations to create // natural, responsive animations that feel connected to user input. // ============================================================================ // Standard Durations (in seconds) // ============================================================================ namespace duration { // Simple transitions (toggle, fade) constexpr float Instant = 0.0f; constexpr float VeryFast = 0.05f; // 50ms constexpr float Fast = 0.1f; // 100ms - simple toggles constexpr float Short = 0.15f; // 150ms // Standard transitions constexpr float Medium = 0.2f; // 200ms - collapse, simple move constexpr float Standard = 0.25f; // 250ms - expand, standard constexpr float Long = 0.3f; // 300ms - large transforms // Complex transitions constexpr float Complex = 0.375f; // 375ms constexpr float VeryLong = 0.5f; // 500ms - elaborate sequences // Screen transitions constexpr float EnterScreen = 0.225f; // Entering screen constexpr float ExitScreen = 0.195f; // Leaving screen constexpr float ScreenChange = 0.3f; // Full screen transition } // ============================================================================ // Easing Curves // ============================================================================ /** * @brief Cubic bezier curve evaluation * * Evaluates a cubic bezier curve defined by control points (0,0), (x1,y1), (x2,y2), (1,1) * * @param t Progress 0.0-1.0 * @param x1 First control point X * @param y1 First control point Y * @param x2 Second control point X * @param y2 Second control point Y * @return Eased value */ float CubicBezier(float t, float x1, float y1, float x2, float y2); /** * @brief Standard easing - for objects moving between on-screen positions * * CSS: cubic-bezier(0.4, 0.0, 0.2, 1.0) * Starts quickly, slows down to rest */ float EaseStandard(float t); /** * @brief Deceleration easing - for objects entering the screen * * CSS: cubic-bezier(0.0, 0.0, 0.2, 1.0) * Starts at full velocity, decelerates to rest */ float EaseDecelerate(float t); /** * @brief Acceleration easing - for objects leaving the screen * * CSS: cubic-bezier(0.4, 0.0, 1.0, 1.0) * Accelerates from rest, exits at full speed */ float EaseAccelerate(float t); /** * @brief Sharp easing - for objects that may return to screen * * CSS: cubic-bezier(0.4, 0.0, 0.6, 1.0) * Quicker than standard, maintains connection */ float EaseSharp(float t); /** * @brief Linear interpolation (no easing) */ float EaseLinear(float t); /** * @brief Overshoot easing - goes past target then settles * * Good for bouncy, playful animations */ float EaseOvershoot(float t, float overshoot = 1.70158f); /** * @brief Elastic easing - springy oscillation */ float EaseElastic(float t); // ============================================================================ // Easing Function Type // ============================================================================ using EasingFunction = float(*)(float); // ============================================================================ // Animated Value // ============================================================================ /** * @brief Animated value with automatic interpolation * * Template class for smooth value transitions. */ template class AnimatedValue { public: AnimatedValue(const T& initialValue = T()) : m_current(initialValue) , m_target(initialValue) , m_start(initialValue) , m_duration(duration::Standard) , m_elapsed(0) , m_easingFunc(EaseStandard) , m_animating(false) {} /** * @brief Set target value with animation */ void animateTo(const T& target, float dur = duration::Standard, EasingFunction easing = EaseStandard) { if (target == m_target && m_animating) return; // Already animating to this target m_start = m_current; m_target = target; m_duration = dur; m_elapsed = 0; m_easingFunc = easing; m_animating = true; } /** * @brief Set value immediately (no animation) */ void set(const T& value) { m_current = value; m_target = value; m_start = value; m_animating = false; } /** * @brief Update animation (call each frame) * @param deltaTime Frame delta time in seconds */ void update(float deltaTime) { if (!m_animating) return; m_elapsed += deltaTime; if (m_elapsed >= m_duration) { m_current = m_target; m_animating = false; } else { float t = m_elapsed / m_duration; float eased = m_easingFunc(t); m_current = lerp(m_start, m_target, eased); } } /** * @brief Get current value */ const T& get() const { return m_current; } /** * @brief Get target value */ const T& getTarget() const { return m_target; } /** * @brief Check if currently animating */ bool isAnimating() const { return m_animating; } /** * @brief Get animation progress (0-1) */ float getProgress() const { if (!m_animating) return 1.0f; return m_elapsed / m_duration; } /** * @brief Implicit conversion to value type */ operator const T&() const { return m_current; } private: T m_current; T m_target; T m_start; float m_duration; float m_elapsed; EasingFunction m_easingFunc; bool m_animating; // Lerp specializations static T lerp(const T& a, const T& b, float t) { return a + (b - a) * t; } }; // Specialization for ImVec2 template<> inline ImVec2 AnimatedValue::lerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } // Specialization for ImVec4/color template<> inline ImVec4 AnimatedValue::lerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4( a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t ); } // ============================================================================ // Animation Sequencer // ============================================================================ /** * @brief Staggered animation for lists * * Creates staggered entrance animations for list items. */ class StaggerAnimation { public: StaggerAnimation(int itemCount, float staggerDelay = 0.05f, float itemDuration = duration::EnterScreen) : m_itemCount(itemCount) , m_staggerDelay(staggerDelay) , m_itemDuration(itemDuration) , m_elapsed(0) , m_running(false) {} /** * @brief Start the stagger animation */ void start() { m_elapsed = 0; m_running = true; } /** * @brief Update animation */ void update(float deltaTime) { if (!m_running) return; m_elapsed += deltaTime; // Check if all items have finished float totalDuration = m_staggerDelay * (m_itemCount - 1) + m_itemDuration; if (m_elapsed >= totalDuration) { m_running = false; } } /** * @brief Get animation progress for a specific item * * @param itemIndex Item index (0-based) * @return Progress 0.0-1.0 (clamped) */ float getItemProgress(int itemIndex) const { if (!m_running && m_elapsed > 0) return 1.0f; // Animation complete if (itemIndex < 0 || itemIndex >= m_itemCount) return 0.0f; float itemStart = m_staggerDelay * itemIndex; float itemElapsed = m_elapsed - itemStart; if (itemElapsed <= 0) return 0.0f; if (itemElapsed >= m_itemDuration) return 1.0f; return EaseDecelerate(itemElapsed / m_itemDuration); } /** * @brief Get eased alpha for item (for fade-in) */ float getItemAlpha(int itemIndex) const { return getItemProgress(itemIndex); } /** * @brief Get Y offset for item (for slide-in from bottom) */ float getItemYOffset(int itemIndex, float maxOffset = 20.0f) const { float progress = getItemProgress(itemIndex); return maxOffset * (1.0f - progress); } bool isRunning() const { return m_running; } private: int m_itemCount; float m_staggerDelay; float m_itemDuration; float m_elapsed; bool m_running; }; // ============================================================================ // Container Transform // ============================================================================ /** * @brief Container transform animation state * * For hero-style transitions where a card expands into a full dialog/page. */ struct ContainerTransform { ImRect startRect; // Starting bounds (e.g., card) ImRect endRect; // Ending bounds (e.g., dialog) float progress; // 0 = start, 1 = end bool expanding; // Direction ContainerTransform() : progress(0) , expanding(true) {} /** * @brief Get interpolated bounds at current progress */ ImRect getCurrentRect() const { float t = expanding ? progress : (1.0f - progress); float eased = EaseStandard(t); return ImRect( ImLerp(startRect.Min, endRect.Min, eased), ImLerp(startRect.Max, endRect.Max, eased) ); } /** * @brief Get corner radius (shrinks as container expands) */ float getCornerRadius(float startRadius, float endRadius) const { float t = expanding ? progress : (1.0f - progress); float eased = EaseStandard(t); return startRadius + (endRadius - startRadius) * eased; } }; // ============================================================================ // Implementation // ============================================================================ inline float CubicBezier(float t, float x1, float y1, float x2, float y2) { // Attempt to find t value for given x (Newton-Raphson approximation) // This is needed because CSS bezier curves are defined in terms of x // For simplicity, we'll use a direct parametric approach // which is accurate enough for UI animations float cx = 3.0f * x1; float bx = 3.0f * (x2 - x1) - cx; float ax = 1.0f - cx - bx; float cy = 3.0f * y1; float by = 3.0f * (y2 - y1) - cy; float ay = 1.0f - cy - by; // Sample y at parameter t // Note: This assumes t directly maps to time, which is an approximation // For more accuracy, we'd need to solve for the bezier parameter given x=t float t2 = t * t; float t3 = t2 * t; return ay * t3 + by * t2 + cy * t; } inline float EaseStandard(float t) { // cubic-bezier(0.4, 0.0, 0.2, 1.0) return CubicBezier(t, 0.4f, 0.0f, 0.2f, 1.0f); } inline float EaseDecelerate(float t) { // cubic-bezier(0.0, 0.0, 0.2, 1.0) return CubicBezier(t, 0.0f, 0.0f, 0.2f, 1.0f); } inline float EaseAccelerate(float t) { // cubic-bezier(0.4, 0.0, 1.0, 1.0) return CubicBezier(t, 0.4f, 0.0f, 1.0f, 1.0f); } inline float EaseSharp(float t) { // cubic-bezier(0.4, 0.0, 0.6, 1.0) return CubicBezier(t, 0.4f, 0.0f, 0.6f, 1.0f); } inline float EaseLinear(float t) { return t; } inline float EaseOvershoot(float t, float overshoot) { // Back ease out t = t - 1.0f; return t * t * ((overshoot + 1.0f) * t + overshoot) + 1.0f; } inline float EaseElastic(float t) { if (t == 0.0f || t == 1.0f) return t; float p = 0.3f; float s = p / 4.0f; return std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * IM_PI) / p) + 1.0f; } } // namespace material } // namespace ui } // namespace dragonx