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