Bug 1244590 - Part 7: Calculate paced spacing. draft
authorBoris Chiou <boris.chiou@gmail.com>
Fri, 27 May 2016 18:09:06 +0800
changeset 372065 561c82f0398271b3f8d48f9893c88774490e8ff9
parent 372064 67513e7b66928878a2f4136beb6da4571c9fe241
child 372066 cf7f3de5d52d4e08876c449dab298ceb90bc3491
push id19429
push userbmo:boris.chiou@gmail.com
push dateFri, 27 May 2016 10:09:46 +0000
bugs1244590
milestone49.0a1
Bug 1244590 - Part 7: Calculate paced spacing. Calculate the paced spacing for each Keyframes by the context element. Use the algorithm in the spec. MozReview-Commit-ID: HFWQwoKhKWt
dom/animation/KeyframeEffect.cpp
dom/animation/KeyframeUtils.cpp
dom/animation/KeyframeUtils.h
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -468,17 +468,17 @@ KeyframeEffectReadOnly::SetKeyframes(nsT
     return;
   }
 
   mKeyframes = Move(aKeyframes);
   // Apply distribute spacing irrespective of the spacing mode. We will apply
   // the specified spacing mode when we generate computed animation property
   // values from the keyframes since both operations require a style context
   // and need to be performed whenever the style context changes.
-  KeyframeUtils::ApplySpacing(mKeyframes, SpacingMode::distribute);
+  KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
 
   if (mAnimation && mAnimation->IsRelevant()) {
     nsNodeUtils::AnimationChanged(mAnimation);
   }
 
   if (aStyleContext) {
     UpdateProperties(aStyleContext);
   }
@@ -519,16 +519,22 @@ KeyframeEffectReadOnly::UpdateProperties
   MOZ_ASSERT(aStyleContext);
 
   nsTArray<AnimationProperty> properties;
   if (mTarget) {
     nsTArray<ComputedKeyframeValues> computedValues =
       KeyframeUtils::GetComputedKeyframeValues(mKeyframes, mTarget->mElement,
                                                aStyleContext);
 
+    if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+      KeyframeUtils::ApplySpacing(mKeyframes, SpacingMode::paced,
+                                  mEffectOptions.mPacedProperty,
+                                  computedValues);
+    }
+
     properties =
       KeyframeUtils::GetAnimationPropertiesFromKeyframes(mKeyframes,
                                                          computedValues);
   }
 
   if (mProperties == properties) {
     return;
   }
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -380,17 +380,27 @@ GetKeyframeListFromPropertyIndexedKeyfra
                                            nsTArray<Keyframe>& aResult,
                                            ErrorResult& aRv);
 
 static bool
 RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
                           nsIDocument* aDocument);
 
 static void
-DistributeRange(const Range<Keyframe>& aKeyframes);
+DistributeRange(const Range<Keyframe>& aKeyframes,
+                const Range<Keyframe>& aFilteredKeyframes);
+
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+          const Range<ComputedKeyframeValues>& aPacedValues,
+          nsCSSProperty aProperty);
+
+static nsTArray<ComputedKeyframeValues>
+GetPacedPropertyKeyframeValues(const nsTArray<ComputedKeyframeValues>& aValues,
+                               nsCSSProperty aProperty);
 
 // ------------------------------------------------------------------
 //
 // Public API
 //
 // ------------------------------------------------------------------
 
 /* static */ nsTArray<Keyframe>
@@ -448,57 +458,125 @@ KeyframeUtils::GetKeyframesFromObject(JS
     keyframes.Clear();
   }
 
   return keyframes;
 }
 
 /* static */ void
 KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
