| 1 | /* |
| 2 | * Copyright (C) 2016 Igalia S.L. |
| 3 | * Copyright (C) 2015 Apple Inc. All rights reserved. |
| 4 | * Copyright (c) 2011, Google Inc. All rights reserved. |
| 5 | * |
| 6 | * Redistribution and use in source and binary forms, with or without |
| 7 | * modification, are permitted provided that the following conditions |
| 8 | * are met: |
| 9 | * 1. Redistributions of source code must retain the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer. |
| 11 | * 2. Redistributions in binary form must reproduce the above copyright |
| 12 | * notice, this list of conditions and the following disclaimer in the |
| 13 | * documentation and/or other materials provided with the distribution. |
| 14 | * |
| 15 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 17 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 18 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 19 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 20 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 21 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 22 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 23 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 24 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 25 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | */ |
| 27 | |
| 28 | #include "config.h" |
| 29 | #include "ScrollAnimationSmooth.h" |
| 30 | |
| 31 | #if ENABLE(SMOOTH_SCROLLING) |
| 32 | |
| 33 | #include "FloatPoint.h" |
| 34 | #include "ScrollableArea.h" |
| 35 | |
| 36 | namespace WebCore { |
| 37 | |
| 38 | static const double frameRate = 60; |
| 39 | static const Seconds tickTime = 1_s / frameRate; |
| 40 | static const Seconds minimumTimerInterval { 1_ms }; |
| 41 | |
| 42 | ScrollAnimationSmooth::ScrollAnimationSmooth(ScrollableArea& scrollableArea, const FloatPoint& position, WTF::Function<void (FloatPoint&&)>&& notifyPositionChangedFunction) |
| 43 | : ScrollAnimation(scrollableArea) |
| 44 | , m_notifyPositionChangedFunction(WTFMove(notifyPositionChangedFunction)) |
| 45 | , m_horizontalData(position.x(), scrollableArea.visibleWidth()) |
| 46 | , m_verticalData(position.y(), scrollableArea.visibleHeight()) |
| 47 | , m_animationTimer(*this, &ScrollAnimationSmooth::animationTimerFired) |
| 48 | { |
| 49 | } |
| 50 | |
| 51 | bool ScrollAnimationSmooth::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier) |
| 52 | { |
| 53 | float minScrollPosition; |
| 54 | float maxScrollPosition; |
| 55 | if (orientation == HorizontalScrollbar) { |
| 56 | minScrollPosition = m_scrollableArea.minimumScrollPosition().x(); |
| 57 | maxScrollPosition = m_scrollableArea.maximumScrollPosition().x(); |
| 58 | } else { |
| 59 | minScrollPosition = m_scrollableArea.minimumScrollPosition().y(); |
| 60 | maxScrollPosition = m_scrollableArea.maximumScrollPosition().y(); |
| 61 | } |
| 62 | bool needToScroll = updatePerAxisData(orientation == HorizontalScrollbar ? m_horizontalData : m_verticalData, granularity, step * multiplier, minScrollPosition, maxScrollPosition); |
| 63 | if (needToScroll && !animationTimerActive()) { |
| 64 | m_startTime = orientation == HorizontalScrollbar ? m_horizontalData.startTime : m_verticalData.startTime; |
| 65 | animationTimerFired(); |
| 66 | } |
| 67 | return needToScroll; |
| 68 | } |
| 69 | |
| 70 | void ScrollAnimationSmooth::stop() |
| 71 | { |
| 72 | m_animationTimer.stop(); |
| 73 | } |
| 74 | |
| 75 | void ScrollAnimationSmooth::updateVisibleLengths() |
| 76 | { |
| 77 | m_horizontalData.visibleLength = m_scrollableArea.visibleWidth(); |
| 78 | m_verticalData.visibleLength = m_scrollableArea.visibleHeight(); |
| 79 | } |
| 80 | |
| 81 | void ScrollAnimationSmooth::setCurrentPosition(const FloatPoint& position) |
| 82 | { |
| 83 | stop(); |
| 84 | m_horizontalData = PerAxisData(position.x(), m_horizontalData.visibleLength); |
| 85 | m_verticalData = PerAxisData(position.y(), m_verticalData.visibleLength); |
| 86 | } |
| 87 | |
| 88 | ScrollAnimationSmooth::~ScrollAnimationSmooth() = default; |
| 89 | |
| 90 | static inline double curveAt(ScrollAnimationSmooth::Curve curve, double t) |
| 91 | { |
| 92 | switch (curve) { |
| 93 | case ScrollAnimationSmooth::Curve::Linear: |
| 94 | return t; |
| 95 | case ScrollAnimationSmooth::Curve::Quadratic: |
| 96 | return t * t; |
| 97 | case ScrollAnimationSmooth::Curve::Cubic: |
| 98 | return t * t * t; |
| 99 | case ScrollAnimationSmooth::Curve::Quartic: |
| 100 | return t * t * t * t; |
| 101 | case ScrollAnimationSmooth::Curve::Bounce: |
| 102 | // Time base is chosen to keep the bounce points simpler: |
| 103 | // 1 (half bounce coming in) + 1 + .5 + .25 |
| 104 | static const double timeBase = 2.75; |
| 105 | static const double timeBaseSquared = timeBase * timeBase; |
| 106 | if (t < 1 / timeBase) |
| 107 | return timeBaseSquared * t * t; |
| 108 | if (t < 2 / timeBase) { |
| 109 | // Invert a [-.5,.5] quadratic parabola, center it in [1,2]. |
| 110 | double t1 = t - 1.5 / timeBase; |
| 111 | const double parabolaAtEdge = 1 - .5 * .5; |
| 112 | return timeBaseSquared * t1 * t1 + parabolaAtEdge; |
| 113 | } |
| 114 | if (t < 2.5 / timeBase) { |
| 115 | // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5]. |
| 116 | double t2 = t - 2.25 / timeBase; |
| 117 | const double parabolaAtEdge = 1 - .25 * .25; |
| 118 | return timeBaseSquared * t2 * t2 + parabolaAtEdge; |
| 119 | } |
| 120 | // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75]. |
| 121 | const double parabolaAtEdge = 1 - .125 * .125; |
| 122 | t -= 2.625 / timeBase; |
| 123 | return timeBaseSquared * t * t + parabolaAtEdge; |
| 124 | } |
| 125 | ASSERT_NOT_REACHED(); |
| 126 | return 0; |
| 127 | } |
| 128 | |
| 129 | static inline double attackCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition) |
| 130 | { |
| 131 | double t = deltaTime / curveT; |
| 132 | double positionFactor = curveAt(curve, t); |
| 133 | return startPosition + positionFactor * (attackPosition - startPosition); |
| 134 | } |
| 135 | |
| 136 | static inline double releaseCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition) |
| 137 | { |
| 138 | double t = deltaTime / curveT; |
| 139 | double positionFactor = 1 - curveAt(curve, 1 - t); |
| 140 | return releasePosition + (positionFactor * (desiredPosition - releasePosition)); |
| 141 | } |
| 142 | |
| 143 | static inline double coastCurve(ScrollAnimationSmooth::Curve curve, double factor) |
| 144 | { |
| 145 | return 1 - curveAt(curve, 1 - factor); |
| 146 | } |
| 147 | |
| 148 | static inline double curveIntegralAt(ScrollAnimationSmooth::Curve curve, double t) |
| 149 | { |
| 150 | switch (curve) { |
| 151 | case ScrollAnimationSmooth::Curve::Linear: |
| 152 | return t * t / 2; |
| 153 | case ScrollAnimationSmooth::Curve::Quadratic: |
| 154 | return t * t * t / 3; |
| 155 | case ScrollAnimationSmooth::Curve::Cubic: |
| 156 | return t * t * t * t / 4; |
| 157 | case ScrollAnimationSmooth::Curve::Quartic: |
| 158 | return t * t * t * t * t / 5; |
| 159 | case ScrollAnimationSmooth::Curve::Bounce: |
| 160 | static const double timeBase = 2.75; |
| 161 | static const double timeBaseSquared = timeBase * timeBase; |
| 162 | static const double timeBaseSquaredOverThree = timeBaseSquared / 3; |
| 163 | double area; |
| 164 | double t1 = std::min(t, 1 / timeBase); |
| 165 | area = timeBaseSquaredOverThree * t1 * t1 * t1; |
| 166 | if (t < 1 / timeBase) |
| 167 | return area; |
| 168 | |
| 169 | t1 = std::min(t - 1 / timeBase, 1 / timeBase); |
| 170 | // The integral of timeBaseSquared * (t1 - .5 / timeBase) * (t1 - .5 / timeBase) + parabolaAtEdge |
| 171 | static const double secondInnerOffset = timeBaseSquared * .5 / timeBase; |
| 172 | double bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - secondInnerOffset) + 1); |
| 173 | area += bounceArea; |
| 174 | if (t < 2 / timeBase) |
| 175 | return area; |
| 176 | |
| 177 | t1 = std::min(t - 2 / timeBase, 0.5 / timeBase); |
| 178 | // The integral of timeBaseSquared * (t1 - .25 / timeBase) * (t1 - .25 / timeBase) + parabolaAtEdge |
| 179 | static const double thirdInnerOffset = timeBaseSquared * .25 / timeBase; |
| 180 | bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - thirdInnerOffset) + 1); |
| 181 | area += bounceArea; |
| 182 | if (t < 2.5 / timeBase) |
| 183 | return area; |
| 184 | |
| 185 | t1 = t - 2.5 / timeBase; |
| 186 | // The integral of timeBaseSquared * (t1 - .125 / timeBase) * (t1 - .125 / timeBase) + parabolaAtEdge |
| 187 | static const double fourthInnerOffset = timeBaseSquared * .125 / timeBase; |
| 188 | bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - fourthInnerOffset) + 1); |
| 189 | area += bounceArea; |
| 190 | return area; |
| 191 | } |
| 192 | ASSERT_NOT_REACHED(); |
| 193 | return 0; |
| 194 | } |
| 195 | |
| 196 | static inline double attackArea(ScrollAnimationSmooth::Curve curve, double startT, double endT) |
| 197 | { |
| 198 | double startValue = curveIntegralAt(curve, startT); |
| 199 | double endValue = curveIntegralAt(curve, endT); |
| 200 | return endValue - startValue; |
| 201 | } |
| 202 | |
| 203 | static inline double releaseArea(ScrollAnimationSmooth::Curve curve, double startT, double endT) |
| 204 | { |
| 205 | double startValue = curveIntegralAt(curve, 1 - endT); |
| 206 | double endValue = curveIntegralAt(curve, 1 - startT); |
| 207 | return endValue - startValue; |
| 208 | } |
| 209 | |
| 210 | static inline void getAnimationParametersForGranularity(ScrollGranularity granularity, Seconds& animationTime, Seconds& repeatMinimumSustainTime, Seconds& attackTime, Seconds& releaseTime, ScrollAnimationSmooth::Curve& coastTimeCurve, Seconds& maximumCoastTime) |
| 211 | { |
| 212 | switch (granularity) { |
| 213 | case ScrollByDocument: |
| 214 | animationTime = tickTime * 10; |
| 215 | repeatMinimumSustainTime = tickTime * 10; |
| 216 | attackTime = tickTime * 10; |
| 217 | releaseTime = tickTime * 10; |
| 218 | coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; |
| 219 | maximumCoastTime = 1_s; |
| 220 | break; |
| 221 | case ScrollByLine: |
| 222 | animationTime = tickTime * 10; |
| 223 | repeatMinimumSustainTime = tickTime * 7; |
| 224 | attackTime = tickTime * 3; |
| 225 | releaseTime = tickTime * 3; |
| 226 | coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; |
| 227 | maximumCoastTime = 1_s; |
| 228 | break; |
| 229 | case ScrollByPage: |
| 230 | animationTime = tickTime * 15; |
| 231 | repeatMinimumSustainTime = tickTime * 10; |
| 232 | attackTime = tickTime * 5; |
| 233 | releaseTime = tickTime * 5; |
| 234 | coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; |
| 235 | maximumCoastTime = 1_s; |
| 236 | break; |
| 237 | case ScrollByPixel: |
| 238 | animationTime = tickTime * 11; |
| 239 | repeatMinimumSustainTime = tickTime * 2; |
| 240 | attackTime = tickTime * 3; |
| 241 | releaseTime = tickTime * 3; |
| 242 | coastTimeCurve = ScrollAnimationSmooth::Curve::Quadratic; |
| 243 | maximumCoastTime = 1250_ms; |
| 244 | break; |
| 245 | default: |
| 246 | ASSERT_NOT_REACHED(); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | bool ScrollAnimationSmooth::updatePerAxisData(PerAxisData& data, ScrollGranularity granularity, float delta, float minScrollPosition, float maxScrollPosition) |
| 251 | { |
| 252 | if (!data.startTime || !delta || (delta < 0) != (data.desiredPosition - data.currentPosition < 0)) { |
| 253 | data.desiredPosition = data.currentPosition; |
| 254 | data.startTime = { }; |
| 255 | } |
| 256 | float newPosition = data.desiredPosition + delta; |
| 257 | |
| 258 | newPosition = std::max(std::min(newPosition, maxScrollPosition), minScrollPosition); |
| 259 | |
| 260 | if (newPosition == data.desiredPosition) |
| 261 | return false; |
| 262 | |
| 263 | Seconds animationTime, repeatMinimumSustainTime, attackTime, releaseTime, maximumCoastTime; |
| 264 | Curve coastTimeCurve; |
| 265 | getAnimationParametersForGranularity(granularity, animationTime, repeatMinimumSustainTime, attackTime, releaseTime, coastTimeCurve, maximumCoastTime); |
| 266 | |
| 267 | data.desiredPosition = newPosition; |
| 268 | if (!data.startTime) |
| 269 | data.attackTime = attackTime; |
| 270 | data.animationTime = animationTime; |
| 271 | data.releaseTime = releaseTime; |
| 272 | |
| 273 | // Prioritize our way out of over constraint. |
| 274 | if (data.attackTime + data.releaseTime > data.animationTime) { |
| 275 | if (data.releaseTime > data.animationTime) |
| 276 | data.releaseTime = data.animationTime; |
| 277 | data.attackTime = data.animationTime - data.releaseTime; |
| 278 | } |
| 279 | |
| 280 | if (!data.startTime) { |
| 281 | // FIXME: This should be the time from the event that got us here. |
| 282 | data.startTime = MonotonicTime::now() - tickTime / 2.; |
| 283 | data.startPosition = data.currentPosition; |
| 284 | data.lastAnimationTime = data.startTime; |
| 285 | } |
| 286 | data.startVelocity = data.currentVelocity; |
| 287 | |
| 288 | double remainingDelta = data.desiredPosition - data.currentPosition; |
| 289 | double attackAreaLeft = 0; |
| 290 | Seconds deltaTime = data.lastAnimationTime - data.startTime; |
| 291 | Seconds attackTimeLeft = std::max(0_s, data.attackTime - deltaTime); |
| 292 | Seconds timeLeft = data.animationTime - deltaTime; |
| 293 | Seconds minTimeLeft = data.releaseTime + std::min(repeatMinimumSustainTime, data.animationTime - data.releaseTime - attackTimeLeft); |
| 294 | if (timeLeft < minTimeLeft) { |
| 295 | data.animationTime = deltaTime + minTimeLeft; |
| 296 | timeLeft = minTimeLeft; |
| 297 | } |
| 298 | |
| 299 | if (maximumCoastTime > (repeatMinimumSustainTime + releaseTime)) { |
| 300 | double targetMaxCoastVelocity = data.visibleLength * .25 * frameRate; |
| 301 | // This needs to be as minimal as possible while not being intrusive to page up/down. |
| 302 | double minCoastDelta = data.visibleLength; |
| 303 | |
| 304 | if (fabs(remainingDelta) > minCoastDelta) { |
| 305 | double maxCoastDelta = maximumCoastTime.value() * targetMaxCoastVelocity; |
| 306 | double coastFactor = std::min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta)); |
| 307 | |
| 308 | // 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. |
| 309 | Seconds coastMinTimeLeft = std::min(maximumCoastTime, minTimeLeft + (maximumCoastTime - minTimeLeft) * coastCurve(coastTimeCurve, coastFactor)); |
| 310 | |
| 311 | if (Seconds additionalTime = std::max(0_s, coastMinTimeLeft - minTimeLeft)) { |
| 312 | Seconds additionalReleaseTime = std::min(additionalTime, additionalTime * (releaseTime / (releaseTime + repeatMinimumSustainTime))); |
| 313 | data.releaseTime = releaseTime + additionalReleaseTime; |
| 314 | data.animationTime = deltaTime + coastMinTimeLeft; |
| 315 | timeLeft = coastMinTimeLeft; |
| 316 | } |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | Seconds releaseTimeLeft = std::min(timeLeft, data.releaseTime); |
| 321 | Seconds sustainTimeLeft = std::max(0_s, timeLeft - releaseTimeLeft - attackTimeLeft); |
| 322 | if (attackTimeLeft) { |
| 323 | double attackSpot = deltaTime / data.attackTime; |
| 324 | attackAreaLeft = attackArea(Curve::Cubic, attackSpot, 1) * data.attackTime.value(); |
| 325 | } |
| 326 | |
| 327 | double releaseSpot = (data.releaseTime - releaseTimeLeft) / data.releaseTime; |
| 328 | double releaseAreaLeft = releaseArea(Curve::Cubic, releaseSpot, 1) * data.releaseTime.value(); |
| 329 | |
| 330 | data.desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft.value() + releaseAreaLeft); |
| 331 | data.releasePosition = data.desiredPosition - data.desiredVelocity * releaseAreaLeft; |
| 332 | if (attackAreaLeft) |
| 333 | data.attackPosition = data.startPosition + data.desiredVelocity * attackAreaLeft; |
| 334 | else |
| 335 | data.attackPosition = data.releasePosition - (data.animationTime - data.releaseTime - data.attackTime).value() * data.desiredVelocity; |
| 336 | |
| 337 | if (sustainTimeLeft) { |
| 338 | double roundOff = data.releasePosition - ((attackAreaLeft ? data.attackPosition : data.currentPosition) + data.desiredVelocity * sustainTimeLeft.value()); |
| 339 | data.desiredVelocity += roundOff / sustainTimeLeft.value(); |
| 340 | } |
| 341 | |
| 342 | return true; |
| 343 | } |
| 344 | |
| 345 | bool ScrollAnimationSmooth::animateScroll(PerAxisData& data, MonotonicTime currentTime) |
| 346 | { |
| 347 | if (!data.startTime) |
| 348 | return false; |
| 349 | |
| 350 | Seconds lastScrollInterval = currentTime - data.lastAnimationTime; |
| 351 | if (lastScrollInterval < minimumTimerInterval) |
| 352 | return true; |
| 353 | |
| 354 | data.lastAnimationTime = currentTime; |
| 355 | |
| 356 | Seconds deltaTime = currentTime - data.startTime; |
| 357 | double newPosition = data.currentPosition; |
| 358 | |
| 359 | if (deltaTime > data.animationTime) { |
| 360 | data = PerAxisData(data.desiredPosition, data.visibleLength); |
| 361 | return false; |
| 362 | } |
| 363 | if (deltaTime < data.attackTime) |
| 364 | newPosition = attackCurve(Curve::Cubic, deltaTime.value(), data.attackTime.value(), data.startPosition, data.attackPosition); |
| 365 | else if (deltaTime < (data.animationTime - data.releaseTime)) |
| 366 | newPosition = data.attackPosition + (deltaTime - data.attackTime).value() * data.desiredVelocity; |
| 367 | else { |
| 368 | // release is based on targeting the exact final position. |
| 369 | Seconds releaseDeltaT = deltaTime - (data.animationTime - data.releaseTime); |
| 370 | newPosition = releaseCurve(Curve::Cubic, releaseDeltaT.value(), data.releaseTime.value(), data.releasePosition, data.desiredPosition); |
| 371 | } |
| 372 | |
| 373 | // Normalize velocity to a per second amount. Could be used to check for jank. |
| 374 | if (lastScrollInterval > 0_s) |
| 375 | data.currentVelocity = (newPosition - data.currentPosition) / lastScrollInterval.value(); |
| 376 | data.currentPosition = newPosition; |
| 377 | |
| 378 | return true; |
| 379 | } |
| 380 | |
| 381 | void ScrollAnimationSmooth::animationTimerFired() |
| 382 | { |
| 383 | MonotonicTime currentTime = MonotonicTime::now(); |
| 384 | Seconds deltaToNextFrame = 1_s * ceil((currentTime - m_startTime).value() * frameRate) / frameRate - (currentTime - m_startTime); |
| 385 | currentTime += deltaToNextFrame; |
| 386 | |
| 387 | bool continueAnimation = false; |
| 388 | if (animateScroll(m_horizontalData, currentTime)) |
| 389 | continueAnimation = true; |
| 390 | if (animateScroll(m_verticalData, currentTime)) |
| 391 | continueAnimation = true; |
| 392 | |
| 393 | if (continueAnimation) |
| 394 | startNextTimer(std::max(minimumTimerInterval, deltaToNextFrame)); |
| 395 | |
| 396 | m_notifyPositionChangedFunction(FloatPoint(m_horizontalData.currentPosition, m_verticalData.currentPosition)); |
| 397 | } |
| 398 | |
| 399 | void ScrollAnimationSmooth::startNextTimer(Seconds delay) |
| 400 | { |
| 401 | m_animationTimer.startOneShot(delay); |
| 402 | } |
| 403 | |
| 404 | bool ScrollAnimationSmooth::animationTimerActive() const |
| 405 | { |
| 406 | return m_animationTimer.isActive(); |
| 407 | } |
| 408 | |
| 409 | } // namespace WebCore |
| 410 | |
| 411 | #endif // ENABLE(SMOOTH_SCROLLING) |
| 412 | |