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 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 57 ScrollAnimatorNone::Parameters::Parameters() 58 : m_isEnabled(false) 59 { 60 } 61 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 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 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 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 128 double ScrollAnimatorNone::PerAxisData::coastCurve(Curve curve, double factor) 129 { 130 return 1 - curveAt(curve, 1 - factor); 131 } 132 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 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 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 195 ScrollAnimatorNone::PerAxisData::PerAxisData(ScrollAnimatorNone* parent, float* currentPosition, int visibleLength) 196 : m_currentPosition(currentPosition) 197 , m_visibleLength(visibleLength) 198 { 199 reset(); 200 } 201 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 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 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. 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 363 void ScrollAnimatorNone::PerAxisData::updateVisibleLength(int visibleLength) 364 { 365 m_visibleLength = visibleLength; 366 } 367 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 377 ScrollAnimatorNone::~ScrollAnimatorNone() 378 { 379 stopAnimationTimerIfNeeded(); 380 } 381 382 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 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 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 454 void ScrollAnimatorNone::cancelAnimations() 455 { 456 m_animationActive = false; 457 } 458 459 void ScrollAnimatorNone::serviceScrollAnimations() 460 { 461 if (m_animationActive) 462 animationTimerFired(); 463 } 464 465 void ScrollAnimatorNone::willEndLiveResize() 466 { 467 updateVisibleLengths(); 468 } 469 470 void ScrollAnimatorNone::didAddVerticalScrollbar(Scrollbar*) 471 { 472 updateVisibleLengths(); 473 } 474 475 void ScrollAnimatorNone::didAddHorizontalScrollbar(Scrollbar*) 476 { 477 updateVisibleLengths(); 478 } 479 480 void ScrollAnimatorNone::updateVisibleLengths() 481 { 482 m_horizontalData.updateVisibleLength(scrollableArea()->visibleWidth()); 483 m_verticalData.updateVisibleLength(scrollableArea()->visibleHeight()); 484 } 485 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 510 void ScrollAnimatorNone::startNextTimer() 511 { 512 if (scrollableArea()->scheduleAnimation()) 513 m_animationActive = true; 514 } 515 516 bool ScrollAnimatorNone::animationTimerActive() 517 { 518 return m_animationActive; 519 } 520 521 void ScrollAnimatorNone::stopAnimationTimerIfNeeded() 522 { 523 if (animationTimerActive()) 524 m_animationActive = false; 525 } 526 527 } // namespace WebCore 528