blob: 6181486d365f08c0fbf2c83b8af0ea42408b9654 [file] [log] [blame]
/*
* Copyright (C) 2016 Igalia S.L.
* Copyright (C) 2021 Apple Inc. All rights reserved.
*
* 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"
#include "ScrollExtents.h"
#include <wtf/text/TextStream.h>
/*
* 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 velocityAccumulationFloor = 0.33;
static constexpr double velocityAccumulationCeil = 1.0;
static constexpr double velocityAccumulationMax = 6.0;
static constexpr Seconds scrollCaptureThreshold { 150_ms };
namespace WebCore {
ScrollAnimationKinetic::PerAxisData::PerAxisData(double lower, double upper, double initialOffset, double initialVelocity)
: m_lower(lower)
, m_upper(upper)
, m_coef1(initialVelocity / decelFriction + initialOffset)
, m_coef2(-initialVelocity / decelFriction)
, m_offset(clampTo(initialOffset, lower, upper))
, m_velocity(initialOffset < lower || initialOffset > upper ? 0 : initialVelocity)
{
}
bool ScrollAnimationKinetic::PerAxisData::animateScroll(Seconds elapsedTime)
{
auto lastOffset = m_offset;
auto lastTime = m_elapsedTime;
m_elapsedTime = elapsedTime;
double exponentialPart = exp(-decelFriction * m_elapsedTime.value());
m_offset = m_coef1 + m_coef2 * exponentialPart;
m_velocity = -decelFriction * m_coef2 * exponentialPart;
if (m_offset < m_lower) {
m_velocity = m_lower - m_offset;
m_offset = m_lower;
} else if (m_offset > m_upper) {
m_velocity = m_upper - m_offset;
m_offset = m_upper;
}
if (fabs(m_velocity) < 1 || (lastTime > 0_s && fabs(m_offset - lastOffset) < 1)) {
m_offset = round(m_offset);
m_velocity = 0;
}
return m_velocity;
}
ScrollAnimationKinetic::ScrollAnimationKinetic(ScrollAnimationClient& client)
: ScrollAnimation(Type::Kinetic, client)
{
}
ScrollAnimationKinetic::~ScrollAnimationKinetic() = default;
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();
}
FloatSize 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());
// FIXME: It's odd that computeVelocity() has the side effect of clearing history.
m_scrollHistory.clear();
return FloatSize(accumDelta.x() * -1 / (last - first).value(), accumDelta.y() * -1 / (last - first).value());
}
bool ScrollAnimationKinetic::startAnimatedScrollWithInitialVelocity(const FloatPoint& initialOffset, const FloatSize& velocity, const FloatSize& previousVelocity, bool mayHScroll, bool mayVScroll)
{
m_initialOffset = initialOffset;
m_initialVelocity = velocity;
stop();
if (velocity.isZero()) {
m_horizontalData = std::nullopt;
m_verticalData = std::nullopt;
return false;
}
auto extents = m_client.scrollExtentsForAnimation(*this);
auto accumulateVelocity = [&](double velocity, double previousVelocity) -> double {
if ((std::signbit(previousVelocity) == std::signbit(velocity))
&& (std::abs(velocity) >= std::abs(previousVelocity * velocityAccumulationFloor))) {
double minVelocity = previousVelocity * velocityAccumulationFloor;
double maxVelocity = previousVelocity * velocityAccumulationCeil;
double accumulationMultiplier = (velocity - minVelocity) / (maxVelocity - minVelocity);
velocity += previousVelocity * std::min(accumulationMultiplier, velocityAccumulationMax);
}
return velocity;
};
if (mayHScroll) {
m_horizontalData = PerAxisData(extents.minimumScrollOffset().x(),
extents.maximumScrollOffset().x(),
initialOffset.x(), accumulateVelocity(velocity.width(), previousVelocity.width()));
} else
m_horizontalData = std::nullopt;
if (mayVScroll) {
m_verticalData = PerAxisData(extents.minimumScrollOffset().y(),
extents.maximumScrollOffset().y(),
initialOffset.y(), accumulateVelocity(velocity.height(), previousVelocity.height()));
} else
m_verticalData = std::nullopt;
m_currentOffset = initialOffset;
didStart(MonotonicTime::now());
return true;
}
bool ScrollAnimationKinetic::retargetActiveAnimation(const FloatPoint&)
{
if (!isActive())
return false;
// FIXME: Implement retargeting.
return false;
}
void ScrollAnimationKinetic::serviceAnimation(MonotonicTime currentTime)
{
auto elapsedTime = timeSinceStart(currentTime);
if (m_horizontalData && !m_horizontalData.value().animateScroll(elapsedTime))
m_horizontalData = std::nullopt;
if (m_verticalData && !m_verticalData.value().animateScroll(elapsedTime))
m_verticalData = std::nullopt;
double x = m_horizontalData ? m_horizontalData.value().offset() : m_currentOffset.x();
double y = m_verticalData ? m_verticalData.value().offset() : m_currentOffset.y();
m_currentOffset = FloatPoint(x, y);
m_client.scrollAnimationDidUpdate(*this, m_currentOffset);
if (!m_horizontalData && !m_verticalData)
didEnd();
}
String ScrollAnimationKinetic::debugDescription() const
{
TextStream textStream;
textStream << "ScrollAnimationKinetic " << this << " active " << isActive() << " current offset " << currentOffset();
return textStream.release();
}
FloatSize ScrollAnimationKinetic::accumulateVelocityFromPreviousGesture(const MonotonicTime lastStartTime, const FloatPoint& lastInitialOffset, const FloatSize& lastInitialVelocity)
{
auto elapsedTime = MonotonicTime::now() - lastStartTime;
auto extents = m_client.scrollExtentsForAnimation(*this);
auto horizontalData = PerAxisData(extents.minimumScrollOffset().x(),
extents.maximumScrollOffset().x(),
lastInitialOffset.x(), lastInitialVelocity.width());
auto verticalData = PerAxisData(extents.minimumScrollOffset().y(),
extents.maximumScrollOffset().y(),
lastInitialOffset.y(), lastInitialVelocity.height());
horizontalData.animateScroll(elapsedTime);
verticalData.animateScroll(elapsedTime);
return FloatSize(horizontalData.velocity(), verticalData.velocity());
}
} // namespace WebCore