Bug 1244590 - Part 7: Calculate paced spacing. draft
authorBoris Chiou <boris.chiou@gmail.com>
Wed, 18 May 2016 01:48:58 +0800
changeset 369282 45cd569bdd7ae6b6a1feb46088dedebd959efe9a
parent 369281 55f22e782b94108fce7fd4226972f1f730cf6e19
child 369283 c2d881e7ae7e15c66f281e24e77b918f5d5a0e21
push id18826
push userbmo:boris.chiou@gmail.com
push dateSat, 21 May 2016 11:25:24 +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/KeyframeEffect.h
dom/animation/KeyframeUtils.cpp
dom/animation/KeyframeUtils.h
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -503,27 +503,98 @@ KeyframeEffectReadOnly::HasAnimationOfPr
   for (size_t i = 0; i < aPropertyCount; i++) {
     if (HasAnimationOfProperty(aProperties[i])) {
       return true;
     }
   }
   return false;
 }
 
+static void
+UpdateKeyframePacedPropertyAnimValue(nsTArray<Keyframe>& aFrames,
+                                     const KeyframeAnimValues& aValues,
+                                     nsCSSProperty aProperty)
+{
+  // 1. If aProperty is a shorthand property, get its components. Otherwise,
+  //    point to the longhand property directly.
+  const nsCSSProperty longhandList[] = {aProperty, eCSSProperty_UNKNOWN};
+  const nsCSSProperty* propList = &longhandList[0];
+  if (nsCSSProps::IsShorthand(aProperty)) {
+    propList = nsCSSProps::SubpropertyEntryFor(aProperty);
+  }
+
+  // 2. Search each component (shorthand) or the longhand property,
+  //    and push into this array.
+  for (size_t i = 0; i < aFrames.Length(); ++i) {
+    Keyframe& frame = aFrames[i];
+    frame.mPacedPropertyAnimValue.Clear();
+
+    size_t subpCount = 0;
+    for (const nsCSSProperty* p = propList; *p != eCSSProperty_UNKNOWN; ++p) {
+      ++subpCount;
+      for (const PropertyStyleAnimationValuePair& pair : aValues[i]) {
+        if (pair.mProperty == *p) {
+          frame.mPacedPropertyAnimValue.AppendElement(pair);
+          break;
+        }
+      }
+    }
+    if (frame.mPacedPropertyAnimValue.Length() != subpCount) {
+      // If the keyframe doesn't have all components for this shorthand
+      // properties, it should be non-paceable. For longhand, the number of
+      // components is always 1.
+      frame.mPacedPropertyAnimValue.Clear();
+    }
+  }
+
+  // debug
+  /*if (nsCSSProps::IsShorthand(aProperty)) {
+    {
+      nsCString debug = nsCSSProps::GetStringValue(aProperty);
+      printf_stderr("[Boris] Get SubpropertyEntryFor(%s)\n", debug.get());
+    }
+    const nsCSSProperty* subp = nsCSSProps::SubpropertyEntryFor(aProperty);
+    for (const nsCSSProperty* p = subp; *p != eCSSProperty_UNKNOWN; ++p) {
+      nsCString debug = nsCSSProps::GetStringValue(*p);
+      printf_stderr("[Boris]   subproperty: %s\n", debug.get());
+    }
+
+    printf_stderr("[Boris] dump paced properties of each keyframe\n");
+    size_t fi = 0;
+    for (auto& f: aFrames) {
+      for (auto& pair : f.mPacedPropertyAnimValue) {
+        nsCString debug = nsCSSProps::GetStringValue(pair.mProperty);
+        printf_stderr("[Boris] keyframe[%zu].mPacedProperty: %s\n",
+            fi, debug.get());
+      }
+      ++fi;
+    }
+  }*/
+}
+
 void
 KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext)
 {
   MOZ_ASSERT(aStyleContext);
 
   nsTArray<AnimationProperty> properties;
   if (mTarget) {
     KeyframeAnimValues keyframeAnimValues =
       KeyframeUtils::CalculateAnimationValues(mKeyframes, mTarget->mElement,
                                               aStyleContext);
 
+    if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
+      // Cache the paced property and its StyleAnimationValues, so we don't have
+      // to search them each time.
+      UpdateKeyframePacedPropertyAnimValue(mKeyframes, keyframeAnimValues,
+                                           mEffectOptions.mPacedProperty);
+      KeyframeUtils::ApplySpacing(mKeyframes, SpacingMode::paced,
+                                  mEffectOptions.mPacedProperty);
+    }
+
     properties =
       KeyframeUtils::GetAnimationPropertiesFromKeyframes(mKeyframes,
                                                          keyframeAnimValues);
   }
 
   if (mProperties == properties) {
     return;
   }
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -98,24 +98,29 @@ struct Keyframe
 
   Keyframe& operator=(const Keyframe& aOther) = default;
   Keyframe& operator=(Keyframe&& aOther)
   {
     mOffset         = aOther.mOffset;
     mComputedOffset = aOther.mComputedOffset;
     mTimingFunction = Move(aOther.mTimingFunction);
     mPropertyValues = Move(aOther.mPropertyValues);
+    mPacedPropertyAnimValue = Move(aOther.mPacedPropertyAnimValue);
     return *this;
   }
 
