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