ScrollSnapAnimatorState.mm   [plain text]


/*
 * Copyright (C) 2014-2015 Apple 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:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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"
#include "ScrollSnapAnimatorState.h"
#include <wtf/CurrentTime.h>

#if ENABLE(CSS_SCROLL_SNAP)

namespace WebCore {

ScrollSnapAnimatorState::ScrollSnapAnimatorState(ScrollEventAxis axis, const Vector<LayoutUnit>& snapOffsets)
    : m_snapOffsets(snapOffsets)
    , m_axis(axis)
    , m_currentState(ScrollSnapState::DestinationReached)
    , m_initialOffset(0)
    , m_targetOffset(0)
    , m_beginTrackingWheelDeltaOffset(0)
{
}

void ScrollSnapAnimatorState::pushInitialWheelDelta(float wheelDelta)
{
    if (m_numWheelDeltasTracked < wheelDeltaWindowSize)
        m_wheelDeltaWindow[m_numWheelDeltasTracked++] = wheelDelta;
}

float ScrollSnapAnimatorState::averageInitialWheelDelta() const
{
    if (!m_numWheelDeltasTracked)
        return 0;

    float sum = 0;
    int numZeroDeltas = 0;
    for (int i = 0; i < m_numWheelDeltasTracked; ++i) {
        sum += m_wheelDeltaWindow[i];
        if (!m_wheelDeltaWindow[i])
            numZeroDeltas++;
    }

    return m_numWheelDeltasTracked == numZeroDeltas ? 0 : sum / (m_numWheelDeltasTracked - numZeroDeltas);
}

void ScrollSnapAnimatorState::clearInitialWheelDeltaWindow()
{
    for (int i = 0; i < m_numWheelDeltasTracked; ++i)
        m_wheelDeltaWindow[i] = 0;

    m_numWheelDeltasTracked = 0;
}

bool ScrollSnapAnimatorState::isSnapping() const
{
    return m_currentState == ScrollSnapState::Gliding || m_currentState == ScrollSnapState::Snapping;
}
    
bool ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta() const
{
    if (m_initialOffset == m_targetOffset || !m_initialScrollDelta)
        return true;
    
    return m_initialOffset < m_targetOffset ? m_initialScrollDelta > 0 : m_initialScrollDelta < 0;
}
    
float ScrollSnapAnimatorState::interpolatedOffsetAtProgress(float progress) const
{
    progress = std::max(0.0f, std::min(1.0f, progress));
    return m_initialOffset + progress * (m_targetOffset - m_initialOffset);
}
    
static const int maxNumScrollSnapParameterEstimationIterations = 10;
static const float scrollSnapDecayFactorConvergenceThreshold = 0.001;
static const float initialScrollSnapCurveMagnitude = 1.1;
static const float minScrollSnapInitialProgress = 0.15;
static const float maxScrollSnapInitialProgress = 0.5;
static const double scrollSnapAnimationDuration = 0.5;

/**
 * Computes and sets parameters required for tracking the progress of a snap animation curve, interpolated
 * or linear. The progress curve s(t) maps time t to progress s; both variables are in the interval [0, 1].
 * The time input t is 0 when the current time is the start of the animation, t = m_startTime, and 1 when the
 * current time is at or after the end of the animation, t = m_startTime + m_scrollSnapAnimationDuration.
 *
 * In this exponential progress model, s(t) = A - A * b^(-kt), where k = 60T is the number of frames in the
 * animation (assuming 60 FPS and an animation duration of T) and A, b are reals greater than or equal to 1.
 * Also note that we are given the initial progress, a value indicating the portion of the curve which our
 * initial scroll delta takes us. This is important when matching the initial speed of the animation to the
 * user's initial momentum scrolling speed. Let this initial progress amount equal v_0. I clamp this initial
 * progress amount to a minimum or maximum value.
 *
 * A is referred to as the curve magnitude, while b is referred to as the decay factor. We solve for A and b,
 * keeping the following constraints in mind:
 *     1. s(0) = 0
 *     2. s(1) = 1
 *     3. s(1/k) = v_0
 *
 * First, observe that s(0) = 0 holds for appropriate values of A, b. Solving for the remaining constraints
 * yields a nonlinear system of two equations. In lieu of a purely analytical solution, an alternating
 * optimization scheme is used to approximate A and b. This technique converges quickly (within 5 iterations
 * or so) for appropriate values of v_0. The optimization terminates early when the decay factor changes by
 * less than a threshold between one iteration and the next.
 */
