ScrollAnimationKinetic.cpp   [plain text]


/*
 * Copyright (C) 2016 Igalia S.L.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "ScrollAnimationKinetic.h"

#include "PlatformWheelEvent.h"

#if USE(GLIB_EVENT_LOOP)
#include <wtf/glib/RunLoopSourcePriority.h>
#endif

/*
 * PerAxisData is a port of GtkKineticScrolling as of GTK+ 3.20,
 * mimicking its API and its behavior.
 *
 * All our curves are second degree linear differential equations, and
 * so they can always be written as linear combinations of 2 base
 * solutions. coef1 and coef2 are the coefficients to these two base
 * solutions, and are computed from the initial position and velocity.
 *
 * In the case of simple deceleration, the differential equation is
 *
 *   y'' = -my'
 *
 * With m the resistence factor. For this we use the following 2
 * base solutions:
 *
 *   f1(x) = 1
 *   f2(x) = exp(-mx)
 *
 * In the case of overshoot, the differential equation is
 *
 *   y'' = -my' - ky
 *
 * With m the resistance, and k the spring stiffness constant. We let
 * k = m^2 / 4, so that the system is critically damped (ie, returns to its
 * equilibrium position as quickly as possible, without oscillating), and offset
 * the whole thing, such that the equilibrium position is at 0. This gives the
 * base solutions
 *
 *   f1(x) = exp(-mx / 2)
 *   f2(x) = t exp(-mx / 2)
 */

static constexpr double decelFriction = 4;
static constexpr double frameRate = 60;
static constexpr double velocityAccumulationFloor = 0.33;
static constexpr double velocityAccumulationCeil = 1.0;
static constexpr double velocityAccumulationMax = 6.0;
static constexpr Seconds tickTime = 1_s / frameRate;
static constexpr Seconds minimumTimerInterval { 1_ms };
static constexpr Seconds scrollCaptureThreshold { 150_ms };

