Bug 1425837 - Part 4: Implement compositor animations on translate/rotate/scale. r=hiro,birtles
authorBoris Chiou <boris.chiou@gmail.com>
Mon, 18 Mar 2019 18:04:50 +0000
changeset 464926 b27d18cccc623d49dfcd89aa07890ab940d859f5
parent 464925 6c55b82deb9ba9b5924ef6bfdecdffaf9f42307a
child 464927 067a9dac440ab23f16c8034f104911a80773705a
push id35729
push useropoprus@mozilla.com
push dateTue, 19 Mar 2019 16:30:13 +0000
treeherdermozilla-central@1d783ed68779 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershiro, birtles
bugs1425837
milestone68.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 1425837 - Part 4: Implement compositor animations on translate/rotate/scale. r=hiro,birtles On the sender side of transactions, we have to convert the individual transforms to the proper types in layers::Animations, and this includes SetAnimatable and the definition in LayersMessages. On the compositor side (i.e. received side of transactions). Basically, we convert the list of layers::Animation into a list of `PropertyAnimationGroup`, which is an intermediate value. And then use this list to do interpolation for each property in `SampleAnimationForEachNode`, which will return a list of `RefPtr<RawServoAnimationValue>`. Depends on D23062 Differential Revision: https://phabricator.services.mozilla.com/D22565
gfx/layers/AnimationHelper.cpp
gfx/layers/AnimationHelper.h
gfx/layers/composite/AsyncCompositionManager.cpp
gfx/layers/ipc/LayersMessages.ipdlh
layout/painting/nsDisplayList.cpp
layout/painting/nsDisplayList.h
layout/style/ServoBindings.h
layout/style/StyleAnimationValue.cpp
layout/style/StyleAnimationValue.h
layout/style/nsStyleStruct.cpp
layout/style/nsStyleStruct.h
servo/ports/geckolib/glue.rs
--- a/gfx/layers/AnimationHelper.cpp
+++ b/gfx/layers/AnimationHelper.cpp
@@ -7,16 +7,17 @@
 #include "AnimationHelper.h"
 #include "mozilla/ComputedTimingFunction.h"      // for ComputedTimingFunction
 #include "mozilla/dom/AnimationEffectBinding.h"  // for dom::FillMode
 #include "mozilla/dom/KeyframeEffectBinding.h"   // for dom::IterationComposite
 #include "mozilla/dom/KeyframeEffect.h"       // for dom::KeyFrameEffectReadOnly
 #include "mozilla/dom/Nullable.h"             // for dom::Nullable
 #include "mozilla/layers/CompositorThread.h"  // for CompositorThreadHolder
 #include "mozilla/layers/LayerAnimationUtils.h"  // for TimingFunctionToComputedTimingFunction