void ScrollSnapAnimationCurveState::initializeSnapProgressCurve(const FloatSize& initialVector, const FloatSize& targetVector, const FloatSize& initialDelta)
{
    float initialProgress = std::max(minScrollSnapInitialProgress, std::min(initialDelta.diagonalLength() / (targetVector - initialVector).diagonalLength(), maxScrollSnapInitialProgress));
    float previousDecayFactor = 1.0f;
    m_snapAnimationCurveMagnitude = initialScrollSnapCurveMagnitude;
    for (int i = 0; i < maxNumScrollSnapParameterEstimationIterations; ++i) {
        m_snapAnimationDecayFactor = m_snapAnimationCurveMagnitude / (m_snapAnimationCurveMagnitude - initialProgress);
        m_snapAnimationCurveMagnitude = 1.0f / (1.0f - std::pow(m_snapAnimationDecayFactor, -60.0f * scrollSnapAnimationDuration));
        if (std::abs(m_snapAnimationDecayFactor - previousDecayFactor) < scrollSnapDecayFactorConvergenceThreshold)
            break;
        
        previousDecayFactor = m_snapAnimationDecayFactor;
    }
    m_startTime = monotonicallyIncreasingTime();
}

/**
 * Computes and sets coefficients required for interpolated snapping when scrolling in 2 dimensions, given
 * initial conditions (the initial and target vectors, along with the initial wheel delta as a vector). The
 * path is a cubic Bezier curve of the form p(s) = INITIAL + (C_1 * s) + (C_2 * s^2) + (C_3 * s^3) where each
 * C_i is a 2D vector and INITIAL is the vector representing the initial scroll offset. s is a real in the
 * interval [0, 1] indicating the "progress" of the curve (i.e. how much of the curve has been traveled).
 *
 * The curve has 4 control points, the first and last of which are the initial and target points, respectively.
 * The distances between adjacent control points are constrained to be the same, making the convex hull an
 * isosceles trapezoid with 3 sides of equal length. Additionally, the vector from the first control point to
 * the second points in the same direction as the initial scroll delta. These constraints ensure two properties:
 *     1. The direction of the snap animation at s=0 will be equal to the direction of the initial scroll delta.
 *     2. Points at regular intervals of s will be evenly spread out.
 *
 * If the initial scroll direction is orthogonal to or points in the opposite direction as the vector from the
 * initial point to the target point, initialization returns early and sets the curve to animate directly to the
 * snap point without interpolation.
 */
void ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary(const FloatSize& initialVector, const FloatSize& targetVector, const FloatSize& initialDelta)
{
    FloatSize startToEndVector = targetVector - initialVector;
    float startToEndDistance = startToEndVector.diagonalLength();
    float initialDeltaMagnitude = initialDelta.diagonalLength();
    float cosTheta = initialDelta.isZero() ? 0 : (initialDelta.width() * startToEndVector.width() + initialDelta.height() * startToEndVector.height()) / (std::max(1.0f, initialDeltaMagnitude) * startToEndDistance);
    if (cosTheta <= 0)
        return;
    
    float sideLength = startToEndDistance / (2.0f * cosTheta + 1.0f);
    FloatSize controlVector1 = initialVector + sideLength * initialDelta / initialDeltaMagnitude;
    FloatSize controlVector2 = controlVector1 + (sideLength * startToEndVector / startToEndDistance);
    m_snapAnimationCurveCoefficients[0] = initialVector;
    m_snapAnimationCurveCoefficients[1] = 3 * (controlVector1 - initialVector);
    m_snapAnimationCurveCoefficients[2] = 3 * (initialVector - 2 * controlVector1 + controlVector2);
    m_snapAnimationCurveCoefficients[3] = 3 * (controlVector1 - controlVector2) - initialVector + targetVector;
    shouldAnimateDirectlyToSnapPoint = false;
}
    
FloatPoint ScrollSnapAnimationCurveState::interpolatedPositionAtProgress(float progress) const
{
    ASSERT(!shouldAnimateDirectlyToSnapPoint);
    progress = std::max(0.0f, std::min(1.0f, progress));
    FloatPoint interpolatedPoint(0.0f, 0.0f);
    for (int i = 0; i < 4; ++i)
        interpolatedPoint += std::pow(progress, i) * m_snapAnimationCurveCoefficients[i];
    
    return interpolatedPoint;
}
    
bool ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime(double time) const
{
    return m_startTime + scrollSnapAnimationDuration < time;
}

float ScrollSnapAnimationCurveState::animationProgressAtTime(double time) const
{
    float timeProgress = std::max(0.0, std::min(1.0, (time - m_startTime) / scrollSnapAnimationDuration));
    return std::min(1.0, m_snapAnimationCurveMagnitude * (1.0 - std::pow(m_snapAnimationDecayFactor, -60.0f * scrollSnapAnimationDuration * timeProgress)));
}
    
} // namespace WebCore

#endif // CSS_SCROLL_SNAP