<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[186469] trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/186469">186469</a></dd>
<dt>Author</dt> <dd>commit-queue@webkit.org</dd>
<dt>Date</dt> <dd>2015-07-07 12:12:22 -0700 (Tue, 07 Jul 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Snap point regions containing X and Y snap points should do a better job animating
https://bugs.webkit.org/show_bug.cgi?id=142523
&lt;rdar://problem/20100753&gt;

Patch by Wenson Hsieh &lt;whsieh@berkeley.edu&gt; on 2015-07-07
Reviewed by Brent Fulgham.

Source/WebCore:

Reimplemented snap point animations to use a single timer for both horizontal and
vertical axes to better support 2D snap scrolling. Instead of making velocity
dependent on progress to the snap point and handling 2D snapping with different
timer update functions, this implementation uses a fixed animation time to coordinate
the snapping animation across both axes.

Test: platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html

* page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h: Refactored to use a single scroll snap timer.
* page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm: See above.
(WebCore::ScrollingTreeFrameScrollingNodeMac::startScrollSnapTimer): See above.
(WebCore::ScrollingTreeFrameScrollingNodeMac::stopScrollSnapTimer): See above.
* platform/cocoa/ScrollController.h: Refactored to use a single scroll snap timer and update function to handle
    snapping in both axes. This entails removing the ScrollEventAxis parameter from various functions below. Also
    removed methods that computed &quot;snap&quot; and &quot;glide&quot; offsets.
(WebCore::ScrollControllerClient::startScrollSnapTimer): See above.
(WebCore::ScrollControllerClient::stopScrollSnapTimer): See above.
* platform/cocoa/ScrollController.mm: See above.
(WebCore::ScrollController::ScrollController): Added new constants used to compute animation offsets.
(WebCore::ScrollController::snapRubberBandTimerFired):  Added a check to prevent the rubber band timer from firing
    alongside the scroll snap timer. This results in scroll snapping taking precedence over rubber banding when
    scrolling against the edge of a container in the case of 2D scrolling. We didn't run into this issue before
    because snapping wasn't working properly at the edges of a 2D scrolling container. In the future, we may want
    to unify both snap scrolling and rubber banding timers to solve this issue.
(WebCore::ScrollController::isScrollSnapInProgress): Refactored to use a single scroll snap timer.
(WebCore::ScrollController::processWheelEventForScrollSnapOnAxis): Fixed an issue where wheel deltas were being pushed
    to the snap state incorrectly.
(WebCore::ScrollController::processWheelEventForScrollSnap): Fixed an issue with 2D snapping where scrolling in 2 axes
    simultaneously would cause the vertical axis to override the horizontal axis. This is more like a sub-issue of fixing
    2D scrolling, and is required for 2D snap animations to work properly.
(WebCore::ScrollController::startScrollSnapTimer): Refactored to use a single scroll snap timer.
(WebCore::ScrollController::stopScrollSnapTimer): See above.
(WebCore::ScrollController::scrollSnapTimerFired): This new method handles snap scroll updates on both axes.
(WebCore::ScrollController::beginScrollSnapAnimation): Refactored to account for single scroll snap timer.
(WebCore::ScrollController::endScrollSnapAnimation): See above.
(WebCore::ScrollController::initializeScrollSnapAnimationParameters): New method that initializes parameters used to
    coordinate the animation state across horizontal and vertical axes.
(WebCore::ScrollController::isSnappingOnAxis):  Checks whether or not a given axis is currently scroll snapping. This will
    return true in the case of active 2D scroll snapping.
(WebCore::ScrollController::hasActiveScrollSnapTimerForAxis): Deleted.
(WebCore::ScrollController::horizontalScrollSnapTimerFired): Deleted.
(WebCore::ScrollController::verticalScrollSnapTimerFired): Deleted.
(WebCore::ScrollController::scrollSnapAnimationUpdate): Deleted.
(WebCore::ScrollController::initializeGlideParameters): Deleted.
(WebCore::snapProgress): Deleted.
(WebCore::clampedSnapMagnitude): Deleted.
(WebCore::ScrollController::computeSnapDelta): Deleted.
(WebCore::snapGlide): Deleted.
(WebCore::ScrollController::computeGlideDelta): Deleted.
* platform/cocoa/ScrollSnapAnimatorState.h:  Added a new datastructure, ScrollSnapAnimationCurveState, which tracks
    the scroll snap animation state across both axes.
* platform/cocoa/ScrollSnapAnimatorState.mm: Removed fields relevant to the former &quot;gliding&quot; model and renamed the
    initial wheel delta variable to reflect this.
(WebCore::ScrollSnapAnimatorState::averageInitialWheelDelta): Fixed an issue where wheel deltas were being pushed
    to the snap state incorrectly.
(WebCore::ScrollSnapAnimatorState::clearInitialWheelDeltaWindow): Tiny for loop incrementor style fix.
(WebCore::ScrollSnapAnimatorState::isSnapping): Checks whether this state is in either snapping or gliding mode.
(WebCore::ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta): Checks whether the scroll velocity is
    consistent with the initial and target offsets.
(WebCore::ScrollSnapAnimatorState::interpolatedOffsetAtProgress): Interpolates the offset for a given progress value.
(WebCore::ScrollSnapAnimationCurveState::ScrollSnapAnimationCurveState): New constants.
(WebCore::ScrollSnapAnimationCurveState::initializeSnapProgressCurve): Abstracts out part of the initialization process.
(WebCore::ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary): Abstracts out part of the
    initialization process.
(WebCore::ScrollSnapAnimationCurveState::interpolatedPositionAtProgress): Abstracts out curve interpolation.
(WebCore::ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime): Added.
(WebCore::ScrollSnapAnimationCurveState::animationProgressAtTime): Added.

LayoutTests:

Tests that snap points are honored when scrolling in a 2D overflow container.

* platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt: Added.
* platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html: Added.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsChangeLog">trunk/LayoutTests/ChangeLog</a></li>
<li><a href="#trunkSourceWebCoreChangeLog">trunk/Source/WebCore/ChangeLog</a></li>
<li><a href="#trunkSourceWebCorepagescrollingmacScrollingTreeFrameScrollingNodeMach">trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h</a></li>
<li><a href="#trunkSourceWebCorepagescrollingmacScrollingTreeFrameScrollingNodeMacmm">trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm</a></li>
<li><a href="#trunkSourceWebCoreplatformcocoaScrollControllerh">trunk/Source/WebCore/platform/cocoa/ScrollController.h</a></li>
<li><a href="#trunkSourceWebCoreplatformcocoaScrollControllermm">trunk/Source/WebCore/platform/cocoa/ScrollController.mm</a></li>
<li><a href="#trunkSourceWebCoreplatformcocoaScrollSnapAnimatorStateh">trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.h</a></li>
<li><a href="#trunkSourceWebCoreplatformcocoaScrollSnapAnimatorStatemm">trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsplatformmacwk2tileddrawingscrollingscrollsnapscrollsnapmandatory2doverflowexpectedtxt">trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt</a></li>
<li><a href="#trunkLayoutTestsplatformmacwk2tileddrawingscrollingscrollsnapscrollsnapmandatory2doverflowhtml">trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkLayoutTestsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/ChangeLog (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/ChangeLog        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/LayoutTests/ChangeLog        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -1,3 +1,16 @@
</span><ins>+2015-07-07  Wenson Hsieh  &lt;whsieh@berkeley.edu&gt;
+
+        Snap point regions containing X and Y snap points should do a better job animating
+        https://bugs.webkit.org/show_bug.cgi?id=142523
+        &lt;rdar://problem/20100753&gt;
+
+        Reviewed by Brent Fulgham.
+
+        Tests that snap points are honored when scrolling in a 2D overflow container.
+
+        * platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt: Added.
+        * platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html: Added.
+
</ins><span class="cx"> 2015-07-07  Andreas Kling  &lt;akling@apple.com&gt;
</span><span class="cx"> 
</span><span class="cx">         REGRESSION(r183706): HTMLImageElement sometimes fails to register as document named item.
</span></span></pre></div>
<a id="trunkLayoutTestsplatformmacwk2tileddrawingscrollingscrollsnapscrollsnapmandatory2doverflowexpectedtxt"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt (0 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt                                (rev 0)
+++ trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow-expected.txt        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -0,0 +1,12 @@
</span><ins>+PASS successfullyParsed is true
+
+TEST COMPLETE
+PASS div successfully scrolled diagonally.
+PASS div successfully snapped diagonally.
+PASS div successfully snapped after dragging along one axis and then scrolling in the other.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+
</ins></span></pre></div>
<a id="trunkLayoutTestsplatformmacwk2tileddrawingscrollingscrollsnapscrollsnapmandatory2doverflowhtml"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html (0 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html                                (rev 0)
+++ trunk/LayoutTests/platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -0,0 +1,166 @@
</span><ins>+&lt;!DOCTYPE HTML&gt;
+&lt;html&gt;
+    &lt;head&gt;
+        &lt;style&gt;
+            #grid-container {
+                width: 400px;
+                height: 400px;
+                overflow: scroll;
+                white-space: nowrap;
+                -webkit-overflow-scrolling: touch;
+                -webkit-scroll-snap-type: mandatory;
+                -webkit-scroll-snap-destination: 0 0;
+                line-height: 0px;
+            }
+
+            .cell {
+                width: 400px;
+                height: 400px;
+                display: inline-block;
+                -webkit-scroll-snap-coordinate: 0 0;
+                background-color: red;
+                margin: 0;
+                padding: 0;
+                position: relative;
+            }
+
+            #green {
+                background-color: green;
+            }
+
+            #snap-from &gt; p {
+                position: absolute;
+                top: 0px;
+                left: 10px;
+                margin-top: 0px;
+            }
+        &lt;/style&gt;
+        &lt;script src=&quot;../../../../../resources/js-test-pre.js&quot;&gt;&lt;/script&gt;
+        &lt;script&gt;
+        var divTarget;
+        var divScrollPositionBeforeGlide;
+        var divScrollPositionBeforeSnap;
+        var divScrollPositionBeforeSingleAxisGlide;
+
+        function finishTest() {
+            finishJSTest();
+            testRunner.notifyDone();
+        }
+
+        function checkForSingleAxisGlide() {
+            if (divTarget.scrollTop == divScrollPositionBeforeSingleAxisGlide.y + 400 &amp;&amp; divTarget.scrollLeft == divScrollPositionBeforeSingleAxisGlide.x)
+                testPassed(&quot;div successfully snapped after dragging along one axis and then scrolling in the other.&quot;);
+            else
+                testFailed(&quot;div did not honor 2D snap points. (single axis scroll followed by flick on other axis)&quot;);
+            finishTest();
+        }
+
+        function scrollAndGlideInSingleAxisTest() {
+            divScrollPositionBeforeSingleAxisGlide = {
+                x: divTarget.scrollLeft,
+                y: divTarget.scrollTop
+            };
+            eventSender.mouseMoveTo(100, 100);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, &quot;began&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, 0, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, &quot;ended&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;none&quot;, &quot;begin&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, &quot;none&quot;, &quot;end&quot;, false);
+            eventSender.callAfterScrollingCompletes(checkForSingleAxisGlide);
+        }
+
+        function checkForScrollSnap() {
+            if (divTarget.scrollTop == divScrollPositionBeforeSnap.y &amp;&amp; divTarget.scrollLeft == divScrollPositionBeforeSnap.x)
+                testPassed(&quot;div successfully snapped diagonally.&quot;);
+            else
+                testFailed(&quot;div did not honor 2D snap points. (diagonal snap)&quot;);
+            setTimeout(scrollAndGlideInSingleAxisTest, 0);
+        }
+
+        function scrollSnapTest() {
+            divScrollPositionBeforeSnap = {
+                x: divTarget.scrollLeft,
+                y: divTarget.scrollTop
+            };
+
+            eventSender.mouseMoveTo(100, 100);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(1, 1, &quot;began&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(1, 1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(1, 1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, &quot;ended&quot;, &quot;none&quot;, false);
+            eventSender.callAfterScrollingCompletes(checkForScrollSnap);
+        }
+
+        function checkForScrollGlide() {
+            // The div should have scrolled (glided) to the next snap point.
+            if (divTarget.scrollTop == divScrollPositionBeforeGlide.y + 400 &amp;&amp; divTarget.scrollLeft == divScrollPositionBeforeGlide.x + 400)
+                testPassed(&quot;div successfully scrolled diagonally.&quot;);
+            else
+                testFailed(&quot;div did not honor 2D snap points. (diagonal glide)&quot;);
+            setTimeout(scrollSnapTest, 0);
+        }
+
+        function scrollGlideTest() {
+            divTarget = document.getElementById(&quot;grid-container&quot;);
+            divScrollPositionBeforeGlide = {
+                x: divTarget.scrollLeft,
+                y: divTarget.scrollTop
+            };
+
+            eventSender.mouseMoveTo(100, 100);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;began&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;changed&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, &quot;ended&quot;, &quot;none&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;none&quot;, &quot;begin&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(-1, -1, &quot;none&quot;, &quot;continue&quot;, false);
+            eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, &quot;none&quot;, &quot;end&quot;, false);
+            eventSender.callAfterScrollingCompletes(checkForScrollGlide);
+        }
+
+        function onLoad() {
+            if (window.eventSender) {
+                window.jsTestIsAsync = true;
+                testRunner.dumpAsText();
+                testRunner.waitUntilDone();
+
+                eventSender.monitorWheelEvents();
+                setTimeout(scrollGlideTest, 0);
+            } else {
+                var messageLocation = document.getElementById(&quot;snap-from&quot;);
+                var message = document.createElement(&quot;p&quot;);
+                message.innerHTML = &quot;&lt;p&gt;This test is better run under DumpRenderTree.&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;To manually test it, place the mouse pointer inside the&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;red box and perform a small swipe to the lower right&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;with some momentum. The grid should scroll to show a&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;green cell. Then scroll a bit to the upper left and&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;release without momentum. It should snap back to show&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;the green cell. Finally, drag slightly to the right and&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;then directly down. It should snap to reveal another&quot; +
+                &quot;&lt;/p&gt;&lt;p&gt;green cell directly below the previous one.&lt;/p&gt;&quot;
+                messageLocation.appendChild(message);
+            }
+        }
+        &lt;/script&gt;
+    &lt;/head&gt;
+    &lt;body onload=&quot;onLoad();&quot;&gt;
+        &lt;div id=&quot;grid-container&quot;&gt;
+            &lt;div class=&quot;cell&quot; id=&quot;snap-from&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;&lt;br/&gt;
+            &lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot; id=&quot;green&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;&lt;br/&gt;
+            &lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot; id=&quot;green&quot;&gt;&lt;/div&gt;&lt;div class=&quot;cell&quot;&gt;&lt;/div&gt;
+        &lt;/div&gt;
+        &lt;script src=&quot;../../../../../resources/js-test-post.js&quot;&gt;&lt;/script&gt;
+    &lt;/body&gt;
+&lt;/html&gt;
</ins></span></pre></div>
<a id="trunkSourceWebCoreChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/ChangeLog (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/ChangeLog        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/ChangeLog        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -1,3 +1,79 @@
</span><ins>+2015-07-07  Wenson Hsieh  &lt;whsieh@berkeley.edu&gt;
+        
+        Snap point regions containing X and Y snap points should do a better job animating
+        https://bugs.webkit.org/show_bug.cgi?id=142523
+        &lt;rdar://problem/20100753&gt;
+
+        Reviewed by Brent Fulgham.
+
+        Reimplemented snap point animations to use a single timer for both horizontal and
+        vertical axes to better support 2D snap scrolling. Instead of making velocity
+        dependent on progress to the snap point and handling 2D snapping with different
+        timer update functions, this implementation uses a fixed animation time to coordinate
+        the snapping animation across both axes.
+
+        Test: platform/mac-wk2/tiled-drawing/scrolling/scroll-snap/scroll-snap-mandatory-2d-overflow.html
+
+        * page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h: Refactored to use a single scroll snap timer.
+        * page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm: See above.
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::startScrollSnapTimer): See above.
+        (WebCore::ScrollingTreeFrameScrollingNodeMac::stopScrollSnapTimer): See above.
+        * platform/cocoa/ScrollController.h: Refactored to use a single scroll snap timer and update function to handle
+            snapping in both axes. This entails removing the ScrollEventAxis parameter from various functions below. Also
+            removed methods that computed &quot;snap&quot; and &quot;glide&quot; offsets.
+        (WebCore::ScrollControllerClient::startScrollSnapTimer): See above.
+        (WebCore::ScrollControllerClient::stopScrollSnapTimer): See above.
+        * platform/cocoa/ScrollController.mm: See above.
+        (WebCore::ScrollController::ScrollController): Added new constants used to compute animation offsets.
+        (WebCore::ScrollController::snapRubberBandTimerFired):  Added a check to prevent the rubber band timer from firing
+            alongside the scroll snap timer. This results in scroll snapping taking precedence over rubber banding when
+            scrolling against the edge of a container in the case of 2D scrolling. We didn't run into this issue before
+            because snapping wasn't working properly at the edges of a 2D scrolling container. In the future, we may want
+            to unify both snap scrolling and rubber banding timers to solve this issue.
+        (WebCore::ScrollController::isScrollSnapInProgress): Refactored to use a single scroll snap timer.
+        (WebCore::ScrollController::processWheelEventForScrollSnapOnAxis): Fixed an issue where wheel deltas were being pushed
+            to the snap state incorrectly.
+        (WebCore::ScrollController::processWheelEventForScrollSnap): Fixed an issue with 2D snapping where scrolling in 2 axes
+            simultaneously would cause the vertical axis to override the horizontal axis. This is more like a sub-issue of fixing
+            2D scrolling, and is required for 2D snap animations to work properly.
+        (WebCore::ScrollController::startScrollSnapTimer): Refactored to use a single scroll snap timer.
+        (WebCore::ScrollController::stopScrollSnapTimer): See above.
+        (WebCore::ScrollController::scrollSnapTimerFired): This new method handles snap scroll updates on both axes.
+        (WebCore::ScrollController::beginScrollSnapAnimation): Refactored to account for single scroll snap timer.
+        (WebCore::ScrollController::endScrollSnapAnimation): See above.
+        (WebCore::ScrollController::initializeScrollSnapAnimationParameters): New method that initializes parameters used to
+            coordinate the animation state across horizontal and vertical axes.
+        (WebCore::ScrollController::isSnappingOnAxis):  Checks whether or not a given axis is currently scroll snapping. This will
+            return true in the case of active 2D scroll snapping.
+        (WebCore::ScrollController::hasActiveScrollSnapTimerForAxis): Deleted.
+        (WebCore::ScrollController::horizontalScrollSnapTimerFired): Deleted.
+        (WebCore::ScrollController::verticalScrollSnapTimerFired): Deleted.
+        (WebCore::ScrollController::scrollSnapAnimationUpdate): Deleted.
+        (WebCore::ScrollController::initializeGlideParameters): Deleted.
+        (WebCore::snapProgress): Deleted.
+        (WebCore::clampedSnapMagnitude): Deleted.
+        (WebCore::ScrollController::computeSnapDelta): Deleted.
+        (WebCore::snapGlide): Deleted.
+        (WebCore::ScrollController::computeGlideDelta): Deleted.
+        * platform/cocoa/ScrollSnapAnimatorState.h:  Added a new datastructure, ScrollSnapAnimationCurveState, which tracks
+            the scroll snap animation state across both axes.
+        * platform/cocoa/ScrollSnapAnimatorState.mm: Removed fields relevant to the former &quot;gliding&quot; model and renamed the
+            initial wheel delta variable to reflect this.
+        (WebCore::ScrollSnapAnimatorState::averageInitialWheelDelta): Fixed an issue where wheel deltas were being pushed
+            to the snap state incorrectly.
+        (WebCore::ScrollSnapAnimatorState::clearInitialWheelDeltaWindow): Tiny for loop incrementor style fix.
+        (WebCore::ScrollSnapAnimatorState::isSnapping): Checks whether this state is in either snapping or gliding mode.
+        (WebCore::ScrollSnapAnimatorState::canReachTargetWithCurrentInitialScrollDelta): Checks whether the scroll velocity is
+            consistent with the initial and target offsets.
+        (WebCore::ScrollSnapAnimatorState::interpolatedOffsetAtProgress): Interpolates the offset for a given progress value.
+        (WebCore::ScrollSnapAnimationCurveState::ScrollSnapAnimationCurveState): New constants.
+        (WebCore::ScrollSnapAnimationCurveState::initializeSnapProgressCurve): Abstracts out part of the initialization process.
+        (WebCore::ScrollSnapAnimationCurveState::initializeInterpolationCoefficientsIfNecessary): Abstracts out part of the
+            initialization process.
+        (WebCore::ScrollSnapAnimationCurveState::interpolatedPositionAtProgress): Abstracts out curve interpolation.
+        (WebCore::ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime): Added.
+        (WebCore::ScrollSnapAnimationCurveState::animationProgressAtTime): Added.
+        
</ins><span class="cx"> 2015-07-07  Chris Dumez  &lt;cdumez@apple.com&gt;
</span><span class="cx"> 
</span><span class="cx">         Unreviewed, attempt to fix iOS build.
</span></span></pre></div>
<a id="trunkSourceWebCorepagescrollingmacScrollingTreeFrameScrollingNodeMach"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.h        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -86,8 +86,8 @@
</span><span class="cx">     LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const override;
</span><span class="cx">     void immediateScrollOnAxis(ScrollEventAxis, float delta) override;
</span><span class="cx">     float pageScaleFactor() const override;
</span><del>-    void startScrollSnapTimer(ScrollEventAxis) override;
-    void stopScrollSnapTimer(ScrollEventAxis) override;
</del><ins>+    void startScrollSnapTimer() override;
+    void stopScrollSnapTimer() override;
</ins><span class="cx">     LayoutSize scrollExtent() const override;
</span><span class="cx"> #endif
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCorepagescrollingmacScrollingTreeFrameScrollingNodeMacmm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/page/scrolling/mac/ScrollingTreeFrameScrollingNodeMac.mm        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -583,16 +583,14 @@
</span><span class="cx">     return frameScaleFactor();
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-void ScrollingTreeFrameScrollingNodeMac::startScrollSnapTimer(ScrollEventAxis)
</del><ins>+void ScrollingTreeFrameScrollingNodeMac::startScrollSnapTimer()
</ins><span class="cx"> {
</span><span class="cx">     scrollingTree().setMainFrameIsScrollSnapping(true);
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-void ScrollingTreeFrameScrollingNodeMac::stopScrollSnapTimer(ScrollEventAxis axis)
</del><ins>+void ScrollingTreeFrameScrollingNodeMac::stopScrollSnapTimer()
</ins><span class="cx"> {
</span><del>-    ScrollEventAxis otherAxis = (axis == ScrollEventAxis::Horizontal) ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
-    if (!m_scrollController.hasActiveScrollSnapTimerForAxis(otherAxis))
-        scrollingTree().setMainFrameIsScrollSnapping(false);
</del><ins>+    scrollingTree().setMainFrameIsScrollSnapping(false);
</ins><span class="cx"> }
</span><span class="cx">     
</span><span class="cx"> LayoutSize ScrollingTreeFrameScrollingNodeMac::scrollExtent() const
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformcocoaScrollControllerh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/cocoa/ScrollController.h (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/cocoa/ScrollController.h        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/platform/cocoa/ScrollController.h        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -86,12 +86,12 @@
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP)
</span><span class="cx">     virtual LayoutUnit scrollOffsetOnAxis(ScrollEventAxis) const = 0;
</span><span class="cx">     virtual void immediateScrollOnAxis(ScrollEventAxis, float delta) = 0;
</span><del>-    virtual void startScrollSnapTimer(ScrollEventAxis)
</del><ins>+    virtual void startScrollSnapTimer()
</ins><span class="cx">     {
</span><span class="cx">         // Override to perform client-specific scroll snap point start logic
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    virtual void stopScrollSnapTimer(ScrollEventAxis)
</del><ins>+    virtual void stopScrollSnapTimer()
</ins><span class="cx">     {
</span><span class="cx">         // Override to perform client-specific scroll snap point end logic
</span><span class="cx">     }
</span><span class="lines">@@ -133,7 +133,6 @@
</span><span class="cx">     void updateScrollSnapState(const ScrollableArea&amp;);
</span><span class="cx"> #if PLATFORM(MAC)
</span><span class="cx">     bool processWheelEventForScrollSnap(const PlatformWheelEvent&amp;);
</span><del>-    bool hasActiveScrollSnapTimerForAxis(ScrollEventAxis) const;
</del><span class="cx"> #endif
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="lines">@@ -153,21 +152,19 @@
</span><span class="cx">     ScrollSnapAnimatorState&amp; scrollSnapPointState(ScrollEventAxis);
</span><span class="cx">     const ScrollSnapAnimatorState&amp; scrollSnapPointState(ScrollEventAxis) const;
</span><span class="cx"> #if PLATFORM(MAC)
</span><del>-    void horizontalScrollSnapTimerFired();
-    void verticalScrollSnapTimerFired();
-    void startScrollSnapTimer(ScrollEventAxis);
-    void stopScrollSnapTimer(ScrollEventAxis);
</del><ins>+    void scrollSnapTimerFired();
+    void startScrollSnapTimer();
+    void stopScrollSnapTimer();
</ins><span class="cx"> 
</span><span class="cx">     void processWheelEventForScrollSnapOnAxis(ScrollEventAxis, const PlatformWheelEvent&amp;);
</span><span class="cx">     bool shouldOverrideWheelEvent(ScrollEventAxis, const PlatformWheelEvent&amp;) const;
</span><span class="cx"> 
</span><span class="cx">     void beginScrollSnapAnimation(ScrollEventAxis, ScrollSnapState);
</span><del>-    void scrollSnapAnimationUpdate(ScrollEventAxis);
-    void endScrollSnapAnimation(ScrollEventAxis, ScrollSnapState);
-
-    void initializeGlideParameters(ScrollEventAxis, bool);
-    float computeSnapDelta(ScrollEventAxis) const;
-    float computeGlideDelta(ScrollEventAxis) const;
</del><ins>+    
+    void endScrollSnapAnimation(ScrollSnapState);
+    void initializeScrollSnapAnimationParameters();
+    bool isSnappingOnAxis(ScrollEventAxis) const;
+    
</ins><span class="cx"> #endif
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="lines">@@ -188,13 +185,13 @@
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP)
</span><del>-    bool m_expectingStatelessScrollSnap { false };
</del><ins>+    bool m_expectingHorizontalStatelessScrollSnap { false };
+    bool m_expectingVerticalStatelessScrollSnap { false };
</ins><span class="cx">     std::unique_ptr&lt;ScrollSnapAnimatorState&gt; m_horizontalScrollSnapState;
</span><span class="cx">     std::unique_ptr&lt;ScrollSnapAnimatorState&gt; m_verticalScrollSnapState;
</span><ins>+    std::unique_ptr&lt;ScrollSnapAnimationCurveState&gt; m_scrollSnapCurveState;
</ins><span class="cx"> #if PLATFORM(MAC)
</span><del>-    // FIXME: Find a way to consolidate both timers into one variable.
-    RunLoop::Timer&lt;ScrollController&gt; m_horizontalScrollSnapTimer;
-    RunLoop::Timer&lt;ScrollController&gt; m_verticalScrollSnapTimer;
</del><ins>+    RunLoop::Timer&lt;ScrollController&gt; m_scrollSnapTimer;
</ins><span class="cx"> #endif
</span><span class="cx"> #endif
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformcocoaScrollControllermm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/cocoa/ScrollController.mm (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/cocoa/ScrollController.mm        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/platform/cocoa/ScrollController.mm        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -32,6 +32,7 @@
</span><span class="cx"> #include &quot;WheelEventTestTrigger.h&quot;
</span><span class="cx"> #include &lt;sys/sysctl.h&gt;
</span><span class="cx"> #include &lt;sys/time.h&gt;
</span><ins>+#include &lt;wtf/CurrentTime.h&gt;
</ins><span class="cx"> 
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP)
</span><span class="cx"> #include &quot;ScrollSnapAnimatorState.h&quot;
</span><span class="lines">@@ -74,19 +75,7 @@
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP) &amp;&amp; PLATFORM(MAC)
</span><del>-static const float snapMagnitudeMax = 25;
-static const float snapMagnitudeMin = 5;
-static const float snapThresholdHigh = 1000;
-static const float snapThresholdLow = 50;
-
</del><span class="cx"> static const float inertialScrollPredictionFactor = 16.7;
</span><del>-static const float initialToFinalMomentumFactor = 1.0 / 40.0;
-
-static const float glideBoostMultiplier = 3.5;
-
-static const float maxTargetWheelDelta = 7;
-static const float minTargetWheelDelta = 3.5;
-
</del><span class="cx"> static const double statelessScrollSnapDelay = 0.5;
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="lines">@@ -135,8 +124,7 @@
</span><span class="cx">     , m_snapRubberbandTimer(RunLoop::current(), this, &amp;ScrollController::snapRubberBandTimerFired)
</span><span class="cx"> #endif
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP) &amp;&amp; PLATFORM(MAC)
</span><del>-    , m_horizontalScrollSnapTimer(RunLoop::current(), this, &amp;ScrollController::horizontalScrollSnapTimerFired)
-    , m_verticalScrollSnapTimer(RunLoop::current(), this, &amp;ScrollController::verticalScrollSnapTimerFired)
</del><ins>+    , m_scrollSnapTimer(RunLoop::current(), this, &amp;ScrollController::scrollSnapTimerFired)
</ins><span class="cx"> #endif
</span><span class="cx"> {
</span><span class="cx"> }
</span><span class="lines">@@ -347,6 +335,9 @@
</span><span class="cx"> 
</span><span class="cx"> void ScrollController::snapRubberBandTimerFired()
</span><span class="cx"> {
</span><ins>+    if (isScrollSnapInProgress())
+        return;
+    
</ins><span class="cx">     if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
</span><span class="cx">         CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;
</span><span class="cx"> 
</span><span class="lines">@@ -423,7 +414,7 @@
</span><span class="cx"> bool ScrollController::isScrollSnapInProgress() const
</span><span class="cx"> {
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP) &amp;&amp; PLATFORM(MAC)
</span><del>-    if (m_inScrollGesture || m_momentumScrollInProgress || m_horizontalScrollSnapTimer.isActive() || m_verticalScrollSnapTimer.isActive())
</del><ins>+    if (m_inScrollGesture || m_momentumScrollInProgress || m_scrollSnapTimer.isActive())
</ins><span class="cx">         return true;
</span><span class="cx"> #endif
</span><span class="cx">     return false;
</span><span class="lines">@@ -496,11 +487,6 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> #if PLATFORM(MAC)
</span><del>-bool ScrollController::hasActiveScrollSnapTimerForAxis(ScrollEventAxis axis) const
-{
-    return (axis == ScrollEventAxis::Horizontal) ? m_horizontalScrollSnapTimer.isActive() : m_verticalScrollSnapTimer.isActive();
-}
-
</del><span class="cx"> static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
</span><span class="cx"> {
</span><span class="cx">     if (phase == PlatformWheelEventPhaseNone) {
</span><span class="lines">@@ -551,7 +537,7 @@
</span><span class="cx">     switch (wheelStatus) {
</span><span class="cx">     case WheelEventStatus::UserScrollBegin:
</span><span class="cx">     case WheelEventStatus::UserScrolling:
</span><del>-        endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
</del><ins>+        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
</ins><span class="cx">         break;
</span><span class="cx">             
</span><span class="cx">     case WheelEventStatus::UserScrollEnd:
</span><span class="lines">@@ -560,7 +546,7 @@
</span><span class="cx">         
</span><span class="cx">     case WheelEventStatus::InertialScrollBegin:
</span><span class="cx">         // Begin tracking wheel deltas for glide prediction.
</span><del>-        endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
</del><ins>+        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
</ins><span class="cx">         snapState.pushInitialWheelDelta(wheelDelta);
</span><span class="cx">         snapState.m_beginTrackingWheelDeltaOffset = m_client.scrollOffsetOnAxis(axis);
</span><span class="cx">         break;
</span><span class="lines">@@ -568,7 +554,7 @@
</span><span class="cx">     case WheelEventStatus::InertialScrolling:
</span><span class="cx">         // This check for DestinationReached ensures that we don't receive another set of momentum events after ending the last glide.
</span><span class="cx">         if (snapState.m_currentState != ScrollSnapState::Gliding &amp;&amp; snapState.m_currentState != ScrollSnapState::DestinationReached) {
</span><del>-            if (snapState.m_numWheelDeltasTracked &lt; snapState.wheelDeltaWindowSize)
</del><ins>+            if (snapState.m_numWheelDeltasTracked &lt; snapState.wheelDeltaWindowSize &amp;&amp; wheelDelta)
</ins><span class="cx">                 snapState.pushInitialWheelDelta(wheelDelta);
</span><span class="cx">             
</span><span class="cx">             if ((snapState.m_numWheelDeltasTracked == snapState.wheelDeltaWindowSize) &amp;&amp; snapState.averageInitialWheelDelta())
</span><span class="lines">@@ -582,14 +568,14 @@
</span><span class="cx">         break;
</span><span class="cx"> 
</span><span class="cx">     case WheelEventStatus::StatelessScrollEvent:
</span><del>-        endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
</del><ins>+        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
</ins><span class="cx">         snapState.clearInitialWheelDeltaWindow();
</span><span class="cx">         snapState.m_shouldOverrideWheelEvent = false;
</span><del>-        m_expectingStatelessScrollSnap = true;
-        if (axis == ScrollEventAxis::Vertical)
-            m_verticalScrollSnapTimer.startOneShot(statelessScrollSnapDelay);
</del><ins>+        m_scrollSnapTimer.startOneShot(statelessScrollSnapDelay);
+        if (axis == ScrollEventAxis::Horizontal)
+            m_expectingHorizontalStatelessScrollSnap = true;
</ins><span class="cx">         else
</span><del>-            m_horizontalScrollSnapTimer.startOneShot(statelessScrollSnapDelay);
</del><ins>+            m_expectingVerticalStatelessScrollSnap = true;
</ins><span class="cx">         break;
</span><span class="cx"> 
</span><span class="cx">     case WheelEventStatus::Unknown:
</span><span class="lines">@@ -607,18 +593,16 @@
</span><span class="cx"> 
</span><span class="cx"> bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent&amp; wheelEvent)
</span><span class="cx"> {
</span><ins>+    bool shouldAllowWheelEventToPropagate = true;
</ins><span class="cx">     if (m_verticalScrollSnapState) {
</span><span class="cx">         processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Vertical, wheelEvent);
</span><del>-        if (shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent))
-            return false;
</del><ins>+        shouldAllowWheelEventToPropagate &amp;= !shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent);
</ins><span class="cx">     }
</span><span class="cx">     if (m_horizontalScrollSnapState) {
</span><span class="cx">         processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Horizontal, wheelEvent);
</span><del>-        if (shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent))
-            return false;
</del><ins>+        shouldAllowWheelEventToPropagate &amp;= !shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent);
</ins><span class="cx">     }
</span><del>-
-    return true;
</del><ins>+    return shouldAllowWheelEventToPropagate;
</ins><span class="cx"> }
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="lines">@@ -647,66 +631,88 @@
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> #if PLATFORM(MAC)
</span><del>-void ScrollController::startScrollSnapTimer(ScrollEventAxis axis)
</del><ins>+void ScrollController::startScrollSnapTimer()
</ins><span class="cx"> {
</span><del>-    RunLoop::Timer&lt;ScrollController&gt;&amp; scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
-    if (!scrollSnapTimer.isActive()) {
-        m_client.startScrollSnapTimer(axis);
-        scrollSnapTimer.startRepeating(1.0 / 60.0);
</del><ins>+    if (!m_scrollSnapTimer.isActive()) {
+        m_client.startScrollSnapTimer();
+        m_scrollSnapTimer.startRepeating(1.0 / 60.0);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><del>-    if (!m_horizontalScrollSnapTimer.isActive() &amp;&amp; !m_verticalScrollSnapTimer.isActive())
</del><ins>+    if (!m_scrollSnapTimer.isActive())
</ins><span class="cx">         return;
</span><span class="cx"> 
</span><span class="cx">     m_client.deferTestsForReason(reinterpret_cast&lt;WheelEventTestTrigger::ScrollableAreaIdentifier&gt;(this), WheelEventTestTrigger::ScrollSnapInProgress);
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-void ScrollController::stopScrollSnapTimer(ScrollEventAxis axis)
</del><ins>+void ScrollController::stopScrollSnapTimer()
</ins><span class="cx"> {
</span><del>-    m_client.stopScrollSnapTimer(axis);
-    RunLoop::Timer&lt;ScrollController&gt;&amp; scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
-    scrollSnapTimer.stop();
</del><ins>+    m_client.stopScrollSnapTimer();
+    m_scrollSnapTimer.stop();
</ins><span class="cx">     
</span><del>-    if (m_horizontalScrollSnapTimer.isActive() || m_verticalScrollSnapTimer.isActive())
</del><ins>+    if (m_scrollSnapTimer.isActive())
</ins><span class="cx">         return;
</span><span class="cx"> 
</span><span class="cx">     m_client.removeTestDeferralForReason(reinterpret_cast&lt;WheelEventTestTrigger::ScrollableAreaIdentifier&gt;(this), WheelEventTestTrigger::ScrollSnapInProgress);
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-void ScrollController::horizontalScrollSnapTimerFired()
</del><ins>+void ScrollController::scrollSnapTimerFired()
</ins><span class="cx"> {
</span><del>-    if (m_expectingStatelessScrollSnap)
-        beginScrollSnapAnimation(ScrollEventAxis::Horizontal, ScrollSnapState::Snapping);
-    else
-        scrollSnapAnimationUpdate(ScrollEventAxis::Horizontal);
-}
-
-void ScrollController::verticalScrollSnapTimerFired()
-{
-    if (m_expectingStatelessScrollSnap)
-        beginScrollSnapAnimation(ScrollEventAxis::Vertical, ScrollSnapState::Snapping);
-    else
-        scrollSnapAnimationUpdate(ScrollEventAxis::Vertical);
-}
-
-void ScrollController::scrollSnapAnimationUpdate(ScrollEventAxis axis)
-{
-    if (axis == ScrollEventAxis::Horizontal &amp;&amp; !m_horizontalScrollSnapState)
</del><ins>+    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
+        if (m_expectingHorizontalStatelessScrollSnap)
+            beginScrollSnapAnimation(ScrollEventAxis::Horizontal, ScrollSnapState::Snapping);
+        if (m_expectingVerticalStatelessScrollSnap)
+            beginScrollSnapAnimation(ScrollEventAxis::Vertical, ScrollSnapState::Snapping);
</ins><span class="cx">         return;
</span><del>-
-    if (axis == ScrollEventAxis::Vertical &amp;&amp; !m_verticalScrollSnapState)
</del><ins>+    }
+        
+    bool snapOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
+    bool snapOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
+    if (snapOnHorizontalAxis &amp;&amp; !m_horizontalScrollSnapState-&gt;canReachTargetWithCurrentInitialScrollDelta()) {
+        m_horizontalScrollSnapState-&gt;m_currentState = ScrollSnapState::DestinationReached;
+        snapOnHorizontalAxis = false;
+    }
+    if (snapOnVerticalAxis &amp;&amp; !m_verticalScrollSnapState-&gt;canReachTargetWithCurrentInitialScrollDelta()) {
+        m_verticalScrollSnapState-&gt;m_currentState = ScrollSnapState::DestinationReached;
+        snapOnVerticalAxis = false;
+    }
+    if (!snapOnHorizontalAxis &amp;&amp; !snapOnVerticalAxis) {
+        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
</ins><span class="cx">         return;
</span><ins>+    }
+    
+    double currentTime = monotonicallyIncreasingTime();
+    if (m_scrollSnapCurveState-&gt;shouldCompleteSnapAnimationImmediatelyAtTime(currentTime)) {
+        float finalHorizontalDelta = 0;
+        float finalVerticalDelta = 0;
+        if (snapOnHorizontalAxis)
+            finalHorizontalDelta = m_horizontalScrollSnapState-&gt;m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
+        if (snapOnVerticalAxis)
+            finalVerticalDelta = m_verticalScrollSnapState-&gt;m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
</ins><span class="cx"> 
</span><del>-    ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
-    if (snapState.m_currentState == ScrollSnapState::DestinationReached)
</del><ins>+        if (finalHorizontalDelta || finalVerticalDelta)
+            m_client.immediateScrollBy(FloatSize(finalHorizontalDelta, finalVerticalDelta));
+
+        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
</ins><span class="cx">         return;
</span><ins>+    }
</ins><span class="cx">     
</span><del>-    ASSERT(snapState.m_currentState == ScrollSnapState::Gliding || snapState.m_currentState == ScrollSnapState::Snapping);
-    float delta = snapState.m_currentState == ScrollSnapState::Snapping ? computeSnapDelta(axis) : computeGlideDelta(axis);
-    if (delta)
-        m_client.immediateScrollOnAxis(axis, delta);
-    else
-        endScrollSnapAnimation(axis, ScrollSnapState::DestinationReached);
</del><ins>+    float animationProgress = m_scrollSnapCurveState-&gt;animationProgressAtTime(currentTime);
+    float horizontalDelta = 0;
+    float verticalDelta = 0;
+    if (m_scrollSnapCurveState-&gt;shouldAnimateDirectlyToSnapPoint) {
+        if (snapOnHorizontalAxis)
+            horizontalDelta = m_horizontalScrollSnapState-&gt;interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
+        if (snapOnVerticalAxis)
+            verticalDelta = m_verticalScrollSnapState-&gt;interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
+
+    } else {
+        FloatPoint interpolatedPoint = m_scrollSnapCurveState-&gt;interpolatedPositionAtProgress(animationProgress);
+        horizontalDelta = interpolatedPoint.x() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
+        verticalDelta = interpolatedPoint.y() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
+    }
+    
+    if (horizontalDelta || verticalDelta)
+        m_client.immediateScrollBy(FloatSize(horizontalDelta, verticalDelta));
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> static inline float projectedInertialScrollDistance(float initialWheelDelta)
</span><span class="lines">@@ -715,21 +721,6 @@
</span><span class="cx">     // In the future, we'll want to find a more accurate way of inertial scroll prediction.
</span><span class="cx">     return inertialScrollPredictionFactor * initialWheelDelta;
</span><span class="cx"> }
</span><del>-
-void ScrollController::initializeGlideParameters(ScrollEventAxis axis, bool shouldIncreaseInitialWheelDelta)
-{
-    ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
-    
-    // FIXME: Glide boost is a hacky way to speed up natural scrolling velocity. We should find a better way to accomplish this.
-    if (shouldIncreaseInitialWheelDelta)
-        snapState.m_glideInitialWheelDelta *= glideBoostMultiplier;
-    
-    // FIXME: There must be a better way to determine a good target delta than multiplying by a factor and clamping to min/max values.
-    float targetFinalWheelDelta = initialToFinalMomentumFactor * (snapState.m_glideInitialWheelDelta &lt; 0 ? -snapState.m_glideInitialWheelDelta : snapState.m_glideInitialWheelDelta);
-    targetFinalWheelDelta = (snapState.m_glideInitialWheelDelta &gt; 0 ? 1 : -1) * std::min(std::max(targetFinalWheelDelta, minTargetWheelDelta), maxTargetWheelDelta);
-    snapState.m_glideMagnitude = (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta) / 2;
-    snapState.m_glidePhaseShift = acos((snapState.m_glideInitialWheelDelta - targetFinalWheelDelta) / (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta));
-}
</del><span class="cx"> #endif
</span><span class="cx"> 
</span><span class="cx"> unsigned ScrollController::activeScrollSnapIndexForAxis(ScrollEventAxis axis) const
</span><span class="lines">@@ -781,9 +772,10 @@
</span><span class="cx"> void ScrollController::beginScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
</span><span class="cx"> {
</span><span class="cx">     ASSERT(newState == ScrollSnapState::Gliding || newState == ScrollSnapState::Snapping);
</span><del>-    if (m_expectingStatelessScrollSnap) {
-        m_expectingStatelessScrollSnap = false;
-        stopScrollSnapTimer(axis);
</del><ins>+    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
+        m_expectingHorizontalStatelessScrollSnap = false;
+        m_expectingVerticalStatelessScrollSnap = false;
+        stopScrollSnapTimer();
</ins><span class="cx">     }
</span><span class="cx">     ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
</span><span class="cx"> 
</span><span class="lines">@@ -811,116 +803,75 @@
</span><span class="cx">     m_activeScrollSnapIndexDidChange = true;
</span><span class="cx">     snapState.m_currentState = newState;
</span><span class="cx">     if (newState == ScrollSnapState::Gliding) {
</span><ins>+        // Check if the other scroll axis needs to animate to the nearest snap point.
+        snapState.m_initialScrollDelta = initialWheelDelta;
</ins><span class="cx">         snapState.m_shouldOverrideWheelEvent = true;
</span><del>-        snapState.m_glideInitialWheelDelta = initialWheelDelta;
-        bool glideRequiresBoost;
-        if (initialWheelDelta &gt; 0)
-            glideRequiresBoost = projectedScrollDestination - offset &lt; snapState.m_targetOffset - projectedScrollDestination;
-        else
-            glideRequiresBoost = offset - projectedScrollDestination &lt; projectedScrollDestination - snapState.m_targetOffset;
-        
-        initializeGlideParameters(axis, glideRequiresBoost);
</del><span class="cx">         snapState.clearInitialWheelDeltaWindow();
</span><ins>+        ScrollEventAxis otherAxis = axis == ScrollEventAxis::Horizontal ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
+        if ((otherAxis == ScrollEventAxis::Horizontal &amp;&amp; m_horizontalScrollSnapState &amp;&amp; m_horizontalScrollSnapState-&gt;m_currentState == ScrollSnapState::UserInteraction)
+            || (otherAxis == ScrollEventAxis::Vertical &amp;&amp; m_verticalScrollSnapState &amp;&amp; m_verticalScrollSnapState-&gt;m_currentState == ScrollSnapState::UserInteraction)) {
+            
+            ScrollSnapAnimatorState&amp; otherState = scrollSnapPointState(otherAxis);
+            if (!otherState.averageInitialWheelDelta()) {
+                float offsetOnOtherAxis = m_client.scrollOffsetOnAxis(otherAxis);
+                float snapOffsetForOtherAxis = scaleFactor * closestSnapOffset&lt;LayoutUnit, float&gt;(otherState.m_snapOffsets, offsetOnOtherAxis, 0, otherState.m_activeSnapIndex);
+                if (offsetOnOtherAxis != snapOffsetForOtherAxis) {
+                    otherState.m_initialOffset = offsetOnOtherAxis;
+                    otherState.m_targetOffset = snapOffsetForOtherAxis;
+                    otherState.m_initialScrollDelta = 0;
+                    otherState.m_currentState = ScrollSnapState::Gliding;
+                }
+            }
+        }
+        
+    } else {
+        snapState.m_initialScrollDelta = initialWheelDelta;
</ins><span class="cx">     }
</span><del>-    startScrollSnapTimer(axis);
</del><ins>+    initializeScrollSnapAnimationParameters();
+    startScrollSnapTimer();
</ins><span class="cx"> }
</span><span class="cx"> 
</span><del>-void ScrollController::endScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
</del><ins>+void ScrollController::endScrollSnapAnimation(ScrollSnapState newState)
</ins><span class="cx"> {
</span><span class="cx">     ASSERT(newState == ScrollSnapState::DestinationReached || newState == ScrollSnapState::UserInteraction);
</span><ins>+    if (m_horizontalScrollSnapState)
+        m_horizontalScrollSnapState-&gt;m_currentState = newState;
</ins><span class="cx"> 
</span><del>-    ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
</del><ins>+    if (m_verticalScrollSnapState)
+        m_verticalScrollSnapState-&gt;m_currentState = newState;
</ins><span class="cx"> 
</span><del>-    if (snapState.m_currentState == ScrollSnapState::Gliding)
-        snapState.clearInitialWheelDeltaWindow();
-    
-    snapState.m_currentState = newState;
-    stopScrollSnapTimer(axis);
</del><ins>+    stopScrollSnapTimer();
</ins><span class="cx"> }
</span><span class="cx"> 
</span><del>-static inline float snapProgress(const LayoutUnit&amp; offset, const ScrollSnapAnimatorState&amp; snapState)
</del><ins>+void ScrollController::initializeScrollSnapAnimationParameters()
</ins><span class="cx"> {
</span><del>-    const float distanceTraveled = static_cast&lt;float&gt;(offset - snapState.m_initialOffset);
-    const float totalDistance = static_cast&lt;float&gt;(snapState.m_targetOffset - snapState.m_initialOffset);
-
-    return distanceTraveled / totalDistance;
-}
-
-static inline float clampedSnapMagnitude(float thresholdedDistance)
-{
-    return snapMagnitudeMin + (snapMagnitudeMax - snapMagnitudeMin) * (thresholdedDistance - snapThresholdLow) / (snapThresholdHigh - snapThresholdLow);
-}
-
-// Computes the amount to scroll by when performing a &quot;snap&quot; operation, i.e. when a user releases the trackpad without flicking. The snap delta
-// is a function of progress t, where t is equal to DISTANCE_TRAVELED / TOTAL_DISTANCE, DISTANCE_TRAVELED is the distance from the initialOffset
-// to the current offset, and TOTAL_DISTANCE is the distance from initialOffset to targetOffset. The snapping equation is as follows:
-// delta(t) = MAGNITUDE * sin(PI * t). MAGNITUDE indicates the top speed reached near the middle of the animation (t = 0.5), and is a linear
-// relationship of the distance traveled, clamped by arbitrary min and max values.
-float ScrollController::computeSnapDelta(ScrollEventAxis axis) const
-{
-    const ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
-
-    LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
-    bool canComputeSnap =  (snapState.m_initialOffset &lt;= offset &amp;&amp; offset &lt; snapState.m_targetOffset) || (snapState.m_targetOffset &lt; offset &amp;&amp; offset &lt;= snapState.m_initialOffset);
-    if (snapState.m_currentState != ScrollSnapState::Snapping || !canComputeSnap)
-        return 0;
</del><ins>+    if (!m_scrollSnapCurveState)
+        m_scrollSnapCurveState = std::make_unique&lt;ScrollSnapAnimationCurveState&gt;();
</ins><span class="cx">     
</span><del>-    float progress = snapProgress(offset, snapState);
</del><ins>+    bool isSnappingOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
+    bool isSnappingOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
+    FloatSize initialVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState-&gt;m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
+        isSnappingOnVerticalAxis ? m_verticalScrollSnapState-&gt;m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
+    FloatSize targetVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState-&gt;m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
+        isSnappingOnVerticalAxis ? m_verticalScrollSnapState-&gt;m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
+    FloatSize initialDelta(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState-&gt;m_initialScrollDelta : 0,
+        isSnappingOnVerticalAxis ? m_verticalScrollSnapState-&gt;m_initialScrollDelta : 0);
</ins><span class="cx"> 
</span><del>-    // Threshold the distance before computing magnitude, so only distances within a certain range are considered.
-    int sign = snapState.m_initialOffset &lt; snapState.m_targetOffset ? 1 : -1;
-    float thresholdedDistance = std::min(std::max&lt;float&gt;((snapState.m_targetOffset - snapState.m_initialOffset) * sign, snapThresholdLow), snapThresholdHigh);
-
-    float magnitude = clampedSnapMagnitude(thresholdedDistance);
-
-    float rawSnapDelta = std::max&lt;float&gt;(1, magnitude * std::sin(piFloat * progress));
-    if ((snapState.m_targetOffset &lt; offset &amp;&amp; offset - rawSnapDelta &lt; snapState.m_targetOffset) || (snapState.m_targetOffset &gt; offset &amp;&amp; offset + rawSnapDelta &gt; snapState.m_targetOffset))
-        return snapState.m_targetOffset - offset;
-    
-    return sign * rawSnapDelta;
</del><ins>+    // Animate directly by default. This flag will be changed as necessary if interpolation is possible.
+    m_scrollSnapCurveState-&gt;shouldAnimateDirectlyToSnapPoint = true;
+    m_scrollSnapCurveState-&gt;initializeSnapProgressCurve(initialVector, targetVector, initialDelta);
+    if (isSnappingOnHorizontalAxis &amp;&amp; isSnappingOnVerticalAxis)
+        m_scrollSnapCurveState-&gt;initializeInterpolationCoefficientsIfNecessary(initialVector, targetVector, initialDelta);
</ins><span class="cx"> }
</span><del>-
-static inline float snapGlide(float progress, const ScrollSnapAnimatorState&amp; snapState)
</del><ins>+    
+bool ScrollController::isSnappingOnAxis(ScrollEventAxis axis) const
</ins><span class="cx"> {
</span><del>-    // FIXME: We might want to investigate why -m_glidePhaseShift results in the behavior we want.
-    return ceil(snapState.m_glideMagnitude * (1.0f + std::cos(piFloat * progress - snapState.m_glidePhaseShift)));
-}
</del><ins>+    if (axis == ScrollEventAxis::Horizontal)
+        return m_horizontalScrollSnapState &amp;&amp; m_horizontalScrollSnapState-&gt;isSnapping();
</ins><span class="cx"> 
</span><del>-// Computes the amount to scroll by when performing a &quot;glide&quot; operation, i.e. when a user releases the trackpad with an initial velocity. Here,
-// we want the scroll offset to animate directly to the snap point.
-//
-// The snap delta is a function of progress t, where: (1) t is equal to DISTANCE_TRAVELED / TOTAL_DISTANCE, (2) DISTANCE_TRAVELED is the distance
-// from the initialOffset to the current offset, and (3) TOTAL_DISTANCE is the distance from initialOffset to targetOffset.
-//
-// The general model of our gliding equation is delta(t) = MAGNITUDE * (1 + cos(PI * t + PHASE_SHIFT)). This was determined after examining the
-// momentum velocity curve as a function of progress. To compute MAGNITUDE and PHASE_SHIFT, we use initial velocity V0 and the final velocity VF,
-// both as wheel deltas (pixels per timestep). VF should be a small value (&lt; 10) chosen based on the initial velocity and TOTAL_DISTANCE.
-// We also enforce the following constraints for the gliding equation:
-//   1. delta(0) = V0, since we want the initial velocity of the gliding animation to match the user's scroll velocity. The exception to this is
-//      when the glide velocity is not enough to naturally reach the next snap point, and thus requires a boost (see initializeGlideParameters)
-//   2. delta(1) = VF, since at t=1, the animation has completed and we want the last wheel delta to match the final velocity VF. Note that this
-//      doesn't guarantee that the final velocity will be exactly VF. However, assuming that the initial velocity is much less than TOTAL_DISTANCE,
-//      the last wheel delta will be very close, if not the same, as VF.
-// For MAGNITUDE = (V0 + VF) / 2 and PHASE_SHIFT = arccos((V0 - VF) / (V0 + VF)), observe that delta(0) and delta(1) evaluate respectively to V0
-// and VF. Thus, we can express our gliding equation all in terms of V0, VF and t.
-float ScrollController::computeGlideDelta(ScrollEventAxis axis) const
-{
-    const ScrollSnapAnimatorState&amp; snapState = scrollSnapPointState(axis);
-
-    LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
-    bool canComputeGlide = (snapState.m_initialOffset &lt;= offset &amp;&amp; offset &lt; snapState.m_targetOffset) || (snapState.m_targetOffset &lt; offset &amp;&amp; offset &lt;= snapState.m_initialOffset);
-    if (snapState.m_currentState != ScrollSnapState::Gliding || !canComputeGlide)
-        return 0;
-
-    const float progress = snapProgress(offset, snapState);
-    const float rawGlideDelta = snapGlide(progress, snapState);
-
-    float glideDelta = snapState.m_initialOffset &lt; snapState.m_targetOffset ? std::max&lt;float&gt;(rawGlideDelta, 1) : std::min&lt;float&gt;(rawGlideDelta, -1);
-    if ((snapState.m_initialOffset &lt; snapState.m_targetOffset &amp;&amp; offset + glideDelta &gt; snapState.m_targetOffset) || (snapState.m_initialOffset &gt; snapState.m_targetOffset &amp;&amp; offset + glideDelta &lt; snapState.m_targetOffset))
-        return snapState.m_targetOffset - offset;
-    
-    return glideDelta;
</del><ins>+    return m_verticalScrollSnapState &amp;&amp; m_verticalScrollSnapState-&gt;isSnapping();
</ins><span class="cx"> }
</span><ins>+    
</ins><span class="cx"> #endif
</span><span class="cx"> #endif
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformcocoaScrollSnapAnimatorStateh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.h (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.h        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.h        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -29,6 +29,8 @@
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP)
</span><span class="cx"> 
</span><span class="cx"> #include &quot;AxisScrollSnapOffsets.h&quot;
</span><ins>+#include &quot;FloatPoint.h&quot;
+#include &quot;FloatSize.h&quot;
</ins><span class="cx"> #include &quot;LayoutUnit.h&quot;
</span><span class="cx"> #include &quot;PlatformWheelEvent.h&quot;
</span><span class="cx"> #include &quot;ScrollTypes.h&quot;
</span><span class="lines">@@ -48,7 +50,10 @@
</span><span class="cx">     void pushInitialWheelDelta(float);
</span><span class="cx">     float averageInitialWheelDelta() const;
</span><span class="cx">     void clearInitialWheelDeltaWindow();
</span><del>-
</del><ins>+    bool isSnapping() const;
+    bool canReachTargetWithCurrentInitialScrollDelta() const;
+    float interpolatedOffsetAtProgress(float) const;
+    
</ins><span class="cx">     static const int wheelDeltaWindowSize = 3;
</span><span class="cx"> 
</span><span class="cx">     Vector&lt;LayoutUnit&gt; m_snapOffsets;
</span><span class="lines">@@ -62,12 +67,32 @@
</span><span class="cx">     int m_numWheelDeltasTracked { 0 };
</span><span class="cx">     unsigned m_activeSnapIndex { 0 };
</span><span class="cx">     float m_wheelDeltaWindow[wheelDeltaWindowSize];
</span><del>-    float m_glideMagnitude { 0 };
-    float m_glidePhaseShift { 0 };
-    float m_glideInitialWheelDelta { 0 };
</del><ins>+    float m_initialScrollDelta { 0 };
</ins><span class="cx">     bool m_shouldOverrideWheelEvent { false };
</span><span class="cx"> };
</span><ins>+    
+/**
+ * Stores state variables necessary to coordinate snapping animations between
+ * horizontal and vertical axes.
+ */
+struct ScrollSnapAnimationCurveState {
+    
+    void initializeSnapProgressCurve(const FloatSize&amp;, const FloatSize&amp;, const FloatSize&amp;);
+    void initializeInterpolationCoefficientsIfNecessary(const FloatSize&amp;, const FloatSize&amp;, const FloatSize&amp;);
+    FloatPoint interpolatedPositionAtProgress(float) const;
+    bool shouldCompleteSnapAnimationImmediatelyAtTime(double) const;
+    float animationProgressAtTime(double) const;
</ins><span class="cx"> 
</span><ins>+    bool shouldAnimateDirectlyToSnapPoint { false };
+    
+private:
+    double m_startTime { 0 };
+    float m_snapAnimationCurveMagnitude { 0 };
+    float m_snapAnimationDecayFactor { 0 };
+    FloatSize m_snapAnimationCurveCoefficients[4] { };
+};
+
+
</ins><span class="cx"> } // namespace WebCore
</span><span class="cx"> 
</span><span class="cx"> #endif // ENABLE(CSS_SCROLL_SNAP)
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformcocoaScrollSnapAnimatorStatemm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm (186468 => 186469)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm        2015-07-07 18:28:45 UTC (rev 186468)
+++ trunk/Source/WebCore/platform/cocoa/ScrollSnapAnimatorState.mm        2015-07-07 19:12:22 UTC (rev 186469)
</span><span class="lines">@@ -25,6 +25,7 @@
</span><span class="cx"> 
</span><span class="cx"> #include &quot;config.h&quot;
</span><span class="cx"> #include &quot;ScrollSnapAnimatorState.h&quot;
</span><ins>+#include &lt;wtf/CurrentTime.h&gt;
</ins><span class="cx"> 
</span><span class="cx"> #if ENABLE(CSS_SCROLL_SNAP)
</span><span class="cx"> 
</span><span class="lines">@@ -52,20 +53,150 @@
</span><span class="cx">         return 0;
</span><span class="cx"> 
</span><span class="cx">     float sum = 0;
</span><del>-    for (int i = 0; i &lt; m_numWheelDeltasTracked; i++)
</del><ins>+    int numZeroDeltas = 0;
+    for (int i = 0; i &lt; m_numWheelDeltasTracked; ++i) {
</ins><span class="cx">         sum += m_wheelDeltaWindow[i];
</span><ins>+        if (!m_wheelDeltaWindow[i])
+            numZeroDeltas++;
+    }
</ins><span class="cx"> 
</span><del>-    return sum / m_numWheelDeltasTracked;
</del><ins>+    return m_numWheelDeltasTracked == numZeroDeltas ? 0 : sum / (m_numWheelDeltasTracked - numZeroDeltas);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> void ScrollSnapAnimatorState::clearInitialWheelDeltaWindow()
</span><span class="cx"> {
</span><del>-    for (int i = 0; i &lt; m_numWheelDeltasTracked; i++)
</del><ins>+    for (int i = 0; i &lt; m_numWheelDeltasTracked; ++i)
</ins><span class="cx">         m_wheelDeltaWindow[i] = 0;
</span><span class="cx"> 
</span><span class="cx">     m_numWheelDeltasTracked = 0;
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+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 &lt; m_targetOffset ? m_initialScrollDelta &gt; 0 : m_initialScrollDelta &lt; 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&amp; initialVector, const FloatSize&amp; targetVector, const FloatSize&amp; 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 &lt; 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) &lt; 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 &quot;progress&quot; 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&amp; initialVector, const FloatSize&amp; targetVector, const FloatSize&amp; 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 &lt;= 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 &lt; 4; ++i)
+        interpolatedPoint += std::pow(progress, i) * m_snapAnimationCurveCoefficients[i];
+    
+    return interpolatedPoint;
+}
+    
+bool ScrollSnapAnimationCurveState::shouldCompleteSnapAnimationImmediatelyAtTime(double time) const
+{
+    return m_startTime + scrollSnapAnimationDuration &lt; 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)));
+}
+    
</ins><span class="cx"> } // namespace WebCore
</span><span class="cx"> 
</span><span class="cx"> #endif // CSS_SCROLL_SNAP
</span></span></pre>
</div>
</div>

</body>
</html>