+#include "mozilla/LayerAnimationInfo.h"  // for GetCSSPropertiesFor()
 #include "mozilla/ServoBindings.h"  // for Servo_ComposeAnimationSegment, etc
 #include "mozilla/StyleAnimationValue.h"  // for StyleAnimationValue, etc
 #include "nsDeviceContext.h"              // for AppUnitsPerCSSPixel
 #include "nsDisplayList.h"                // for nsDisplayTransform, etc
 
 namespace mozilla {
 namespace layers {
 
@@ -134,53 +135,47 @@ nsTArray<PropertyAnimationGroup>* Compos
 
 void CompositorAnimationStorage::SetAnimations(uint64_t aId,
                                                const AnimationArray& aValue) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   mAnimations.Put(aId, new nsTArray<PropertyAnimationGroup>(
                            AnimationHelper::ExtractAnimations(aValue)));
 }
 
-AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
+enum class CanSkipCompose {
+  IfPossible,
+  No,
+};
+static AnimationHelper::SampleResult SampleAnimationForProperty(
     TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
-    nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
-    RefPtr<RawServoAnimationValue>& aAnimationValue,
-    const AnimatedValue* aPreviousValue) {
-  MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
-             "Should be called with animations");
-
-  // FIXME: Will extend this list to multiple properties. For now, its length is
-  // always 1.
-  MOZ_ASSERT(aPropertyAnimationGroups.Length() == 1);
-  PropertyAnimationGroup& propertyAnimationGroup = aPropertyAnimationGroups[0];
-
+    const AnimatedValue* aPreviousValue, CanSkipCompose aCanSkipCompose,
+    nsTArray<PropertyAnimation>& aPropertyAnimations,
+    RefPtr<RawServoAnimationValue>& aAnimationValue) {
+  MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
   bool hasInEffectAnimations = false;
 #ifdef DEBUG
   // In cases where this function returns a SampleResult::Skipped, we actually
   // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
   // call site that the value that would have been computed matches the stored
   // value that we end up using. This flag is used to ensure we populate
   // aAnimationValue in this scenario.
   bool shouldBeSkipped = false;
 #endif
-  bool isSingleAnimation = propertyAnimationGroup.mAnimations.Length() == 1;
-  // Initialize by using base style.
-  aAnimationValue = propertyAnimationGroup.mBaseStyle;
   // Process in order, since later animations override earlier ones.
-  for (PropertyAnimation& animation : propertyAnimationGroup.mAnimations) {
+  for (PropertyAnimation& animation : aPropertyAnimations) {
     MOZ_ASSERT(
         (!animation.mOriginTime.IsNull() &&
          animation.mStartTime.type() == MaybeTimeDuration::TTimeDuration) ||
             animation.mIsNotPlaying,
         "If we are playing, we should have an origin time and a start time");
 
     // Determine if the animation was play-pending and used a ready time later
     // than the previous frame time.
     //
-    // To determine this, _all_ of the following consitions need to hold:
+    // To determine this, _all_ of the following conditions need to hold:
     //
     // * There was no previous animation value (i.e. this is the first frame for
     //   the animation since it was sent to the compositor), and
     // * The animation is playing, and
     // * There is a previous frame time, and
     // * The ready time of the animation is ahead of the previous frame time.
     //
     bool hasFutureReadyTime = false;
@@ -230,32 +225,33 @@ AnimationHelper::SampleResult AnimationH
 
     if (computedTiming.mProgress.IsNull()) {
       continue;
     }
 
     dom::IterationCompositeOperation iterCompositeOperation =
         animation.mIterationComposite;
 
-    // Skip caluculation if the progress hasn't changed since the last
+    // Skip calculation if the progress hasn't changed since the last
     // calculation.
     // Note that we don't skip calculate this animation if there is another
     // animation since the other animation might be 'accumulate' or 'add', or
     // might have a missing keyframe (i.e. this animation value will be used in
     // the missing keyframe).
     // FIXME Bug 1455476: We should do this optimizations for the case where
-    // the layer has multiple animations.
-    if (isSingleAnimation && !dom::KeyframeEffect::HasComputedTimingChanged(
-                                 computedTiming, iterCompositeOperation,
-                                 animation.mProgressOnLastCompose,
-                                 animation.mCurrentIterationOnLastCompose)) {
+    // the layer has multiple animations and multiple properties.
+    if (aCanSkipCompose == CanSkipCompose::IfPossible &&
+        !dom::KeyframeEffect::HasComputedTimingChanged(
+            computedTiming, iterCompositeOperation,
+            animation.mProgressOnLastCompose,
+            animation.mCurrentIterationOnLastCompose)) {
 #ifdef DEBUG
       shouldBeSkipped = true;
 #else
-      return SampleResult::Skipped;
+      return AnimationHelper::SampleResult::Skipped;
 #endif
     }
 
     uint32_t segmentIndex = 0;
     size_t segmentSize = animation.mSegments.Length();
     PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
     while (segment->mEndPortion < computedTiming.mProgress.Value() &&
            segmentIndex < segmentSize - 1) {
@@ -265,31 +261,32 @@ AnimationHelper::SampleResult AnimationH
 
     double positionInSegment =
         (computedTiming.mProgress.Value() - segment->mStartPortion) /
         (segment->mEndPortion - segment->mStartPortion);
 
     double portion = ComputedTimingFunction::GetPortion(
         segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
 
-    // Like above optimization, skip caluculation if the target segment isn't
+    // Like above optimization, skip calculation if the target segment isn't
     // changed and if the portion in the segment isn't changed.
     // This optimization is needed for CSS animations/transitions with step
-    // timing functions (e.g. the throbber animation on tab or frame based
+    // timing functions (e.g. the throbber animation on tabs or frame based
     // animations).
     // FIXME Bug 1455476: Like the above optimization, we should apply this
-    // optimizations for multiple animation cases as well.
-    if (isSingleAnimation &&
+    // optimizations for multiple animation cases and multiple properties as
+    // well.
+    if (aCanSkipCompose == CanSkipCompose::IfPossible &&
         animation.mSegmentIndexOnLastCompose == segmentIndex &&
         !animation.mPortionInSegmentOnLastCompose.IsNull() &&
         animation.mPortionInSegmentOnLastCompose.Value() == portion) {
 #ifdef DEBUG
       shouldBeSkipped = true;
 #else
-      return SampleResult::Skipped;
+      return AnimationHelper::SampleResult::Skipped;
 #endif
     }
 
     AnimationPropertySegment animSegment;
     animSegment.mFromKey = 0.0;
     animSegment.mToKey = 1.0;
     animSegment.mFromValue = AnimationValue(segment->mStartValue);
     animSegment.mToValue = AnimationValue(segment->mEndValue);
@@ -301,27 +298,71 @@ AnimationHelper::SampleResult AnimationH
         Servo_ComposeAnimationSegment(
             &animSegment, aAnimationValue,
             animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
             portion, computedTiming.mCurrentIteration)
             .Consume();
 
 #ifdef DEBUG
     if (shouldBeSkipped) {
-      return SampleResult::Skipped;
+      return AnimationHelper::SampleResult::Skipped;
     }
 #endif
 
     hasInEffectAnimations = true;
     animation.mProgressOnLastCompose = computedTiming.mProgress;
     animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
     animation.mSegmentIndexOnLastCompose = segmentIndex;
     animation.mPortionInSegmentOnLastCompose.SetValue(portion);
   }
 
+  return hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled
+                               : AnimationHelper::SampleResult::None;
+}
+
+AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
+    TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
+    const AnimatedValue* aPreviousValue,
+    nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
+    nsTArray<RefPtr<RawServoAnimationValue>>& aAnimationValues /* out */) {
+  MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
+             "Should be called with animation data");
+  MOZ_ASSERT(aAnimationValues.IsEmpty(),
+             "Should be called with empty aAnimationValues");
+
+  for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
+    // Initialize animation value with base style.
+    RefPtr<RawServoAnimationValue> currValue = group.mBaseStyle;
+
+    CanSkipCompose canSkipCompose = aPropertyAnimationGroups.Length() == 1 &&
+                                            group.mAnimations.Length() == 1
+                                        ? CanSkipCompose::IfPossible
+                                        : CanSkipCompose::No;
+    SampleResult result = SampleAnimationForProperty(
+        aPreviousFrameTime, aCurrentFrameTime, aPreviousValue, canSkipCompose,
+        group.mAnimations, currValue);
+
+    // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
+    // the result is skipped only if the property count == 1.
+    if (result == SampleResult::Skipped) {
+#ifdef DEBUG
+      aAnimationValues.AppendElement(std::move(currValue));
+#endif
+      return SampleResult::Skipped;
+    }
+
+    if (result != SampleResult::Sampled) {
+      continue;
+    }
+
+    // Insert the interpolation result into the output array.
+    MOZ_ASSERT(currValue);
+    aAnimationValues.AppendElement(std::move(currValue));
+  }
+
 #ifdef DEBUG
   // Sanity check that all of animation data are the same.
   const AnimationData& lastData =
       aPropertyAnimationGroups.LastElement().mAnimationData;
   for (const PropertyAnimationGroup& group : aPropertyAnimationGroups) {
     const AnimationData& data = group.mAnimationData;
     MOZ_ASSERT(data.type() == lastData.type(),
                "The type of AnimationData should be the same");
@@ -337,17 +378,18 @@ AnimationHelper::SampleResult AnimationH
                        lastTransformData.transformOrigin() &&
                    transformData.bounds() == lastTransformData.bounds() &&
                    transformData.appUnitsPerDevPixel() ==
                        lastTransformData.appUnitsPerDevPixel(),
                "All of members of TransformData should be the same");
   }
 #endif
 
-  return hasInEffectAnimations ? SampleResult::Sampled : SampleResult::None;
+  return aAnimationValues.IsEmpty() ? SampleResult::None
+                                    : SampleResult::Sampled;
 }
 
 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
   // Adjust fill mode so that if the main thread is delayed in clearing
   // this animation we don't introduce flicker by jumping back to the old
   // underlying value.
   auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
   float playbackRate = aAnimation.playbackRate();
@@ -373,32 +415,48 @@ static dom::FillMode GetAdjustedFillMode
       break;
   }
   return fillMode;
 }
 
 nsTArray<PropertyAnimationGroup> AnimationHelper::ExtractAnimations(
     const AnimationArray& aAnimations) {
   nsTArray<PropertyAnimationGroup> propertyAnimationGroupArray;
-  // FIXME: We only have one entry for now. In the next patch, we will extend
-  // this to multiple properties.
-  auto* propertyAnimationGroup = propertyAnimationGroupArray.AppendElement();
-  if (!aAnimations.IsEmpty()) {
-    propertyAnimationGroup->mProperty = aAnimations.LastElement().property();
-    propertyAnimationGroup->mAnimationData = aAnimations.LastElement().data();
-  }
+
+  nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
+  PropertyAnimationGroup* currData = nullptr;
+  DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
 
   for (const Animation& animation : aAnimations) {
+    // Animations with same property are grouped together, so we can just
+    // check if the current property is the same as the previous one for
+    // knowing this is a new group.
+    if (prevID != animation.property()) {
+      // Got a different group, we should create a different array.
+      currData = propertyAnimationGroupArray.AppendElement();
+      currData->mProperty = animation.property();
+      currData->mAnimationData = animation.data();
+      prevID = animation.property();
+
+      // Reset the debug pointer.
+      currBaseStyle = nullptr;
+    }
+
+    MOZ_ASSERT(currData);
     if (animation.baseStyle().type() != Animatable::Tnull_t) {
-      propertyAnimationGroup->mBaseStyle = AnimationValue::FromAnimatable(
+      MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
+                 "Should be the same base style");
+
+      currData->mBaseStyle = AnimationValue::FromAnimatable(
           animation.property(), animation.baseStyle());
+      currBaseStyle = &animation.baseStyle();
     }
 
     PropertyAnimation* propertyAnimation =
-        propertyAnimationGroup->mAnimations.AppendElement();
+        currData->mAnimations.AppendElement();
 
     propertyAnimation->mOriginTime = animation.originTime();
     propertyAnimation->mStartTime = animation.startTime();
     propertyAnimation->mHoldTime = animation.holdTime();
     propertyAnimation->mPlaybackRate = animation.playbackRate();
     propertyAnimation->mIterationComposite =
         static_cast<dom::IterationCompositeOperation>(
             animation.iterationComposite());
@@ -425,19 +483,40 @@ nsTArray<PropertyAnimationGroup> Animati
           AnimationUtils::TimingFunctionToComputedTimingFunction(
               segment.sampleFn()),
           segment.startPortion(), segment.endPortion(),
           static_cast<dom::CompositeOperation>(segment.startComposite()),
           static_cast<dom::CompositeOperation>(segment.endComposite())});
     }
   }
 
-  if (propertyAnimationGroup->IsEmpty()) {
-    propertyAnimationGroupArray.Clear();
+#ifdef DEBUG
+  // Sanity check that the grouped animation data is correct by looking at the
+  // property set.
+  if (!propertyAnimationGroupArray.IsEmpty()) {
+    nsCSSPropertyIDSet seenProperties;
+    for (const auto& group : propertyAnimationGroupArray) {
+      nsCSSPropertyID id = group.mProperty;
+
+      MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
+      seenProperties.AddProperty(id);
+    }
+
+    MOZ_ASSERT(
+        seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+            DisplayItemType::TYPE_TRANSFORM)) ||
+            seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+                DisplayItemType::TYPE_OPACITY)) ||
+            seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
+                DisplayItemType::TYPE_BACKGROUND_COLOR)),
+        "The property set of output should be the subset of transform-like "
+        "properties, opacity, or background_color.");
   }