+  bool IsPaceable() const { return !mPacedPropertyAnimValue.IsEmpty(); }
+
   Maybe<double>                 mOffset;
   double                        mComputedOffset = -1.0;
   Maybe<ComputedTimingFunction> mTimingFunction; // Nothing() here means
                                                  // "linear"
   nsTArray<PropertyValuePair>   mPropertyValues;
+  // The member data is only used when spacing mode is paced.
+  nsTArray<PropertyStyleAnimationValuePair> mPacedPropertyAnimValue;
 };
 
 struct AnimationPropertySegment
 {
   float mFromKey, mToKey;
   StyleAnimationValue mFromValue, mToValue;
   Maybe<ComputedTimingFunction> mTimingFunction;
 
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -382,17 +382,22 @@ GetKeyframeListFromPropertyIndexedKeyfra
                                            nsTArray<Keyframe>& aResult,
                                            ErrorResult& aRv);
 
 static bool
 RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
                           nsIDocument* aDocument);
 
 static void
-ApplyDistributingOffset(const Range<Keyframe>& aDistributeRange);
+ApplyDistributingOffset(const Range<Keyframe>& aDistributeRange,
+                        size_t aPacedA = 0, size_t aPacedB = 0);
+
+static void
+ApplyPacedOffset(const Range<Keyframe>& aPaceRange,
+                 nsCSSProperty aProperty);
 
 // ------------------------------------------------------------------
 //
 // Public API
 //
 // ------------------------------------------------------------------
 
 /* static */ nsTArray<Keyframe>
@@ -450,22 +455,30 @@ KeyframeUtils::GetKeyframesFromObject(JS
     keyframes.Clear();
   }
 
   return keyframes;
 }
 
 /* static */ void
 KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
