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
create(ScrollableArea * scrollableArea)46 PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea)
47 {
48 return adoptPtr(new ScrollAnimatorWin(scrollableArea));
49 }
50
51 const double ScrollAnimatorWin::animationTimerDelay = 0.01;
52
PerAxisData(ScrollAnimatorWin * parent,float * currentPos)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
ScrollAnimatorWin(ScrollableArea * scrollableArea)64 ScrollAnimatorWin::ScrollAnimatorWin(ScrollableArea* scrollableArea)
65 : ScrollAnimator(scrollableArea)
66 , m_horizontalData(this, &m_currentPosX)
67 , m_verticalData(this, &m_currentPosY)
68 {
69 }
70
~ScrollAnimatorWin()71 ScrollAnimatorWin::~ScrollAnimatorWin()
72 {
73 stopAnimationTimerIfNeeded(&m_horizontalData);
74 stopAnimationTimerIfNeeded(&m_verticalData);
75 }
76
scroll(ScrollbarOrientation orientation,ScrollGranularity granularity,float step,float multiplier)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
scrollToOffsetWithoutAnimation(const FloatPoint & offset)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
accelerationTime()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
animationTimerFired(Timer<ScrollAnimatorWin> * timer)208 void ScrollAnimatorWin::animationTimerFired(Timer<ScrollAnimatorWin>* timer)
209 {
210 animateScroll((timer == &m_horizontalData.m_animationTimer) ? &m_horizontalData : &m_verticalData);
211 }
212
stopAnimationTimerIfNeeded(PerAxisData * data)213 void ScrollAnimatorWin::stopAnimationTimerIfNeeded(PerAxisData* data)
214 {
215 if (data->m_animationTimer.isActive())
216 data->m_animationTimer.stop();
217 }
218
animateScroll(PerAxisData * data)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