-                            SpacingMode aSpacingMode)
+                            SpacingMode aSpacingMode,
+                            nsCSSProperty aProperty,
+                            nsTArray<ComputedKeyframeValues>& aComputedValues)
 {
   if (aKeyframes.IsEmpty()) {
     return;
   }
 
+  nsTArray<ComputedKeyframeValues> pacedValues;
+  if (aSpacingMode == SpacingMode::paced) {
+    MOZ_ASSERT(IsAnimatableProperty(aProperty),
+               "Only support animatable property for paced spacing");
+
+    pacedValues = GetPacedPropertyKeyframeValues(aComputedValues, aProperty);
+    // Reset the computed offsets if using paced spacing.
+    for (Keyframe& keyframe : aKeyframes) {
+      keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet;
+    }
+  }
+
   // If the first keyframes have an unspecified offset, fill it in with 0%.
   // If there is only a single keyframe, then it gets 100%.
   if (aKeyframes.Length() > 1) {
     Keyframe& firstElement = aKeyframes[0];
     firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
     // We will fill in the last keyframe's offset below
   } else {
     Keyframe& lastElement = aKeyframes.LastElement();
     lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
   }
 
   // Fill in remaining missing offsets.
+  static const Range<Keyframe> sEmptyRange;
   const Keyframe* const last = aKeyframes.cend() - 1;
-  RangedPtr<Keyframe> keyframeA(aKeyframes.begin(), aKeyframes.Length());
+  const RangedPtr<Keyframe> begin(aKeyframes.begin(), aKeyframes.Length());
+  RangedPtr<Keyframe> keyframeA = begin;
   while (keyframeA != last) {
     // Find keyframe A and keyframe B *between* which we will apply spacing.
     RangedPtr<Keyframe> keyframeB = keyframeA + 1;
     while (keyframeB.get()->mOffset.isNothing() && keyframeB != last) {
       ++keyframeB;
     }
     keyframeB.get()->mComputedOffset = keyframeB.get()->mOffset.valueOr(1.0);
 
     // Fill computed offsets in (keyframe A, keyframe B).
+    const size_t rangeLen = keyframeB - keyframeA + 1;
     if (aSpacingMode == SpacingMode::distribute) {
-      DistributeRange(Range<Keyframe>(keyframeA.get(),
-                                      keyframeB - keyframeA + 1));
+      DistributeRange(Range<Keyframe>(keyframeA.get(), rangeLen), sEmptyRange);
     } else {
-      // TODO
-      MOZ_ASSERT(false, "not implement yet");
+      // a) Find Paced A (first paceable keyframe) and
+      //    Paced B (last paceable keyframe) in [keyframe A, keyframe B].
+      //    Note: if pacedValues[i] is empty, the keyframe i is not paceable.
+      RangedPtr<Keyframe> pacedA = keyframeA;
+      while (pacedA <= keyframeB && pacedValues[pacedA - begin].IsEmpty()) {
+        ++pacedA;
+      }
+      RangedPtr<Keyframe> pacedB = keyframeB;
+      while (pacedB >= keyframeA && pacedValues[pacedB - begin].IsEmpty()) {
+        if (pacedB == keyframeA) {
+          break;
+        }
+        --pacedB;
+      }
+      // As spec says, if there is no paceable keyframe
+      // in [keyframe A, keyframe B], we let Paced A and Paced refer to
+      // keyframe B.
+      if (pacedA > pacedB) {
+        pacedA = pacedB = keyframeB;
+      }
+      // b) Apply evenly distributing offsets in (A, Paced A] and [Paced B, B).
+      DistributeRange(Range<Keyframe>(keyframeA.get(), rangeLen),
+                      Range<Keyframe>(keyframeA.get(), pacedA - keyframeA + 1));
+      DistributeRange(Range<Keyframe>(keyframeA.get(), rangeLen),
+                      Range<Keyframe>(pacedB.get(), keyframeB - pacedB + 1));
+      // c) Apply paced offsets in (Paced A, Paced B).
+      const size_t idx = pacedA - begin;
+      const size_t pacedLen = pacedB - pacedA + 1;
+      PaceRange(Range<Keyframe>(pacedA.get(), pacedLen),
+                Range<ComputedKeyframeValues>(&pacedValues[idx], pacedLen),
+                aProperty);
+      // d) Fill null computed offsets in (Paced A, Paced B).
+      for (RangedPtr<Keyframe> frame = pacedA + 1; frame < pacedB; ++frame) {
+        if ((*frame).mComputedOffset != Keyframe::kComputedOffsetNotSet) {
+          continue;
+        }
+
+        RangedPtr<Keyframe> start = frame - 1;
+        RangedPtr<Keyframe> end = frame + 1;
+        while (end < pacedB &&
+               (*end).mComputedOffset == Keyframe::kComputedOffsetNotSet) {
+          ++end;
+        }
+        DistributeRange(Range<Keyframe>(start.get(), end - start + 1),
+                        sEmptyRange);
+        frame = end;
+      }
     }
-
     keyframeA = keyframeB;
   }
 }
 
+/* static */ void
+KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
+{
+  static nsTArray<ComputedKeyframeValues> sEmptyArray;
+  ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN,
+               sEmptyArray);
+}
+
 /* static */ nsTArray<ComputedKeyframeValues>
 KeyframeUtils::GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
                                          dom::Element* aElement,
                                          nsStyleContext* aStyleContext)
 {
   MOZ_ASSERT(aStyleContext);
   MOZ_ASSERT(aElement);
 
@@ -1198,27 +1276,204 @@ RequiresAdditiveAnimation(const nsTArray
     }
   }
 
   return !propertiesWithFromValue.Equals(properties) ||
          !propertiesWithToValue.Equals(properties);
 }
 
 /**
- * Evenly distribute the computed offsets in (A, B). We should pass the
- * range keyframes in [A, B] and use A, B to calculate computed offsets in
- * (A, B).
+ * Evenly distribute the computed offsets in (A, B).
+ * We pass the range keyframes in [A, B] and use A, B to calculate distributing
+ * computed offsets in (A, B).
  *
  * @param aKeyframes The sequence of keyframes between whose endpoints we should
  *   apply distribute spacing.
+ * @param aFilteredKeyframes The range of keyframes we want to apply to. If this
+ *   is an empty range, we apply to all keyframes.
  */
 static void