-                            SpacingMode aSpacingMode)
+                            SpacingMode aSpacingMode,
+                            nsCSSProperty aProperty)
 {
   if (aKeyframes.IsEmpty()) {
     return;
   }
 
+  // Reset the computed offsets if using paced spacing.
+  if (aSpacingMode == SpacingMode::paced) {
+    for (Keyframe& keyframe : aKeyframes) {
+      keyframe.mComputedOffset = -1.0;
+    }
+  }
+
   // If the first or last keyframes have an unspecified offset,
   // fill them in with 0% and 100%.  If there is only a single keyframe,
   // then it gets 100%.
   Keyframe& lastElement = aKeyframes.LastElement();
   lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
   if (aKeyframes.Length() > 1) {
     Keyframe& firstElement = aKeyframes[0];
     firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
@@ -480,18 +493,56 @@ KeyframeUtils::ApplySpacing(nsTArray<Key
       ++j;
     }
     aKeyframes[j].mComputedOffset = aKeyframes[j].mOffset.valueOr(1.0);
 
     // Fill computed offsets in (A, B)
     if (aSpacingMode == SpacingMode::distribute) {
       ApplyDistributingOffset(Range<Keyframe>(&aKeyframes[i], j - i + 1));
     } else {
-      // TODO
-      MOZ_ASSERT(false, "not implement yet");
+      // a) Find Paced A (first paceable frame) and
+      //    Paced B (last paceable frame) in [A, B].
+      size_t pA = i;
+      while (pA <= j && !aKeyframes[pA].IsPaceable()) {
+        ++pA;
+      }
+      size_t pB = j;
+      while (pB >= i && !aKeyframes[pB].IsPaceable()) {
+        if (pB == 0) {
+          // Avoid overflow.
+          break;
+        }
+        --pB;
+      }
+      // If no Paced A (which also means no Paced B), let both refer to B
+      if (pA == j + 1) {
+        pA = pB = j;
+      }
+      // b) Apply evenly distributing offsets in (A, Paced A] and [Paced B, B).
+      //    We should use relative indexes here, so pA - i and pB - i.
+      ApplyDistributingOffset(Range<Keyframe>(&aKeyframes[i], j - i + 1),
+                              pA - i, pB - i);
+      // c) Apply paced offsets in (Paced A, Paced B).
+      ApplyPacedOffset(Range<Keyframe>(&aKeyframes[pA], pB - pA + 1),
+                       aProperty);
+      // d) Fill null computed offsets in (Paced A, Paced B).
+      for (size_t k = pA + 1; k < pB; ++k) {
+        if (aKeyframes[k].mComputedOffset != -1.0) {
+          continue;
+        }
+
+        size_t startIdx = k - 1;
+        size_t endIdx = k + 1;
+        while (endIdx < pB && aKeyframes[endIdx].mComputedOffset == -1.0) {
+          ++endIdx;
+        }
+        ApplyDistributingOffset(
+          Range<Keyframe>(&aKeyframes[startIdx], endIdx - startIdx + 1));
+        k = endIdx + 1;
+      }
     }
     i = j;
   }
 }
 
 /* static */ KeyframeAnimValues
 KeyframeUtils::CalculateAnimationValues(const nsTArray<Keyframe>& aFrames,
                                         dom::Element* aElement,
@@ -1225,24 +1276,161 @@ RequiresAdditiveAnimation(const nsTArray
     }
   }
 
   return !propertiesWithFromValue.Equals(properties) ||
          !propertiesWithToValue.Equals(properties);
 }
 
 /**
- * Apply evenly distributing computed offsets.
+ * Apply evenly distributing computed offsets in (A, Paced A], [Paced B, B).
+ * We should pass the range keyframes in [A, B] and use A, B to calculate
+ * computed offsets in (A, B).
  *
  * @param aDistributeRange The set of keyframes.
+ * @param aPacedA The index of Paced A. (default 0)
+ * @param aPacedB The index of Paced B. (default 0)
  */
 static void
-ApplyDistributingOffset(const Range<Keyframe>& aDistributeRange)
+ApplyDistributingOffset(const Range<Keyframe>& aDistributeRange,
+                        size_t aPacedA, size_t aPacedB)
 {
+  MOZ_ASSERT(aPacedA >= 0 && aPacedB < aDistributeRange.length(),
+             "Out of range");
   const size_t n = aDistributeRange.length() - 1;
   const double start = aDistributeRange[0].mComputedOffset;
   const double diffOffset = aDistributeRange[n].mComputedOffset - start;
   for (size_t i = 1; i < n; ++i) {
+    // Skip (Paced A, Paced B).
+    if (aPacedA < i && i < aPacedB) {
+      continue;
+    }
     aDistributeRange[i].mComputedOffset = start + double(i) / n * diffOffset;
   }
 }
 
