diff options
author | Steve Block <steveblock@google.com> | 2011-05-06 11:45:16 +0100 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2011-05-12 13:44:10 +0100 |
commit | cad810f21b803229eb11403f9209855525a25d57 (patch) | |
tree | 29a6fd0279be608e0fe9ffe9841f722f0f4e4269 /Source/WebCore/platform/ScrollAnimatorWin.cpp | |
parent | 121b0cf4517156d0ac5111caf9830c51b69bae8f (diff) | |
download | external_webkit-cad810f21b803229eb11403f9209855525a25d57.zip external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.gz external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.bz2 |
Merge WebKit at r75315: Initial merge by git.
Change-Id: I570314b346ce101c935ed22a626b48c2af266b84
Diffstat (limited to 'Source/WebCore/platform/ScrollAnimatorWin.cpp')
-rw-r--r-- | Source/WebCore/platform/ScrollAnimatorWin.cpp | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/Source/WebCore/platform/ScrollAnimatorWin.cpp b/Source/WebCore/platform/ScrollAnimatorWin.cpp new file mode 100644 index 0000000..025aa71 --- /dev/null +++ b/Source/WebCore/platform/ScrollAnimatorWin.cpp @@ -0,0 +1,301 @@ +/* + * 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 "ScrollbarClient.h" +#include "ScrollbarTheme.h" +#include <algorithm> +#include <wtf/CurrentTime.h> + +namespace WebCore { + +// static +ScrollAnimator* ScrollAnimator::create(ScrollbarClient* client) +{ + return new ScrollAnimatorWin(client); +} + +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(ScrollbarClient* client) + : ScrollAnimator(client) + , 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_client->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::setScrollPositionAndStopAnimation(ScrollbarOrientation orientation, float pos) +{ + PerAxisData* data = (orientation == HorizontalScrollbar) ? &m_horizontalData : &m_verticalData; + stopAnimationTimerIfNeeded(data); + *data->m_currentPos = pos; + data->m_desiredPos = pos; + data->m_currentVelocity = 0; + data->m_desiredVelocity = 0; +} + +// static +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(); + } + m_client->setScrollOffsetFromAnimation(IntPoint(*m_horizontalData.m_currentPos, *m_verticalData.m_currentPos)); +} + +} // namespace WebCore + +#endif // ENABLE(SMOOTH_SCROLLING) |