1/*
2 * Copyright (c) 2011, 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 "ScrollAnimatorNone.h"
36
37#include "FloatPoint.h"
38#include "NotImplemented.h"
39#include "ScrollableArea.h"
40#include "ScrollbarTheme.h"
41#include <algorithm>
42#include <wtf/CurrentTime.h>
43#include <wtf/PassOwnPtr.h>
44
45using namespace std;
46
47namespace WebCore {
48
49const double kFrameRate = 60;
50const double kTickTime = 1 / kFrameRate;
51const double kMinimumTimerInterval = .001;
52const double kZoomTicks = 11;
53
54PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea)
55{
56    if (scrollableArea && scrollableArea->scrollAnimatorEnabled())
57        return adoptPtr(new ScrollAnimatorNone(scrollableArea));
58    return adoptPtr(new ScrollAnimator(scrollableArea));
59}
60
61ScrollAnimatorNone::Parameters::Parameters()
62    : m_isEnabled(false)
63{
64}
65
66ScrollAnimatorNone::Parameters::Parameters(bool isEnabled, double animationTime, double repeatMinimumSustainTime, Curve attackCurve, double attackTime, Curve releaseCurve, double releaseTime, Curve coastTimeCurve, double maximumCoastTime)
67    : m_isEnabled(isEnabled)
68    , m_animationTime(animationTime)
69    , m_repeatMinimumSustainTime(repeatMinimumSustainTime)
70    , m_attackCurve(attackCurve)
71    , m_attackTime(attackTime)
72    , m_releaseCurve(releaseCurve)
73    , m_releaseTime(releaseTime)
74    , m_coastTimeCurve(coastTimeCurve)
75    , m_maximumCoastTime(maximumCoastTime)
76{
77}
78
79double ScrollAnimatorNone::PerAxisData::curveAt(Curve curve, double t)
80{
81    switch (curve) {
82    case Linear:
83        return t;
84    case Quadratic:
85        return t * t;
86    case Cubic:
87        return t * t * t;
88    case Quartic:
89        return t * t * t * t;
90    case Bounce:
91        // Time base is chosen to keep the bounce points simpler:
92        // 1 (half bounce coming in) + 1 + .5 + .25
93        const double kTimeBase = 2.75;
94        const double kTimeBaseSquared = kTimeBase * kTimeBase;
95        if (t < 1 / kTimeBase)
96            return kTimeBaseSquared * t * t;
97        if (t < 2 / kTimeBase) {
98            // Invert a [-.5,.5] quadratic parabola, center it in [1,2].
99            double t1 = t - 1.5 / kTimeBase;
100            const double kParabolaAtEdge = 1 - .5 * .5;
101            return kTimeBaseSquared * t1 * t1 + kParabolaAtEdge;
102        }
103        if (t < 2.5 / kTimeBase) {
104            // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5].
105            double t2 = t - 2.25 / kTimeBase;
106            const double kParabolaAtEdge = 1 - .25 * .25;
107            return kTimeBaseSquared * t2 * t2 + kParabolaAtEdge;
108        }
109            // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75].
110        const double kParabolaAtEdge = 1 - .125 * .125;
111        t -= 2.625 / kTimeBase;
112        return kTimeBaseSquared * t * t + kParabolaAtEdge;
113    }
114    ASSERT_NOT_REACHED();
115    return 0;
116}
117
118double ScrollAnimatorNone::PerAxisData::attackCurve(Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition)
119{
120    double t = deltaTime / curveT;
121    double positionFactor = curveAt(curve, t);
122    return startPosition + positionFactor * (attackPosition - startPosition);
123}
124
125double ScrollAnimatorNone::PerAxisData::releaseCurve(Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition)
126{
127    double t = deltaTime / curveT;
128    double positionFactor = 1 - curveAt(curve, 1 - t);
129    return releasePosition + (positionFactor * (desiredPosition - releasePosition));
130}
131
132double ScrollAnimatorNone::PerAxisData::coastCurve(Curve curve, double factor)
133{
134    return 1 - curveAt(curve, 1 - factor);
135}
136
137double ScrollAnimatorNone::PerAxisData::curveIntegralAt(Curve curve, double t)
138{
139    switch (curve) {
140    case Linear:
141        return t * t / 2;
142    case Quadratic:
143        return t * t * t / 3;
144    case Cubic:
145        return t * t * t * t / 4;
146    case Quartic:
147        return t * t * t * t * t / 5;
148    case Bounce:
149        const double kTimeBase = 2.75;
150        const double kTimeBaseSquared = kTimeBase * kTimeBase;
151        const double kTimeBaseSquaredOverThree = kTimeBaseSquared / 3;
152        double area;
153        double t1 = min(t, 1 / kTimeBase);
154        area = kTimeBaseSquaredOverThree * t1 * t1 * t1;
155        if (t < 1 / kTimeBase)
156            return area;
157
158        t1 = min(t - 1 / kTimeBase, 1 / kTimeBase);
159        // The integral of kTimeBaseSquared * (t1 - .5 / kTimeBase) * (t1 - .5 / kTimeBase) + kParabolaAtEdge
160        const double kSecondInnerOffset = kTimeBaseSquared * .5 / kTimeBase;
161        double bounceArea = t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kSecondInnerOffset) + 1);
162        area += bounceArea;
163        if (t < 2 / kTimeBase)
164            return area;
165
166        t1 = min(t - 2 / kTimeBase, 0.5 / kTimeBase);
167        // The integral of kTimeBaseSquared * (t1 - .25 / kTimeBase) * (t1 - .25 / kTimeBase) + kParabolaAtEdge
168        const double kThirdInnerOffset = kTimeBaseSquared * .25 / kTimeBase;
169        bounceArea =  t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kThirdInnerOffset) + 1);
170        area += bounceArea;
171        if (t < 2.5 / kTimeBase)
172            return area;
173
174        t1 = t - 2.5 / kTimeBase;
175        // The integral of kTimeBaseSquared * (t1 - .125 / kTimeBase) * (t1 - .125 / kTimeBase) + kParabolaAtEdge
176        const double kFourthInnerOffset = kTimeBaseSquared * .125 / kTimeBase;
177        bounceArea = t1 * (t1 * (kTimeBaseSquaredOverThree * t1 - kFourthInnerOffset) + 1);
178        area += bounceArea;
179        return area;
180    }
181    ASSERT_NOT_REACHED();
182    return 0;
183}
184
185double ScrollAnimatorNone::PerAxisData::attackArea(Curve curve, double startT, double endT)
186{
187    double startValue = curveIntegralAt(curve, startT);
188    double endValue = curveIntegralAt(curve, endT);
189    return endValue - startValue;
190}
191
192double ScrollAnimatorNone::PerAxisData::releaseArea(Curve curve, double startT, double endT)
193{
194    double startValue = curveIntegralAt(curve, 1 - endT);
195    double endValue = curveIntegralAt(curve, 1 - startT);
196    return endValue - startValue;
197}
198
199ScrollAnimatorNone::PerAxisData::PerAxisData(ScrollAnimatorNone* parent, float* currentPosition, int visibleLength)
200    : m_currentPosition(currentPosition)
201    , m_visibleLength(visibleLength)
202{
203    reset();
204}
205
206void ScrollAnimatorNone::PerAxisData::reset()
207{
208    m_currentVelocity = 0;
209
210    m_desiredPosition = 0;
211    m_desiredVelocity = 0;
212
213    m_startPosition = 0;
214    m_startTime = 0;
215    m_startVelocity = 0;
216
217    m_animationTime = 0;
218    m_lastAnimationTime = 0;
219
220    m_attackPosition = 0;
221    m_attackTime = 0;
222    m_attackCurve = Quadratic;
223
224    m_releasePosition = 0;
225    m_releaseTime = 0;
226    m_releaseCurve = Quadratic;
227}
228
229
230bool ScrollAnimatorNone::PerAxisData::updateDataFromParameters(float step, float multiplier, float scrollableSize, double currentTime, Parameters* parameters)
231{
232    float delta = step * multiplier;
233    if (!m_startTime || !delta || (delta < 0) != (m_desiredPosition - *m_currentPosition < 0)) {
234        m_desiredPosition = *m_currentPosition;
235        m_startTime = 0;
236    }
237    float newPosition = m_desiredPosition + delta;
238
239    if (newPosition < 0 || newPosition > scrollableSize)
240        newPosition = max(min(newPosition, scrollableSize), 0.0f);
241
242    if (newPosition == m_desiredPosition)
243        return false;
244
245    m_desiredPosition = newPosition;
246
247    if (!m_startTime) {
248        m_attackTime = parameters->m_attackTime;
249        m_attackCurve = parameters->m_attackCurve;
250    }
251    m_animationTime = parameters->m_animationTime;
252    m_releaseTime = parameters->m_releaseTime;
253    m_releaseCurve = parameters->m_releaseCurve;
254
255    // Prioritize our way out of over constraint.
256    if (m_attackTime + m_releaseTime > m_animationTime) {
257        if (m_releaseTime > m_animationTime)
258            m_releaseTime = m_animationTime;
259        m_attackTime = m_animationTime - m_releaseTime;
260    }
261
262    if (!m_startTime) {
263        // FIXME: This should be the time from the event that got us here.
264        m_startTime = currentTime - kTickTime / 2;
265        m_startPosition = *m_currentPosition;
266        m_lastAnimationTime = m_startTime;
267    }
268    m_startVelocity = m_currentVelocity;
269
270    double remainingDelta = m_desiredPosition - *m_currentPosition;
271
272    double attackAreaLeft = 0;
273
274    double deltaTime = m_lastAnimationTime - m_startTime;
275    double attackTimeLeft = max(0., m_attackTime - deltaTime);
276    double timeLeft = m_animationTime - deltaTime;
277    double minTimeLeft = m_releaseTime + min(parameters->m_repeatMinimumSustainTime, m_animationTime - m_releaseTime - attackTimeLeft);
278    if (timeLeft < minTimeLeft) {
279        m_animationTime = deltaTime + minTimeLeft;
280        timeLeft = minTimeLeft;
281    }
282
283    if (parameters->m_maximumCoastTime > (parameters->m_repeatMinimumSustainTime + parameters->m_releaseTime)) {
284        double targetMaxCoastVelocity = m_visibleLength * .25 * kFrameRate;
285        // This needs to be as minimal as possible while not being intrusive to page up/down.
286        double minCoastDelta = m_visibleLength;
287
288        if (fabs(remainingDelta) > minCoastDelta) {
289            double maxCoastDelta = parameters->m_maximumCoastTime * targetMaxCoastVelocity;
290            double coastFactor = min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta));
291
292            // 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.
293            double coastMinTimeLeft = min(parameters->m_maximumCoastTime, minTimeLeft + coastCurve(parameters->m_coastTimeCurve, coastFactor) * (parameters->m_maximumCoastTime - minTimeLeft));
294
295            double additionalTime = max(0., coastMinTimeLeft - minTimeLeft);
296            if (additionalTime) {
297                double additionalReleaseTime = min(additionalTime, parameters->m_releaseTime / (parameters->m_releaseTime + parameters->m_repeatMinimumSustainTime) * additionalTime);
298                m_releaseTime = parameters->m_releaseTime + additionalReleaseTime;
299                m_animationTime = deltaTime + coastMinTimeLeft;
300                timeLeft = coastMinTimeLeft;
301            }
302        }
303    }
304
305    double releaseTimeLeft = min(timeLeft, m_releaseTime);
306    double sustainTimeLeft = max(0., timeLeft - releaseTimeLeft - attackTimeLeft);
307
308    if (attackTimeLeft) {
309        double attackSpot = deltaTime / m_attackTime;
310        attackAreaLeft = attackArea(m_attackCurve, attackSpot, 1) * m_attackTime;
311    }
312
313    double releaseSpot = (m_releaseTime - releaseTimeLeft) / m_releaseTime;
314    double releaseAreaLeft  = releaseArea(m_releaseCurve, releaseSpot, 1) * m_releaseTime;
315
316    m_desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft + releaseAreaLeft);
317    m_releasePosition = m_desiredPosition - m_desiredVelocity * releaseAreaLeft;
318    if (attackAreaLeft)
319        m_attackPosition = m_startPosition + m_desiredVelocity * attackAreaLeft;
320    else
321        m_attackPosition = m_releasePosition - (m_animationTime - m_releaseTime - m_attackTime) * m_desiredVelocity;
322
323    if (sustainTimeLeft) {
324        double roundOff = m_releasePosition - ((attackAreaLeft ? m_attackPosition : *m_currentPosition) + m_desiredVelocity * sustainTimeLeft);
325        m_desiredVelocity += roundOff / sustainTimeLeft;
326    }
327
328    return true;
329}
330
331// FIXME: Add in jank detection trace events into this function.
332bool ScrollAnimatorNone::PerAxisData::animateScroll(double currentTime)
333{
334    double lastScrollInterval = currentTime - m_lastAnimationTime;
335    if (lastScrollInterval < kMinimumTimerInterval)
336        return true;
337
338    m_lastAnimationTime = currentTime;
339
340    double deltaTime = currentTime - m_startTime;
341    double newPosition = *m_currentPosition;
342
343    if (deltaTime > m_animationTime) {
344        *m_currentPosition = m_desiredPosition;
345        reset();
346        return false;
347    }
348    if (deltaTime < m_attackTime)
349        newPosition = attackCurve(m_attackCurve, deltaTime, m_attackTime, m_startPosition, m_attackPosition);
350    else if (deltaTime < (m_animationTime - m_releaseTime))
351        newPosition = m_attackPosition + (deltaTime - m_attackTime) * m_desiredVelocity;
352    else {
353        // release is based on targeting the exact final position.
354        double releaseDeltaT = deltaTime - (m_animationTime - m_releaseTime);
355        newPosition = releaseCurve(m_releaseCurve, releaseDeltaT, m_releaseTime, m_releasePosition, m_desiredPosition);
356    }
357
358    // Normalize velocity to a per second amount. Could be used to check for jank.
359    if (lastScrollInterval > 0)
360        m_currentVelocity = (newPosition - *m_currentPosition) / lastScrollInterval;
361    *m_currentPosition = newPosition;
362
363    return true;
364}
365
366void ScrollAnimatorNone::PerAxisData::updateVisibleLength(int visibleLength)
367{
368    m_visibleLength = visibleLength;
369}
370
371ScrollAnimatorNone::ScrollAnimatorNone(ScrollableArea* scrollableArea)
372    : ScrollAnimator(scrollableArea)
373    , m_horizontalData(this, &m_currentPosX, scrollableArea->visibleWidth())
374    , m_verticalData(this, &m_currentPosY, scrollableArea->visibleHeight())
375    , m_startTime(0)
376#if USE(REQUEST_ANIMATION_FRAME_TIMER)
377    , m_animationTimer(this, &ScrollAnimatorNone::animationTimerFired)
378#else
379    , m_animationActive(false)
380#endif
381{
382}
383
384ScrollAnimatorNone::~ScrollAnimatorNone()
385{
386    stopAnimationTimerIfNeeded();
387}
388
389ScrollAnimatorNone::Parameters ScrollAnimatorNone::parametersForScrollGranularity(ScrollGranularity granularity) const
390{
391    switch (granularity) {
392    case ScrollByDocument:
393        return Parameters(true, 20 * kTickTime, 10 * kTickTime, Cubic, 10 * kTickTime, Cubic, 10 * kTickTime, Linear, 1);
394    case ScrollByLine:
395        return Parameters(true, 10 * kTickTime, 7 * kTickTime, Cubic, 3 * kTickTime, Cubic, 3 * kTickTime, Linear, 1);
396    case ScrollByPage:
397        return Parameters(true, 15 * kTickTime, 10 * kTickTime, Cubic, 5 * kTickTime, Cubic, 5 * kTickTime, Linear, 1);
398    case ScrollByPixel:
399        return Parameters(true, 11 * kTickTime, 2 * kTickTime, Cubic, 3 * kTickTime, Cubic, 3 * kTickTime, Quadratic, 1.25);
400    default:
401        ASSERT_NOT_REACHED();
402    }
403    return Parameters();
404}
405
406bool ScrollAnimatorNone::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier)
407{
408    if (!m_scrollableArea->scrollAnimatorEnabled())
409        return ScrollAnimator::scroll(orientation, granularity, step, multiplier);
410
411    // FIXME: get the type passed in. MouseWheel could also be by line, but should still have different
412    // animation parameters than the keyboard.
413    Parameters parameters;
414    switch (granularity) {
415    case ScrollByDocument:
416    case ScrollByLine:
417    case ScrollByPage:
418    case ScrollByPixel:
419        parameters = parametersForScrollGranularity(granularity);
420        break;
421    case ScrollByPrecisePixel:
422        return ScrollAnimator::scroll(orientation, granularity, step, multiplier);
423    }
424
425    // If the individual input setting is disabled, bail.
426    if (!parameters.m_isEnabled)
427        return ScrollAnimator::scroll(orientation, granularity, step, multiplier);
428
429    // This is an animatable scroll. Set the animation in motion using the appropriate parameters.
430    float scrollableSize = static_cast<float>(m_scrollableArea->scrollSize(orientation));
431
432    PerAxisData& data = (orientation == VerticalScrollbar) ? m_verticalData : m_horizontalData;
433    bool needToScroll = data.updateDataFromParameters(step, multiplier, scrollableSize, monotonicallyIncreasingTime(), &parameters);
434    if (needToScroll && !animationTimerActive()) {
435        m_startTime = data.m_startTime;
436        animationWillStart();
437        animationTimerFired();
438    }
439    return needToScroll;
440}
441
442void ScrollAnimatorNone::scrollToOffsetWithoutAnimation(const FloatPoint& offset)
443{
444    stopAnimationTimerIfNeeded();
445
446    FloatSize delta = FloatSize(offset.x() - *m_horizontalData.m_currentPosition, offset.y() - *m_verticalData.m_currentPosition);
447
448    m_horizontalData.reset();
449    *m_horizontalData.m_currentPosition = offset.x();
450    m_horizontalData.m_desiredPosition = offset.x();
451
452    m_verticalData.reset();
453    *m_verticalData.m_currentPosition = offset.y();
454    m_verticalData.m_desiredPosition = offset.y();
455
456    notifyPositionChanged(delta);
457}
458
459#if !USE(REQUEST_ANIMATION_FRAME_TIMER)
460void ScrollAnimatorNone::cancelAnimations()
461{
462    m_animationActive = false;
463}
464
465void ScrollAnimatorNone::serviceScrollAnimations()
466{
467    if (m_animationActive)
468        animationTimerFired();
469}
470#endif
471
472void ScrollAnimatorNone::willEndLiveResize()
473{
474    updateVisibleLengths();
475}
476
477void ScrollAnimatorNone::didAddVerticalScrollbar(Scrollbar*)
478{
479    updateVisibleLengths();
480}
481
482void ScrollAnimatorNone::didAddHorizontalScrollbar(Scrollbar*)
483{
484    updateVisibleLengths();
485}
486
487void ScrollAnimatorNone::updateVisibleLengths()
488{
489    m_horizontalData.updateVisibleLength(scrollableArea()->visibleWidth());
490    m_verticalData.updateVisibleLength(scrollableArea()->visibleHeight());
491}
492
493#if USE(REQUEST_ANIMATION_FRAME_TIMER)
494void ScrollAnimatorNone::animationTimerFired(Timer<ScrollAnimatorNone>* timer)
495{
496    animationTimerFired();
497}
498#endif
499
500void ScrollAnimatorNone::animationTimerFired()
501{
502    double currentTime = monotonicallyIncreasingTime();
503    double deltaToNextFrame = ceil((currentTime - m_startTime) * kFrameRate) / kFrameRate - (currentTime - m_startTime);
504    currentTime += deltaToNextFrame;
505
506    bool continueAnimation = false;
507    if (m_horizontalData.m_startTime && m_horizontalData.animateScroll(currentTime))
508        continueAnimation = true;
509    if (m_verticalData.m_startTime && m_verticalData.animateScroll(currentTime))
510        continueAnimation = true;
511
512    if (continueAnimation)
513#if USE(REQUEST_ANIMATION_FRAME_TIMER)
514        startNextTimer(max(kMinimumTimerInterval, deltaToNextFrame));
515#else
516        startNextTimer();
517    else
518        m_animationActive = false;
519#endif
520
521    notifyPositionChanged(FloatSize());
522
523    if (!continueAnimation)
524        animationDidFinish();
525}
526
527#if USE(REQUEST_ANIMATION_FRAME_TIMER)
528void ScrollAnimatorNone::startNextTimer(double delay)
529{
530    m_animationTimer.startOneShot(delay);
531}
532#else
533void ScrollAnimatorNone::startNextTimer()
534{
535    if (scrollableArea()->scheduleAnimation())
536        m_animationActive = true;
537}
538#endif
539
540bool ScrollAnimatorNone::animationTimerActive()
541{
542#if USE(REQUEST_ANIMATION_FRAME_TIMER)
543    return m_animationTimer.isActive();
544#else
545    return m_animationActive;
546#endif
547}
548
549void ScrollAnimatorNone::stopAnimationTimerIfNeeded()
550{
551    if (animationTimerActive())
552#if USE(REQUEST_ANIMATION_FRAME_TIMER)
553        m_animationTimer.stop();
554#else
555        m_animationActive = false;
556#endif
557}
558
559} // namespace WebCore
560
561#endif // ENABLE(SMOOTH_SCROLLING)
562