Migrate from ints to unsigneds when referring to indices into strings
[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 #if ENABLE(SMOOTH_SCROLLING)
32
33 #include "FloatPoint.h"
34 #include "ScrollableArea.h"
35 #include <wtf/CurrentTime.h>
36
37 namespace WebCore {
38
39 static const double frameRate = 60;
40 static const double tickTime = 1 / frameRate;
41 static const double minimumTimerInterval = .001;
42
43 ScrollAnimationSmooth::ScrollAnimationSmooth(ScrollableArea& scrollableArea, const FloatPoint& position, std::function<void (FloatPoint&&)>&& notifyPositionChangedFunction)
44     : ScrollAnimation(scrollableArea)
45     , m_notifyPositionChangedFunction(WTFMove(notifyPositionChangedFunction))
46     , m_horizontalData(position.x(), scrollableArea.visibleWidth())
47     , m_verticalData(position.y(), scrollableArea.visibleHeight())
48 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
49     , m_animationTimer(*this, &ScrollAnimationSmooth::animationTimerFired)
50 #else
51     , m_animationActive(false)
52 #endif
53 {
54 }
55
56 bool ScrollAnimationSmooth::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier)
57 {
58     float minScrollPosition;
59     float maxScrollPosition;
60     if (orientation == HorizontalScrollbar) {
61         minScrollPosition = m_scrollableArea.minimumScrollPosition().x();
62         maxScrollPosition = m_scrollableArea.maximumScrollPosition().x();
63     } else {
64         minScrollPosition = m_scrollableArea.minimumScrollPosition().y();
65         maxScrollPosition = m_scrollableArea.maximumScrollPosition().y();
66     }
67     bool needToScroll = updatePerAxisData(orientation == HorizontalScrollbar ? m_horizontalData : m_verticalData, granularity, step * multiplier, minScrollPosition, maxScrollPosition);
68     if (needToScroll && !animationTimerActive()) {
69         m_startTime = orientation == HorizontalScrollbar ? m_horizontalData.startTime : m_verticalData.startTime;
70         animationTimerFired();
71     }
72     return needToScroll;
73 }
74
75 void ScrollAnimationSmooth::stop()
76 {
77 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
78     m_animationTimer.stop();
79 #else
80     m_animationActive = false;
81 #endif
82 }
83
84 void ScrollAnimationSmooth::updateVisibleLengths()
85 {
86     m_horizontalData.visibleLength = m_scrollableArea.visibleWidth();
87     m_verticalData.visibleLength = m_scrollableArea.visibleHeight();
88 }
89
90 void ScrollAnimationSmooth::setCurrentPosition(const FloatPoint& position)
91 {
92     stop();
93     m_horizontalData = PerAxisData(position.x(), m_horizontalData.visibleLength);
94     m_verticalData = PerAxisData(position.y(), m_verticalData.visibleLength);
95 }
96
97 #if !USE(REQUEST_ANIMATION_FRAME_TIMER)
98 void ScrollAnimationSmooth::serviceAnimation()
99 {
100     if (m_animationActive)
101         animationTimerFired();
102 }
103 #endif
104
105 ScrollAnimationSmooth::~ScrollAnimationSmooth()
106 {
107 }
108
109 static inline double curveAt(ScrollAnimationSmooth::Curve curve, double t)
110 {
111     switch (curve) {
112     case ScrollAnimationSmooth::Curve::Linear:
113         return t;
114     case ScrollAnimationSmooth::Curve::Quadratic:
115         return t * t;
116     case ScrollAnimationSmooth::Curve::Cubic:
117         return t * t * t;
118     case ScrollAnimationSmooth::Curve::Quartic:
119         return t * t * t * t;
120     case ScrollAnimationSmooth::Curve::Bounce:
121         // Time base is chosen to keep the bounce points simpler:
122         // 1 (half bounce coming in) + 1 + .5 + .25
123         static const double timeBase = 2.75;
124         static const double timeBaseSquared = timeBase * timeBase;
125         if (t < 1 / timeBase)
126             return timeBaseSquared * t * t;
127         if (t < 2 / timeBase) {
128             // Invert a [-.5,.5] quadratic parabola, center it in [1,2].
129             double t1 = t - 1.5 / timeBase;
130             const double parabolaAtEdge = 1 - .5 * .5;
131             return timeBaseSquared * t1 * t1 + parabolaAtEdge;
132         }
133         if (t < 2.5 / timeBase) {
134             // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5].
135             double t2 = t - 2.25 / timeBase;
136             const double parabolaAtEdge = 1 - .25 * .25;
137             return timeBaseSquared * t2 * t2 + parabolaAtEdge;
138         }
139         // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75].
140         const double parabolaAtEdge = 1 - .125 * .125;
141         t -= 2.625 / timeBase;
142         return timeBaseSquared * t * t + parabolaAtEdge;
143     }
144     ASSERT_NOT_REACHED();
145     return 0;
146 }
147
148 static inline double attackCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition)
149 {
150     double t = deltaTime / curveT;
151     double positionFactor = curveAt(curve, t);
152     return startPosition + positionFactor * (attackPosition - startPosition);
153 }
154
155 static inline double releaseCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition)
156 {
157     double t = deltaTime / curveT;
158     double positionFactor = 1 - curveAt(curve, 1 - t);
159     return releasePosition + (positionFactor * (desiredPosition - releasePosition));
160 }
161
162 static inline double coastCurve(ScrollAnimationSmooth::Curve curve, double factor)
163 {
164     return 1 - curveAt(curve, 1 - factor);
165 }
166
167 static inline double curveIntegralAt(ScrollAnimationSmooth::Curve curve, double t)
168 {
169     switch (curve) {
170     case ScrollAnimationSmooth::Curve::Linear:
171         return t * t / 2;
172     case ScrollAnimationSmooth::Curve::Quadratic:
173         return t * t * t / 3;
174     case ScrollAnimationSmooth::Curve::Cubic:
175         return t * t * t * t / 4;
176     case ScrollAnimationSmooth::Curve::Quartic:
177         return t * t * t * t * t / 5;
178     case ScrollAnimationSmooth::Curve::Bounce:
179         static const double timeBase = 2.75;
180         static const double timeBaseSquared = timeBase * timeBase;
181         static const double timeBaseSquaredOverThree = timeBaseSquared / 3;
182         double area;
183         double t1 = std::min(t, 1 / timeBase);
184         area = timeBaseSquaredOverThree * t1 * t1 * t1;
185         if (t < 1 / timeBase)
186             return area;
187
188         t1 = std::min(t - 1 / timeBase, 1 / timeBase);
189         // The integral of timeBaseSquared * (t1 - .5 / timeBase) * (t1 - .5 / timeBase) + parabolaAtEdge
190         static const double secondInnerOffset = timeBaseSquared * .5 / timeBase;
191         double bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - secondInnerOffset) + 1);
192         area += bounceArea;
193         if (t < 2 / timeBase)
194             return area;
195
196         t1 = std::min(t - 2 / timeBase, 0.5 / timeBase);
197         // The integral of timeBaseSquared * (t1 - .25 / timeBase) * (t1 - .25 / timeBase) + parabolaAtEdge
198         static const double thirdInnerOffset = timeBaseSquared * .25 / timeBase;
199         bounceArea =  t1 * (t1 * (timeBaseSquaredOverThree * t1 - thirdInnerOffset) + 1);
200         area += bounceArea;
201         if (t < 2.5 / timeBase)
202             return area;
203
204         t1 = t - 2.5 / timeBase;
205         // The integral of timeBaseSquared * (t1 - .125 / timeBase) * (t1 - .125 / timeBase) + parabolaAtEdge
206         static const double fourthInnerOffset = timeBaseSquared * .125 / timeBase;
207         bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - fourthInnerOffset) + 1);
208         area += bounceArea;
209         return area;
210     }
211     ASSERT_NOT_REACHED();
212     return 0;
213 }
214
215 static inline double attackArea(ScrollAnimationSmooth::Curve curve, double startT, double endT)
216 {
217     double startValue = curveIntegralAt(curve, startT);
218     double endValue = curveIntegralAt(curve, endT);
219     return endValue - startValue;
220 }
221
222 static inline double releaseArea(ScrollAnimationSmooth::Curve curve, double startT, double endT)
223 {
224     double startValue = curveIntegralAt(curve, 1 - endT);
225     double endValue = curveIntegralAt(curve, 1 - startT);
226     return endValue - startValue;
227 }
228
229 static inline void getAnimationParametersForGranularity(ScrollGranularity granularity, double& animationTime, double& repeatMinimumSustainTime, double& attackTime, double& releaseTime, ScrollAnimationSmooth::Curve& coastTimeCurve, double& maximumCoastTime)
230 {
231     switch (granularity) {
232     case ScrollByDocument:
233         animationTime = 20 * tickTime;
234         repeatMinimumSustainTime = 10 * tickTime;
235         attackTime = 10 * tickTime;
236         releaseTime = 10 * tickTime;
237         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
238         maximumCoastTime = 1;
239         break;
240     case ScrollByLine:
241         animationTime = 10 * tickTime;
242         repeatMinimumSustainTime = 7 * tickTime;
243         attackTime = 3 * tickTime;
244         releaseTime = 3 * tickTime;
245         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
246         maximumCoastTime = 1;
247         break;
248     case ScrollByPage:
249         animationTime = 15 * tickTime;
250         repeatMinimumSustainTime = 10 * tickTime;
251         attackTime = 5 * tickTime;
252         releaseTime = 5 * tickTime;
253         coastTimeCurve = ScrollAnimationSmooth::Curve::Linear;
254         maximumCoastTime = 1;
255         break;
256     case ScrollByPixel:
257         animationTime = 11 * tickTime;
258         repeatMinimumSustainTime = 2 * tickTime;
259         attackTime = 3 * tickTime;
260         releaseTime = 3 * tickTime;
261         coastTimeCurve = ScrollAnimationSmooth::Curve::Quadratic;
262         maximumCoastTime = 1.25;
263         break;
264     default:
265         ASSERT_NOT_REACHED();
266     }
267 }
268
269 bool ScrollAnimationSmooth::updatePerAxisData(PerAxisData& data, ScrollGranularity granularity, float delta, float minScrollPosition, float maxScrollPosition)
270 {
271     if (!data.startTime || !delta || (delta < 0) != (data.desiredPosition - data.currentPosition < 0)) {
272         data.desiredPosition = data.currentPosition;
273         data.startTime = 0;
274     }
275     float newPosition = data.desiredPosition + delta;
276
277     newPosition = std::max(std::min(newPosition, maxScrollPosition), minScrollPosition);
278
279     if (newPosition == data.desiredPosition)
280         return false;
281
282     double animationTime, repeatMinimumSustainTime, attackTime, releaseTime, maximumCoastTime;
283     Curve coastTimeCurve;
284     getAnimationParametersForGranularity(granularity, animationTime, repeatMinimumSustainTime, attackTime, releaseTime, coastTimeCurve, maximumCoastTime);
285
286     data.desiredPosition = newPosition;
287     if (!data.startTime)
288         data.attackTime = attackTime;
289     data.animationTime = animationTime;
290     data.releaseTime = releaseTime;
291
292     // Prioritize our way out of over constraint.
293     if (data.attackTime + data.releaseTime > data.animationTime) {
294         if (data.releaseTime > data.animationTime)
295             data.releaseTime = data.animationTime;
296         data.attackTime = data.animationTime - data.releaseTime;
297     }
298
299     if (!data.startTime) {
300         // FIXME: This should be the time from the event that got us here.
301         data.startTime = monotonicallyIncreasingTime() - tickTime / 2;
302         data.startPosition = data.currentPosition;
303         data.lastAnimationTime = data.startTime;
304     }
305     data.startVelocity = data.currentVelocity;
306
307     double remainingDelta = data.desiredPosition - data.currentPosition;
308     double attackAreaLeft = 0;
309     double deltaTime = data.lastAnimationTime - data.startTime;
310     double attackTimeLeft = std::max(0., data.attackTime - deltaTime);
311     double timeLeft = data.animationTime - deltaTime;
312     double minTimeLeft = data.releaseTime + std::min(repeatMinimumSustainTime, data.animationTime - data.releaseTime - attackTimeLeft);
313     if (timeLeft < minTimeLeft) {
314         data.animationTime = deltaTime + minTimeLeft;
315         timeLeft = minTimeLeft;
316     }
317
318     if (maximumCoastTime > (repeatMinimumSustainTime + releaseTime)) {
319         double targetMaxCoastVelocity = data.visibleLength * .25 * frameRate;
320         // This needs to be as minimal as possible while not being intrusive to page up/down.
321         double minCoastDelta = data.visibleLength;
322
323         if (fabs(remainingDelta) > minCoastDelta) {
324             double maxCoastDelta = maximumCoastTime * targetMaxCoastVelocity;
325             double coastFactor = std::min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta));
326
327             // 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.
328             double coastMinTimeLeft = std::min(maximumCoastTime, minTimeLeft + coastCurve(coastTimeCurve, coastFactor) * (maximumCoastTime - minTimeLeft));
329
330             if (double additionalTime = std::max(0., coastMinTimeLeft - minTimeLeft)) {
331                 double additionalReleaseTime = std::min(additionalTime, releaseTime / (releaseTime + repeatMinimumSustainTime) * additionalTime);
332                 data.releaseTime = releaseTime + additionalReleaseTime;
333                 data.animationTime = deltaTime + coastMinTimeLeft;
334                 timeLeft = coastMinTimeLeft;
335             }
336         }
337     }
338
339     double releaseTimeLeft = std::min(timeLeft, data.releaseTime);
340     double sustainTimeLeft = std::max(0., timeLeft - releaseTimeLeft - attackTimeLeft);
341     if (attackTimeLeft) {
342         double attackSpot = deltaTime / data.attackTime;
343         attackAreaLeft = attackArea(Curve::Cubic, attackSpot, 1) * data.attackTime;
344     }
345
346     double releaseSpot = (data.releaseTime - releaseTimeLeft) / data.releaseTime;
347     double releaseAreaLeft = releaseArea(Curve::Cubic, releaseSpot, 1) * data.releaseTime;
348
349     data.desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft + releaseAreaLeft);
350     data.releasePosition = data.desiredPosition - data.desiredVelocity * releaseAreaLeft;
351     if (attackAreaLeft)
352         data.attackPosition = data.startPosition + data.desiredVelocity * attackAreaLeft;
353     else
354         data.attackPosition = data.releasePosition - (data.animationTime - data.releaseTime - data.attackTime) * data.desiredVelocity;
355
356     if (sustainTimeLeft) {
357         double roundOff = data.releasePosition - ((attackAreaLeft ? data.attackPosition : data.currentPosition) + data.desiredVelocity * sustainTimeLeft);
358         data.desiredVelocity += roundOff / sustainTimeLeft;
359     }
360
361     return true;
362 }
363
364 bool ScrollAnimationSmooth::animateScroll(PerAxisData& data, double currentTime)
365 {
366     if (!data.startTime)
367         return false;
368
369     double lastScrollInterval = currentTime - data.lastAnimationTime;
370     if (lastScrollInterval < minimumTimerInterval)
371         return true;
372
373     data.lastAnimationTime = currentTime;
374
375     double deltaTime = currentTime - data.startTime;
376     double newPosition = data.currentPosition;
377
378     if (deltaTime > data.animationTime) {
379         data = PerAxisData(data.desiredPosition, data.visibleLength);
380         return false;
381     }
382     if (deltaTime < data.attackTime)
383         newPosition = attackCurve(Curve::Cubic, deltaTime, data.attackTime, data.startPosition, data.attackPosition);
384     else if (deltaTime < (data.animationTime - data.releaseTime))
385         newPosition = data.attackPosition + (deltaTime - data.attackTime) * data.desiredVelocity;
386     else {
387         // release is based on targeting the exact final position.
388         double releaseDeltaT = deltaTime - (data.animationTime - data.releaseTime);
389         newPosition = releaseCurve(Curve::Cubic, releaseDeltaT, data.releaseTime, data.releasePosition, data.desiredPosition);
390     }
391
392     // Normalize velocity to a per second amount. Could be used to check for jank.
393     if (lastScrollInterval > 0)
394         data.currentVelocity = (newPosition - data.currentPosition) / lastScrollInterval;
395     data.currentPosition = newPosition;
396
397     return true;
398 }
399
400 void ScrollAnimationSmooth::animationTimerFired()
401 {
402     double currentTime = monotonicallyIncreasingTime();
403     double deltaToNextFrame = ceil((currentTime - m_startTime) * frameRate) / frameRate - (currentTime - m_startTime);
404     currentTime += deltaToNextFrame;
405
406     bool continueAnimation = false;
407     if (animateScroll(m_horizontalData, currentTime))
408         continueAnimation = true;
409     if (animateScroll(m_verticalData, currentTime))
410         continueAnimation = true;
411
412     if (continueAnimation)
413 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
414         startNextTimer(std::max(minimumTimerInterval, deltaToNextFrame));
415 #else
416         startNextTimer();
417     else
418         m_animationActive = false;
419 #endif
420
421     m_notifyPositionChangedFunction(FloatPoint(m_horizontalData.currentPosition, m_verticalData.currentPosition));
422 }
423
424 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
425 void ScrollAnimationSmooth::startNextTimer(double delay)
426 {
427     m_animationTimer.startOneShot(delay);
428 }
429 #else
430 void ScrollAnimationSmooth::startNextTimer()
431 {
432     if (m_scrollableArea.scheduleAnimation())
433         m_animationActive = true;
434 }
435 #endif
436
437 bool ScrollAnimationSmooth::animationTimerActive() const
438 {
439 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
440     return m_animationTimer.isActive();
441 #else
442     return m_animationActive;
443 #endif
444 }
445
446 } // namespace WebCore
447
448 #endif // ENABLE(SMOOTH_SCROLLING)