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
--- 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.
*