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