/* * Copyright (C) 2016 Igalia S.L. * Copyright (C) 2015 Apple Inc. All rights reserved. * Copyright (c) 2011, 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: * 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 "ScrollAnimationSmooth.h" #include "FloatPoint.h" #include "ScrollableArea.h" #if USE(GLIB_EVENT_LOOP) #include #endif namespace WebCore { static const double frameRate = 60; static const Seconds tickTime = 1_s / frameRate; static const Seconds minimumTimerInterval { 1_ms }; static const double smoothFactorForProgrammaticScroll = 1; ScrollAnimationSmooth::PerAxisData::PerAxisData(ScrollbarOrientation orientation, const FloatPoint& position, ScrollExtentsCallback& extentsCallback) { auto extents = extentsCallback(); switch (orientation) { case HorizontalScrollbar: currentPosition = position.x(); visibleLength = extents.visibleSize.width(); break; case VerticalScrollbar: currentPosition = position.y(); visibleLength = extents.visibleSize.height(); break; } desiredPosition = currentPosition; } ScrollAnimationSmooth::ScrollAnimationSmooth(ScrollExtentsCallback&& scrollExtentsFunction, const FloatPoint& position, NotifyPositionChangedCallback&& notifyPositionChangedFunction, NotifyAnimationStoppedCallback&& notifyAnimationStoppedFunction) : m_scrollExtentsFunction(WTFMove(scrollExtentsFunction)) , m_notifyPositionChangedFunction(WTFMove(notifyPositionChangedFunction)) , m_notifyAnimationStoppedFunction(WTFMove(notifyAnimationStoppedFunction)) , m_horizontalData(HorizontalScrollbar, position, m_scrollExtentsFunction) , m_verticalData(VerticalScrollbar, position, m_scrollExtentsFunction) , m_animationTimer(RunLoop::current(), this, &ScrollAnimationSmooth::animationTimerFired) { #if USE(GLIB_EVENT_LOOP) m_animationTimer.setPriority(WTF::RunLoopSourcePriority::DisplayRefreshMonitorTimer); #endif } bool ScrollAnimationSmooth::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier) { float minScrollPosition; float maxScrollPosition; auto extents = m_scrollExtentsFunction(); if (orientation == HorizontalScrollbar) { minScrollPosition = extents.minimumScrollPosition.x(); maxScrollPosition = extents.maximumScrollPosition.x(); } else { minScrollPosition = extents.minimumScrollPosition.y(); maxScrollPosition = extents.maximumScrollPosition.y(); } auto& data = orientation == HorizontalScrollbar ? m_horizontalData : m_verticalData; bool needToScroll = updatePerAxisData(data, granularity, data.desiredPosition + (step * multiplier), minScrollPosition, maxScrollPosition); if (needToScroll && !isActive()) { m_startTime = orientation == HorizontalScrollbar ? m_horizontalData.startTime : m_verticalData.startTime; animationTimerFired(); } return needToScroll; } void ScrollAnimationSmooth::scroll(const FloatPoint& position) { ScrollGranularity granularity = ScrollByPage; auto extents = m_scrollExtentsFunction(); bool needToScroll = updatePerAxisData(m_horizontalData, granularity, position.x(), extents.minimumScrollPosition.x(), extents.maximumScrollPosition.x(), smoothFactorForProgrammaticScroll); needToScroll |= updatePerAxisData(m_verticalData, granularity, position.y(), extents.minimumScrollPosition.y(), extents.maximumScrollPosition.y(), smoothFactorForProgrammaticScroll); if (needToScroll && !isActive()) { m_startTime = m_horizontalData.startTime; animationTimerFired(); } }; void ScrollAnimationSmooth::stop() { m_animationTimer.stop(); m_notifyAnimationStoppedFunction(); } void ScrollAnimationSmooth::updateVisibleLengths() { auto extents = m_scrollExtentsFunction(); m_horizontalData.visibleLength = extents.visibleSize.width(); m_verticalData.visibleLength = extents.visibleSize.height(); } void ScrollAnimationSmooth::setCurrentPosition(const FloatPoint& position) { stop(); m_horizontalData = PerAxisData(position.x(), m_horizontalData.visibleLength); m_verticalData = PerAxisData(position.y(), m_verticalData.visibleLength); } ScrollAnimationSmooth::~ScrollAnimationSmooth() = default; static inline double curveAt(ScrollAnimationSmooth::Curve curve, double t) { switch (curve) { case ScrollAnimationSmooth::Curve::Linear: return t; case ScrollAnimationSmooth::Curve::Quadratic: return t * t; case ScrollAnimationSmooth::Curve::Cubic: return t * t * t; case ScrollAnimationSmooth::Curve::Quartic: return t * t * t * t; case ScrollAnimationSmooth::Curve::Bounce: // Time base is chosen to keep the bounce points simpler: // 1 (half bounce coming in) + 1 + .5 + .25 static const double timeBase = 2.75; static const double timeBaseSquared = timeBase * timeBase; if (t < 1 / timeBase) return timeBaseSquared * t * t; if (t < 2 / timeBase) { // Invert a [-.5,.5] quadratic parabola, center it in [1,2]. double t1 = t - 1.5 / timeBase; const double parabolaAtEdge = 1 - .5 * .5; return timeBaseSquared * t1 * t1 + parabolaAtEdge; } if (t < 2.5 / timeBase) { // Invert a [-.25,.25] quadratic parabola, center it in [2,2.5]. double t2 = t - 2.25 / timeBase; const double parabolaAtEdge = 1 - .25 * .25; return timeBaseSquared * t2 * t2 + parabolaAtEdge; } // Invert a [-.125,.125] quadratic parabola, center it in [2.5,2.75]. const double parabolaAtEdge = 1 - .125 * .125; t -= 2.625 / timeBase; return timeBaseSquared * t * t + parabolaAtEdge; } ASSERT_NOT_REACHED(); return 0; } static inline double attackCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double startPosition, double attackPosition) { double t = deltaTime / curveT; double positionFactor = curveAt(curve, t); return startPosition + positionFactor * (attackPosition - startPosition); } static inline double releaseCurve(ScrollAnimationSmooth::Curve curve, double deltaTime, double curveT, double releasePosition, double desiredPosition) { double t = deltaTime / curveT; double positionFactor = 1 - curveAt(curve, 1 - t); return releasePosition + (positionFactor * (desiredPosition - releasePosition)); } static inline double coastCurve(ScrollAnimationSmooth::Curve curve, double factor) { return 1 - curveAt(curve, 1 - factor); } static inline double curveIntegralAt(ScrollAnimationSmooth::Curve curve, double t) { switch (curve) { case ScrollAnimationSmooth::Curve::Linear: return t * t / 2; case ScrollAnimationSmooth::Curve::Quadratic: return t * t * t / 3; case ScrollAnimationSmooth::Curve::Cubic: return t * t * t * t / 4; case ScrollAnimationSmooth::Curve::Quartic: return t * t * t * t * t / 5; case ScrollAnimationSmooth::Curve::Bounce: static const double timeBase = 2.75; static const double timeBaseSquared = timeBase * timeBase; static const double timeBaseSquaredOverThree = timeBaseSquared / 3; double area; double t1 = std::min(t, 1 / timeBase); area = timeBaseSquaredOverThree * t1 * t1 * t1; if (t < 1 / timeBase) return area; t1 = std::min(t - 1 / timeBase, 1 / timeBase); // The integral of timeBaseSquared * (t1 - .5 / timeBase) * (t1 - .5 / timeBase) + parabolaAtEdge static const double secondInnerOffset = timeBaseSquared * .5 / timeBase; double bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - secondInnerOffset) + 1); area += bounceArea; if (t < 2 / timeBase) return area; t1 = std::min(t - 2 / timeBase, 0.5 / timeBase); // The integral of timeBaseSquared * (t1 - .25 / timeBase) * (t1 - .25 / timeBase) + parabolaAtEdge static const double thirdInnerOffset = timeBaseSquared * .25 / timeBase; bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - thirdInnerOffset) + 1); area += bounceArea; if (t < 2.5 / timeBase) return area; t1 = t - 2.5 / timeBase; // The integral of timeBaseSquared * (t1 - .125 / timeBase) * (t1 - .125 / timeBase) + parabolaAtEdge static const double fourthInnerOffset = timeBaseSquared * .125 / timeBase; bounceArea = t1 * (t1 * (timeBaseSquaredOverThree * t1 - fourthInnerOffset) + 1); area += bounceArea; return area; } ASSERT_NOT_REACHED(); return 0; } static inline double attackArea(ScrollAnimationSmooth::Curve curve, double startT, double endT) { double startValue = curveIntegralAt(curve, startT); double endValue = curveIntegralAt(curve, endT); return endValue - startValue; } static inline double releaseArea(ScrollAnimationSmooth::Curve curve, double startT, double endT) { double startValue = curveIntegralAt(curve, 1 - endT); double endValue = curveIntegralAt(curve, 1 - startT); return endValue - startValue; } static inline void getAnimationParametersForGranularity(ScrollGranularity granularity, Seconds& animationTime, Seconds& repeatMinimumSustainTime, Seconds& attackTime, Seconds& releaseTime, ScrollAnimationSmooth::Curve& coastTimeCurve, Seconds& maximumCoastTime) { switch (granularity) { case ScrollByDocument: animationTime = tickTime * 10; repeatMinimumSustainTime = tickTime * 10; attackTime = tickTime * 10; releaseTime = tickTime * 10; coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; maximumCoastTime = 1_s; break; case ScrollByLine: animationTime = tickTime * 10; repeatMinimumSustainTime = tickTime * 7; attackTime = tickTime * 3; releaseTime = tickTime * 3; coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; maximumCoastTime = 1_s; break; case ScrollByPage: animationTime = tickTime * 15; repeatMinimumSustainTime = tickTime * 10; attackTime = tickTime * 5; releaseTime = tickTime * 5; coastTimeCurve = ScrollAnimationSmooth::Curve::Linear; maximumCoastTime = 1_s; break; case ScrollByPixel: animationTime = tickTime * 11; repeatMinimumSustainTime = tickTime * 2; attackTime = tickTime * 3; releaseTime = tickTime * 3; coastTimeCurve = ScrollAnimationSmooth::Curve::Quadratic; maximumCoastTime = 1250_ms; break; default: ASSERT_NOT_REACHED(); } } bool ScrollAnimationSmooth::updatePerAxisData(PerAxisData& data, ScrollGranularity granularity, float newPosition, float minScrollPosition, float maxScrollPosition, double smoothFactor) { if (!data.startTime || newPosition == data.currentPosition) { data.desiredPosition = data.currentPosition; data.startTime = { }; } newPosition = std::max(std::min(newPosition, maxScrollPosition), minScrollPosition); if (newPosition == data.desiredPosition) return false; Seconds animationTime, repeatMinimumSustainTime, attackTime, releaseTime, maximumCoastTime; Curve coastTimeCurve; getAnimationParametersForGranularity(granularity, animationTime, repeatMinimumSustainTime, attackTime, releaseTime, coastTimeCurve, maximumCoastTime); animationTime *= smoothFactor; repeatMinimumSustainTime *= smoothFactor; attackTime *= smoothFactor; releaseTime *= smoothFactor; maximumCoastTime *= smoothFactor; data.desiredPosition = newPosition; if (!data.startTime) data.attackTime = attackTime; data.animationTime = animationTime; data.releaseTime = releaseTime; // Prioritize our way out of over constraint. if (data.attackTime + data.releaseTime > data.animationTime) { if (data.releaseTime > data.animationTime) data.releaseTime = data.animationTime; data.attackTime = data.animationTime - data.releaseTime; } if (!data.startTime) { // FIXME: This should be the time from the event that got us here. data.startTime = MonotonicTime::now() - tickTime / 2.; data.startPosition = data.currentPosition; data.lastAnimationTime = data.startTime; } data.startVelocity = data.currentVelocity; double remainingDelta = data.desiredPosition - data.currentPosition; double attackAreaLeft = 0; Seconds deltaTime = data.lastAnimationTime - data.startTime; Seconds attackTimeLeft = std::max(0_s, data.attackTime - deltaTime); Seconds timeLeft = data.animationTime - deltaTime; Seconds minTimeLeft = data.releaseTime + std::min(repeatMinimumSustainTime, data.animationTime - data.releaseTime - attackTimeLeft); if (timeLeft < minTimeLeft) { data.animationTime = deltaTime + minTimeLeft; timeLeft = minTimeLeft; } if (maximumCoastTime > (repeatMinimumSustainTime + releaseTime)) { double targetMaxCoastVelocity = data.visibleLength * .25 * frameRate; // This needs to be as minimal as possible while not being intrusive to page up/down. double minCoastDelta = data.visibleLength; if (fabs(remainingDelta) > minCoastDelta) { double maxCoastDelta = maximumCoastTime.value() * targetMaxCoastVelocity; double coastFactor = std::min(1., (fabs(remainingDelta) - minCoastDelta) / (maxCoastDelta - minCoastDelta)); // 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. Seconds coastMinTimeLeft = std::min(maximumCoastTime, minTimeLeft + (maximumCoastTime - minTimeLeft) * coastCurve(coastTimeCurve, coastFactor)); if (Seconds additionalTime = std::max(0_s, coastMinTimeLeft - minTimeLeft)) { Seconds additionalReleaseTime = std::min(additionalTime, additionalTime * (releaseTime / (releaseTime + repeatMinimumSustainTime))); data.releaseTime = releaseTime + additionalReleaseTime; data.animationTime = deltaTime + coastMinTimeLeft; timeLeft = coastMinTimeLeft; } } } Seconds releaseTimeLeft = std::min(timeLeft, data.releaseTime); Seconds sustainTimeLeft = std::max(0_s, timeLeft - releaseTimeLeft - attackTimeLeft); if (attackTimeLeft) { double attackSpot = deltaTime / data.attackTime; attackAreaLeft = attackArea(Curve::Cubic, attackSpot, 1) * data.attackTime.value(); } double releaseSpot = (data.releaseTime - releaseTimeLeft) / data.releaseTime; double releaseAreaLeft = releaseArea(Curve::Cubic, releaseSpot, 1) * data.releaseTime.value(); data.desiredVelocity = remainingDelta / (attackAreaLeft + sustainTimeLeft.value() + releaseAreaLeft); data.releasePosition = data.desiredPosition - data.desiredVelocity * releaseAreaLeft; if (attackAreaLeft) data.attackPosition = data.startPosition + data.desiredVelocity * attackAreaLeft; else data.attackPosition = data.releasePosition - (data.animationTime - data.releaseTime - data.attackTime).value() * data.desiredVelocity; if (sustainTimeLeft) { double roundOff = data.releasePosition - ((attackAreaLeft ? data.attackPosition : data.currentPosition) + data.desiredVelocity * sustainTimeLeft.value()); data.desiredVelocity += roundOff / sustainTimeLeft.value(); } return true; } bool ScrollAnimationSmooth::animateScroll(PerAxisData& data, MonotonicTime currentTime) { if (!data.startTime) return false; Seconds lastScrollInterval = currentTime - data.lastAnimationTime; if (lastScrollInterval < minimumTimerInterval) return true; data.lastAnimationTime = currentTime; Seconds deltaTime = currentTime - data.startTime; double newPosition = data.currentPosition; if (deltaTime > data.animationTime) { data = PerAxisData(data.desiredPosition, data.visibleLength); return false; } if (deltaTime < data.attackTime) newPosition = attackCurve(Curve::Cubic, deltaTime.value(), data.attackTime.value(), data.startPosition, data.attackPosition); else if (deltaTime < (data.animationTime - data.releaseTime)) newPosition = data.attackPosition + (deltaTime - data.attackTime).value() * data.desiredVelocity; else { // release is based on targeting the exact final position. Seconds releaseDeltaT = deltaTime - (data.animationTime - data.releaseTime); newPosition = releaseCurve(Curve::Cubic, releaseDeltaT.value(), data.releaseTime.value(), data.releasePosition, data.desiredPosition); } // Normalize velocity to a per second amount. Could be used to check for jank. if (lastScrollInterval > 0_s) data.currentVelocity = (newPosition - data.currentPosition) / lastScrollInterval.value(); data.currentPosition = newPosition; return true; } void ScrollAnimationSmooth::animationTimerFired() { MonotonicTime currentTime = MonotonicTime::now(); Seconds deltaToNextFrame = 1_s * ceil((currentTime - m_startTime).value() * frameRate) / frameRate - (currentTime - m_startTime); currentTime += deltaToNextFrame; bool continueAnimation = false; if (animateScroll(m_horizontalData, currentTime)) continueAnimation = true; if (animateScroll(m_verticalData, currentTime)) continueAnimation = true; if (continueAnimation) startNextTimer(std::max(minimumTimerInterval, deltaToNextFrame)); else m_notifyAnimationStoppedFunction(); m_notifyPositionChangedFunction(FloatPoint(m_horizontalData.currentPosition, m_verticalData.currentPosition)); } void ScrollAnimationSmooth::startNextTimer(Seconds delay) { m_animationTimer.startOneShot(delay); } bool ScrollAnimationSmooth::isActive() const { return m_animationTimer.isActive(); } } // namespace WebCore