Scroll snapping on Mac should use AppKit animations
[WebKit-https.git] / Source / WebCore / page / scrolling / ScrollingMomentumCalculator.cpp
1 /*
2  * Copyright (C) 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "ScrollingMomentumCalculator.h"
28
29 #include "FloatPoint.h"
30 #include "FloatSize.h"
31 #include <wtf/CurrentTime.h>
32
33 namespace WebCore {
34
35 static const double scrollSnapAnimationDuration = 1;
36
37 ScrollingMomentumCalculator::ScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
38     : m_initialDelta(initialDelta)
39     , m_initialVelocity(initialVelocity)
40     , m_initialScrollOffset(initialOffset.x(), initialOffset.y())
41     , m_targetScrollOffset(targetOffset.x(), targetOffset.y())
42     , m_viewportSize(viewportSize)
43     , m_contentSize(contentSize)
44 {
45 }
46
47 #if !HAVE(NSSCROLLING_FILTERS)
48
49 std::unique_ptr<ScrollingMomentumCalculator> ScrollingMomentumCalculator::create(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
50 {
51     return std::make_unique<BasicScrollingMomentumCalculator>(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity);
52 }
53
54 #endif
55
56 BasicScrollingMomentumCalculator::BasicScrollingMomentumCalculator(const FloatSize& viewportSize, const FloatSize& contentSize, const FloatPoint& initialOffset, const FloatPoint& targetOffset, const FloatSize& initialDelta, const FloatPoint& initialVelocity)
57     : ScrollingMomentumCalculator(viewportSize, contentSize, initialOffset, targetOffset, initialDelta, initialVelocity)
58 {
59 }
60
61 FloatSize BasicScrollingMomentumCalculator::linearlyInterpolatedOffsetAtProgress(float progress) const
62 {
63     return m_initialScrollOffset + progress * (m_targetScrollOffset - m_initialScrollOffset);
64 }
65
66 FloatSize BasicScrollingMomentumCalculator::cubicallyInterpolatedOffsetAtProgress(float progress) const
67 {
68     ASSERT(!m_forceLinearAnimationCurve);
69     FloatSize interpolatedPoint;
70     for (int i = 0; i < 4; ++i)
71         interpolatedPoint += std::pow(progress, i) * m_snapAnimationCurveCoefficients[i];
72
73     return interpolatedPoint;
74 }
75
76 FloatPoint BasicScrollingMomentumCalculator::scrollOffsetAfterElapsedTime(double seconds)
77 {
78     if (m_momentumCalculatorRequiresInitialization) {
79         initializeSnapProgressCurve();
80         initializeInterpolationCoefficientsIfNecessary();
81         m_momentumCalculatorRequiresInitialization = false;
82     }
83
84     float progress = animationProgressAfterElapsedTime(seconds);
85     auto offsetAsSize = m_forceLinearAnimationCurve ? linearlyInterpolatedOffsetAtProgress(progress) : cubicallyInterpolatedOffsetAtProgress(progress);
86     return FloatPoint(offsetAsSize.width(), offsetAsSize.height());
87 }
88
89 double BasicScrollingMomentumCalculator::animationDuration()
90 {
91     return scrollSnapAnimationDuration;
92 }
93
94 /**
95  * Computes and sets coefficients required for interpolated snapping when scrolling in 2 dimensions, given
96  * initial conditions (the initial and target vectors, along with the initial wheel delta as a vector). The
97  * path is a cubic Bezier curve of the form p(s) = INITIAL + (C_1 * s) + (C_2 * s^2) + (C_3 * s^3) where each
98  * C_i is a 2D vector and INITIAL is the vector representing the initial scroll offset. s is a real in the
99  * interval [0, 1] indicating the "progress" of the curve (i.e. how much of the curve has been traveled).
100  *
101  * The curve has 4 control points, the first and last of which are the initial and target points, respectively.
102  * The distances between adjacent control points are constrained to be the same, making the convex hull an
103  * isosceles trapezoid with 3 sides of equal length. Additionally, the vector from the first control point to
104  * the second points in the same direction as the initial scroll delta. These constraints ensure two properties:
105  *     1. The direction of the snap animation at s=0 will be equal to the direction of the initial scroll delta.
106  *     2. Points at regular intervals of s will be evenly spread out.
107  *
108  * If the initial scroll direction is orthogonal to or points in the opposite direction as the vector from the
109  * initial point to the target point, initialization returns early and sets the curve to animate directly to the
110  * snap point without cubic interpolation.
111  *
112  * FIXME: This should be refactored to use UnitBezier.
113  */
114 void BasicScrollingMomentumCalculator::initializeInterpolationCoefficientsIfNecessary()
115 {
116     m_forceLinearAnimationCurve = true;
117     float initialDeltaMagnitude = m_initialDelta.diagonalLength();
118     if (initialDeltaMagnitude < 1) {
119         // The initial wheel delta is so insignificant that we're better off considering this to have the same effect as finishing a scroll gesture with no momentum.
120         // Thus, cubic interpolation isn't needed here.
121         return;
122     }
123
124     FloatSize startToEndVector = m_targetScrollOffset - m_initialScrollOffset;
125     float startToEndDistance = startToEndVector.diagonalLength();
126     if (!startToEndDistance) {
127         // The start and end positions are the same, so we shouldn't try to interpolate a path.
128         return;
129     }
130
131     float cosTheta = (m_initialDelta.width() * startToEndVector.width() + m_initialDelta.height() * startToEndVector.height()) / (initialDeltaMagnitude * startToEndDistance);
132     if (cosTheta <= 0) {
133         // It's possible that the user is not scrolling towards the target snap offset (for instance, scrolling against a corner when 2D scroll snapping).
134         // In this case, just let the scroll offset animate to the target without computing a cubic curve.
135         return;
136     }
137
138     float sideLength = startToEndDistance / (2.0f * cosTheta + 1.0f);
139     FloatSize controlVector1 = m_initialScrollOffset + sideLength * m_initialDelta / initialDeltaMagnitude;
140     FloatSize controlVector2 = controlVector1 + (sideLength * startToEndVector / startToEndDistance);
141     m_snapAnimationCurveCoefficients[0] = m_initialScrollOffset;
142     m_snapAnimationCurveCoefficients[1] = 3 * (controlVector1 - m_initialScrollOffset);
143     m_snapAnimationCurveCoefficients[2] = 3 * (m_initialScrollOffset - 2 * controlVector1 + controlVector2);
144     m_snapAnimationCurveCoefficients[3] = 3 * (controlVector1 - controlVector2) - m_initialScrollOffset + m_targetScrollOffset;
145     m_forceLinearAnimationCurve = false;
146 }
147
148 static const float framesPerSecond = 60.0f;
149
150 /**
151  * Computes and sets parameters required for tracking the progress of a snap animation curve, interpolated
152  * or linear. The progress curve s(t) maps time t to progress s; both variables are in the interval [0, 1].
153  * The time input t is 0 when the current time is the start of the animation, t = 0, and 1 when the current
154  * time is at or after the end of the animation, t = m_scrollSnapAnimationDuration.
155  *
156  * In this exponential progress model, s(t) = A - A * b^(-kt), where k = 60T is the number of frames in the
157  * animation (assuming 60 FPS and an animation duration of T) and A, b are reals greater than or equal to 1.
158  * Also note that we are given the initial progress, a value indicating the portion of the curve which our
159  * initial scroll delta takes us. This is important when matching the initial speed of the animation to the
160  * user's initial momentum scrolling speed. Let this initial progress amount equal v_0. I clamp this initial
161  * progress amount to a minimum or maximum value.
162  *
163  * A is referred to as the curve magnitude, while b is referred to as the decay factor. We solve for A and b,
164  * keeping the following constraints in mind:
165  *     1. s(0) = 0
166  *     2. s(1) = 1
167  *     3. s(1/k) = v_0
168  *
169  * First, observe that s(0) = 0 holds for appropriate values of A, b. Solving for the remaining constraints
170  * yields a nonlinear system of two equations. In lieu of a purely analytical solution, an alternating
171  * optimization scheme is used to approximate A and b. This technique converges quickly (within 5 iterations
172  * or so) for appropriate values of v_0. The optimization terminates early when the decay factor changes by
173  * less than a threshold between one iteration and the next.
174  */
175 void BasicScrollingMomentumCalculator::initializeSnapProgressCurve()
176 {
177     static const int maxNumScrollSnapParameterEstimationIterations = 10;
178     static const float scrollSnapDecayFactorConvergenceThreshold = 0.001;
179     static const float initialScrollSnapCurveMagnitude = 1.1;
180     static const float minScrollSnapInitialProgress = 0.1;
181     static const float maxScrollSnapInitialProgress = 0.5;
182
183     FloatSize alignmentVector = m_initialDelta * (m_targetScrollOffset - m_initialScrollOffset);
184     float initialProgress;
185     if (alignmentVector.width() + alignmentVector.height() > 0)
186         initialProgress = clampTo(m_initialDelta.diagonalLength() / (m_targetScrollOffset - m_initialScrollOffset).diagonalLength(), minScrollSnapInitialProgress, maxScrollSnapInitialProgress);
187     else
188         initialProgress = minScrollSnapInitialProgress;
189
190     float previousDecayFactor = 1.0f;
191     m_snapAnimationCurveMagnitude = initialScrollSnapCurveMagnitude;
192     for (int i = 0; i < maxNumScrollSnapParameterEstimationIterations; ++i) {
193         m_snapAnimationDecayFactor = m_snapAnimationCurveMagnitude / (m_snapAnimationCurveMagnitude - initialProgress);
194         m_snapAnimationCurveMagnitude = 1.0f / (1.0f - std::pow(m_snapAnimationDecayFactor, -framesPerSecond * scrollSnapAnimationDuration));
195         if (std::abs(m_snapAnimationDecayFactor - previousDecayFactor) < scrollSnapDecayFactorConvergenceThreshold)
196             break;
197
198         previousDecayFactor = m_snapAnimationDecayFactor;
199     }
200 }
201
202 float BasicScrollingMomentumCalculator::animationProgressAfterElapsedTime(double seconds) const
203 {
204     float timeProgress = clampTo<float>(seconds / scrollSnapAnimationDuration, 0, 1);
205     return std::min(1.0, m_snapAnimationCurveMagnitude * (1.0 - std::pow(m_snapAnimationDecayFactor, -framesPerSecond * scrollSnapAnimationDuration * timeProgress)));
206 }
207
208 }; // namespace WebCore