+#endif
+
   return propertyAnimationGroupArray;
 }
 
 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
   static uint32_t sNextId = 0;
   ++sNextId;
 
   uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
@@ -461,43 +540,47 @@ bool AnimationHelper::SampleAnimations(C
   for (auto iter = aStorage->ConstAnimationsTableIter(); !iter.Done();
        iter.Next()) {
     auto& propertyAnimationGroups = *iter.UserData();
     if (propertyAnimationGroups.IsEmpty()) {
       continue;
     }
 
     isAnimating = true;
-    RefPtr<RawServoAnimationValue> animationValue;
+    nsTArray<RefPtr<RawServoAnimationValue>> animationValues;
     AnimatedValue* previousValue = aStorage->GetAnimatedValue(iter.Key());
     AnimationHelper::SampleResult sampleResult =
         AnimationHelper::SampleAnimationForEachNode(
-            aPreviousFrameTime, aCurrentFrameTime, propertyAnimationGroups,
-            animationValue, previousValue);
+            aPreviousFrameTime, aCurrentFrameTime, previousValue,
+            propertyAnimationGroups, animationValues);
 
     if (sampleResult != AnimationHelper::SampleResult::Sampled) {
       continue;
     }
 
     const PropertyAnimationGroup& lastPropertyAnimationGroup =
         propertyAnimationGroups.LastElement();
 
     // Store the AnimatedValue
     switch (lastPropertyAnimationGroup.mProperty) {
       case eCSSProperty_opacity: {
+        MOZ_ASSERT(animationValues.Length() == 1);
         aStorage->SetAnimatedValue(
-            iter.Key(), Servo_AnimationValue_GetOpacity(animationValue));
+            iter.Key(), Servo_AnimationValue_GetOpacity(animationValues[0]));
         break;
       }
+      case eCSSProperty_rotate:
+      case eCSSProperty_scale:
+      case eCSSProperty_translate:
       case eCSSProperty_transform: {
         const TransformData& transformData =
             lastPropertyAnimationGroup.mAnimationData.get_TransformData();
 
         gfx::Matrix4x4 transform =
-            ServoAnimationValueToMatrix4x4(animationValue, transformData);
+            ServoAnimationValueToMatrix4x4(animationValues, transformData);
         gfx::Matrix4x4 frameTransform = transform;
         // If the parent has perspective transform, then the offset into
         // reference frame coordinates is already on this transform. If not,
         // then we need to ask for it to be added here.
         if (!transformData.hasPerspectiveParent()) {
           nsLayoutUtils::PostTranslate(transform, transformData.origin(),
                                        transformData.appUnitsPerDevPixel(),
                                        true);
@@ -514,26 +597,55 @@ bool AnimationHelper::SampleAnimations(C
         MOZ_ASSERT_UNREACHABLE("Unhandled animated property");
     }
   }
 
   return isAnimating;
 }
 
 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
-    const RefPtr<RawServoAnimationValue>& aValue,
+    const nsTArray<RefPtr<RawServoAnimationValue>>& aValues,
     const TransformData& aTransformData) {
   // FIXME: Bug 1457033: We should convert servo's animation value to matrix
   // directly without nsCSSValueSharedList.
-  RefPtr<nsCSSValueSharedList> list;
-  Servo_AnimationValue_GetTransform(aValue, &list);
+  // TODO: Bug 1429305: Support compositor animations for motion-path.
+  RefPtr<nsCSSValueSharedList> transform, translate, rotate, scale;
+  for (const auto& value : aValues) {
+    MOZ_ASSERT(value);
+    RefPtr<nsCSSValueSharedList> list;
+    nsCSSPropertyID id = Servo_AnimationValue_GetTransform(value, &list);
+    switch (id) {
+      case eCSSProperty_transform:
+        MOZ_ASSERT(!transform);
+        transform = list.forget();
+        break;
+      case eCSSProperty_translate:
+        MOZ_ASSERT(!translate);
+        translate = list.forget();
+        break;
+      case eCSSProperty_rotate:
+        MOZ_ASSERT(!rotate);
+        rotate = list.forget();
+        break;
+      case eCSSProperty_scale:
+        MOZ_ASSERT(!scale);
+        scale = list.forget();
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
+    }
+  }
+  RefPtr<nsCSSValueSharedList> individualList =
+      nsStyleDisplay::GenerateCombinedIndividualTransform(translate, rotate,
+                                                          scale);
+
   // We expect all our transform data to arrive in device pixels
   gfx::Point3D transformOrigin = aTransformData.transformOrigin();
-  nsDisplayTransform::FrameTransformProperties props(std::move(list),
-                                                     transformOrigin);
+  nsDisplayTransform::FrameTransformProperties props(
+      std::move(individualList), std::move(transform), transformOrigin);
 
   return nsDisplayTransform::GetResultingTransformMatrix(
       props, aTransformData.origin(), aTransformData.appUnitsPerDevPixel(), 0,
       &aTransformData.bounds());
 }
 
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/AnimationHelper.h
+++ b/gfx/layers/AnimationHelper.h
@@ -215,37 +215,91 @@ class CompositorAnimationStorage final {
  */
 class AnimationHelper {
  public:
   enum class SampleResult { None, Skipped, Sampled };
 
   /**
    * Sample animations based on a given time stamp for a element(layer) with
    * its animation data.
-   * Generally |aPreviousFrameTimeStamp| is used for the sampling if it's
+   * Generally |aPreviousFrameTime| is used for the sampling if it's
    * supplied to make the animation more in sync with other animations on the
    * main-thread.  But in the case where the animation just started at the time
-   * when the animation was sent to the compositor, |aCurrentTime| is used for
-   * the sampling instead to avoid flickering the animation.
+   * when the animation was sent to the compositor, |aCurrentFrameTime| is used
+   * for sampling instead to avoid flicker.
    *
    * Returns SampleResult::None if none of the animations are producing a result
    * (e.g. they are in the delay phase with no backwards fill),
    * SampleResult::Skipped if the animation output did not change since the last
    * call of this function,
    * SampleResult::Sampled if the animation output was updated.
+   *
+   * Using the same example from ExtractAnimations (below):
+   *
+   * Input |aPropertyAnimationGroups| (ignoring the base animation style):
+   *
+   * [
+   *   Group A: [ { rotate, Animation A }, { rotate, Animation B } ],
+   *   Group B: [ { scale, Animation B } ],
+   *   Group C: [ { transform, Animation A }, { transform, Animation B } ],
+   * ]
+   *
+   * For each property group, this function interpolates each animation in turn,
+   * using the result of interpolating one animation as input for the next such
+   * that it reduces each property group to a single output value:
+   *
+   * [
+   *   { rotate, RawServoAnimationValue },
+   *   { scale, RawServoAnimationValue },
+   *   { transform, RawServoAnimationValue },
+   * ]
+   *
+   * For transform animations, the caller (SampleAnimations) will combine the
+   * result of the various transform properties into a final matrix.
    */
   static SampleResult SampleAnimationForEachNode(
       TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
+      const AnimatedValue* aPreviousValue,
       nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
-      RefPtr<RawServoAnimationValue>& aAnimationValue,
-      const AnimatedValue* aPreviousValue);
+      nsTArray<RefPtr<RawServoAnimationValue>>& aAnimationValues);
 
   /**
    * Extract organized animation data by property into an array of
-   * PropertyAnimationGroup.
+   * PropertyAnimationGroup objects.
+   *
+   * For example, suppose we have the following animations:
+   *
+   *   Animation A: [ transform, rotate ]
+   *   Animation B: [ rotate, scale ]
+   *   Animation C: [ transform ]
+   *   Animation D: [ opacity ]
+   *
+   * When we go to send transform-like properties to the compositor, we
+   * sort them as follows:
+   *
+   *   [
+   *     { rotate: Animation A (rotate segments only) },
+   *     { rotate: Animation B ( " " ) },
+   *     { scale: Animation B (scale segments only) },
+   *     { transform: Animation A (transform segments only) },
+   *     { transform: Animation C ( " " ) },
+   *   ]
+   *
+   * In this function, we group these animations together by property producing
+   * output such as the following:
+   *
+   *   [
+   *     [ { rotate, Animation A }, { rotate, Animation B } ],
+   *     [ { scale, Animation B } ],
+   *     [ { transform, Animation A }, { transform, Animation B } ],
+   *   ]
+   *
+   * In the process of grouping these animations, we also convert their values
+   * from the rather compact representation we use for transferring across the
+   * IPC boundary into something we can readily use for sampling.
    */
   static nsTArray<PropertyAnimationGroup> ExtractAnimations(
       const AnimationArray& aAnimations);
 
   /**
    * Get a unique id to represent the compositor animation between child
    * and parent side. This id will be used as a key to store animation
    * data in the CompositorAnimationStorage per compositor.
@@ -266,21 +320,21 @@ class AnimationHelper {
    *
    * Note: This is called only by WebRender.
    */
   static bool SampleAnimations(CompositorAnimationStorage* aStorage,
                                TimeStamp aPreviousFrameTime,
                                TimeStamp aCurrentFrameTime);
 
   /**
-   * Convert an animation value into a matrix given the corresponding transform
-   * parameters. |aValue| must be a transform-like value (e.g. transform,
-   * translate etc.).
+   * Convert an array of animation values into a matrix given the corresponding
+   * transform parameters. |aValue| must be a transform-like value
+   * (e.g. transform, translate etc.).
    */
   static gfx::Matrix4x4 ServoAnimationValueToMatrix4x4(
-      const RefPtr<RawServoAnimationValue>& aValue,
+      const nsTArray<RefPtr<RawServoAnimationValue>>& aValue,
       const TransformData& aTransformData);
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif  // mozilla_layers_AnimationHelper_h
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -613,57 +613,58 @@ static Matrix4x4 FrameTransformToTransfo
   if (ContainerLayer* c = aLayer->AsContainerLayer()) {
     transformInDevice.PostScale(c->GetInheritedXScale(),
                                 c->GetInheritedYScale(), 1);
   }
 
   return transformInDevice;
 }
 