namespace WebCore {

ScrollAnimationKinetic::PerAxisData::PerAxisData(double lower, double upper, double initialPosition, double initialVelocity)
    : m_lower(lower)
    , m_upper(upper)
    , m_coef1(initialVelocity / decelFriction + initialPosition)
    , m_coef2(-initialVelocity / decelFriction)
    , m_position(clampTo(initialPosition, lower, upper))
    , m_velocity(initialPosition < lower || initialPosition > upper ? 0 : initialVelocity)
{
}

bool ScrollAnimationKinetic::PerAxisData::animateScroll(Seconds timeDelta)
{
    auto lastPosition = m_position;
    auto lastTime = m_elapsedTime;
    m_elapsedTime += timeDelta;

    double exponentialPart = exp(-decelFriction * m_elapsedTime.value());
    m_position = m_coef1 + m_coef2 * exponentialPart;
    m_velocity = -decelFriction * m_coef2 * exponentialPart;

    if (m_position < m_lower) {
        m_velocity = m_lower - m_position;
        m_position = m_lower;
    } else if (m_position > m_upper) {
        m_velocity = m_upper - m_position;
        m_position = m_upper;
    }

    if (fabs(m_velocity) < 1 || (lastTime && fabs(m_position - lastPosition) < 1)) {
        m_position = round(m_position);
        m_velocity = 0;
    }

    return m_velocity;
}

ScrollAnimationKinetic::ScrollAnimationKinetic(ScrollExtentsCallback&& scrollExtentsFunction, NotifyPositionChangedCallback&& notifyPositionChangedFunction)
    : m_scrollExtentsFunction(WTFMove(scrollExtentsFunction))
    , m_notifyPositionChangedFunction(WTFMove(notifyPositionChangedFunction))
    , m_animationTimer(RunLoop::current(), this, &ScrollAnimationKinetic::animationTimerFired)
{
#if USE(GLIB_EVENT_LOOP)
    m_animationTimer.setPriority(WTF::RunLoopSourcePriority::DisplayRefreshMonitorTimer);
#endif
}

ScrollAnimationKinetic::~ScrollAnimationKinetic() = default;

void ScrollAnimationKinetic::stop()
{
    m_animationTimer.stop();
}

bool ScrollAnimationKinetic::isActive() const
{
    return m_animationTimer.isActive();
}

void ScrollAnimationKinetic::appendToScrollHistory(const PlatformWheelEvent& event)
{
    m_scrollHistory.removeAllMatching([&event] (PlatformWheelEvent& otherEvent) -> bool {
        return (event.timestamp() - otherEvent.timestamp()) > scrollCaptureThreshold;
    });

    m_scrollHistory.append(event);
}

void ScrollAnimationKinetic::clearScrollHistory()
{
    m_scrollHistory.clear();
}

FloatPoint ScrollAnimationKinetic::computeVelocity()
{
    if (m_scrollHistory.isEmpty())
        return { };

    auto first = m_scrollHistory[0].timestamp();
    auto last = m_scrollHistory.rbegin()->timestamp();

    if (last == first)
        return { };

    FloatPoint accumDelta;
    for (const auto& scrollEvent : m_scrollHistory)
        accumDelta += FloatPoint(scrollEvent.deltaX(), scrollEvent.deltaY());

    m_scrollHistory.clear();

    return FloatPoint(accumDelta.x() * -1 / (last - first).value(), accumDelta.y() * -1 / (last - first).value());
}

void ScrollAnimationKinetic::start(const FloatPoint& initialPosition, const FloatPoint& velocity, bool mayHScroll, bool mayVScroll)
{
    stop();

    if (!velocity.x() && !velocity.y()) {
        m_position = initialPosition;
        m_horizontalData = WTF::nullopt;
        m_verticalData = WTF::nullopt;
        return;
    }

    auto delta = deltaToNextFrame();
    auto extents = m_scrollExtentsFunction();

    auto accumulateVelocity = [&](double velocity, Optional<PerAxisData> axisData) -> double {
        if (axisData && axisData.value().animateScroll(delta)) {
            double lastVelocity = axisData.value().velocity();
            if ((std::signbit(lastVelocity) == std::signbit(velocity))
                && (std::abs(velocity) >= std::abs(lastVelocity * velocityAccumulationFloor))) {
                double minVelocity = lastVelocity * velocityAccumulationFloor;
                double maxVelocity = lastVelocity * velocityAccumulationCeil;
                double accumulationMultiplier = (velocity - minVelocity) / (maxVelocity - minVelocity);
                velocity += lastVelocity * std::min(accumulationMultiplier, velocityAccumulationMax);
            }
        }

        return velocity;
    };

    if (mayHScroll) {
        m_horizontalData = PerAxisData(extents.minimumScrollPosition.x(),
            extents.maximumScrollPosition.x(),
            initialPosition.x(), accumulateVelocity(velocity.x(), m_horizontalData));
    } else
        m_horizontalData = WTF::nullopt;

    if (mayVScroll) {
        m_verticalData = PerAxisData(extents.minimumScrollPosition.y(),
            extents.maximumScrollPosition.y(),
            initialPosition.y(), accumulateVelocity(velocity.y(), m_verticalData));
    } else
        m_verticalData = WTF::nullopt;

    m_position = initialPosition;
    m_startTime = MonotonicTime::now() - tickTime / 2.;
    animationTimerFired();
}

void ScrollAnimationKinetic::animationTimerFired()
{
    auto delta = deltaToNextFrame();

    if (m_horizontalData && !m_horizontalData.value().animateScroll(delta))
        m_horizontalData = WTF::nullopt;

    if (m_verticalData && !m_verticalData.value().animateScroll(delta))
        m_verticalData = WTF::nullopt;

    // If one of the axes didn't finish its animation we must continue it.
    if (m_horizontalData || m_verticalData)
        m_animationTimer.startOneShot(std::max(minimumTimerInterval, delta));

    double x = m_horizontalData ? m_horizontalData.value().position() : m_position.x();
    double y = m_verticalData ? m_verticalData.value().position() : m_position.y();
    m_position = FloatPoint(x, y);
    m_notifyPositionChangedFunction(FloatPoint(m_position));
}

Seconds ScrollAnimationKinetic::deltaToNextFrame()
{
    MonotonicTime currentTime = MonotonicTime::now();
    return 1_s * ceil((currentTime - m_startTime).value() * frameRate) / frameRate - (currentTime - m_startTime);
}

} // namespace WebCore