-DistributeRange(const Range<Keyframe>& aKeyframes)
+DistributeRange(const Range<Keyframe>& aKeyframes,
+                const Range<Keyframe>& aFilteredKeyframes)
 {
+  // aFilteredKeyframes could be an empty range. If it is, we should apply
+  // distribute spacing to all the keyframes (excluding A and B).
+  const RangedPtr<Keyframe> filterStart = aFilteredKeyframes.start()
+                                          ? aFilteredKeyframes.start()
+                                          : aKeyframes.start() + 1;
+  const RangedPtr<Keyframe> filterEnd = aFilteredKeyframes.end()
+                                        ? aFilteredKeyframes.end()
+                                        : aKeyframes.end() - 1;
+
   const size_t n = aKeyframes.length() - 1;
   const double startOffset = aKeyframes[0].mComputedOffset;
   const double diffOffset = aKeyframes[n].mComputedOffset - startOffset;
-  for (size_t i = 1; i < n; ++i) {
+  // Note: we don't need to apply computed offsets to A and B, so skip them.
+  const size_t startIdx = std::max(1UL, filterStart - aKeyframes.start());
+  const size_t endIdx = std::min(n, filterEnd - aKeyframes.start());
+  for (size_t i = startIdx; i < endIdx; ++i) {
     aKeyframes[i].mComputedOffset = startOffset + double(i) / n * diffOffset;
   }
 }
 
+/**
+ * Apply paced computed offsets in (Paced A, Paced B).
+ *
+ * @param aKeyframes The sequence of keyframes between whose endpoints we should
+ *   apply paced distribute, [Paced A, Paced B], and both Paced A & Paced B
+ *   should be paceable.
+ * @param aPacedValues The sequence of computed values of the paced property.
+ *   We get this by GetPacedPropertyKeyframeValues().
+ * @param aProperty The paced property.
+ */
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+          const Range<ComputedKeyframeValues>& aPacedValues,
+          nsCSSProperty aProperty)
+{
+  const size_t len = aKeyframes.length();
+  if (len < 3) {
+    return;
+  }
+
+  auto IsPaceable = [](ComputedKeyframeValues& aComputedValues) {
+    return !aComputedValues.IsEmpty();
+  };
+
+  const size_t pacedA = 0;
+  const size_t pacedB = len - 1;
+  MOZ_ASSERT(IsPaceable(aPacedValues[pacedA]) &&
+             IsPaceable(aPacedValues[pacedB]),
+             "Both Paced A and Paced B should be paceable.");
+
+  // a) Calculate cumulative dist in [Paced A, Paced B].
+  //    Cumulative distance array stores the cumulative distances in
+  //    [Paced A, Paced B]. If the Keyframe is not paceable, just copy the
+  //    cumulative distance from the previous one.
+  nsTArray<double> cumulativeDist(len);
+  cumulativeDist.SetLength(len);
+  cumulativeDist[0] = 0.0;
+  size_t preIdx = pacedA;
+  bool pacedIsShorthand = nsCSSProps::IsShorthand(aProperty);
+  size_t subpCount = 0;
+  if (pacedIsShorthand) {
+    const nsCSSProperty* subp = nsCSSProps::SubpropertyEntryFor(aProperty);
+    while (*subp != eCSSProperty_UNKNOWN) {
+      ++subpCount;
+      ++subp;
+    }
+  }
+  bool failed = false;
+  for (size_t i = pacedA + 1; i <= pacedB; ++i) {
+    if (!IsPaceable(aPacedValues[i])) {
+      cumulativeDist[i] = cumulativeDist[i - 1];
+      continue;
+    }
+
+    double dist = 0.0;
+    if (pacedIsShorthand) {
+      ComputedKeyframeValues& prePairs = aPacedValues[preIdx];
+      ComputedKeyframeValues& curPairs = aPacedValues[i];
+
+      // Calculate distance.
+      // mPacedPropertyAnimValue is sorted, so the components should be aligned.
+      for (size_t subIdx = 0; subIdx < subpCount; ++subIdx) {
+        const StyleAnimationValue& preValue = prePairs[subIdx].mValue;
+        const StyleAnimationValue& curValue = curPairs[subIdx].mValue;
+        nsCSSProperty subProperty = prePairs[subIdx].mProperty;
+        MOZ_ASSERT(curPairs[subIdx].mProperty == subProperty,
+                   "subProperty mismatch");
+
+        double componentDist = 0.0;
+        if (!StyleAnimationValue::ComputeDistance(subProperty, preValue,
+                                                  curValue, componentDist)) {
+          failed = true;
+          break;
+        }
+
+        // FIXME: Any way to avoid overflow?
+        dist += componentDist * componentDist;
+      }
+
+      if (failed) {
+        break;
+      }
+
+      dist = sqrt(dist);
+    } else {
+      // If the property is longhand, we just use the 1st value.
+      const StyleAnimationValue& preValue = aPacedValues[preIdx][0].mValue;
+      const StyleAnimationValue& curValue = aPacedValues[i][0].mValue;
+      if (!StyleAnimationValue::ComputeDistance(aProperty, preValue,
+                                                curValue, dist)) {
+        failed = true;
+        break;
+      }
+    }
+    cumulativeDist[i] = cumulativeDist[i - 1] + dist;
+    preIdx = i;
+  }
+
+  if (failed || cumulativeDist[pacedB] == 0.0) {
+    return;
+  }
+
+  // b) Apply computed offset.
+  const double offsetA = aKeyframes[pacedA].mComputedOffset;
+  const double diffOffset = aKeyframes[pacedB].mComputedOffset - offsetA;
+  const double totalDist = cumulativeDist[pacedB];
+  for (size_t i = pacedA + 1; i < pacedB; ++i) {
+    if (!IsPaceable(aPacedValues[i])) {
+      continue;
+    }
+    aKeyframes[i].mComputedOffset =
+      offsetA + diffOffset * cumulativeDist[i] / totalDist;
+  }
+}
+
+/**
+ * Get computed values of the paced property for each keyframe.
+ *
+ * @param aValues The computed values got by GetComputedKeyframeValues.
+ * @param aProperty The paced property.
+ * @return The computed values for the paced property. The length will be the
+ *   same as aValues.
+ */
+static nsTArray<ComputedKeyframeValues>
+GetPacedPropertyKeyframeValues(const nsTArray<ComputedKeyframeValues>& aValues,
+                               nsCSSProperty aProperty)
+{
+  nsTArray<ComputedKeyframeValues> result;
+
+  // a) If aProperty is a shorthand property, get its components. Otherwise,
+  //    just add the longhand property into the set.
+  size_t propertyCount = 0;
+  nsCSSPropertySet propSet;
+  if (nsCSSProps::IsShorthand(aProperty)) {
+    const nsCSSProperty* p = nsCSSProps::SubpropertyEntryFor(aProperty);
+    for (; *p != eCSSProperty_UNKNOWN; ++p) {
+      propSet.AddProperty(*p);
+      ++propertyCount;
+    }
+  } else {
+    propSet.AddProperty(aProperty);
+    propertyCount = 1;
+  }
+
+  // b) Search each component (shorthand) or the longhand property,
+  for (const ComputedKeyframeValues& computedValues : aValues) {
+    ComputedKeyframeValues* pacedValues = result.AppendElement();
+    for (const PropertyStyleAnimationValuePair& pair : computedValues) {
+      if (propSet.HasProperty(pair.mProperty)) {
+        pacedValues->AppendElement(pair);
+      }
+    }
+    if (pacedValues->Length() != propertyCount) {
+      // If the keyframe doesn't have all components of this shorthand
+      // properties, it should be non-paceable. For longhand, the number of
+      // components is always 1.
+      pacedValues->Clear();
+    }
+  }
+  return result;
+}
+
 } // namespace mozilla
