1 /*
2 * Copyright (c) 2011, Google 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 are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "config.h"
32
33 #include "platform/scroll/ScrollAnimatorNone.h"
34
35 #include <algorithm>
36 #include "platform/scroll/ScrollableArea.h"
37 #include "wtf/CurrentTime.h"
38 #include "wtf/PassOwnPtr.h"
39
40 #include "platform/TraceEvent.h"
41
42 namespace blink {
43
44 const double kFrameRate = 60;
45 const double kTickTime = 1 / kFrameRate;
46 const double kMinimumTimerInterval = .001;
47
create(ScrollableArea * scrollableArea)48 PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea)
49 {
50 if (scrollableArea && scrollableArea->scrollAnimatorEnabled())
51 return adoptPtr(new ScrollAnimatorNone(scrollableArea));
52 return adoptPtr(new ScrollAnimator(scrollableArea));
53 }
54
Parameters()55 ScrollAnimatorNone::Parameters::Parameters()
56 : m_isEnabled(false)
57 {
58 }
59
Parameters(bool isEnabled,double animationTime,double repeatMinimumSustainTime,Curve attackCurve,double attackTime,Curve releaseCurve,double releaseTime,Curve coastTimeCurve,double maximumCoastTime)60 ScrollAnimatorNone::Parameters::Parameters(bool isEnabled, double animationTime, double repeatMinimumSustainTime, Curve attackCurve, double attackTime, Curve releaseCurve, double releaseTime, Curve coastTimeCurve, double maximumCoastTime)
61 : m_isEnabled(isEnabled)
62 , m_animationTime(animationTime)
63 , m_repeatMinimumSustainTime(repeatMinimumSustainTime)
64 , m_attackCurve(attackCurve)
65 , m_attackTime(attackTime)
66 , m_releaseCurve(releaseCurve)
67 , m_releaseTime(releaseTime)
68 , m_coastTimeCurve(coastTimeCurve)
69 , m_maximumCoastTime(maximumCoastTime)
70 {
71 }
72
curveAt(Curve curve,double t)73 double ScrollAnimatorNone::PerAxisData::curveAt(Curve curve, double t)
74 {
75 switch (curve) {
76 case Linear:
77 return t;
78 case Quadratic:
79 return t * t;
80 case Cubic:
81 return t * t * t;
82 case Quartic:
83 return t * t * t * t;
84 case Bounce:
85 // Time base is chosen to keep the bounce points simpler:
86 // 1 (half bounce coming in) + 1 + .5 + .25
87 const double kTimeBase = 2.75;
88 const double kTimeBaseSquared = kTimeBase * kTimeBase;
89 if (t < 1 / kTimeBase)
90 return kTimeBaseSquared * t * t;
91 if (t < 2 / kTimeBase) {
92 // Invert a [-.5,.5] quadratic parabola, center it in [1,2].
93 double t1 = t - 1.5 / kTimeBase;
94 const double kParabolaAtEdge = 1 - .5 * .5;
95 return kTimeBaseSquared * t1 * t1 + kParabolaAtEdge;
96 }
97 if (t < 2.5 / kTimeBase) {
98 // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5].
99 double t2 = t - 2.25 / kTimeBase;
100 const double kParabolaAtEdge = 1 - .25 * .25;
101 return kTimeBaseSquared * t2 * t2 + kParabolaAtEdge;
102 }
103 // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75].
104 const double kParabolaAtEdge = 1 - .125 * .125;
105 t -= 2.625 / kTimeBase;
106 return kTimeBaseSquared * t * t + kParabolaAtEdge;
107 }
108 ASSERT_NOT_REACHED();
109 return 0;
110 }
111
attackCurve(Curve curve,double deltaTime,double curveT,double startPosition,double attackPosition)112 double ScrollAnimatorNone::PerAxisData::attackCurve(Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition)
113 {
114 double t = deltaTime / curveT;
115 double positionFactor = curveAt(curve, t);
116 return startPosition + positionFactor * (attackPosition - startPosition);
117 }
118
releaseCurve(Curve curve,double deltaTime,double curveT,double releasePosition,double desiredPosition)119 double ScrollAnimatorNone::PerAxisData::releaseCurve(Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition)
120 {
121 double t = deltaTime / curveT;
122 double positionFactor = 1 - curveAt(curve, 1 - t);
123 return releasePosition + (positionFactor * (desiredPosition - releasePosition));
124 }
125
coastCurve(Curve curve,double factor)126 double ScrollAnimatorNone::PerAxisData::coastCurve(Curve curve, double factor)
127 {
128 return 1 - curveAt(curve, 1 - factor);
129 }
130
curveIntegralAt(Curve curve,double t)131 double ScrollAnimatorNone::PerAxisData::curveIntegralAt(Curve curve, double t)
132 {
133 switch (curve) {
134 case Linear:
135 return t * t / 2;
136 case Quadratic:
137 return t * t * t / 3;
138 case Cubic:
139 return t * t * t * t / 4;
140 case Quartic:
141 return t * t * t * t * t / 5;
142 case Bounce:
143 const double kTimeBase = 2.75;
144 const double kTimeBaseSquared = kTimeBase * kTimeBase;
145 const double kTimeBaseSquaredOverThree = kTimeBaseSquared / 3;
146 double area;
147 double t1 = std::min(t, 1 / kTimeBase);
148 area = kTimeBaseSquaredOverThree * t1 * t1 * t1;
149 if (t < 1 / kTimeBase)
150 return area;
151
152 t1 = std::min(t - 1 / kTimeBase, 1 / kTimeBase);
153 // The integral of kTimeBaseSquared * (t1 - .5 / kTimeBase) * (t1 - .5 / kTimeBase) + kParabolaAtEdge
154 const double kSecondInnerOffset = kTimeBaseSquared * .5 / kTimeBase;
155 double bounceArea = t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kSecondInnerOffset) + 1);
156 area += bounceArea;
157 if (t < 2 / kTimeBase)
158 return area;
159
160 t1 = std::min(t - 2 / kTimeBase, 0.5 / kTimeBase);
161 // The integral of kTimeBaseSquared * (t1 - .25 / kTimeBase) * (t1 - .25 / kTimeBase) + kParabolaAtEdge
162 const double kThirdInnerOffset = kTimeBaseSquared * .25 / kTimeBase;
163 bounceArea = t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kThirdInnerOffset) + 1);
164 area += bounceArea;
165 if (t < 2.5 / kTimeBase)
166 return area;
167
168 t1 = t - 2.5 / kTimeBase;
169 // The integral of kTimeBaseSquared * (t1 - .125 / kTimeBase) * (t1 - .125 / kTimeBase) + kParabolaAtEdge
170 const double kFourthInnerOffset = kTimeBaseSquared * .125 / kTimeBase;
171 bounceArea = t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kFourthInnerOffset) + 1);
172 area += bounceArea;
173 return area;
174 }
175 ASSERT_NOT_REACHED();
176 return 0;
177 }
178
attackArea(Curve curve,double startT,double endT)179 double ScrollAnimatorNone::PerAxisData::attackArea(Curve curve, double startT, double endT)
180 {
181 double startValue = curveIntegralAt(curve, startT);
182 double endValue = curveIntegralAt(curve, endT);
183 return endValue - startValue;
184 }
185
releaseArea(Curve curve,double startT,double endT)186 double ScrollAnimatorNone::PerAxisData::releaseArea(Curve curve, double startT, double endT)
187 {
188 double startValue = curveIntegralAt(curve, 1 - endT);
189 double endValue = curveIntegralAt(curve, 1 - startT);
190 return endValue - startValue;
191 }
192
PerAxisData(ScrollAnimatorNone * parent,float * currentPosition,int visibleLength)193 ScrollAnimatorNone::PerAxisData::PerAxisData(ScrollAnimatorNone* parent, float* currentPosition, int visibleLength)
194 : m_currentPosition(currentPosition)
195 , m_visibleLength(visibleLength)
196 {
197 reset();
198 }
199
reset()200 void ScrollAnimatorNone::PerAxisData::reset()
201 {
202 m_currentVelocity = 0;
203
204 m_desiredPosition = 0;
205 m_desiredVelocity = 0;
206
207 m_startPosition = 0;
208 m_startTime = 0;
209 m_startVelocity = 0;
210
211 m_animationTime = 0;
212 m_lastAnimationTime = 0;
213
214 m_attackPosition = 0;
215 m_attackTime = 0;
216 m_attackCurve = Quadratic;
217
218 m_releasePosition = 0;
219 m_releaseTime = 0;
220 m_releaseCurve = Quadratic;
221 }
222
223
updateDataFromParameters(float step,float delta,float scrollableSize,double currentTime,Parameters * parameters)224 bool ScrollAnimatorNone::PerAxisData::updateDataFromParameters(float step, float delta, float scrollableSize, double currentTime, Parameters* parameters)
225 {
226 float pixelDelta = step * delta;
227 if (!m_startTime || !pixelDelta || (pixelDelta < 0) != (m_desiredPosition - *m_currentPosition < 0)) {
228 m_desiredPosition = *m_currentPosition;
229 m_startTime = 0;
230 }
231 float newPosition = m_desiredPosition + pixelDelta;
232
233 if (newPosition < 0 || newPosition > scrollableSize)
234 newPosition = std::max(std::min(newPosition, scrollableSize), 0.0f);
235
236 if (newPosition == m_desiredPosition)
237 return false;
238
239 m_desiredPosition = newPosition;
240
241 if (!m_startTime) {
242 m_attackTime = parameters->m_attackTime;
243 m_attackCurve = parameters->m_attackCurve;
244 }
245 m_animationTime = parameters->m_animationTime;
246 m_releaseTime = parameters->m_releaseTime;
247 m_releaseCurve = parameters->m_releaseCurve;
248
249 // Prioritize our way out of over constraint.
250 if (m_attackTime + m_releaseTime > m_animationTime) {
251 if (m_releaseTime > m_animationTime)
252 m_releaseTime = m_animationTime;
253 m_attackTime = m_animationTime - m_releaseTime;
254 }
255
256 if (!m_startTime) {
257 // FIXME: This should be the time from the event that got us here.
258 m_startTime = currentTime - kTickTime / 2;
259 m_startPosition = *m_currentPosition;
260 m_lastAnimationTime = m_startTime;
261 }
262 m_startVelocity = m_currentVelocity;
263
264 double remainingDelta = m_desiredPosition - *m_currentPosition;
265
266 double attackAreaLeft = 0;
267
268 double deltaTime = m_lastAnimationTime - m_startTime;
269 double attackTimeLeft = std::max(0., m_attackTime - deltaTime);
270 double timeLeft = m_animationTime - deltaTime;
271 double minTimeLeft = m_releaseTime + std::min(parameters->m_repeatMinimumSustainTime, m_animationTime - m_releaseTime - attackTimeLeft);
272 if (timeLeft < minTimeLeft) {
273 m_animationTime = deltaTime + minTimeLeft;
274 timeLeft = minTimeLeft;
275 }
276
277 if (parameters->m_maximumCoastTime > (parameters->m_repeatMinimumSustainTime + parameters->m_releaseTime)) {
278 double targetMaxCoastVelocity = m_visibleLength * .25 * kFrameRate;
279 // This needs to be as minimal as possible while not being intrusive to page up/down.
280 double minCoastDelta = m_visibleLength;
281
282 if (fabs(remainingDelta) > minCoastDelta) {
283 double maxCoastDelta = parameters->m_maximumCoastTime * targetMaxCoastVelocity;
284 double coastFactor = std::min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta));
285
286 // 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.
287 double coastMinTimeLeft = std::min(parameters->m_maximumCoastTime, minTimeLeft + coastCurve(parameters->m_coastTimeCurve, coastFactor) * (parameters->m_maximumCoastTime - minTimeLeft));
288
289 double additionalTime = std::max(0., coastMinTimeLeft - minTimeLeft);
290 if (additionalTime) {
291 double additionalReleaseTime = std::min(additionalTime, parameters->m_releaseTime / (parameters->m_releaseTime + parameters->m_repeatMinimumSustainTime) * additionalTime);
292 m_releaseTime = parameters->m_releaseTime + additionalReleaseTime;
293 m_animationTime = deltaTime + coastMinTimeLeft;
294 timeLeft = coastMinTimeLeft;
295 }
296 }
297 }
298
299 double releaseTimeLeft = std::min(timeLeft, m_releaseTime);
300 double sustainTimeLeft = std::max(0., timeLeft - releaseTimeLeft - attackTimeLeft);
301
302 if (attackTimeLeft) {
303 double attackSpot = deltaTime / m_attackTime;
304 attackAreaLeft = attackArea(m_attackCurve, attackSpot, 1) * m_attackTime;
305 }
306
307 double releaseSpot = (m_releaseTime - releaseTimeLeft) / m_releaseTime;
308 double releaseAreaLeft = releaseArea(m_releaseCurve, releaseSpot, 1) * m_releaseTime;
309
310 m_desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft + releaseAreaLeft);
311 m_releasePosition = m_desiredPosition - m_desiredVelocity * releaseAreaLeft;
312 if (attackAreaLeft)
313 m_attackPosition = m_startPosition + m_desiredVelocity * attackAreaLeft;
314 else
315 m_attackPosition = m_releasePosition - (m_animationTime - m_releaseTime - m_attackTime) * m_desiredVelocity;
316
317 if (sustainTimeLeft) {
318 double roundOff = m_releasePosition - ((attackAreaLeft ? m_attackPosition : *m_currentPosition) + m_desiredVelocity * sustainTimeLeft);
319 m_desiredVelocity += roundOff / sustainTimeLeft;
320 }
321
322 return true;
323 }
324
newScrollAnimationPosition(double deltaTime)325 inline double ScrollAnimatorNone::PerAxisData::newScrollAnimationPosition(double deltaTime)
326 {
327 if (deltaTime < m_attackTime)
328 return attackCurve(m_attackCurve, deltaTime, m_attackTime, m_startPosition, m_attackPosition);
329 if (deltaTime < (m_animationTime - m_releaseTime))
330 return m_attackPosition + (deltaTime - m_attackTime) * m_desiredVelocity;
331 // release is based on targeting the exact final position.
332 double releaseDeltaT = deltaTime - (m_animationTime - m_releaseTime);
333 return releaseCurve(m_releaseCurve, releaseDeltaT, m_releaseTime, m_releasePosition, m_desiredPosition);
334 }
335
336 // FIXME: Add in jank detection trace events into this function.
animateScroll(double currentTime)337 bool ScrollAnimatorNone::PerAxisData::animateScroll(double currentTime)
338 {
339 double lastScrollInterval = currentTime - m_lastAnimationTime;
340 if (lastScrollInterval < kMinimumTimerInterval)
341 return true;
342
343 m_lastAnimationTime = currentTime;
344
345 double deltaTime = currentTime - m_startTime;
346
347 if (deltaTime > m_animationTime) {
348 *m_currentPosition = m_desiredPosition;
349 reset();
350 return false;
351 }
352 double newPosition = newScrollAnimationPosition(deltaTime);
353 // Normalize velocity to a per second amount. Could be used to check for jank.
354 if (lastScrollInterval > 0)
355 m_currentVelocity = (newPosition - *m_currentPosition) / lastScrollInterval;
356 *m_currentPosition = newPosition;
357
358 return true;
359 }
360
updateVisibleLength(int visibleLength)361 void ScrollAnimatorNone::PerAxisData::updateVisibleLength(int visibleLength)
362 {
363 m_visibleLength = visibleLength;
364 }
365
ScrollAnimatorNone(ScrollableArea * scrollableArea)366 ScrollAnimatorNone::ScrollAnimatorNone(ScrollableArea* scrollableArea)
367 : ScrollAnimator(scrollableArea)
368 , m_horizontalData(this, &m_currentPosX, scrollableArea->visibleWidth())
369 , m_verticalData(this, &m_currentPosY, scrollableArea->visibleHeight())
370 , m_startTime(0)
371 , m_animationActive(false)
372 {
373 }
374
~ScrollAnimatorNone()375 ScrollAnimatorNone::~ScrollAnimatorNone()
376 {
377 stopAnimationTimerIfNeeded();
378 }
379
parametersForScrollGranularity(ScrollGranularity granularity) const380 ScrollAnimatorNone::Parameters ScrollAnimatorNone::parametersForScrollGranularity(ScrollGranularity granularity) const
381 {
382 switch (granularity) {
383 case ScrollByDocument:
384 return Parameters(true, 20 * kTickTime, 10 * kTickTime, Cubic, 10 * kTickTime, Cubic, 10 * kTickTime, Linear, 1);
385 case ScrollByLine:
386 return Parameters(true, 10 * kTickTime, 7 * kTickTime, Cubic, 3 * kTickTime, Cubic, 3 * kTickTime, Linear, 1);
387 case ScrollByPage:
388 return Parameters(true, 15 * kTickTime, 10 * kTickTime, Cubic, 5 * kTickTime, Cubic, 5 * kTickTime, Linear, 1);
389 case ScrollByPixel:
390 return Parameters(true, 11 * kTickTime, 2 * kTickTime, Cubic, 3 * kTickTime, Cubic, 3 * kTickTime, Quadratic, 1.25);
391 default:
392 ASSERT_NOT_REACHED();
393 }
394 return Parameters();
395 }
396
scroll(ScrollbarOrientation orientation,ScrollGranularity granularity,float step,float delta)397 bool ScrollAnimatorNone::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float delta)
398 {
399 if (!m_scrollableArea->scrollAnimatorEnabled())
400 return ScrollAnimator::scroll(orientation, granularity, step, delta);
401
402 TRACE_EVENT0("blink", "ScrollAnimatorNone::scroll");
403
404 // FIXME: get the type passed in. MouseWheel could also be by line, but should still have different
405 // animation parameters than the keyboard.
406 Parameters parameters;
407 switch (granularity) {
408 case ScrollByDocument:
409 case ScrollByLine:
410 case ScrollByPage:
411 case ScrollByPixel:
412 parameters = parametersForScrollGranularity(granularity);
413 break;
414 case ScrollByPrecisePixel:
415 return ScrollAnimator::scroll(orientation, granularity, step, delta);
416 }
417
418 // If the individual input setting is disabled, bail.
419 if (!parameters.m_isEnabled)
420 return ScrollAnimator::scroll(orientation, granularity, step, delta);
421
422 // This is an animatable scroll. Set the animation in motion using the appropriate parameters.
423 float scrollableSize = static_cast<float>(m_scrollableArea->scrollSize(orientation));
424
425 PerAxisData& data = (orientation == VerticalScrollbar) ? m_verticalData : m_horizontalData;
426 bool needToScroll = data.updateDataFromParameters(step, delta, scrollableSize, WTF::monotonicallyIncreasingTime(), ¶meters);
427 if (needToScroll && !animationTimerActive()) {
428 m_startTime = data.m_startTime;
429 animationWillStart();
430 animationTimerFired();
431 }
432 return needToScroll;
433 }
434
scrollToOffsetWithoutAnimation(const FloatPoint & offset)435 void ScrollAnimatorNone::scrollToOffsetWithoutAnimation(const FloatPoint& offset)
436 {
437 stopAnimationTimerIfNeeded();
438
439 m_horizontalData.reset();
440 *m_horizontalData.m_currentPosition = offset.x();
441 m_horizontalData.m_desiredPosition = offset.x();
442 m_currentPosX = offset.x();
443
444 m_verticalData.reset();
445 *m_verticalData.m_currentPosition = offset.y();
446 m_verticalData.m_desiredPosition = offset.y();
447 m_currentPosY = offset.y();
448
449 notifyPositionChanged();
450 }
451
cancelAnimations()452 void ScrollAnimatorNone::cancelAnimations()
453 {
454 m_animationActive = false;
455 }
456
serviceScrollAnimations()457 void ScrollAnimatorNone::serviceScrollAnimations()
458 {
459 if (m_animationActive)
460 animationTimerFired();
461 }
462
willEndLiveResize()463 void ScrollAnimatorNone::willEndLiveResize()
464 {
465 updateVisibleLengths();
466 }
467
didAddVerticalScrollbar(Scrollbar *)468 void ScrollAnimatorNone::didAddVerticalScrollbar(Scrollbar*)
469 {
470 updateVisibleLengths();
471 }
472
didAddHorizontalScrollbar(Scrollbar *)473 void ScrollAnimatorNone::didAddHorizontalScrollbar(Scrollbar*)
474 {
475 updateVisibleLengths();
476 }
477
updateVisibleLengths()478 void ScrollAnimatorNone::updateVisibleLengths()
479 {
480 m_horizontalData.updateVisibleLength(scrollableArea()->visibleWidth());
481 m_verticalData.updateVisibleLength(scrollableArea()->visibleHeight());
482 }
483
animationTimerFired()484 void ScrollAnimatorNone::animationTimerFired()
485 {
486 TRACE_EVENT0("blink", "ScrollAnimatorNone::animationTimerFired");
487
488 double currentTime = WTF::monotonicallyIncreasingTime();
489
490 bool continueAnimation = false;
491 if (m_horizontalData.m_startTime && m_horizontalData.animateScroll(currentTime))
492 continueAnimation = true;
493 if (m_verticalData.m_startTime && m_verticalData.animateScroll(currentTime))
494 continueAnimation = true;
495
496 if (continueAnimation)
497 startNextTimer();
498 else
499 m_animationActive = false;
500
501 TRACE_EVENT0("blink", "ScrollAnimatorNone::notifyPositionChanged");
502 notifyPositionChanged();
503
504 if (!continueAnimation)
505 animationDidFinish();
506 }
507
startNextTimer()508 void ScrollAnimatorNone::startNextTimer()
509 {
510 if (scrollableArea()->scheduleAnimation())
511 m_animationActive = true;
512 }
513
animationTimerActive()514 bool ScrollAnimatorNone::animationTimerActive()
515 {
516 return m_animationActive;
517 }
518
stopAnimationTimerIfNeeded()519 void ScrollAnimatorNone::stopAnimationTimerIfNeeded()
520 {
521 if (animationTimerActive())
522 m_animationActive = false;
523 }
524
525 } // namespace blink
526