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 | |
36 | namespace WebCore { |
37 | |
38 | static const double frameRate = 60; |
39 | static const Seconds tickTime = 1_s / frameRate; |
40 | static const Seconds minimumTimerInterval { 1_ms }; |
41 | |
42 | ScrollAnimationSmooth::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 | |
51 | bool 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 | |
70 | void ScrollAnimationSmooth::stop() |
71 | { |
72 | m_animationTimer.stop(); |
73 | } |
74 | |
75 | void ScrollAnimationSmooth::updateVisibleLengths() |
76 | { |
77 | m_horizontalData.visibleLength = m_scrollableArea.visibleWidth(); |
78 | m_verticalData.visibleLength = m_scrollableArea.visibleHeight(); |
79 | } |
80 | |
81 | void 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 | |
88 | ScrollAnimationSmooth::~ScrollAnimationSmooth() = default; |
89 | |
90 | static 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 | |
129 | static 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 | |
136 | static 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 | |
143 | static inline double coastCurve(ScrollAnimationSmooth::Curve curve, double factor) |
144 | { |
145 | return 1 - curveAt(curve, 1 - factor); |
146 | } |
147 | |
148 | static 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 | |
196 | static 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 | |
203 | static 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 | |
210 | static 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 | |
250 | bool 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 | |
345 | bool 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 | |
381 | void 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 | |
399 | void ScrollAnimationSmooth::startNextTimer(Seconds delay) |
400 | { |
401 | m_animationTimer.startOneShot(delay); |
402 | } |
403 | |
404 | bool ScrollAnimationSmooth::animationTimerActive() const |
405 | { |
406 | return m_animationTimer.isActive(); |
407 | } |
408 | |
409 | } // namespace WebCore |
410 | |
411 | #endif // ENABLE(SMOOTH_SCROLLING) |
412 | |