--- a/dom/animation/KeyframeUtils.h
+++ b/dom/animation/KeyframeUtils.h
@@ -81,19 +81,32 @@ public:
   /**
    * Fills in the mComputedOffset member of each keyframe in the given array
    * using the specified spacing mode.
    *
    * https://w3c.github.io/web-animations/#spacing-keyframes
    *
    * @param aKeyframes The set of keyframes to adjust.
    * @param aSpacingMode The spacing mode to apply.
+   * @param aProperty The paced property.
+   * @param aComputedValues The set of computed keyframe values got by
+   *   GetComputedKeyframeValues.
    */
   static void ApplySpacing(nsTArray<Keyframe>& aKeyframes,
-                           SpacingMode aSpacingMode);
+                           SpacingMode aSpacingMode,
+                           nsCSSProperty aProperty,
+                           nsTArray<ComputedKeyframeValues>& aComputedValues);
+
+  /**
+   * Fills in the mComputedOffset member of each keyframe in the given array
+   * using distribute spacing mode.
+   *
+   * @param aKeyframes The set of keyframes to adjust.
+   */
+  static void ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes);
 
   /**
    * Converts an array of Keyframe objects into an array of AnimationProperty
    * objects. This involves creating an array of computed values for each
    * longhand property and determining the offset and timing function to use
    * for each value.
    *
    * @param aKeyframes The input keyframes.