Bug 1245748 - Use CSSAnimationBuilder::BuildAnimationFrames to set up CSS animations using Keyframe objects; r?heycam draft
authorBrian Birtles <birtles@gmail.com>
Wed, 30 Mar 2016 13:01:13 +0900
changeset 345709 450112644a65c1c01337389a78ffac5cd780b242
parent 345708 425195ff458ab3fd6066cf06999ee717c7158722
child 345710 a95fb23bd763a225712f674e973fc00ca4990e6b
push id14148
push userbbirtles@mozilla.com
push dateWed, 30 Mar 2016 04:59:38 +0000
reviewersheycam
bugs1245748
milestone48.0a1
Bug 1245748 - 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();