+/**
+ * Apply paced computed offsets in (Paced A, Paced B). We need the target
+ * element and its style context to calculate the StyleAnimationValue.
+ *
+ * @param aPaceRange The set of keyframes in [Paced A, Paced B].
+ * @param aProperty The paced property. (default eCSSProperty_UNKNOWN)
+ */
+static void
+ApplyPacedOffset(const Range<Keyframe>& aPaceRange,
+                 nsCSSProperty aProperty)
+{
+  const size_t len = aPaceRange.length();
+  if (len < 3) {
+    // No space to fill.
+    return;
+  }
+
+  const size_t pacedA = 0;
+  const size_t pacedB = len - 1;
+  MOZ_ASSERT(aPaceRange[pacedA].IsPaceable() &&
+             aPaceRange[pacedB].IsPaceable(),
+             "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.
+  std::vector<double> cumulativeDist(len, 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 = 1; i < len; ++i) {
+    if (!aPaceRange[i].IsPaceable()) {
+      cumulativeDist[i] = cumulativeDist[i - 1];
+      continue;
+    }
+
+    double dist = 0.0;
+    if (pacedIsShorthand) {
+      nsTArray<PropertyStyleAnimationValuePair>& prePairs =
+        aPaceRange[preIdx].mPacedPropertyAnimValue;
+      nsTArray<PropertyStyleAnimationValuePair>& curPairs =
+        aPaceRange[i].mPacedPropertyAnimValue;
+
+      // Calculate distance.
+      // mPacedPropertyAnimValue is sorted, so the components should be aligned.
+      for (size_t subIdx = 0; subIdx < subpCount; ++subIdx) {
+        // calculate comp dist
+        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;
+        }
+        // debug
+        /*{
+          nsCString debug = nsCSSProps::GetStringValue(subProperty);
+          printf_stderr("[Boris] pair(%zu, %zu): distance of [%s] is %lf\n",
+              preIdx, i,
+              debug.get(), componentDist);
+        }*/
+
+        // FIXME: Any way to avoid overflow?
+        dist += componentDist * componentDist;
+      }
+
+      if (failed) {
+        break;
+      }
+
+      dist = sqrt(dist);
+      /*{
+        nsCString debug = nsCSSProps::GetStringValue(aProperty);
+        printf_stderr("[Boris] pair(%zu, %zu): sqrt distance of [%s] is %lf\n",
+            preIdx, i, debug.get(), dist);
+      }*/
+    } else {
+      // Note: IsPaceable() means mPacedPropertyAnimValue is not empty. If the
+      // paced property is longhand, we just use the 1st value (index 0).
+      const StyleAnimationValue& preValue =
+        aPaceRange[preIdx].mPacedPropertyAnimValue[0].mValue;
+      const StyleAnimationValue& curValue =
+        aPaceRange[i].mPacedPropertyAnimValue[0].mValue;
+
+      if (!StyleAnimationValue::ComputeDistance(aProperty, preValue,
+                                                curValue, dist)) {
+        failed = true;
+        break;
+      }
+    }
+
+    cumulativeDist[i] = cumulativeDist[i - 1] + dist;
+    preIdx = i;
+  }
+
+  if (failed || cumulativeDist[len - 1] == 0.0) {
+    return;
+  }
+
+  // b) Apply computed offset.
+  const double offsetA = aPaceRange[pacedA].mComputedOffset;
+  const double diffOffset = aPaceRange[pacedB].mComputedOffset - offsetA;
+  const double totalDist = cumulativeDist[len - 1];
+  for (size_t i = pacedA + 1; i < pacedB; ++i) {
+    if (!aPaceRange[i].IsPaceable()) {
+      continue;
+    }
+    aPaceRange[i].mComputedOffset =
+      offsetA + diffOffset * cumulativeDist[i] / totalDist;
+  }
+}
+
 } // namespace mozilla
--- a/dom/animation/KeyframeUtils.h
+++ b/dom/animation/KeyframeUtils.h
@@ -52,38 +52,42 @@ public:
   GetKeyframesFromObject(JSContext* aCx,
                          JS::Handle<JSObject*> aFrames,
                          ErrorResult& aRv);
 
   /**
    * Calculate the StyleAnimationValues of properties of each keyframe.
    *
    * @param aFrames The input keyframes.
-   * @param aElement The context element.
+   * @param aElement The context element for resolving paced property values
+   *                 against.
    * @param aStyleContext The style context to use when computing values.
    * @return The set of nsTArray<PropertyStyleAnimationValuePair>. The length
    *   should be the same as aFrames.
    */
   static KeyframeAnimValues
   CalculateAnimationValues(const nsTArray<Keyframe>& aFrames,
                            dom::Element* aElement,
                            nsStyleContext* aStyleContext);
 
   /**
    * Fills in the mComputedOffset member of each keyframe in the given array
-   * using the specified spacing mode.
+   * using the specified spacing mode. If dom::Element or nsStyleContext are
+   * null, paced spacing mode falls back to distribute spacing made
+   * (as per spec).
    *
    * http://w3c.github.io/web-animations/#spacing-keyframes
    *
    * @param aKeyframes The set of keyframes to adjust.
    * @param aSpacingMode The applied spacing mode to aKeyframes.
    */
   static void
   ApplySpacing(nsTArray<Keyframe>& aKeyframes,
-               SpacingMode aSpacingMode);
+               SpacingMode aSpacingMode,
+               nsCSSProperty aProperty = eCSSProperty_UNKNOWN);
 
   /**
    * Converts an array of Keyframe objects into an array of AnimationProperty
    * objects. This involves expanding shorthand properties into longhand
    * properties, creating an array of computed values for each longhand
    * property and determining the offset and timing function to use for each
    * value.
    *