File indexing completed on 2024-11-10 04:57:09

0001 /*
0002     SPDX-FileCopyrightText: 2022 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "springmotion.h"
0008 
0009 #include <cmath>
0010 
0011 namespace KWin
0012 {
0013 
0014 static qreal lerp(qreal a, qreal b, qreal t)
0015 {
0016     return a * (1 - t) + b * t;
0017 }
0018 
0019 SpringMotion::SpringMotion()
0020     : SpringMotion(200.0, 1.1)
0021 {
0022 }
0023 
0024 SpringMotion::SpringMotion(qreal springConstant, qreal dampingRatio)
0025     : m_prev({0, 0})
0026     , m_next({0, 0})
0027     , m_t(1.0)
0028     , m_timestep(1.0 / 100.0)
0029     , m_anchor(0)
0030     , m_springConstant(springConstant)
0031     , m_dampingRatio(dampingRatio)
0032     , m_dampingCoefficient(2 * std::sqrt(m_springConstant) * m_dampingRatio)
0033     , m_epsilon(1.0)
0034 {
0035 }
0036 
0037 bool SpringMotion::isMoving() const
0038 {
0039     return std::fabs(position() - anchor()) > m_epsilon || std::fabs(velocity()) > m_epsilon;
0040 }
0041 
0042 qreal SpringMotion::springConstant() const
0043 {
0044     return m_springConstant;
0045 }
0046 
0047 qreal SpringMotion::dampingRatio() const
0048 {
0049     return m_dampingRatio;
0050 }
0051 
0052 qreal SpringMotion::velocity() const
0053 {
0054     return lerp(m_prev.velocity, m_next.velocity, m_t);
0055 }
0056 
0057 void SpringMotion::setVelocity(qreal velocity)
0058 {
0059     m_next = State{
0060         .position = position(),
0061         .velocity = velocity,
0062     };
0063     m_t = 1.0;
0064 }
0065 
0066 qreal SpringMotion::position() const
0067 {
0068     return lerp(m_prev.position, m_next.position, m_t);
0069 }
0070 
0071 void SpringMotion::setPosition(qreal position)
0072 {
0073     m_next = State{
0074         .position = position,
0075         .velocity = velocity(),
0076     };
0077     m_t = 1.0;
0078 }
0079 
0080 qreal SpringMotion::epsilon() const
0081 {
0082     return m_epsilon;
0083 }
0084 
0085 void SpringMotion::setEpsilon(qreal epsilon)
0086 {
0087     m_epsilon = epsilon;
0088 }
0089 
0090 qreal SpringMotion::anchor() const
0091 {
0092     return m_anchor;
0093 }
0094 
0095 void SpringMotion::setAnchor(qreal anchor)
0096 {
0097     m_anchor = anchor;
0098 }
0099 
0100 SpringMotion::Slope SpringMotion::evaluate(const State &state, qreal dt, const Slope &slope)
0101 {
0102     const State next{
0103         .position = state.position + slope.dp * dt,
0104         .velocity = state.velocity + slope.dv * dt,
0105     };
0106 
0107     // The math here follows from the mass-spring-damper model equation.
0108     const qreal springForce = (m_anchor - next.position) * m_springConstant;
0109     const qreal dampingForce = -next.velocity * m_dampingCoefficient;
0110     const qreal acceleration = springForce + dampingForce;
0111 
0112     return Slope{
0113         .dp = state.velocity,
0114         .dv = acceleration,
0115     };
0116 }
0117 
0118 SpringMotion::State SpringMotion::integrate(const State &state, qreal dt)
0119 {
0120     // Use Runge-Kutta method (RK4) to integrate the mass-spring-damper equation.
0121     const Slope initial{
0122         .dp = 0,
0123         .dv = 0,
0124     };
0125     const Slope k1 = evaluate(state, 0.0, initial);
0126     const Slope k2 = evaluate(state, 0.5 * dt, k1);
0127     const Slope k3 = evaluate(state, 0.5 * dt, k2);
0128     const Slope k4 = evaluate(state, dt, k3);
0129 
0130     const qreal dpdt = 1.0 / 6.0 * (k1.dp + 2 * k2.dp + 2 * k3.dp + k4.dp);
0131     const qreal dvdt = 1.0 / 6.0 * (k1.dv + 2 * k2.dv + 2 * k3.dv + k4.dv);
0132 
0133     return State{
0134         .position = state.position + dpdt * dt,
0135         .velocity = state.velocity + dvdt * dt,
0136     };
0137 }
0138 
0139 void SpringMotion::advance(std::chrono::milliseconds delta)
0140 {
0141     if (!isMoving()) {
0142         return;
0143     }
0144 
0145     // If m_springConstant is infinite, we have an animation time factor of zero.
0146     // As such, we should advance to the target immediately.
0147     if (std::isinf(m_springConstant)) {
0148         m_next = State{
0149             .position = m_anchor,
0150             .velocity = 0.0,
0151         };
0152         return;
0153     }
0154 
0155     // If the delta interval is not multiple of m_timestep precisely, the previous and
0156     // the next samples will be linearly interpolated to get current position and velocity.
0157     const qreal steps = (delta.count() / 1000.0) / m_timestep;
0158     for (m_t += steps; m_t > 1.0; m_t -= 1.0) {
0159         m_prev = m_next;
0160         m_next = integrate(m_next, m_timestep);
0161     }
0162 }
0163 
0164 } // namespace KWin