-static void ApplyAnimatedValue(Layer* aLayer,
-                               CompositorAnimationStorage* aStorage,
-                               nsCSSPropertyID aProperty,
-                               const AnimationData& aAnimationData,
-                               const RefPtr<RawServoAnimationValue>& aValue) {
-  if (!aValue) {
-    // Return gracefully if we have no valid AnimationValue.
-    return;
-  }
+static void ApplyAnimatedValue(
+    Layer* aLayer, CompositorAnimationStorage* aStorage,
+    nsCSSPropertyID aProperty, const AnimationData& aAnimationData,
+    const nsTArray<RefPtr<RawServoAnimationValue>>& aValues) {
+  MOZ_ASSERT(!aValues.IsEmpty());
 
   HostLayer* layerCompositor = aLayer->AsHostLayer();
   switch (aProperty) {
     case eCSSProperty_background_color: {
+      MOZ_ASSERT(aValues.Length() == 1);
       // We don't support 'color' animations on the compositor yet so we never
       // meet currentColor on the compositor.
       nscolor color =
-          Servo_AnimationValue_GetColor(aValue, NS_RGBA(0, 0, 0, 0));
+          Servo_AnimationValue_GetColor(aValues[0], NS_RGBA(0, 0, 0, 0));
       aLayer->AsColorLayer()->SetColor(gfx::Color::FromABGR(color));
       aStorage->SetAnimatedValue(aLayer->GetCompositorAnimationsId(), color);
 
       layerCompositor->SetShadowOpacity(aLayer->GetOpacity());
       layerCompositor->SetShadowOpacitySetByAnimation(false);
       layerCompositor->SetShadowBaseTransform(aLayer->GetBaseTransform());
       layerCompositor->SetShadowTransformSetByAnimation(false);
       break;
     }
     case eCSSProperty_opacity: {
-      float opacity = Servo_AnimationValue_GetOpacity(aValue);
+      MOZ_ASSERT(aValues.Length() == 1);
+      float opacity = Servo_AnimationValue_GetOpacity(aValues[0]);
       layerCompositor->SetShadowOpacity(opacity);
       layerCompositor->SetShadowOpacitySetByAnimation(true);
       aStorage->SetAnimatedValue(aLayer->GetCompositorAnimationsId(), opacity);
 
       layerCompositor->SetShadowBaseTransform(aLayer->GetBaseTransform());
       layerCompositor->SetShadowTransformSetByAnimation(false);
       break;
     }
+    case eCSSProperty_rotate:
+    case eCSSProperty_scale:
+    case eCSSProperty_translate:
     case eCSSProperty_transform: {
       const TransformData& transformData = aAnimationData.get_TransformData();
 
       Matrix4x4 frameTransform =
-          AnimationHelper::ServoAnimationValueToMatrix4x4(aValue,
+          AnimationHelper::ServoAnimationValueToMatrix4x4(aValues,
                                                           transformData);
 
       Matrix4x4 transform = FrameTransformToTransformInDevice(
           frameTransform, aLayer, transformData);
 
       layerCompositor->SetShadowBaseTransform(transform);
       layerCompositor->SetShadowTransformSetByAnimation(true);
       aStorage->SetAnimatedValue(aLayer->GetCompositorAnimationsId(),
@@ -690,32 +691,34 @@ static bool SampleAnimations(Layer* aLay
     if (propertyAnimationGroups.IsEmpty()) {
       return;
     }
 
     isAnimating = true;
     AnimatedValue* previousValue =
         aStorage->GetAnimatedValue(layer->GetCompositorAnimationsId());
 
-    RefPtr<RawServoAnimationValue> animationValue;
+    nsTArray<RefPtr<RawServoAnimationValue>> animationValues;
     AnimationHelper::SampleResult sampleResult =
         AnimationHelper::SampleAnimationForEachNode(
-            aPreviousFrameTime, aCurrentFrameTime, propertyAnimationGroups,
-            animationValue, previousValue);
+            aPreviousFrameTime, aCurrentFrameTime, previousValue,
+            propertyAnimationGroups, animationValues);
 
     const PropertyAnimationGroup& lastPropertyAnimationGroup =
         propertyAnimationGroups.LastElement();
 
     switch (sampleResult) {
-      case AnimationHelper::SampleResult::Sampled: {
+      case AnimationHelper::SampleResult::Sampled:
+        // We assume all transform like properties (on the same frame) live in
+        // a single same layer, so using the transform data of the last element
+        // should be fine.
         ApplyAnimatedValue(
             layer, aStorage, lastPropertyAnimationGroup.mProperty,
-            lastPropertyAnimationGroup.mAnimationData, animationValue);
+            lastPropertyAnimationGroup.mAnimationData, animationValues);
         break;
-      }
       case AnimationHelper::SampleResult::Skipped:
         switch (lastPropertyAnimationGroup.mProperty) {
           case eCSSProperty_background_color:
           case eCSSProperty_opacity: {
             if (lastPropertyAnimationGroup.mProperty == eCSSProperty_opacity) {
               MOZ_ASSERT(
                   layer->AsHostLayer()->GetShadowOpacitySetByAnimation());
 #ifdef DEBUG
@@ -729,33 +732,36 @@ static bool SampleAnimations(Layer* aLay
             // Even if opacity or background-color  animation value has
             // unchanged, we have to set the shadow base transform value
             // here since the value might have been changed by APZC.
             HostLayer* layerCompositor = layer->AsHostLayer();
             layerCompositor->SetShadowBaseTransform(layer->GetBaseTransform());
             layerCompositor->SetShadowTransformSetByAnimation(false);
             break;
           }
+          case eCSSProperty_rotate:
+          case eCSSProperty_scale:
+          case eCSSProperty_translate:
           case eCSSProperty_transform: {
             MOZ_ASSERT(
                 layer->AsHostLayer()->GetShadowTransformSetByAnimation());
             MOZ_ASSERT(previousValue);
 #ifdef DEBUG
             const TransformData& transformData =
                 lastPropertyAnimationGroup.mAnimationData.get_TransformData();
             Matrix4x4 frameTransform =
-                AnimationHelper::ServoAnimationValueToMatrix4x4(animationValue,
+                AnimationHelper::ServoAnimationValueToMatrix4x4(animationValues,
                                                                 transformData);
             Matrix4x4 transformInDevice = FrameTransformToTransformInDevice(
                 frameTransform, layer, transformData);
             MOZ_ASSERT(previousValue->mTransform.mTransformInDevSpace
                            .FuzzyEqualsMultiplicative(transformInDevice));
 #endif
             // In the case of transform we have to set the unchanged
-            // transform value again becasue APZC might have modified the
+            // transform value again because APZC might have modified the
             // previous shadow base transform value.
             HostLayer* layerCompositor = layer->AsHostLayer();
             layerCompositor->SetShadowBaseTransform(
                 // FIXME: Bug 1459775: It seems possible that we somehow try
                 // to sample animations and skip it even if the previous value
                 // has been discarded from the animation storage when we enable
                 // layer tree cache. So for the safety, in the case where we
                 // have no previous animation value, we set non-animating value
--- a/gfx/layers/ipc/LayersMessages.ipdlh
+++ b/gfx/layers/ipc/LayersMessages.ipdlh
@@ -135,16 +135,23 @@ struct SkewX { CSSAngle x; };
 struct SkewY { CSSAngle y; };
 struct TransformMatrix { Matrix4x4 value; };
 struct Translation {
   float x;
   float y;
   float z;
 };
 
+union Rotate {
+  // null_t represents |rotate: none|.
+  null_t;
+  Rotation;
+  Rotation3D;
+};
+
 union TransformFunction {
   Perspective;
   RotationX;
   RotationY;
   RotationZ;
   Rotation;
   Rotation3D;
   Scale;
@@ -159,16 +166,19 @@ union MaybeTimeDuration {
   null_t;
   TimeDuration;
 };
 
 union Animatable {
   null_t;
   float;
   nscolor;
+  Rotate;
+  Scale;
+  Translation;
   TransformFunction[];
 };
 
 struct AnimationSegment {
   Animatable startState;
   Animatable endState;
   float startPortion;
   float endPortion;
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -176,16 +176,118 @@ nsCString ActiveScrolledRoot::ToString(
   }
   return std::move(str);
 }
 
 static inline CSSAngle MakeCSSAngle(const nsCSSValue& aValue) {
   return CSSAngle(aValue.GetAngleValue(), aValue.GetUnit());
 }
 
+static Rotate GetRotate(const nsCSSValue& aValue) {
+  Rotate result = null_t();
+  if (aValue.GetUnit() == eCSSUnit_None) {
+    return result;
+  }
+
+  const nsCSSValue::Array* array = aValue.GetArrayValue();
+  switch (nsStyleTransformMatrix::TransformFunctionOf(array)) {
+    case eCSSKeyword_rotate:
+      result = Rotate(Rotation(MakeCSSAngle(array->Item(1))));
+      break;
+    case eCSSKeyword_rotate3d:
+      result = Rotate(Rotation3D(
+          array->Item(1).GetFloatValue(), array->Item(2).GetFloatValue(),
+          array->Item(3).GetFloatValue(), MakeCSSAngle(array->Item(4))));
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported rotate");
+  }
+  return result;
+}
+
+static Scale GetScale(const nsCSSValue& aValue) {
+  Scale result(1., 1., 1.);
+  if (aValue.GetUnit() == eCSSUnit_None) {
+    // Use (1, 1, 1) to replace the none case.
+    return result;
+  }
+
+  const nsCSSValue::Array* array = aValue.GetArrayValue();
+  switch (nsStyleTransformMatrix::TransformFunctionOf(array)) {
+    case eCSSKeyword_scalex:
+      result.x() = array->Item(1).GetFloatValue();
+      break;
+    case eCSSKeyword_scaley:
+      result.y() = array->Item(1).GetFloatValue();
+      break;
+    case eCSSKeyword_scalez:
+      result.z() = array->Item(1).GetFloatValue();
+      break;
+    case eCSSKeyword_scale:
+      result.x() = array->Item(1).GetFloatValue();
+      // scale(x) is shorthand for scale(x, x);
+      result.y() =
+          array->Count() == 2 ? result.x() : array->Item(2).GetFloatValue();
+      break;
+    case eCSSKeyword_scale3d:
+      result.x() = array->Item(1).GetFloatValue();
+      result.y() = array->Item(2).GetFloatValue();
+      result.z() = array->Item(3).GetFloatValue();
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported scale");
+  }
+  return result;
+}
+
+static Translation GetTranslate(const nsCSSValue& aValue,
+                                TransformReferenceBox& aRefBox) {
+  Translation result(0, 0, 0);
+  if (aValue.GetUnit() == eCSSUnit_None) {
+    // Use (0, 0, 0) to replace the none case.
+    return result;
+  }
+
+  const nsCSSValue::Array* array = aValue.GetArrayValue();
+  switch (nsStyleTransformMatrix::TransformFunctionOf(array)) {
+    case eCSSKeyword_translatex:
+      result.x() = nsStyleTransformMatrix::ProcessTranslatePart(
+          array->Item(1), &aRefBox, &TransformReferenceBox::Width);
+      break;
+    case eCSSKeyword_translatey:
+      result.y() = nsStyleTransformMatrix::ProcessTranslatePart(
+          array->Item(1), &aRefBox, &TransformReferenceBox::Height);
+      break;
+    case eCSSKeyword_translatez:
+      result.z() =
+          nsStyleTransformMatrix::ProcessTranslatePart(array->Item(1), nullptr);
+      break;
+    case eCSSKeyword_translate:
+      result.x() = nsStyleTransformMatrix::ProcessTranslatePart(
+          array->Item(1), &aRefBox, &TransformReferenceBox::Width);
+      // translate(x) is shorthand for translate(x, 0)
+      if (array->Count() == 3) {
+        result.y() = nsStyleTransformMatrix::ProcessTranslatePart(
+            array->Item(2), &aRefBox, &TransformReferenceBox::Height);
+      }
+      break;
+    case eCSSKeyword_translate3d:
+      result.x() = nsStyleTransformMatrix::ProcessTranslatePart(
+          array->Item(1), &aRefBox, &TransformReferenceBox::Width);
+      result.y() = nsStyleTransformMatrix::ProcessTranslatePart(
+          array->Item(2), &aRefBox, &TransformReferenceBox::Height);
+      result.z() =
+          nsStyleTransformMatrix::ProcessTranslatePart(array->Item(3), nullptr);
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported translate");
+  }
+  return result;
+}
+
 static void AddTransformFunctions(
     const nsCSSValueList* aList, mozilla::ComputedStyle* aStyle,
     nsPresContext* aPresContext, TransformReferenceBox& aRefBox,
     InfallibleTArray<TransformFunction>& aFunctions) {
   if (aList->mValue.GetUnit() == eCSSUnit_None) {
     return;
   }
 
@@ -205,99 +307,36 @@ static void AddTransformFunctions(
         aFunctions.AppendElement(RotationY(theta));
         break;
       }
       case eCSSKeyword_rotatez: {
         CSSAngle theta = MakeCSSAngle(array->Item(1));
         aFunctions.AppendElement(RotationZ(theta));
         break;
       }
-      case eCSSKeyword_rotate: {
-        CSSAngle theta = MakeCSSAngle(array->Item(1));
-        aFunctions.AppendElement(Rotation(theta));
-        break;
-      }
-      case eCSSKeyword_rotate3d: {
-        double x = array->Item(1).GetFloatValue();
-        double y = array->Item(2).GetFloatValue();
-        double z = array->Item(3).GetFloatValue();
-        CSSAngle theta = MakeCSSAngle(array->Item(4));
-        aFunctions.AppendElement(Rotation3D(x, y, z, theta));
-        break;
-      }
-      case eCSSKeyword_scalex: {
-        double x = array->Item(1).GetFloatValue();
-        aFunctions.AppendElement(Scale(x, 1, 1));
+      case eCSSKeyword_rotate:
+        aFunctions.AppendElement(GetRotate(currElem).get_Rotation());
         break;
-      }
-      case eCSSKeyword_scaley: {
-        double y = array->Item(1).GetFloatValue();
-        aFunctions.AppendElement(Scale(1, y, 1));
-        break;
-      }
-      case eCSSKeyword_scalez: {
-        double z = array->Item(1).GetFloatValue();
-        aFunctions.AppendElement(Scale(1, 1, z));
-        break;
-      }
-      case eCSSKeyword_scale: {
-        double x = array->Item(1).GetFloatValue();
-        // scale(x) is shorthand for scale(x, x);
-        double y = array->Count() == 2 ? x : array->Item(2).GetFloatValue();
-        aFunctions.AppendElement(Scale(x, y, 1));
-        break;
-      }
-      case eCSSKeyword_scale3d: {
-        double x = array->Item(1).GetFloatValue();
-        double y = array->Item(2).GetFloatValue();
-        double z = array->Item(3).GetFloatValue();
-        aFunctions.AppendElement(Scale(x, y, z));
+      case eCSSKeyword_rotate3d:
+        aFunctions.AppendElement(GetRotate(currElem).get_Rotation3D());
         break;
-      }
-      case eCSSKeyword_translatex: {
-        double x = nsStyleTransformMatrix::ProcessTranslatePart(
-            array->Item(1), &aRefBox, &TransformReferenceBox::Width);
-        aFunctions.AppendElement(Translation(x, 0, 0));
-        break;
-      }
-      case eCSSKeyword_translatey: {
-        double y = nsStyleTransformMatrix::ProcessTranslatePart(
-            array->Item(1), &aRefBox, &TransformReferenceBox::Height);
-        aFunctions.AppendElement(Translation(0, y, 0));
-        break;
-      }
-      case eCSSKeyword_translatez: {
-        double z = nsStyleTransformMatrix::ProcessTranslatePart(array->Item(1),
-                                                                nullptr);
-        aFunctions.AppendElement(Translation(0, 0, z));
+      case eCSSKeyword_scalex:
+      case eCSSKeyword_scaley:
+      case eCSSKeyword_scalez:
+      case eCSSKeyword_scale:
+      case eCSSKeyword_scale3d:
+        aFunctions.AppendElement(GetScale(currElem));
         break;
-      }
-      case eCSSKeyword_translate: {
-        double x = nsStyleTransformMatrix::ProcessTranslatePart(
-            array->Item(1), &aRefBox, &TransformReferenceBox::Width);
-        // translate(x) is shorthand for translate(x, 0)
-        double y = 0;
-        if (array->Count() == 3) {
-          y = nsStyleTransformMatrix::ProcessTranslatePart(
-              array->Item(2), &aRefBox, &TransformReferenceBox::Height);
-        }
-        aFunctions.AppendElement(Translation(x, y, 0));
+      case eCSSKeyword_translatex:
+      case eCSSKeyword_translatey:
+      case eCSSKeyword_translatez:
+      case eCSSKeyword_translate:
+      case eCSSKeyword_translate3d:
+        aFunctions.AppendElement(GetTranslate(currElem, aRefBox));
         break;
-      }
-      case eCSSKeyword_translate3d: {
-        double x = nsStyleTransformMatrix::ProcessTranslatePart(
-            array->Item(1), &aRefBox, &TransformReferenceBox::Width);
-        double y = nsStyleTransformMatrix::ProcessTranslatePart(
-            array->Item(2), &aRefBox, &TransformReferenceBox::Height);
-        double z = nsStyleTransformMatrix::ProcessTranslatePart(array->Item(3),
-                                                                nullptr);
-
-        aFunctions.AppendElement(Translation(x, y, z));
-        break;
-      }
       case eCSSKeyword_skewx: {
         CSSAngle x = MakeCSSAngle(array->Item(1));
         aFunctions.AppendElement(SkewX(x));
         break;
       }
       case eCSSKeyword_skewy: {
         CSSAngle y = MakeCSSAngle(array->Item(1));
         aFunctions.AppendElement(SkewY(y));
@@ -409,16 +448,17 @@ static TimingFunction ToTimingFunction(
 
 static void SetAnimatable(nsCSSPropertyID aProperty,
                           const AnimationValue& aAnimationValue,
                           nsIFrame* aFrame, TransformReferenceBox& aRefBox,
                           layers::Animatable& aAnimatable) {
   MOZ_ASSERT(aFrame);
 
   if (aAnimationValue.IsNull()) {
+    printf_stderr("[Boris] set null animatable\n");
     aAnimatable = null_t();
     return;
   }
 
   switch (aProperty) {
     case eCSSProperty_background_color: {
       // We don't support color animation on the compositor yet so that we can
       // resolve currentColor at this moment.
@@ -434,21 +474,44 @@ static void SetAnimatable(nsCSSPropertyI
         foreground = aFrame->Style()->StyleColor()->mColor;
       }
       aAnimatable = aAnimationValue.GetColor(foreground);
       break;
     }
     case eCSSProperty_opacity:
       aAnimatable = aAnimationValue.GetOpacity();
       break;
+    case eCSSProperty_rotate: {
+      RefPtr<const nsCSSValueSharedList> list =
+          aAnimationValue.GetTransformList();
+      MOZ_ASSERT(list && list->mHead && !list->mHead->mNext,
+                 "should have only one nsCSSValueList for rotate");
+      aAnimatable = GetRotate(list->mHead->mValue);
+      break;
+    }
+    case eCSSProperty_scale: {
+      RefPtr<const nsCSSValueSharedList> list =
+          aAnimationValue.GetTransformList();
+      MOZ_ASSERT(list && list->mHead && !list->mHead->mNext,
+                 "should have only one nsCSSValueList for scale");
+      aAnimatable = GetScale(list->mHead->mValue);
+      break;
+    }
+    case eCSSProperty_translate: {
+      RefPtr<const nsCSSValueSharedList> list =
+          aAnimationValue.GetTransformList();
+      MOZ_ASSERT(list && list->mHead && !list->mHead->mNext,
+                 "should have only one nsCSSValueList for translate");
+      aAnimatable = GetTranslate(list->mHead->mValue, aRefBox);
+      break;
+    }
     case eCSSProperty_transform: {
       aAnimatable = InfallibleTArray<TransformFunction>();
-      MOZ_ASSERT(aAnimationValue.mServo);
-      RefPtr<nsCSSValueSharedList> list;
-      Servo_AnimationValue_GetTransform(aAnimationValue.mServo, &list);
+      RefPtr<const nsCSSValueSharedList> list =
+          aAnimationValue.GetTransformList();
       AddTransformFunctions(list, aFrame, aRefBox, aAnimatable);
       break;
     }
     default:
       MOZ_ASSERT_UNREACHABLE("Unsupported property");
   }
 }
 
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -6714,24 +6714,24 @@ class nsDisplayTransform : public nsDisp
   static bool ComputePerspectiveMatrix(const nsIFrame* aFrame,
                                        float aAppUnitsPerPixel,
                                        Matrix4x4& aOutMatrix);
 
   struct FrameTransformProperties {
     FrameTransformProperties(const nsIFrame* aFrame, float aAppUnitsPerPixel,
                              const nsRect* aBoundsOverride);
     // This constructor is used on the compositor (for animations).
-    // Bug 1186329, Bug 1425837, If we want to support compositor animationsf
-    // or individual transforms and motion path, we may need to update this.
-    // For now, let mIndividualTransformList and mMotion as nullptr and
-    // Nothing().
+    // FIXME: Bug 1186329: if we want to support compositor animations for
+    // motion path, we need to update this. For now, let mMotion be Nothing().
     FrameTransformProperties(
+        RefPtr<const nsCSSValueSharedList>&& aIndividualTransform,
         RefPtr<const nsCSSValueSharedList>&& aTransformList,
         const Point3D& aToTransformOrigin)
         : mFrame(nullptr),
+          mIndividualTransformList(std::move(aIndividualTransform)),
           mTransformList(std::move(aTransformList)),
           mToTransformOrigin(aToTransformOrigin) {}
 
     bool HasTransform() const {
       return mIndividualTransformList || mTransformList || mMotion.isSome();
     }
 
     const nsIFrame* mFrame;
--- a/layout/style/ServoBindings.h
+++ b/layout/style/ServoBindings.h
@@ -574,21 +574,21 @@ void Servo_AnimationValue_Serialize(RawS
 nscolor Servo_AnimationValue_GetColor(RawServoAnimationValueBorrowed value,
                                       nscolor foregroundColor);
 RawServoAnimationValueStrong Servo_AnimationValue_Color(nsCSSPropertyID,
                                                         nscolor);
 
 float Servo_AnimationValue_GetOpacity(RawServoAnimationValueBorrowed value);
 RawServoAnimationValueStrong Servo_AnimationValue_Opacity(float);
 
-void Servo_AnimationValue_GetTransform(RawServoAnimationValueBorrowed value,
-                                       RefPtr<nsCSSValueSharedList>* list);
+nsCSSPropertyID Servo_AnimationValue_GetTransform(
+    RawServoAnimationValueBorrowed value, RefPtr<nsCSSValueSharedList>* list);
 
 RawServoAnimationValueStrong Servo_AnimationValue_Transform(
-    const nsCSSValueSharedList& list);
+    nsCSSPropertyID property, const nsCSSValueSharedList& list);
 
 bool Servo_AnimationValue_DeepEqual(RawServoAnimationValueBorrowed,
                                     RawServoAnimationValueBorrowed);
 
 RawServoDeclarationBlockStrong Servo_AnimationValue_Uncompute(
     RawServoAnimationValueBorrowed value);
 
 RawServoAnimationValueStrong Servo_AnimationValue_Compute(
--- a/layout/style/StyleAnimationValue.cpp
+++ b/layout/style/StyleAnimationValue.cpp
@@ -335,52 +335,105 @@ AnimationValue AnimationValue::FromStrin
   }
 
   result.mServo = shell->StyleSet()->ComputeAnimationValue(
       aElement, declarations, computedStyle);
   return result;
 }
 
 /* static */
-AnimationValue AnimationValue::Opacity(float aOpacity) {
-  AnimationValue result;
-  result.mServo = Servo_AnimationValue_Opacity(aOpacity).Consume();
-  return result;
-}
-
-/* static */
-AnimationValue AnimationValue::Transform(nsCSSValueSharedList& aList) {
-  AnimationValue result;
-  result.mServo = Servo_AnimationValue_Transform(aList).Consume();
-  return result;
-}
-
-/* static */ already_AddRefed<RawServoAnimationValue>
-AnimationValue::FromAnimatable(nsCSSPropertyID aProperty,
-                               const layers::Animatable& aAnimatable) {
+already_AddRefed<RawServoAnimationValue> AnimationValue::FromAnimatable(
+    nsCSSPropertyID aProperty, const layers::Animatable& aAnimatable) {
   RefPtr<RawServoAnimationValue> result;
 
   switch (aAnimatable.type()) {
     case layers::Animatable::Tnull_t:
       break;
     case layers::Animatable::TArrayOfTransformFunction: {
       const InfallibleTArray<layers::TransformFunction>& transforms =
           aAnimatable.get_ArrayOfTransformFunction();
       auto listOrError = CreateCSSValueList(transforms);
       if (listOrError.isOk()) {
         RefPtr<nsCSSValueSharedList> list = listOrError.unwrap();
         MOZ_ASSERT(list, "Transform list should be non null");
-        result = Servo_AnimationValue_Transform(*list).Consume();
+        result = Servo_AnimationValue_Transform(eCSSProperty_transform, *list)
+                     .Consume();
       }
       break;
     }
     case layers::Animatable::Tfloat:
       result = Servo_AnimationValue_Opacity(aAnimatable.get_float()).Consume();
       break;
     case layers::Animatable::Tnscolor:
       result = Servo_AnimationValue_Color(aProperty, aAnimatable.get_nscolor())
                    .Consume();
       break;
+    case layers::Animatable::TRotate: {
+      RefPtr<nsCSSValueSharedList> list = new nsCSSValueSharedList;
+      list->mHead = new nsCSSValueList;
+
+      const layers::Rotate& r = aAnimatable.get_Rotate();
+      if (r.type() == layers::Rotate::Tnull_t) {
+        list->mHead->mValue.SetNoneValue();
+      } else {
+        RefPtr<nsCSSValue::Array> arr;
+        if (r.type() == layers::Rotate::TRotation) {
+          const layers::CSSAngle& angle = r.get_Rotation().angle();
+          arr = AppendFunction(eCSSKeyword_rotate);
+          auto rv = SetCSSAngle(angle, arr->Item(1));
+          if (rv.isErr()) {
+            arr->Item(1).SetFloatValue(0.0, eCSSUnit_Degree);
+          }
+        } else {
+          MOZ_ASSERT(r.type() == layers::Rotate::TRotation3D,
+                     "Should be rotate3D");
+          float x = r.get_Rotation3D().x();
+          float y = r.get_Rotation3D().y();
+          float z = r.get_Rotation3D().z();
+          const layers::CSSAngle& angle = r.get_Rotation3D().angle();
+          arr = AppendFunction(eCSSKeyword_rotate3d);
+          arr->Item(1).SetFloatValue(x, eCSSUnit_Number);
+          arr->Item(2).SetFloatValue(y, eCSSUnit_Number);
+          arr->Item(3).SetFloatValue(z, eCSSUnit_Number);
+          auto rv = SetCSSAngle(angle, arr->Item(4));
+          if (rv.isErr()) {
+            arr->Item(4).SetFloatValue(0.0, eCSSUnit_Degree);
+          }
+        }
+        list->mHead->mValue.SetArrayValue(arr, eCSSUnit_Function);
+      }
+      result =
+          Servo_AnimationValue_Transform(eCSSProperty_rotate, *list).Consume();
+      break;
+    }
+    case layers::Animatable::TScale: {
+      const layers::Scale& scale = aAnimatable.get_Scale();
+      RefPtr<nsCSSValue::Array> arr = AppendFunction(eCSSKeyword_scale3d);
+      arr->Item(1).SetFloatValue(scale.x(), eCSSUnit_Number);
+      arr->Item(2).SetFloatValue(scale.y(), eCSSUnit_Number);
+      arr->Item(3).SetFloatValue(scale.z(), eCSSUnit_Number);
+
+      RefPtr<nsCSSValueSharedList> list = new nsCSSValueSharedList;
+      list->mHead = new nsCSSValueList;
+      list->mHead->mValue.SetArrayValue(arr, eCSSUnit_Function);
+      result =
+          Servo_AnimationValue_Transform(eCSSProperty_scale, *list).Consume();
+      break;
+    }
+    case layers::Animatable::TTranslation: {
+      const layers::Translation& translate = aAnimatable.get_Translation();
+      RefPtr<nsCSSValue::Array> arr = AppendFunction(eCSSKeyword_translate3d);
+      arr->Item(1).SetFloatValue(translate.x(), eCSSUnit_Pixel);
+      arr->Item(2).SetFloatValue(translate.y(), eCSSUnit_Pixel);
+      arr->Item(3).SetFloatValue(translate.z(), eCSSUnit_Pixel);
+
+      RefPtr<nsCSSValueSharedList> list = new nsCSSValueSharedList;
+      list->mHead = new nsCSSValueList;
+      list->mHead->mValue.SetArrayValue(arr, eCSSUnit_Function);
+      result = Servo_AnimationValue_Transform(eCSSProperty_translate, *list)
+                   .Consume();
+      break;
+    }
     default:
       MOZ_ASSERT_UNREACHABLE("Unsupported type");
   }
   return result.forget();
 }
--- a/layout/style/StyleAnimationValue.h
+++ b/layout/style/StyleAnimationValue.h
@@ -100,20 +100,16 @@ struct AnimationValue {
 
   // Create an AnimaitonValue from a string. This method flushes style, so we
   // should use this carefully. Now, it is only used by
   // nsDOMWindowUtils::ComputeAnimationDistance.
   static AnimationValue FromString(nsCSSPropertyID aProperty,
                                    const nsAString& aValue,
                                    dom::Element* aElement);
 
-  // Create an AnimationValue from an opacity value.
-  static AnimationValue Opacity(float aOpacity);
-  // Create an AnimationValue from a transform list.
-  static AnimationValue Transform(nsCSSValueSharedList& aList);
   // Create an already_AddRefed<RawServoAnimationValue> from a
   // layers::Animatable. Basically, this function should return AnimationValue,
   // but it seems the caller, AnimationHelper, only needs
   // RawServoAnimationValue, so we return its already_AddRefed<> to avoid
   // adding/removing a redundant ref-count.
   static already_AddRefed<RawServoAnimationValue> FromAnimatable(
       nsCSSPropertyID aProperty, const layers::Animatable& aAnimatable);
 
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -3357,60 +3357,71 @@ nsChangeHint nsStyleDisplay::CalcDiffere
   return hint;
 }
 
 bool nsStyleDisplay::TransformChanged(const nsStyleDisplay& aNewData) const {
   return TransformListChanged(mSpecifiedTransform,
                               aNewData.mSpecifiedTransform);
 }
 
-void nsStyleDisplay::GenerateCombinedIndividualTransform() {
-  MOZ_ASSERT(!mIndividualTransform);
-
+/* static */
+already_AddRefed<nsCSSValueSharedList>
+nsStyleDisplay::GenerateCombinedIndividualTransform(
+    nsCSSValueSharedList* aTranslate, nsCSSValueSharedList* aRotate,
+    nsCSSValueSharedList* aScale) {
   // Follow the order defined in the spec to append transform functions.
   // https://drafts.csswg.org/css-transforms-2/#ctm
   AutoTArray<nsCSSValueSharedList*, 3> shareLists;
-  if (mSpecifiedTranslate) {
-    shareLists.AppendElement(mSpecifiedTranslate.get());
-  }
-  if (mSpecifiedRotate) {
-    shareLists.AppendElement(mSpecifiedRotate.get());
-  }
-  if (mSpecifiedScale) {
-    shareLists.AppendElement(mSpecifiedScale.get());
-  }
-
-  if (shareLists.Length() == 0) {
-    return;
-  }
+  if (aTranslate) {
+    shareLists.AppendElement(aTranslate);
+  }
+
+  if (aRotate) {
+    shareLists.AppendElement(aRotate);
+  }
+
+  if (aScale) {
+    shareLists.AppendElement(aScale);
+  }
+
+  if (shareLists.IsEmpty()) {
+    return nullptr;
+  }
+
   if (shareLists.Length() == 1) {
-    mIndividualTransform = shareLists[0];
-    return;
+    return RefPtr<nsCSSValueSharedList>(shareLists[0]).forget();
   }
 
   // In common, we may have 3 transform functions:
-  // 1. one rotate function in mSpecifiedRotate,
-  // 2. one translate function in mSpecifiedTranslate,
-  // 3. one scale function in mSpecifiedScale.
+  // 1. one rotate function in aRotate,
+  // 2. one translate function in aTranslate,
+  // 3. one scale function in aScale.
   AutoTArray<nsCSSValueList*, 3> valueLists;
   for (auto list : shareLists) {
     if (list) {
       valueLists.AppendElement(list->mHead->Clone());
     }
   }
 
   // Check we have at least one list or else valueLists.Length() - 1 below will
   // underflow.
-  MOZ_ASSERT(valueLists.Length());
+  MOZ_ASSERT(!valueLists.IsEmpty());
 
   for (uint32_t i = 0; i < valueLists.Length() - 1; i++) {
     valueLists[i]->mNext = valueLists[i + 1];
   }
 
-  mIndividualTransform = new nsCSSValueSharedList(valueLists[0]);
+  RefPtr<nsCSSValueSharedList> list = new nsCSSValueSharedList(valueLists[0]);
+  return list.forget();
+}
+
+void nsStyleDisplay::GenerateCombinedIndividualTransform() {
+  MOZ_ASSERT(!mIndividualTransform);
+  mIndividualTransform = GenerateCombinedIndividualTransform(
+      mSpecifiedTranslate, mSpecifiedRotate, mSpecifiedScale);
 }
 
 // --------------------
 // nsStyleVisibility
 //
 
 nsStyleVisibility::nsStyleVisibility(const Document& aDocument)
     : mDirection(aDocument.GetBidiOptions() == IBMBIDI_TEXTDIRECTION_RTL
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -2264,16 +2264,26 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
 
   /**
    * Returns the final combined individual transform.
    **/
   already_AddRefed<nsCSSValueSharedList> GetCombinedTransform() const {
     return mIndividualTransform ? do_AddRef(mIndividualTransform) : nullptr;
   }
 
+  /**
+   * Returns the combined transform list based on translate, rotate, scale
+   * individual transforms. The combination order is defined in
+   * https://drafts.csswg.org/css-transforms-2/#ctm
+   */
+  static already_AddRefed<nsCSSValueSharedList>
+  GenerateCombinedIndividualTransform(nsCSSValueSharedList* aTranslate,
+                                      nsCSSValueSharedList* aRotate,
+                                      nsCSSValueSharedList* aScale);
+
   void GenerateCombinedIndividualTransform();
 };
 
 struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleTable {
   explicit nsStyleTable(const mozilla::dom::Document&);
   nsStyleTable(const nsStyleTable& aOther);
   ~nsStyleTable();
   void TriggerImageLoads(mozilla::dom::Document&, const nsStyleTable*) {}
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -851,59 +851,93 @@ pub extern "C" fn Servo_AnimationValue_C
         _ => panic!("Should be background-color property"),
     }
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_AnimationValue_GetTransform(
     value: RawServoAnimationValueBorrowed,
     list: *mut structs::RefPtr<nsCSSValueSharedList>,
-) {
+) -> nsCSSPropertyID {
     let list = &mut *list;
     let value = AnimationValue::as_arc(&value);
     match **value {
         AnimationValue::Transform(ref servo_list) => {
             if servo_list.0.is_empty() {
                 list.set_move(RefPtr::from_addrefed(Gecko_NewNoneTransform()));
             } else {
                 gecko_properties::convert_transform(&servo_list.0, list);
             }
+            nsCSSPropertyID::eCSSProperty_transform
         },
         AnimationValue::Translate(ref v) => {
             if let Some(v) = v.to_transform_operation() {
                 gecko_properties::convert_transform(&[v], list);
             } else {
                 list.set_move(RefPtr::from_addrefed(Gecko_NewNoneTransform()));
             }
+            nsCSSPropertyID::eCSSProperty_translate
         },
         AnimationValue::Rotate(ref v) => {
             if let Some(v) = v.to_transform_operation() {
                 gecko_properties::convert_transform(&[v], list);
             } else {
                 list.set_move(RefPtr::from_addrefed(Gecko_NewNoneTransform()));
             }
+            nsCSSPropertyID::eCSSProperty_rotate
         },
         AnimationValue::Scale(ref v) => {
             if let Some(v) = v.to_transform_operation() {
                 gecko_properties::convert_transform(&[v], list);
             } else {
                 list.set_move(RefPtr::from_addrefed(Gecko_NewNoneTransform()));
             }
+            nsCSSPropertyID::eCSSProperty_scale
         },
         _ => unreachable!("Unsupported transform-like animation value"),
     }
 }
 
 #[no_mangle]
-pub extern "C" fn Servo_AnimationValue_Transform(
+pub unsafe extern "C" fn Servo_AnimationValue_Transform(
+    property: nsCSSPropertyID,
     list: *const nsCSSValueSharedList,
 ) -> RawServoAnimationValueStrong {
-    let list = unsafe { (&*list).mHead.as_ref() };
+    use style::values::computed::transform::{Rotate, Scale, Translate};
+
+    let property = LonghandId::from_nscsspropertyid(property)
+        .expect("We don't have shorthand property animation value");
+    let list = (&*list).mHead.as_ref();
     let transform = gecko_properties::clone_transform_from_list(list);
-    Arc::new(AnimationValue::Transform(transform)).into_strong()
+    match property {
+        LonghandId::Rotate => {
+            let rotate = if transform.0.is_empty() {
+                style::values::generics::transform::Rotate::None
+            } else {
+                debug_assert_eq!(transform.0.len(), 1);
+                Rotate::from_transform_operation(&(transform.0)[0])
+            };
+            Arc::new(AnimationValue::Rotate(rotate)).into_strong()
+        },
+        LonghandId::Scale => {
+            debug_assert_eq!(transform.0.len(), 1);
+            Arc::new(AnimationValue::Scale(Scale::from_transform_operation(&(transform.0)[0])))
+                .into_strong()
+        },
+        LonghandId::Translate => {
+            debug_assert_eq!(transform.0.len(), 1);
+            Arc::new(AnimationValue::Translate(
+                    Translate::from_transform_operation(&(transform.0)[0])))
+                .into_strong()
+        },
+        LonghandId::Transform => {
+            Arc::new(AnimationValue::Transform(transform)).into_strong()
+        },
+        _ => unreachable!("Unsupported transform-like animation value"),
+    }
 }
 
 #[no_mangle]
 pub extern "C" fn Servo_AnimationValue_DeepEqual(
     this: RawServoAnimationValueBorrowed,
     other: RawServoAnimationValueBorrowed,
 ) -> bool {
     let this_value = AnimationValue::as_arc(&this);