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