2ca578a30f1b31a297dc981dfe52ca30672e7a7b
[WebKit-https.git] / Source / WebCore / platform / ScrollAnimationSmooth.cpp
1 /*
2  * Copyright (C) 2016 Igalia S.L.
3  * Copyright (C) 2015 Apple Inc. All rights reserved.
4  * Copyright (c) 2011, Google Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
16  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
19  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
25  * THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 #include "config.h"
29 #include "ScrollAnimationSmooth.h"
30
31 #include "FloatPoint.h"
32 #include "ScrollableArea.h"
33
34 namespace WebCore {
35
36 static const double frameRate = 60;
37 static const Seconds tickTime = 1_s / frameRate;
38 static const Seconds minimumTimerInterval { 1_ms };
39 static const double smoothFactorForProgrammaticScroll = 5;
40
41 ScrollAnimationSmooth::ScrollAnimationSmooth(ScrollableArea& scrollableArea, const FloatPoint& position, WTF::Function<void (FloatPoint&&)>&& notifyPositionChangedFunction)
42     : ScrollAnimation(scrollableArea)
43     , m_notifyPositionChangedFunction(WTFMove(notifyPositionChangedFunction))
44     , m_horizontalData(position.x(), scrollableArea.visibleWidth())
45     , m_verticalData(position.y(), scrollableArea.visibleHeight())
46     , m_animationTimer(*this, &ScrollAnimationSmooth::animationTimerFired)
47 {
48 }
49
50 bool ScrollAnimationSmooth::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier)
51 {
52     float minScrollPosition;
53     float maxScrollPosition;
54     if (orientation == HorizontalScrollbar) {
55         minScrollPosition = m_scrollableArea.minimumScrollPosition().x();
56         maxScrollPosition = m_scrollableArea.maximumScrollPosition().x();
57     } else {
58         minScrollPosition = m_scrollableArea.minimumScrollPosition().y();
59         maxScrollPosition = m_scrollableArea.maximumScrollPosition().y();
60     }
61     bool needToScroll = updatePerAxisData(orientation == HorizontalScrollbar ? m_horizontalData : m_verticalData, granularity, step * multiplier, minScrollPosition, maxScrollPosition);
62     if (needToScroll && !animationTimerActive()) {
63         m_startTime = orientation == HorizontalScrollbar ? m_horizontalData.startTime : m_verticalData.startTime;
64         animationTimerFired();
65     }
66     return needToScroll;
67 }
68
69 void ScrollAnimationSmooth::scroll(const FloatPoint& position)
70 {
71     ScrollGranularity granularity = ScrollByPage;
72     bool needToScroll = updatePerAxisData(m_horizontalData, granularity, position.x() - m_horizontalData.currentPosition, m_scrollableArea.minimumScrollPosition().x(), m_scrollableArea.maximumScrollPosition().x(), smoothFactorForProgrammaticScroll);
73     needToScroll |=
74         updatePerAxisData(m_verticalData, granularity, position.y() - m_verticalData.currentPosition, m_scrollableArea.minimumScrollPosition().y(), m_scrollableArea.maximumScrollPosition().y(), smoothFactorForProgrammaticScroll);
75     if (needToScroll && !animationTimerActive()) {
76         m_startTime = m_horizontalData.startTime;
77         animationTimerFired();
78     }
79 };
80
81 void ScrollAnimationSmooth::stop()
82 {
83     m_animationTimer.stop();
84     m_scrollableArea.setScrollBehaviorStatus(ScrollBehaviorStatus::NotInAnimation);
85 }
86
87 void ScrollAnimationSmooth::updateVisibleLengths()
88 {
89     m_horizontalData.visibleLength = m_scrollableArea.visibleWidth();
90     m_verticalData.visibleLength = m_scrollableArea.visibleHeight();
91 }
92
93 void ScrollAnimationSmooth::setCurrentPosition(const FloatPoint& position)
94 {
95     stop();
96     m_horizontalData = PerAxisData(position.x(), m_horizontalData.visibleLength);
97     m_verticalData = PerAxisData(position.y(), m_verticalData.visibleLength);
98 }
99
100 ScrollAnimationSmooth::~ScrollAnimationSmooth() = default;
101
102 static inline double curveAt(ScrollAnimationSmooth::Curve curve, double t)
103 {
104     switch (curve) {
105     case ScrollAnimationSmooth::Curve::Linear:
106         return t;
107     case ScrollAnimationSmooth::Curve::Quadratic:
108         return t * t;
109     case ScrollAnimationSmooth::Curve::Cubic:
110         return t * t * t;
111     case ScrollAnimationSmooth::Curve::Quartic:
112         return t * t * t * t;
113     case ScrollAnimationSmooth::Curve::Bounce:
114         // Time base is chosen to keep the bounce points simpler:
115         // 1 (half bounce coming in) + 1 + .5 + .25
116         static const double timeBase = 2.75;
117         static const double timeBaseSquared = timeBase * timeBase;
118         if (t < 1 / timeBase)
119             return timeBaseSquared * t * t;
120         if (t < 2 / timeBase) {
121             // Invert a [-.5,.5] quadratic parabola, center it in [1,2].
122             double t1 = t - 1.5 / timeBase;
123             const double parabolaAtEdge = 1 - .5 * .5;
124             return timeBaseSquared * t1 * t1 + parabolaAtEdge;
125         }
126         if (t < 2.5 / timeBase) {
127             // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5].
128             double t2 = t - 2.25 / timeBase;
129             const double parabolaAtEdge = 1 - .25 * .25;
130             return timeBaseSquared * t2 * t2 + parabolaAtEdge;
131         }
132         // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75].
133         const double parabolaAtEdge = 1 - .125 * .125;
134         t -= 2.625 / timeBase;
135         return timeBaseSquared * t * t + parabolaAtEdge;
136     }
137     ASSERT_NOT_REACHED();
138     return 0;
139 }
140
141 static inline double attackCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition)
142 {
143     double t = deltaTime / curveT;
144     double positionFactor = curveAt(curve, t);
145     return startPosition + positionFactor * (attackPosition - startPosition);
146 }
147
148 static inline double releaseCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition)
149 {
150     double t = deltaTime / curveT;
151     double positionFactor = 1 - curveAt(curve, 1 - t);
152     return releasePosition + (positionFactor * (desiredPosition - releasePosition));
153 }
154
155 static inline double coastCurve(ScrollAnimationSmooth::Curve curve, double factor)
156 {
157     return 1 - curveAt(curve, 1 - factor);
158 }
159
160 static inline double curveIntegralAt(ScrollAnimationSmooth::Curve curve, double t)
161 {
162     switch (curve) {
163     case ScrollAnimationSmooth::Curve::Linear:
164         return t * t / 2;
165     case ScrollAnimationSmooth::Curve::Quadratic:
166         return t * t * t / 3;
167     case ScrollAnimationSmooth::Curve::Cubic:
168         return t * t * t * t / 4;
169     case ScrollAnimationSmooth::Curve::Quartic:
170         return t * t * t * t * t / 5;
171     case ScrollAnimationSmooth::Curve::Bounce:
172         static const double timeBase = 2.75;
173         static const double timeBaseSquared = timeBase * timeBase;
174         static const double timeBaseSquaredOverThree = timeBaseSquared / 3;
175         double area;
176         double t1 = std::min(t, 1 / timeBase);
177         area = timeBaseSquaredOverThree * t1 * t1 * t1;
178         if (t < 1 / timeBase)
179             return area;
180
181         t1 = std::min(t - 1 / timeBase, 1 / timeBase);
182         // The integral of timeBaseSquared * (t1 - .5 / timeBase) * (t1 - .5 / timeBase) + parabolaAtEdge
183         static const double secondInnerOffset = timeBaseSquared * .5 / timeBase;
184         double bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - secondInnerOffset) + 1);
185         area += bounceArea;
186         if (t < 2 / timeBase)
187             return area;
188
189         t1 = std::min(t - 2 / timeBase, 0.5 / timeBase);
190         // The integral of timeBaseSquared * (t1 - .25 / timeBase) * (t1 - .25 / timeBase) + parabolaAtEdge
191         static const double thirdInnerOffset = timeBaseSquared * .25 / timeBase;
192         bounceArea =  t1 * (t1 * (timeBaseSquaredOverThree * t1 - thirdInnerOffset) + 1);
193         area += bounceArea;
194         if (t < 2.5 / timeBase)
195             return area;
196
197         t1 = t - 2.5 / timeBase;
198         // The integral of timeBaseSquared * (t1 - .125 / timeBase) * (t1 - .125 / timeBase) + parabolaAtEdge
199         static const double fourthInnerOffset = timeBaseSquared * .125 / timeBase;
200         bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - fourthInnerOffset) + 1);
201         area += bounceArea;
202         return area;
203     }
204     ASSERT_NOT_REACHED();
205     return 0;
206 }
207
208 static inline double attackArea(ScrollAnimationSmooth::Curve curve, double startT, double endT)
209 {
210     double startValue = curveIntegralAt(curve, startT);
211     double endValue = curveIntegralAt(curve, endT);
212     return endValue - startValue;
213 }
214
215 static inline double releaseArea(ScrollAnimationSmooth::Curve curve, double startT, double endT)
216 {
217     double startValue = curveIntegralAt(curve, 1 - endT);
218     double endValue = curveIntegralAt(curve, 1 - startT);
219     return endValue - startValue;
220 }
221
222 static inline void getAnimationParametersForGranularity(ScrollGranularity granularity, Seconds& animationTime, Seconds& repeatMinimumSustainTime, Seconds& attackTime, Seconds& releaseTime, ScrollAnimationSmooth::Curve& coastTimeCurve, Seconds& maximumCoastTime)
223 {
224     switch (granularity) {
225     case ScrollByDocument:
226         animationTime = tickTime * 10;
227         repeatMinimumSustainTime = tickTime * 10;
228         attackTime = tickTime * 10;
229         releaseTime = tickTime * 10;
230         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
231         maximumCoastTime = 1_s;
232         break;
233     case ScrollByLine:
234         animationTime = tickTime * 10;
235         repeatMinimumSustainTime = tickTime * 7;
236         attackTime = tickTime * 3;
237         releaseTime = tickTime * 3;
238         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
239         maximumCoastTime = 1_s;
240         break;
241     case ScrollByPage:
242         animationTime = tickTime * 15;
243         repeatMinimumSustainTime = tickTime * 10;
244         attackTime = tickTime * 5;
245         releaseTime = tickTime * 5;
246         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
247         maximumCoastTime = 1_s;
248         break;
249     case ScrollByPixel:
250         animationTime = tickTime * 11;
251         repeatMinimumSustainTime = tickTime * 2;
252         attackTime = tickTime * 3;
253         releaseTime = tickTime * 3;
254         coastTimeCurve = ScrollAnimationSmooth::Curve::Quadratic;
255         maximumCoastTime = 1250_ms;
256         break;
257     default:
258         ASSERT_NOT_REACHED();
259     }
260 }
261
262 bool ScrollAnimationSmooth::updatePerAxisData(PerAxisData& data, ScrollGranularity granularity, float delta, float minScrollPosition, float maxScrollPosition, double smoothFactor)
263 {
264     if (!data.startTime || !delta || (delta < 0) != (data.desiredPosition - data.currentPosition < 0)) {
265         data.desiredPosition = data.currentPosition;
266         data.startTime = { };
267     }
268     float newPosition = data.desiredPosition + delta;
269
270     newPosition = std::max(std::min(newPosition, maxScrollPosition), minScrollPosition);
271
272     if (newPosition == data.desiredPosition)
273         return false;
274
275     Seconds animationTime, repeatMinimumSustainTime, attackTime, releaseTime, maximumCoastTime;
276     Curve coastTimeCurve;
277     getAnimationParametersForGranularity(granularity, animationTime, repeatMinimumSustainTime, attackTime, releaseTime, coastTimeCurve, maximumCoastTime);
278
279     animationTime *= smoothFactor;
280     repeatMinimumSustainTime *= smoothFactor;
281     attackTime *= smoothFactor;
282     releaseTime *= smoothFactor;
283     maximumCoastTime *= smoothFactor;
284
285     data.desiredPosition = newPosition;
286     if (!data.startTime)
287         data.attackTime = attackTime;
288     data.animationTime = animationTime;
289     data.releaseTime = releaseTime;
290
291     // Prioritize our way out of over constraint.
292     if (data.attackTime + data.releaseTime > data.animationTime) {
293         if (data.releaseTime > data.animationTime)
294             data.releaseTime = data.animationTime;
295         data.attackTime = data.animationTime - data.releaseTime;
296     }
297
298     if (!data.startTime) {
299         // FIXME: This should be the time from the event that got us here.
300         data.startTime = MonotonicTime::now() - tickTime / 2.;
301         data.startPosition = data.currentPosition;
302         data.lastAnimationTime = data.startTime;
303     }
304     data.startVelocity = data.currentVelocity;
305
306     double remainingDelta = data.desiredPosition - data.currentPosition;
307     double attackAreaLeft = 0;
308     Seconds deltaTime = data.lastAnimationTime - data.startTime;
309     Seconds attackTimeLeft = std::max(0_s, data.attackTime - deltaTime);
310     Seconds timeLeft = data.animationTime - deltaTime;
311     Seconds minTimeLeft = data.releaseTime + std::min(repeatMinimumSustainTime, data.animationTime - data.releaseTime - attackTimeLeft);
312     if (timeLeft < minTimeLeft) {
313         data.animationTime = deltaTime + minTimeLeft;
314         timeLeft = minTimeLeft;
315     }
316
317     if (maximumCoastTime > (repeatMinimumSustainTime + releaseTime)) {
318         double targetMaxCoastVelocity = data.visibleLength * .25 * frameRate;
319         // This needs to be as minimal as possible while not being intrusive to page up/down.
320         double minCoastDelta = data.visibleLength;
321
322         if (fabs(remainingDelta) > minCoastDelta) {
323             double maxCoastDelta = maximumCoastTime.value() * targetMaxCoastVelocity;
324             double coastFactor = std::min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta));
325
326             // We could play with the curve here - linear seems a little soft. Initial testing makes me want to feed into the sustain time more aggressively.
327             Seconds coastMinTimeLeft = std::min(maximumCoastTime, minTimeLeft + (maximumCoastTime - minTimeLeft) * coastCurve(coastTimeCurve, coastFactor));
328
329             if (Seconds additionalTime = std::max(0_s, coastMinTimeLeft - minTimeLeft)) {
330                 Seconds additionalReleaseTime = std::min(additionalTime, additionalTime * (releaseTime / (releaseTime + repeatMinimumSustainTime)));
331                 data.releaseTime = releaseTime + additionalReleaseTime;
332                 data.animationTime = deltaTime + coastMinTimeLeft;
333                 timeLeft = coastMinTimeLeft;
334             }
335         }
336     }
337
338     Seconds releaseTimeLeft = std::min(timeLeft, data.releaseTime);
339     Seconds sustainTimeLeft = std::max(0_s, timeLeft - releaseTimeLeft - attackTimeLeft);
340     if (attackTimeLeft) {
341         double attackSpot = deltaTime / data.attackTime;
342         attackAreaLeft = attackArea(Curve::Cubic, attackSpot, 1) * data.attackTime.value();
343     }
344
345     double releaseSpot = (data.releaseTime - releaseTimeLeft) / data.releaseTime;
346     double releaseAreaLeft = releaseArea(Curve::Cubic, releaseSpot, 1) * data.releaseTime.value();
347
348     data.desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft.value() + releaseAreaLeft);
349     data.releasePosition = data.desiredPosition - data.desiredVelocity * releaseAreaLeft;
350     if (attackAreaLeft)
351         data.attackPosition = data.startPosition + data.desiredVelocity * attackAreaLeft;
352     else
353         data.attackPosition = data.releasePosition - (data.animationTime - data.releaseTime - data.attackTime).value() * data.desiredVelocity;
354
355     if (sustainTimeLeft) {
356         double roundOff = data.releasePosition - ((attackAreaLeft ? data.attackPosition : data.currentPosition) + data.desiredVelocity * sustainTimeLeft.value());
357         data.desiredVelocity += roundOff / sustainTimeLeft.value();
358     }
359
360     return true;
361 }
362
363 bool ScrollAnimationSmooth::animateScroll(PerAxisData& data, MonotonicTime currentTime)
364 {
365     if (!data.startTime)
366         return false;
367
368     Seconds lastScrollInterval = currentTime - data.lastAnimationTime;
369     if (lastScrollInterval < minimumTimerInterval)
370         return true;
371
372     data.lastAnimationTime = currentTime;
373
374     Seconds deltaTime = currentTime - data.startTime;
375     double newPosition = data.currentPosition;
376
377     if (deltaTime > data.animationTime) {
378         data = PerAxisData(data.desiredPosition, data.visibleLength);
379         return false;
380     }
381     if (deltaTime < data.attackTime)
382         newPosition = attackCurve(Curve::Cubic, deltaTime.value(), data.attackTime.value(), data.startPosition, data.attackPosition);
383     else if (deltaTime < (data.animationTime - data.releaseTime))
384         newPosition = data.attackPosition + (deltaTime - data.attackTime).value() * data.desiredVelocity;
385     else {
386         // release is based on targeting the exact final position.
387         Seconds releaseDeltaT = deltaTime - (data.animationTime - data.releaseTime);
388         newPosition = releaseCurve(Curve::Cubic, releaseDeltaT.value(), data.releaseTime.value(), data.releasePosition, data.desiredPosition);
389     }
390
391     // Normalize velocity to a per second amount. Could be used to check for jank.
392     if (lastScrollInterval > 0_s)
393         data.currentVelocity = (newPosition - data.currentPosition) / lastScrollInterval.value();
394     data.currentPosition = newPosition;
395
396     return true;
397 }
398
399 void ScrollAnimationSmooth::animationTimerFired()
400 {
401     MonotonicTime currentTime = MonotonicTime::now();
402     Seconds deltaToNextFrame = 1_s * ceil((currentTime - m_startTime).value() * frameRate) / frameRate - (currentTime - m_startTime);
403     currentTime += deltaToNextFrame;
404
405     bool continueAnimation = false;
406     if (animateScroll(m_horizontalData, currentTime))
407         continueAnimation = true;
408     if (animateScroll(m_verticalData, currentTime))
409         continueAnimation = true;
410
411     if (continueAnimation)
412         startNextTimer(std::max(minimumTimerInterval, deltaToNextFrame));
413     else
414         m_scrollableArea.setScrollBehaviorStatus(ScrollBehaviorStatus::NotInAnimation);
415
416     m_notifyPositionChangedFunction(FloatPoint(m_horizontalData.currentPosition, m_verticalData.currentPosition));
417 }
418
419 void ScrollAnimationSmooth::startNextTimer(Seconds delay)
420 {
421     m_animationTimer.startOneShot(delay);
422 }
423
424 bool ScrollAnimationSmooth::animationTimerActive() const
425 {
426     return m_animationTimer.isActive();
427 }
428
429 } // namespace WebCore