1 /* 2 * Copyright (c) 2010, 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 #if ENABLE(SMOOTH_SCROLLING) 34 35 #include "ScrollAnimatorWin.h" 36 37 #include "FloatPoint.h" 38 #include "ScrollableArea.h" 39 #include "ScrollbarTheme.h" 40 #include <algorithm> 41 #include <wtf/CurrentTime.h> 42 #include <wtf/PassOwnPtr.h> 43 44 namespace WebCore { 45 46 PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea) 47 { 48 return adoptPtr(new ScrollAnimatorWin(scrollableArea)); 49 } 50 51 const double ScrollAnimatorWin::animationTimerDelay = 0.01; 52 53 ScrollAnimatorWin::PerAxisData::PerAxisData(ScrollAnimatorWin* parent, float* currentPos) 54 : m_currentPos(currentPos) 55 , m_desiredPos(0) 56 , m_currentVelocity(0) 57 , m_desiredVelocity(0) 58 , m_lastAnimationTime(0) 59 , m_animationTimer(parent, &ScrollAnimatorWin::animationTimerFired) 60 { 61 } 62 63 64 ScrollAnimatorWin::ScrollAnimatorWin(ScrollableArea* scrollableArea) 65 : ScrollAnimator(scrollableArea) 66 , m_horizontalData(this, &m_currentPosX) 67 , m_verticalData(this, &m_currentPosY) 68 { 69 } 70 71 ScrollAnimatorWin::~ScrollAnimatorWin() 72 { 73 stopAnimationTimerIfNeeded(&m_horizontalData); 74 stopAnimationTimerIfNeeded(&m_verticalData); 75 } 76 77 bool ScrollAnimatorWin::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier) 78 { 79 // Don't animate jumping to the beginning or end of the document. 80 if (granularity == ScrollByDocument) 81 return ScrollAnimator::scroll(orientation, granularity, step, multiplier); 82 83 // This is an animatable scroll. Calculate the scroll delta. 84 PerAxisData* data = (orientation == VerticalScrollbar) ? &m_verticalData : &m_horizontalData; 85 float newPos = std::max(std::min(data->m_desiredPos + (step * multiplier), static_cast<float>(m_scrollableArea->scrollSize(orientation))), 0.0f); 86 if (newPos == data->m_desiredPos) 87 return false; 88 data->m_desiredPos = newPos; 89 90 // Calculate the animation velocity. 91 if (*data->m_currentPos == data->m_desiredPos) 92 return false; 93 bool alreadyAnimating = data->m_animationTimer.isActive(); 94 // There are a number of different sources of scroll requests. We want to 95 // make both keyboard and wheel-generated scroll requests (which can come at 96 // unpredictable rates) and autoscrolling from holding down the mouse button 97 // on a scrollbar part (where the request rate can be obtained from the 98 // scrollbar theme) feel smooth, responsive, and similar. 99 // 100 // When autoscrolling, the scrollbar's autoscroll timer will call us to 101 // increment the desired position by |step| (with |multiplier| == 1) every 102 // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() seconds. If we set 103 // the desired velocity to exactly this rate, smooth scrolling will neither 104 // race ahead (and then have to slow down) nor increasingly lag behind, but 105 // will be smooth and synchronized. 106 // 107 // Note that because of the acceleration period, the current position in 108 // this case would lag the desired one by a small, constant amount (see 109 // comments on animateScroll()); the exact amount is given by 110 // lag = |step| - v(0.5tA + tD) 111 // Where 112 // v = The steady-state velocity, 113 // |step| / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() 114 // tA = accelerationTime() 115 // tD = The time we pretend has already passed when starting to scroll, 116 // |animationTimerDelay| 117 // 118 // This lag provides some buffer against timer jitter so we're less likely 119 // to hit the desired position and stop (and thus have to re-accelerate, 120 // causing a visible hitch) while waiting for the next autoscroll increment. 121 // 122 // Thus, for autoscroll-timer-triggered requests, the ideal steady-state 123 // distance to travel in each time interval is: 124 // float animationStep = step; 125 // Note that when we're not already animating, this is exactly the same as 126 // the distance to the target position. We'll return to that in a moment. 127 // 128 // For keyboard and wheel scrolls, we don't know when the next increment 129 // will be requested. If we set the target velocity based on how far away 130 // from the target position we are, then for keyboard/wheel events that come 131 // faster than the autoscroll delay, we'll asymptotically approach the 132 // velocity needed to stay smoothly in sync with the user's actions; for 133 // events that come slower, we'll scroll one increment and then pause until 134 // the next event fires. 135 float animationStep = fabs(newPos - *data->m_currentPos); 136 // If a key is held down (or the wheel continually spun), then once we have 137 // reached a velocity close to the steady-state velocity, we're likely to 138 // hit the desired position at around the same time we'd expect the next 139 // increment to occur -- bad because it leads to hitching as described above 140 // (if autoscroll-based requests didn't result in a small amount of constant 141 // lag). So if we're called again while already animating, we want to trim 142 // the animationStep slightly to maintain lag like what's described above. 143 // (I say "maintain" since we'll already be lagged due to the acceleration 144 // during the first scroll period.) 145 // 146 // Remember that trimming won't cause us to fall steadily further behind 147 // here, because the further behind we are, the larger the base step value 148 // above. Given the scrolling algorithm in animateScroll(), the practical 149 // effect will actually be that, assuming a constant trim factor, we'll lag 150 // by a constant amount depending on the rate at which increments occur 151 // compared to the autoscroll timer delay. The exact lag is given by 152 // lag = |step| * ((r / k) - 1) 153 // Where 154 // r = The ratio of the autoscroll repeat delay, 155 // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(), to the 156 // key/wheel repeat delay (i.e. > 1 when keys repeat faster) 157 // k = The velocity trim constant given below 158 // 159 // We want to choose the trim factor such that for calls that come at the 160 // autoscroll timer rate, we'll wind up with the same lag as in the 161 // "perfect" case described above (or, to put it another way, we'll end up 162 // with |animationStep| == |step| * |multiplier| despite the actual distance 163 // calculated above being larger than that). This will result in "perfect" 164 // behavior for autoscrolling without having to special-case it. 165 if (alreadyAnimating) 166 animationStep /= (2.0 - ((1.0 / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()) * (0.5 * accelerationTime() + animationTimerDelay))); 167 // The result of all this is that single keypresses or wheel flicks will 168 // scroll in the same time period as single presses of scrollbar elements; 169 // holding the mouse down on a scrollbar part will scroll as fast as 170 // possible without hitching; and other repeated scroll events will also 171 // scroll with the same time lag as holding down the mouse on a scrollbar 172 // part. 173 data->m_desiredVelocity = animationStep / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(); 174 175 // If we're not already scrolling, start. 176 if (!alreadyAnimating) 177 animateScroll(data); 178 return true; 179 } 180 181 void ScrollAnimatorWin::scrollToOffsetWithoutAnimation(const FloatPoint& offset) 182 { 183 stopAnimationTimerIfNeeded(&m_horizontalData); 184 stopAnimationTimerIfNeeded(&m_verticalData); 185 186 *m_horizontalData.m_currentPos = offset.x(); 187 m_horizontalData.m_desiredPos = offset.x(); 188 m_horizontalData.m_currentVelocity = 0; 189 m_horizontalData.m_desiredVelocity = 0; 190 191 *m_verticalData.m_currentPos = offset.y(); 192 m_verticalData.m_desiredPos = offset.y(); 193 m_verticalData.m_currentVelocity = 0; 194 m_verticalData.m_desiredVelocity = 0; 195 196 notityPositionChanged(); 197 } 198 199 double ScrollAnimatorWin::accelerationTime() 200 { 201 // We elect to use ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() as 202 // the length of time we'll take to accelerate from 0 to our target 203 // velocity. Choosing a larger value would produce a more pronounced 204 // acceleration effect. 205 return ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(); 206 } 207 208 void ScrollAnimatorWin::animationTimerFired(Timer<ScrollAnimatorWin>* timer) 209 { 210 animateScroll((timer == &m_horizontalData.m_animationTimer) ? &m_horizontalData : &m_verticalData); 211 } 212 213 void ScrollAnimatorWin::stopAnimationTimerIfNeeded(PerAxisData* data) 214 { 215 if (data->m_animationTimer.isActive()) 216 data->m_animationTimer.stop(); 217 } 218 219 void ScrollAnimatorWin::animateScroll(PerAxisData* data) 220 { 221 // Note on smooth scrolling perf versus non-smooth scrolling perf: 222 // The total time to perform a complete scroll is given by 223 // t = t0 + 0.5tA - tD + tS 224 // Where 225 // t0 = The time to perform the scroll without smooth scrolling 226 // tA = The acceleration time, 227 // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() (see below) 228 // tD = |animationTimerDelay| 229 // tS = A value less than or equal to the time required to perform a 230 // single scroll increment, i.e. the work done due to calling 231 // client()->valueChanged() (~0 for simple pages, larger for complex 232 // pages). 233 // 234 // Because tA and tD are fairly small, the total lag (as users perceive it) 235 // is negligible for simple pages and roughly tS for complex pages. Without 236 // knowing in advance how large tS is it's hard to do better than this. 237 // Perhaps we could try to remember previous values and forward-compensate. 238 239 240 // We want to update the scroll position based on the time it's been since 241 // our last update. This may be longer than our ideal time, especially if 242 // the page is complex or the system is slow. 243 // 244 // To avoid feeling laggy, if we've just started smooth scrolling we pretend 245 // we've already accelerated for one ideal interval, so that we'll scroll at 246 // least some distance immediately. 247 double lastScrollInterval = data->m_currentVelocity ? (WTF::currentTime() - data->m_lastAnimationTime) : animationTimerDelay; 248 249 // Figure out how far we've actually traveled and update our current 250 // velocity. 251 float distanceTraveled; 252 if (data->m_currentVelocity < data->m_desiredVelocity) { 253 // We accelerate at a constant rate until we reach the desired velocity. 254 float accelerationRate = data->m_desiredVelocity / accelerationTime(); 255 256 // Figure out whether contant acceleration has caused us to reach our 257 // target velocity. 258 float potentialVelocityChange = accelerationRate * lastScrollInterval; 259 float potentialNewVelocity = data->m_currentVelocity + potentialVelocityChange; 260 if (potentialNewVelocity > data->m_desiredVelocity) { 261 // We reached the target velocity at some point between our last 262 // update and now. The distance traveled can be calculated in two 263 // pieces: the distance traveled while accelerating, and the 264 // distance traveled after reaching the target velocity. 265 float actualVelocityChange = data->m_desiredVelocity - data->m_currentVelocity; 266 float accelerationInterval = actualVelocityChange / accelerationRate; 267 // The distance traveled under constant acceleration is the area 268 // under a line segment with a constant rising slope. Break this 269 // into a triangular portion atop a rectangular portion and sum. 270 distanceTraveled = ((data->m_currentVelocity + (actualVelocityChange / 2)) * accelerationInterval); 271 // The distance traveled at the target velocity is simply 272 // (target velocity) * (remaining time after accelerating). 273 distanceTraveled += (data->m_desiredVelocity * (lastScrollInterval - accelerationInterval)); 274 data->m_currentVelocity = data->m_desiredVelocity; 275 } else { 276 // Constant acceleration through the entire time interval. 277 distanceTraveled = (data->m_currentVelocity + (potentialVelocityChange / 2)) * lastScrollInterval; 278 data->m_currentVelocity = potentialNewVelocity; 279 } 280 } else { 281 // We've already reached the target velocity, so the distance we've 282 // traveled is simply (current velocity) * (elapsed time). 283 distanceTraveled = data->m_currentVelocity * lastScrollInterval; 284 // If our desired velocity has decreased, drop the current velocity too. 285 data->m_currentVelocity = data->m_desiredVelocity; 286 } 287 288 // Now update the scroll position based on the distance traveled. 289 if (distanceTraveled >= fabs(data->m_desiredPos - *data->m_currentPos)) { 290 // We've traveled far enough to reach the desired position. Stop smooth 291 // scrolling. 292 *data->m_currentPos = data->m_desiredPos; 293 data->m_currentVelocity = 0; 294 data->m_desiredVelocity = 0; 295 } else { 296 // Not yet at the target position. Travel towards it and set up the 297 // next update. 298 if (*data->m_currentPos > data->m_desiredPos) 299 distanceTraveled = -distanceTraveled; 300 *data->m_currentPos += distanceTraveled; 301 data->m_animationTimer.startOneShot(animationTimerDelay); 302 data->m_lastAnimationTime = WTF::currentTime(); 303 } 304 305 notityPositionChanged(); 306 } 307 308 } // namespace WebCore 309 310 #endif // ENABLE(SMOOTH_SCROLLING) 311