Bug 1260655 - Use CSSAnimationBuilder::BuildAnimationFrames to set up CSS animations using Keyframe objects; r=heycam
authorBrian Birtles <birtles@gmail.com>
Wed, 30 Mar 2016 13:01:13 +0900
changeset 292564 063370aa01ab3d039f132372abd3d8c4adddf73e
parent 292563 d846bcdce3e9e8a192042f51834627da6253908e
child 292565 52c0128aff10f4c50ae3ab34c64cef8844497942
push id74886
push userbbirtles@mozilla.com
push dateMon, 11 Apr 2016 05:50:31 +0000
treeherdermozilla-inbound@52c0128aff10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersheycam
bugs1260655
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1260655 - Use CSSAnimationBuilder::BuildAnimationFrames to set up CSS animations using Keyframe objects; r=heycam MozReview-Commit-ID: BMLvYP8iIIa
dom/animation/test/css-animations/file_keyframeeffect-getframes.html
layout/style/nsAnimationManager.cpp
layout/style/test/test_animations.html
layout/style/test/test_animations_omta.html
--- a/dom/animation/test/css-animations/file_keyframeeffect-getframes.html
+++ b/dom/animation/test/css-animations/file_keyframeeffect-getframes.html
@@ -10,18 +10,18 @@
 }
 
 @keyframes anim-only-timing {
   from { animation-timing-function: linear; }
   to   { }
 }
 
 @keyframes anim-only-non-animatable {
-  from { display: none; }
-  to   { display: inline; }
+  from { animation-duration: 3s; }
+  to   { animation-duration: 5s; }
 }
 
 @keyframes anim-simple {
   from { color: black; }
   to   { color: white; }
 }
 
 @keyframes anim-simple-three {
@@ -54,16 +54,23 @@
 @keyframes anim-omit-from {
   to   { color: blue; }
 }
 
 @keyframes anim-omit-from-to {
   50%  { color: blue; }
 }
 
+@keyframes anim-partially-omit-to {
+  from { margin-top: 50px;
+         margin-bottom: 100px; }
+  to   { margin-top: 150px !important; /* ignored */
+         margin-bottom: 200px; }
+}
+
 @keyframes anim-different-props {
   from { color: black; margin-top: 8px; }
   25%  { color: blue; }
   75%  { margin-top: 12px; }
   to   { color: white; margin-top: 16px }
 }
 
 @keyframes anim-different-props-and-easing {
@@ -95,31 +102,57 @@
   from { margin-top: 0px; animation-timing-function: steps(1, end); }
   from { margin-right: 0px; animation-timing-function: step-end; }
   from { margin-bottom: 0px; animation-timing-function: steps(1); }
   50%  { margin-top: 10px; animation-timing-function: step-end; }
   50%  { margin-right: 10px; animation-timing-function: step-end; }
   50%  { margin-bottom: 10px; animation-timing-function: step-end; }
   to   { margin-top: 20px; margin-right: 20px; margin-bottom: 20px; }
 }
+
+@keyframes anim-overriding {
+  from          { padding-top: 50px }
+  50%, from     { padding-top: 30px } /* wins: 0% */
+  75%, 85%, 50% { padding-top: 20px } /* wins: 75%, 50% */
+  100%, 85%     { padding-top: 70px } /* wins: 100% */
+  85.1%         { padding-top: 60px } /* wins: 85.1% */
+  85%           { padding-top: 30px } /* wins: 85% */
+}
+
+@keyframes anim-filter {
+  to { filter: blur(5px) sepia(60%) saturate(30%); }
+}
+
+@keyframes anim-text-shadow {
+  to { text-shadow: none; }
+}
+
+@keyframes anim-background-size {
+  to { background-size: 50%, 6px, contain }
+}
 </style>
 <body>
 <script>
 "use strict";
 
 function getFrames(e) {
   return e.getAnimations()[0].effect.getFrames();
 }
 
 function assert_frames_equal(a, b, name) {
   assert_equals(Object.keys(a).sort().toString(),
                 Object.keys(b).sort().toString(),
                 "properties on " + name);
   for (var p in a) {
-    assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+    if (p === 'offset' || p === 'computedOffset') {
+      assert_approx_equals(a[p], b[p], 0.00001,
+                           "value for '" + p + "' on " + name);
+    } else {
+      assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
+    }
   }
 }
 
 // animation-timing-function values to test with, where the value
 // is exactly the same as its serialization, sorted by the order
 // getFrames() will group frames with the same easing function
 // together (by nsTimingFunction::Compare).
 const kTimingFunctionValues = [
@@ -137,17 +170,16 @@ const kTimingFunctionValues = [
   "steps(2)",
   "steps(2, end)",
   "cubic-bezier(0, 0, 1, 1)",
   "cubic-bezier(0, 0.25, 0.75, 1)",
 ];
 
 test(function(t) {
   var div = addDiv(t);
-  var frames;
 
   div.style.animation = 'anim-empty 100s';
   assert_equals(getFrames(div).length, 0,
                 "number of frames with empty @keyframes");
 
   div.style.animation = 'anim-empty-frames 100s';
   assert_equals(getFrames(div).length, 0,
                 "number of frames when @keyframes has empty keyframes");
@@ -168,20 +200,18 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 0)" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(255, 255, 255)" },
+    { offset: 0, computedOffset: 0, easing: "ease", color: "black" },
+    { offset: 1, computedOffset: 1, easing: "ease", color: "white" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for a simple ' +
    'animation');
 
@@ -190,17 +220,17 @@ test(function(t) {
     var div = addDiv(t);
 
     div.style.animation = 'anim-simple-three 100s ' + easing;
     var frames = getFrames(div);
 
     assert_equals(frames.length, 3, "number of frames");
 
     for (var i = 0; i < frames.length; i++) {
-      assert_equals(frames[i].easing, i == frames.length - 1 ? "linear" : easing,
+      assert_equals(frames[i].easing, easing,
                     "value for 'easing' on ComputedKeyframe #" + i);
     }
   });
 }, 'KeyframeEffectReadOnly.getFrames() returns frames with expected easing ' +
    'values, when the easing comes from animation-timing-function on the ' +
    'element');
 
 test(function(t) {
@@ -209,52 +239,52 @@ test(function(t) {
   div.style.animation = 'anim-simple-timing 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 3, "number of frames");
   assert_equals(frames[0].easing, "linear",
                 "value of 'easing' on ComputedKeyframe #0");
   assert_equals(frames[1].easing, "ease-in-out",
                 "value of 'easing' on ComputedKeyframe #1");
-  assert_equals(frames[2].easing, "linear",
+  assert_equals(frames[2].easing, "step-end",
                 "value of 'easing' on ComputedKeyframe #2");
 }, 'KeyframeEffectReadOnly.getFrames() returns frames with expected easing ' +
    'values, when the easing is specified on each keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-timing-some 100s step-start';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 3, "number of frames");
   assert_equals(frames[0].easing, "linear",
                 "value of 'easing' on ComputedKeyframe #0");
   assert_equals(frames[1].easing, "step-start",
                 "value of 'easing' on ComputedKeyframe #1");
-  assert_equals(frames[2].easing, "linear",
+  assert_equals(frames[2].easing, "step-start",
                 "value of 'easing' on ComputedKeyframe #2");
 }, 'KeyframeEffectReadOnly.getFrames() returns frames with expected easing ' +
    'values, when the easing is specified on some keyframes');
 
 test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-shorthand 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
-      marginTop: "8px", marginRight: "8px",
-      marginBottom: "8px", marginLeft: "8px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      marginTop: "16px", marginRight: "16px",
-      marginBottom: "16px", marginLeft: "16px" },
+      marginBottom: "8px", marginLeft: "8px",
+      marginRight: "8px", marginTop: "8px" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      marginBottom: "16px", marginLeft: "16px",
+      marginRight: "16px", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for a simple ' +
    'animation that specifies a single shorthand property');
 
@@ -263,19 +293,18 @@ test(function(t) {
 
   div.style.animation = 'anim-omit-to 100s';
   div.style.color = 'white';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 255)" },
-    { offset: 1, computedOffset: 1, easing: "linear",
+    { offset: 0, computedOffset: 0, easing: "ease", color: "blue" },
+    { offset: 1, computedOffset: 1, easing: "ease",
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with a 0% keyframe and no 100% keyframe');
@@ -287,18 +316,17 @@ test(function(t) {
   div.style.color = 'white';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
       color: "rgb(255, 255, 255)" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(0, 0, 255)" },
+    { offset: 1, computedOffset: 1, easing: "ease", color: "blue" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with a 100% keyframe and no 0% keyframe');
 
@@ -307,47 +335,69 @@ test(function(t) {
 
   div.style.animation = 'anim-omit-from-to 100s';
   div.style.color = 'white';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0,   computedOffset: 0,   easing: "ease",
       color: "rgb(255, 255, 255)" },
-    { offset: 0.5, computedOffset: 0.5, easing: "ease",
-      color: "rgb(0, 0, 255)" },
-    { offset: 1, computedOffset: 1, easing: "linear",
+    { offset: 0.5, computedOffset: 0.5, easing: "ease", color: "blue" },
+    { offset: 1,   computedOffset: 1,   easing: "ease",
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with no 0% or 100% keyframe but with a 50% keyframe');
 
 test(function(t) {
   var div = addDiv(t);
 
+  div.style.animation = 'anim-partially-omit-to 100s';
+  div.style.marginTop = '250px';
+  var frames = getFrames(div);
+
+  assert_equals(frames.length, 2, "number of frames");
+
+  var expected = [
+    { offset: 0, computedOffset: 0, easing: "ease",
+      marginTop: '50px', marginBottom: '100px' },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      marginTop: '250px', marginBottom: '200px' },
+  ];
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+  }
+}, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
+   'animation with a partially complete 100% keyframe (because the ' +
+   '!important rule is ignored)');
+
+test(function(t) {
+  var div = addDiv(t);
+
   div.style.animation = 'anim-different-props 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 0)", marginTop: "8px" },
+      color: "black", marginTop: "8px" },
     { offset: 0.25, computedOffset: 0.25, easing: "ease",
-      color: "rgb(0, 0, 255)" },
+      color: "blue" },
     { offset: 0.75, computedOffset: 0.75, easing: "ease",
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(255, 255, 255)", marginTop: "16px" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      color: "white", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with different properties on different keyframes, all ' +
    'with the same easing function');
@@ -357,23 +407,23 @@ test(function(t) {
 
   div.style.animation = 'anim-different-props-and-easing 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "linear",
-      color: "rgb(0, 0, 0)", marginTop: "8px" },
+      color: "black", marginTop: "8px" },
     { offset: 0.25, computedOffset: 0.25, easing: "step-end",
-      color: "rgb(0, 0, 255)" },
+      color: "blue" },
     { offset: 0.75, computedOffset: 0.75, easing: "ease-in",
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(255, 255, 255)", marginTop: "16px" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      color: "white", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with different properties on different keyframes, with ' +
    'a different easing function on each');
@@ -383,19 +433,19 @@ test(function(t) {
 
   div.style.animation = 'anim-merge-offset 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(255, 255, 255)", marginTop: "16px" },
+      color: "black", marginTop: "8px" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      color: "white", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with multiple keyframes for the same time, and all with ' +
    'the same easing function');
@@ -404,22 +454,22 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset-and-easing 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   var expected = [
+    { offset: 0, computedOffset: 0, easing: "step-end",
+      color: "black", fontSize: "16px" },
     { offset: 0, computedOffset: 0, easing: "linear",
       marginTop: "8px", paddingLeft: "2px" },
-    { offset: 0, computedOffset: 0, easing: "step-end",
-      color: "rgb(0, 0, 0)", fontSize: "16px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
-      color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
+    { offset: 1, computedOffset: 1, easing: "ease",
+      color: "white", fontSize: "32px", marginTop: "16px",
       paddingLeft: "4px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
@@ -429,30 +479,146 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-no-merge-equiv-easing 100s';
   var frames = getFrames(div);
 
   assert_equals(frames.length, 5, "number of frames");
 
   var expected = [
+    { offset: 0, computedOffset: 0, easing: "steps(1, end)",
+      marginTop: "0px" },
     { offset: 0, computedOffset: 0, easing: "step-end",
       marginRight: "0px" },
     { offset: 0, computedOffset: 0, easing: "steps(1)",
       marginBottom: "0px" },
-    { offset: 0, computedOffset: 0, easing: "steps(1, end)",
-      marginTop: "0px" },
     { offset: 0.5, computedOffset: 0.5, easing: "step-end",
       marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
-    { offset: 1, computedOffset: 1, easing: "linear",
+    { offset: 1, computedOffset: 1, easing: "ease",
       marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getFrames() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
    'different but equivalent easing functions');
 
+test(function(t) {
+  var div = addDiv(t);
+
+  div.style.animation = 'anim-overriding 100s';
+  var frames = getFrames(div);
+
+  assert_equals(frames.length, 6, "number of frames");
+
+  var expected = [
+    { offset: 0, computedOffset: 0, easing: "ease",
+      paddingTop: "30px" },
+    { offset: 0.5, computedOffset: 0.5, easing: "ease",
+      paddingTop: "20px" },
+    { offset: 0.75, computedOffset: 0.75, easing: "ease",
+      paddingTop: "20px" },
+    { offset: 0.85, computedOffset: 0.85, easing: "ease",
+      paddingTop: "30px" },
+    { offset: 0.851, computedOffset: 0.851, easing: "ease",
+      paddingTop: "60px" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      paddingTop: "70px" },
+  ];
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+  }
+}, 'KeyframeEffectReadOnly.getFrames() returns expected frames for ' +
+   'overlapping keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for filter, "none", is correctly represented.
+
+test(function(t) {
+  var div = addDiv(t);
+
+  div.style.animation = 'anim-filter 100s';
+  var frames = getFrames(div);
+
+  assert_equals(frames.length, 2, "number of frames");
+
+  var expected = [
+    { offset: 0, computedOffset: 0, easing: "ease",
+      filter: "none" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      filter: "blur(5px) sepia(60%) saturate(30%)" },
+  ];
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+  }
+}, 'KeyframeEffectReadOnly.getFrames() returns expected values for ' +
+   'animations with filter properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// computed value for text-shadow and a "none" specified on a keyframe
+// are correctly represented.
+
+test(function(t) {
+  var div = addDiv(t);
+
+  div.style.textShadow = '1px 1px 2px black, 0 0 16px blue, 0 0 3.2px blue';
+  div.style.animation = 'anim-text-shadow 100s';
+  var frames = getFrames(div);
+
+  assert_equals(frames.length, 2, "number of frames");
+
+  var expected = [
+    { offset: 0, computedOffset: 0, easing: "ease",
+      textShadow: "1px 1px 2px 0px rgb(0, 0, 0),"
+                  + " 0px 0px 16px 0px rgb(0, 0, 255),"
+                  + " 0px 0px 3.2px 0px rgb(0, 0, 255)" },
+    { offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" },
+  ];
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+  }
+}, 'KeyframeEffectReadOnly.getFrames() returns expected values for ' +
+   'animations with text-shadow properties and missing keyframes');
+
+// Gecko-specific test case: We are specifically concerned here that the
+// initial value for background-size and the specified list are correctly
+// represented.
+
+test(function(t) {
+  var div = addDiv(t);
+
+  div.style.animation = 'anim-background-size 100s';
+  var frames = getFrames(div);
+
+  assert_equals(frames.length, 2, "number of frames");
+
+  var expected = [
+    { offset: 0, computedOffset: 0, easing: "ease",
+      backgroundSize: "auto auto" },
+    { offset: 1, computedOffset: 1, easing: "ease",
+      backgroundSize: "50% auto, 6px auto, contain" },
+  ];
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+  }
+
+  // Test inheriting a background-size value
+
+  expected[0].backgroundSize = div.style.backgroundSize =
+    "30px auto, 40% auto, auto auto";
+  frames = getFrames(div);
+
+  for (var i = 0; i < frames.length; i++) {
+    assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i
+                        + " after updating current style");
+  }
+}, 'KeyframeEffectReadOnly.getFrames() returns expected values for ' +
+   'animations with background-size properties and missing keyframes');
+
 done();
 </script>
 </body>
--- a/layout/style/nsAnimationManager.cpp
+++ b/layout/style/nsAnimationManager.cpp
@@ -328,30 +328,30 @@ PopExistingAnimation(const nsAString& aN
 
   return nullptr;
 }
 
 static void
 UpdateOldAnimationPropertiesWithNew(
     CSSAnimation& aOld,
     TimingParams& aNewTiming,
-    InfallibleTArray<AnimationProperty>& aNewProperties,
-    bool aNewIsStylePaused)
+    nsTArray<Keyframe>& aNewFrames,
+    bool aNewIsStylePaused,
+    nsStyleContext* aStyleContext)
 {
   bool animationChanged = false;
 
   // Update the old from the new so we can keep the original object
   // identity (and any expando properties attached to it).
   if (aOld.GetEffect()) {
     KeyframeEffectReadOnly* oldEffect = aOld.GetEffect();
     animationChanged =
       oldEffect->SpecifiedTiming() != aNewTiming;
     oldEffect->SetSpecifiedTiming(aNewTiming);
-    animationChanged |=
-      oldEffect->UpdateProperties(aNewProperties);
+    oldEffect->SetFrames(Move(aNewFrames), aStyleContext);
   }
 
   // Handle changes in play state. If the animation is idle, however,
   // changes to animation-play-state should *not* restart it.
   if (aOld.PlayState() != AnimationPlayState::Idle) {
     // CSSAnimation takes care of override behavior so that,
     // for example, if the author has called pause(), that will
     // override the animation-play-state.
@@ -633,18 +633,18 @@ CSSAnimationBuilder::Build(nsPresContext
                            const StyleAnimation& aSrc,
                            const nsCSSKeyframesRule* aRule)
 {
   MOZ_ASSERT(aPresContext);
   MOZ_ASSERT(aRule);
 
   TimingParams timing = TimingParamsFrom(aSrc);
 
-  InfallibleTArray<AnimationProperty> animationProperties;
-  BuildAnimationProperties(aPresContext, aSrc, aRule, animationProperties);
+  nsTArray<Keyframe> keyframes =
+    BuildAnimationFrames(aPresContext, aSrc, aRule);
 
   bool isStylePaused =
     aSrc.GetPlayState() == NS_STYLE_ANIMATION_PLAY_STATE_PAUSED;
 
   // Find the matching animation with animation name in the old list
   // of animations and remove the matched animation from the list.
   RefPtr<CSSAnimation> oldAnim =
     PopExistingAnimation(aSrc.GetName(), mCollection);
@@ -655,26 +655,27 @@ CSSAnimationBuilder::Build(nsPresContext
     // old list of animations.
     // This means that we honor dynamic changes, which isn't what the
     // spec says to do, but WebKit seems to honor at least some of
     // them.  See
     // http://lists.w3.org/Archives/Public/www-style/2011Apr/0079.html
     // In order to honor what the spec said, we'd copy more data over.
     UpdateOldAnimationPropertiesWithNew(*oldAnim,
                                         timing,
-                                        animationProperties,
-                                        isStylePaused);
+                                        keyframes,
+                                        isStylePaused,
+                                        mStyleContext);
     return oldAnim.forget();
   }
 
   RefPtr<KeyframeEffectReadOnly> effect =
     new KeyframeEffectReadOnly(aPresContext->Document(), mTarget,
                                mStyleContext->GetPseudoType(), timing);
 
-  effect->Properties() = Move(animationProperties);
+  effect->SetFrames(Move(keyframes), mStyleContext);
 
   RefPtr<CSSAnimation> animation =
     new CSSAnimation(aPresContext->Document()->GetScopeObject(),
                      aSrc.GetName());
   animation->SetOwningElement(
     OwningElementRef(*mTarget, mStyleContext->GetPseudoType()));
 
   animation->SetTimeline(mTimeline);
--- a/layout/style/test/test_animations.html
+++ b/layout/style/test/test_animations.html
@@ -45,17 +45,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   @keyframes kf2 {
     from { margin-top: 150px }
     50% { margin-top: 50px }
   }
   @keyframes kf3 {
     25% { margin-top: 100px }
   }
   @keyframes kf4 {
-    to, from { display: inline; margin-top: 37px }
+    to, from { border-collapse: collapse; margin-top: 37px }
   }
   @keyframes kf_cascade1 {
     from { padding-top: 50px }
     50%, from { padding-top: 30px }      /* wins: 0% */
     75%, 85%, 50% { padding-top: 20px }  /* wins: 75%, 50% */
     100%, 85% { padding-top: 70px }      /* wins: 100% */
     85.1% { padding-top: 60px }          /* wins: 85.1% */
     85% { padding-top: 30px }            /* wins: 85% */
@@ -561,33 +561,33 @@ advance_clock(50);
 is(cs.marginTop, "50px", "no-0%-no-100% at 2.0s");
 done_div();
 
 // Test that non-animatable properties are ignored.
 // Simultaneously, test that the block is still honored, and that
 // we still override the value when two consecutive keyframes have
 // the same value.
 new_div("animation: kf4 ease 10s");
-is(cs.display, "block",
+is(cs.borderCollapse, "separate",
    "non-animatable properties should be ignored (linear, 0s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (linear, 0s)");
 advance_clock(1000);
-is(cs.display, "block",
+is(cs.borderCollapse, "separate",
    "non-animatable properties should be ignored (linear, 1s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (linear, 1s)");
 done_div();
 new_div("animation: kf4 step-start 10s");
-is(cs.display, "block",
+is(cs.borderCollapse, "separate",
    "non-animatable properties should be ignored (step-start, 0s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (step-start, 0s)");
 advance_clock(1000);
-is(cs.display, "block",
+is(cs.borderCollapse, "separate",
    "non-animatable properties should be ignored (step-start, 1s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (step-start, 1s)");
 done_div();
 
 // Test cascading of the keyframes within an @keyframes rule.
 new_div("animation: kf_cascade1 linear 10s");
 //   0%: 30px
--- a/layout/style/test/test_animations_omta.html
+++ b/layout/style/test/test_animations_omta.html
@@ -56,17 +56,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     @keyframes kf2 {
       from { transform: translate(150px) }
       50% { transform: translate(50px) }
     }
     @keyframes kf3 {
       25% { transform: translate(100px) }
     }
     @keyframes kf4 {
-      to, from { display: inline; transform: translate(37px) }
+      to, from { border-collapse: collapse; transform: translate(37px) }
     }
     @keyframes kf_cascade1 {
       from { transform: translate(50px) }
       50%, from { transform: translate(30px) }      /* wins: 0% */
       75%, 85%, 50% { transform: translate(20px) }  /* wins: 75%, 50% */
       100%, 85% { transform: translate(70px) }      /* wins: 100% */
       85.1% { transform: translate(60px) }          /* wins: 85.1% */
       85% { transform: translate(30px) }            /* wins: 85% */
@@ -632,35 +632,35 @@ addAsyncAnimTest(function *() {
 
   // Test that non-animatable properties are ignored.
   // Simultaneously, test that the block is still honored, and that
   // we still override the value when two consecutive keyframes have
   // the same value.
   new_div("animation: kf4 ease 10s");
   yield waitForPaintsFlushed();
   var cs = window.getComputedStyle(gDiv);
-  is(cs.display, "block",
+  is(cs.borderCollapse, "separate",
      "non-animatable properties should be ignored (linear, 0s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (linear, 0s)");
   advance_clock(1000);
-  is(cs.display, "block",
+  is(cs.borderCollapse, "separate",
      "non-animatable properties should be ignored (linear, 1s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (linear, 1s)");
   done_div();
   new_div("animation: kf4 step-start 10s");
   yield waitForPaintsFlushed();
   cs = window.getComputedStyle(gDiv);
-  is(cs.display, "block",
+  is(cs.borderCollapse, "separate",
      "non-animatable properties should be ignored (step-start, 0s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (step-start, 0s)");
   advance_clock(1000);
-  is(cs.display, "block",
+  is(cs.borderCollapse, "separate",
      "non-animatable properties should be ignored (step-start, 1s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (step-start, 1s)");
   done_div();
 
   // Test cascading of the keyframes within an @keyframes rule.
   new_div("animation: kf_cascade1 linear 10s");
   yield waitForPaintsFlushed();