| /* |
| * Copyright (c) 2010, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| |
| #if ENABLE(SMOOTH_SCROLLING) |
| |
| #include "ScrollAnimatorWin.h" |
| |
| #include "FloatPoint.h" |
| #include "ScrollableArea.h" |
| #include "ScrollbarTheme.h" |
| #include <algorithm> |
| #include <wtf/CurrentTime.h> |
| #include <wtf/PassOwnPtr.h> |
| |
| namespace WebCore { |
| |
| PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea) |
| { |
| return adoptPtr(new ScrollAnimatorWin(scrollableArea)); |
| } |
| |
| const double ScrollAnimatorWin::animationTimerDelay = 0.01; |
| |
| ScrollAnimatorWin::PerAxisData::PerAxisData(ScrollAnimatorWin* parent, float* currentPos) |
| : m_currentPos(currentPos) |
| , m_desiredPos(0) |
| , m_currentVelocity(0) |
| , m_desiredVelocity(0) |
| , m_lastAnimationTime(0) |
| , m_animationTimer(parent, &ScrollAnimatorWin::animationTimerFired) |
| { |
| } |
| |
| |
| ScrollAnimatorWin::ScrollAnimatorWin(ScrollableArea* scrollableArea) |
| : ScrollAnimator(scrollableArea) |
| , m_horizontalData(this, &m_currentPosX) |
| , m_verticalData(this, &m_currentPosY) |
| { |
| } |
| |
| ScrollAnimatorWin::~ScrollAnimatorWin() |
| { |
| stopAnimationTimerIfNeeded(&m_horizontalData); |
| stopAnimationTimerIfNeeded(&m_verticalData); |
| } |
| |
| bool ScrollAnimatorWin::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier) |
| { |
| // Don't animate jumping to the beginning or end of the document. |
| if (granularity == ScrollByDocument) |
| return ScrollAnimator::scroll(orientation, granularity, step, multiplier); |
| |
| // This is an animatable scroll. Calculate the scroll delta. |
| PerAxisData* data = (orientation == VerticalScrollbar) ? &m_verticalData : &m_horizontalData; |
| float newPos = std::max(std::min(data->m_desiredPos + (step * multiplier), static_cast<float>(m_scrollableArea->scrollSize(orientation))), 0.0f); |
| if (newPos == data->m_desiredPos) |
| return false; |
| data->m_desiredPos = newPos; |
| |
| // Calculate the animation velocity. |
| if (*data->m_currentPos == data->m_desiredPos) |
| return false; |
| bool alreadyAnimating = data->m_animationTimer.isActive(); |
| // There are a number of different sources of scroll requests. We want to |
| // make both keyboard and wheel-generated scroll requests (which can come at |
| // unpredictable rates) and autoscrolling from holding down the mouse button |
| // on a scrollbar part (where the request rate can be obtained from the |
| // scrollbar theme) feel smooth, responsive, and similar. |
| // |
| // When autoscrolling, the scrollbar's autoscroll timer will call us to |
| // increment the desired position by |step| (with |multiplier| == 1) every |
| // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() seconds. If we set |
| // the desired velocity to exactly this rate, smooth scrolling will neither |
| // race ahead (and then have to slow down) nor increasingly lag behind, but |
| // will be smooth and synchronized. |
| // |
| // Note that because of the acceleration period, the current position in |
| // this case would lag the desired one by a small, constant amount (see |
| // comments on animateScroll()); the exact amount is given by |
| // lag = |step| - v(0.5tA + tD) |
| // Where |
| // v = The steady-state velocity, |
| // |step| / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() |
| // tA = accelerationTime() |
| // tD = The time we pretend has already passed when starting to scroll, |
| // |animationTimerDelay| |
| // |
| // This lag provides some buffer against timer jitter so we're less likely |
| // to hit the desired position and stop (and thus have to re-accelerate, |
| // causing a visible hitch) while waiting for the next autoscroll increment. |
| // |
| // Thus, for autoscroll-timer-triggered requests, the ideal steady-state |
| // distance to travel in each time interval is: |
| // float animationStep = step; |
| // Note that when we're not already animating, this is exactly the same as |
| // the distance to the target position. We'll return to that in a moment. |
| // |
| // For keyboard and wheel scrolls, we don't know when the next increment |
| // will be requested. If we set the target velocity based on how far away |
| // from the target position we are, then for keyboard/wheel events that come |
| // faster than the autoscroll delay, we'll asymptotically approach the |
| // velocity needed to stay smoothly in sync with the user's actions; for |
| // events that come slower, we'll scroll one increment and then pause until |
| // the next event fires. |
| float animationStep = fabs(newPos - *data->m_currentPos); |
| // If a key is held down (or the wheel continually spun), then once we have |
| // reached a velocity close to the steady-state velocity, we're likely to |
| // hit the desired position at around the same time we'd expect the next |
| // increment to occur -- bad because it leads to hitching as described above |
| // (if autoscroll-based requests didn't result in a small amount of constant |
| // lag). So if we're called again while already animating, we want to trim |
| // the animationStep slightly to maintain lag like what's described above. |
| // (I say "maintain" since we'll already be lagged due to the acceleration |
| // during the first scroll period.) |
| // |
| // Remember that trimming won't cause us to fall steadily further behind |
| // here, because the further behind we are, the larger the base step value |
| // above. Given the scrolling algorithm in animateScroll(), the practical |
| // effect will actually be that, assuming a constant trim factor, we'll lag |
| // by a constant amount depending on the rate at which increments occur |
| // compared to the autoscroll timer delay. The exact lag is given by |
| // lag = |step| * ((r / k) - 1) |
| // Where |
| // r = The ratio of the autoscroll repeat delay, |
| // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(), to the |
| // key/wheel repeat delay (i.e. > 1 when keys repeat faster) |
| // k = The velocity trim constant given below |
| // |
| // We want to choose the trim factor such that for calls that come at the |
| // autoscroll timer rate, we'll wind up with the same lag as in the |
| // "perfect" case described above (or, to put it another way, we'll end up |
| // with |animationStep| == |step| * |multiplier| despite the actual distance |
| // calculated above being larger than that). This will result in "perfect" |
| // behavior for autoscrolling without having to special-case it. |
| if (alreadyAnimating) |
| animationStep /= (2.0 - ((1.0 / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()) * (0.5 * accelerationTime() + animationTimerDelay))); |
| // The result of all this is that single keypresses or wheel flicks will |
| // scroll in the same time period as single presses of scrollbar elements; |
| // holding the mouse down on a scrollbar part will scroll as fast as |
| // possible without hitching; and other repeated scroll events will also |
| // scroll with the same time lag as holding down the mouse on a scrollbar |
| // part. |
| data->m_desiredVelocity = animationStep / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(); |
| |
| // If we're not already scrolling, start. |
| if (!alreadyAnimating) |
| animateScroll(data); |
| return true; |
| } |
| |
| void ScrollAnimatorWin::scrollToOffsetWithoutAnimation(const FloatPoint& offset) |
| { |
| stopAnimationTimerIfNeeded(&m_horizontalData); |
| stopAnimationTimerIfNeeded(&m_verticalData); |
| |
| *m_horizontalData.m_currentPos = offset.x(); |
| m_horizontalData.m_desiredPos = offset.x(); |
| m_horizontalData.m_currentVelocity = 0; |
| m_horizontalData.m_desiredVelocity = 0; |
| |
| *m_verticalData.m_currentPos = offset.y(); |
| m_verticalData.m_desiredPos = offset.y(); |
| m_verticalData.m_currentVelocity = 0; |
| m_verticalData.m_desiredVelocity = 0; |
| |
| notityPositionChanged(); |
| } |
| |
| double ScrollAnimatorWin::accelerationTime() |
| { |
| // We elect to use ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() as |
| // the length of time we'll take to accelerate from 0 to our target |
| // velocity. Choosing a larger value would produce a more pronounced |
| // acceleration effect. |
| return ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(); |
| } |
| |
| void ScrollAnimatorWin::animationTimerFired(Timer<ScrollAnimatorWin>* timer) |
| { |
| animateScroll((timer == &m_horizontalData.m_animationTimer) ? &m_horizontalData : &m_verticalData); |
| } |
| |
| void ScrollAnimatorWin::stopAnimationTimerIfNeeded(PerAxisData* data) |
| { |
| if (data->m_animationTimer.isActive()) |
| data->m_animationTimer.stop(); |
| } |
| |
| void ScrollAnimatorWin::animateScroll(PerAxisData* data) |
| { |
| // Note on smooth scrolling perf versus non-smooth scrolling perf: |
| // The total time to perform a complete scroll is given by |
| // t = t0 + 0.5tA - tD + tS |
| // Where |
| // t0 = The time to perform the scroll without smooth scrolling |
| // tA = The acceleration time, |
| // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() (see below) |
| // tD = |animationTimerDelay| |
| // tS = A value less than or equal to the time required to perform a |
| // single scroll increment, i.e. the work done due to calling |
| // client()->valueChanged() (~0 for simple pages, larger for complex |
| // pages). |
| // |
| // Because tA and tD are fairly small, the total lag (as users perceive it) |
| // is negligible for simple pages and roughly tS for complex pages. Without |
| // knowing in advance how large tS is it's hard to do better than this. |
| // Perhaps we could try to remember previous values and forward-compensate. |
| |
| |
| // We want to update the scroll position based on the time it's been since |
| // our last update. This may be longer than our ideal time, especially if |
| // the page is complex or the system is slow. |
| // |
| // To avoid feeling laggy, if we've just started smooth scrolling we pretend |
| // we've already accelerated for one ideal interval, so that we'll scroll at |
| // least some distance immediately. |
| double lastScrollInterval = data->m_currentVelocity ? (WTF::currentTime() - data->m_lastAnimationTime) : animationTimerDelay; |
| |
| // Figure out how far we've actually traveled and update our current |
| // velocity. |
| float distanceTraveled; |
| if (data->m_currentVelocity < data->m_desiredVelocity) { |
| // We accelerate at a constant rate until we reach the desired velocity. |
| float accelerationRate = data->m_desiredVelocity / accelerationTime(); |
| |
| // Figure out whether contant acceleration has caused us to reach our |
| // target velocity. |
| float potentialVelocityChange = accelerationRate * lastScrollInterval; |
| float potentialNewVelocity = data->m_currentVelocity + potentialVelocityChange; |
| if (potentialNewVelocity > data->m_desiredVelocity) { |
| // We reached the target velocity at some point between our last |
| // update and now. The distance traveled can be calculated in two |
| // pieces: the distance traveled while accelerating, and the |
| // distance traveled after reaching the target velocity. |
| float actualVelocityChange = data->m_desiredVelocity - data->m_currentVelocity; |
| float accelerationInterval = actualVelocityChange / accelerationRate; |
| // The distance traveled under constant acceleration is the area |
| // under a line segment with a constant rising slope. Break this |
| // into a triangular portion atop a rectangular portion and sum. |
| distanceTraveled = ((data->m_currentVelocity + (actualVelocityChange / 2)) * accelerationInterval); |
| // The distance traveled at the target velocity is simply |
| // (target velocity) * (remaining time after accelerating). |
| distanceTraveled += (data->m_desiredVelocity * (lastScrollInterval - accelerationInterval)); |
| data->m_currentVelocity = data->m_desiredVelocity; |
| } else { |
| // Constant acceleration through the entire time interval. |
| distanceTraveled = (data->m_currentVelocity + (potentialVelocityChange / 2)) * lastScrollInterval; |
| data->m_currentVelocity = potentialNewVelocity; |
| } |
| } else { |
| // We've already reached the target velocity, so the distance we've |
| // traveled is simply (current velocity) * (elapsed time). |
| distanceTraveled = data->m_currentVelocity * lastScrollInterval; |
| // If our desired velocity has decreased, drop the current velocity too. |
| data->m_currentVelocity = data->m_desiredVelocity; |
| } |
| |
| // Now update the scroll position based on the distance traveled. |
| if (distanceTraveled >= fabs(data->m_desiredPos - *data->m_currentPos)) { |
| // We've traveled far enough to reach the desired position. Stop smooth |
| // scrolling. |
| *data->m_currentPos = data->m_desiredPos; |
| data->m_currentVelocity = 0; |
| data->m_desiredVelocity = 0; |
| } else { |
| // Not yet at the target position. Travel towards it and set up the |
| // next update. |
| if (*data->m_currentPos > data->m_desiredPos) |
| distanceTraveled = -distanceTraveled; |
| *data->m_currentPos += distanceTraveled; |
| data->m_animationTimer.startOneShot(animationTimerDelay); |
| data->m_lastAnimationTime = WTF::currentTime(); |
| } |
| |
| notityPositionChanged(); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(SMOOTH_SCROLLING) |