diff --git a/src/effects/slide/CMakeLists.txt b/src/effects/slide/CMakeLists.txt index 8a3656e822..57e77b657e 100644 --- a/src/effects/slide/CMakeLists.txt +++ b/src/effects/slide/CMakeLists.txt @@ -4,6 +4,7 @@ set(slide_SOURCES main.cpp slide.cpp + springmotion.cpp ) kconfig_add_kcfg_files(slide_SOURCES diff --git a/src/effects/slide/slide.cpp b/src/effects/slide/slide.cpp index c797ff5ea8..07448081f2 100644 --- a/src/effects/slide/slide.cpp +++ b/src/effects/slide/slide.cpp @@ -25,8 +25,6 @@ SlideEffect::SlideEffect() initConfig(); reconfigure(ReconfigureAll); - m_timeLine.setEasingCurve(QEasingCurve::OutCubic); - connect(effects, QOverload::of(&EffectsHandler::desktopChanged), this, &SlideEffect::desktopChanged); connect(effects, QOverload::of(&EffectsHandler::desktopChanging), @@ -61,8 +59,11 @@ void SlideEffect::reconfigure(ReconfigureFlags) { SlideConfig::self()->read(); - m_fullAnimationDuration = animationTime(500); - m_timeLine.setDuration(std::chrono::milliseconds(m_fullAnimationDuration)); + const qreal springConstant = 200.0 / effects->animationTimeFactor(); + const qreal dampingRatio = 1.1; + + m_motionX = SpringMotion(springConstant, dampingRatio); + m_motionY = SpringMotion(springConstant, dampingRatio); m_hGap = SlideConfig::horizontalGap(); m_vGap = SlideConfig::verticalGap(); @@ -96,10 +97,13 @@ void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::millisec timeDelta = presentTime - m_lastPresentTime; } m_lastPresentTime = presentTime; - m_timeLine.update(timeDelta); if (m_state == State::ActiveAnimation) { - m_currentPosition = m_startPos + (m_endPos - m_startPos) * m_timeLine.value(); + m_motionX.advance(timeDelta); + m_motionY.advance(timeDelta); + const QSize virtualSpaceSize = effects->virtualScreenSize(); + m_currentPosition.setX(m_motionX.position() / virtualSpaceSize.width()); + m_currentPosition.setY(m_motionY.position() / virtualSpaceSize.height()); } const int w = effects->desktopGridWidth(); @@ -306,7 +310,7 @@ void SlideEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowP void SlideEffect::postPaintScreen() { - if (m_state == State::ActiveAnimation && m_timeLine.done()) { + if (m_state == State::ActiveAnimation && !m_motionX.isMoving() && !m_motionY.isMoving()) { finishedSwitching(); } @@ -351,7 +355,6 @@ void SlideEffect::startAnimation(int old, int current, EffectWindow *movingWindo m_state = State::ActiveAnimation; m_movingWindow = movingWindow; - m_timeLine.reset(); m_startPos = m_currentPosition; m_endPos = effects->desktopGridCoords(current); @@ -359,19 +362,11 @@ void SlideEffect::startAnimation(int old, int current, EffectWindow *movingWindo optimizePath(); } - // Find an apropriate duration - QPointF distance = m_startPos - m_endPos; - distance.setX(std::abs(distance.x())); - distance.setY(std::abs(distance.y())); - if (distance.x() < 1 && distance.y() < 1) { - if (distance.x() > distance.y()) { - m_timeLine.setDuration(std::chrono::milliseconds(std::max(1, (int)(m_fullAnimationDuration * distance.x())))); - } else { - m_timeLine.setDuration(std::chrono::milliseconds(std::max(1, (int)(m_fullAnimationDuration * distance.y())))); - } - } else { - m_timeLine.setDuration(std::chrono::milliseconds(m_fullAnimationDuration)); - } + const QSize virtualSpaceSize = effects->virtualScreenSize(); + m_motionX.setAnchor(m_endPos.x() * virtualSpaceSize.width()); + m_motionX.setPosition(m_startPos.x() * virtualSpaceSize.width()); + m_motionY.setAnchor(m_endPos.y() * virtualSpaceSize.height()); + m_motionY.setPosition(m_startPos.y() * virtualSpaceSize.height()); effects->setActiveFullScreenEffect(this); effects->addRepaintFull(); diff --git a/src/effects/slide/slide.h b/src/effects/slide/slide.h index 71831d2a96..ea81fefb75 100644 --- a/src/effects/slide/slide.h +++ b/src/effects/slide/slide.h @@ -15,6 +15,8 @@ // kwineffects #include +#include "springmotion.h" + namespace KWin { @@ -45,7 +47,6 @@ namespace KWin class SlideEffect : public Effect { Q_OBJECT - Q_PROPERTY(int duration READ duration) Q_PROPERTY(int horizontalGap READ horizontalGap) Q_PROPERTY(int verticalGap READ verticalGap) Q_PROPERTY(bool slideDocks READ slideDocks) @@ -69,7 +70,6 @@ public: static bool supported(); - int duration() const; int horizontalGap() const; int verticalGap() const; bool slideDocks() const; @@ -101,7 +101,6 @@ private: int m_vGap; bool m_slideDocks; bool m_slideBackground; - int m_fullAnimationDuration; // Miliseconds for 1 complete desktop switch enum class State { Inactive, @@ -110,7 +109,8 @@ private: }; State m_state = State::Inactive; - TimeLine m_timeLine; + SpringMotion m_motionX; + SpringMotion m_motionY; // When the desktop isn't desktopChanging(), these two variables are used to control the animation path. // They use desktops as a unit. @@ -136,11 +136,6 @@ private: EffectWindowList m_elevatedWindows; }; -inline int SlideEffect::duration() const -{ - return m_fullAnimationDuration; -} - inline int SlideEffect::horizontalGap() const { return m_hGap; diff --git a/src/effects/slide/slide.kcfg b/src/effects/slide/slide.kcfg index 183c7930fb..42de60a304 100644 --- a/src/effects/slide/slide.kcfg +++ b/src/effects/slide/slide.kcfg @@ -6,9 +6,6 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > - - 0 - 45 diff --git a/src/effects/slide/slide_config.ui b/src/effects/slide/slide_config.ui index 4c9cb31ed6..1fc7b63103 100644 --- a/src/effects/slide/slide_config.ui +++ b/src/effects/slide/slide_config.ui @@ -11,39 +11,6 @@ - - - - - - Duration: - - - - - - - - 0 - 0 - - - - Default - - - milliseconds - - - 9999 - - - 10 - - - - - diff --git a/src/effects/slide/springmotion.cpp b/src/effects/slide/springmotion.cpp new file mode 100644 index 0000000000..5007d29ff2 --- /dev/null +++ b/src/effects/slide/springmotion.cpp @@ -0,0 +1,154 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "springmotion.h" + +#include + +namespace KWin +{ + +static qreal lerp(qreal a, qreal b, qreal t) +{ + return a * (1 - t) + b * t; +} + +SpringMotion::SpringMotion() + : SpringMotion(200.0, 1.1) +{ +} + +SpringMotion::SpringMotion(qreal springConstant, qreal dampingRatio) + : m_prev({0, 0}) + , m_next({0, 0}) + , m_t(1.0) + , m_timestep(1.0 / 100.0) + , m_anchor(0) + , m_springConstant(springConstant) + , m_dampingRatio(dampingRatio) + , m_dampingCoefficient(2 * std::sqrt(m_springConstant) * m_dampingRatio) + , m_epsilon(1.0) +{ +} + +bool SpringMotion::isMoving() const +{ + return std::fabs(position() - anchor()) > m_epsilon || std::fabs(velocity()) > m_epsilon; +} + +qreal SpringMotion::springConstant() const +{ + return m_springConstant; +} + +qreal SpringMotion::dampingRatio() const +{ + return m_dampingRatio; +} + +qreal SpringMotion::velocity() const +{ + return lerp(m_prev.velocity, m_next.velocity, m_t); +} + +void SpringMotion::setVelocity(qreal velocity) +{ + m_next = State{ + .position = position(), + .velocity = velocity, + }; + m_t = 1.0; +} + +qreal SpringMotion::position() const +{ + return lerp(m_prev.position, m_next.position, m_t); +} + +void SpringMotion::setPosition(qreal position) +{ + m_next = State{ + .position = position, + .velocity = velocity(), + }; + m_t = 1.0; +} + +qreal SpringMotion::epsilon() const +{ + return m_epsilon; +} + +void SpringMotion::setEpsilon(qreal epsilon) +{ + m_epsilon = epsilon; +} + +qreal SpringMotion::anchor() const +{ + return m_anchor; +} + +void SpringMotion::setAnchor(qreal anchor) +{ + m_anchor = anchor; +} + +SpringMotion::Slope SpringMotion::evaluate(const State &state, qreal dt, const Slope &slope) +{ + const State next{ + .position = state.position + slope.dp * dt, + .velocity = state.velocity + slope.dv * dt, + }; + + // The math here follows from the mass-spring-damper model equation. + const qreal springForce = (m_anchor - next.position) * m_springConstant; + const qreal dampingForce = -next.velocity * m_dampingCoefficient; + const qreal acceleration = springForce + dampingForce; + + return Slope{ + .dp = state.velocity, + .dv = acceleration, + }; +} + +SpringMotion::State SpringMotion::integrate(const State &state, qreal dt) +{ + // Use Runge-Kutta method (RK4) to integrate the mass-spring-damper equation. + const Slope initial{ + .dp = 0, + .dv = 0, + }; + const Slope k1 = evaluate(state, 0.0, initial); + const Slope k2 = evaluate(state, 0.5 * dt, k1); + const Slope k3 = evaluate(state, 0.5 * dt, k2); + const Slope k4 = evaluate(state, dt, k3); + + const qreal dpdt = 1.0 / 6.0 * (k1.dp + 2 * k2.dp + 2 * k3.dp + k4.dp); + const qreal dvdt = 1.0 / 6.0 * (k1.dv + 2 * k2.dv + 2 * k3.dv + k4.dv); + + return State{ + .position = state.position + dpdt * dt, + .velocity = state.velocity + dvdt * dt, + }; +} + +void SpringMotion::advance(std::chrono::milliseconds delta) +{ + if (!isMoving()) { + return; + } + + // If the delta interval is not multiple of m_timestep precisely, the previous and + // the next samples will be linearly interpolated to get current position and velocity. + const qreal steps = (delta.count() / 1000.0) / m_timestep; + for (m_t += steps; m_t > 1.0; m_t -= 1.0) { + m_prev = m_next; + m_next = integrate(m_next, m_timestep); + } +} + +} // namespace KWin diff --git a/src/effects/slide/springmotion.h b/src/effects/slide/springmotion.h new file mode 100644 index 0000000000..cd580a6fde --- /dev/null +++ b/src/effects/slide/springmotion.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ + +/** + * The SpringMotion class simulates the motion of a spring along one dimension using the + * mass-spring-damper model. The sping constant parameter controls the acceleration of the + * spring. The damping ratio controls the oscillation of the spring. + */ +class SpringMotion +{ +public: + SpringMotion(); + SpringMotion(qreal springConstant, qreal dampingRatio); + + /** + * Advance the simulation by the given @a delta milliseconds. + */ + void advance(std::chrono::milliseconds delta); + bool isMoving() const; + + /** + * Returns the current velocity. + */ + qreal velocity() const; + void setVelocity(qreal velocity); + + /** + * Returns the current position. + */ + qreal position() const; + void setPosition(qreal position); + + /** + * Returns the anchor position. It's the position that the spring is pulled towards. + */ + qreal anchor() const; + void setAnchor(qreal anchor); + + /** + * Returns the spring constant. It controls the acceleration of the spring. + */ + qreal springConstant() const; + + /** + * Returns the damping ratio. It controls the oscillation of the spring. Potential values: + * + * - 0 or undamped: the spring will oscillate indefinitely + * - less than 1 or underdamped: the mass tends to overshoot its starting position, but with + * every oscillation some energy is dissipated and the oscillation dies away + * - 1 or critically damped: the mass will fail to overshoot and make a single oscillation + * - greater than 1 or overdamped: the mass slowly returns to the anchor position without + * overshooting + */ + qreal dampingRatio() const; + + /** + * If the distance of the mass between two consecutive simulations is smaller than the epsilon + * value, consider that the mass has stopped moving. + */ + qreal epsilon() const; + void setEpsilon(qreal epsilon); + +private: + struct State + { + qreal position; + qreal velocity; + }; + + struct Slope + { + qreal dp; + qreal dv; + }; + + State integrate(const State &state, qreal dt); + Slope evaluate(const State &state, qreal dt, const Slope &slope); + + State m_prev; + State m_next; + qreal m_t; + qreal m_timestep; + + qreal m_anchor; + qreal m_springConstant; + qreal m_dampingRatio; + qreal m_dampingCoefficient; + qreal m_epsilon; +}; + +} // namespace KWin