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