summaryrefslogtreecommitdiffstats
path: root/dom/animation/KeyframeUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/KeyframeUtils.cpp')
-rw-r--r--dom/animation/KeyframeUtils.cpp1667
1 files changed, 1667 insertions, 0 deletions
diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp
new file mode 100644
index 000000000..8e396f84c
--- /dev/null
+++ b/dom/animation/KeyframeUtils.cpp
@@ -0,0 +1,1667 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/KeyframeUtils.h"
+
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Move.h"
+#include "mozilla/RangedArray.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/TimingParams.h"
+#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyframeEffectBinding.h"
+#include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc.
+#include "jsapi.h" // For ForOfIterator etc.
+#include "nsClassHashtable.h"
+#include "nsCSSParser.h"
+#include "nsCSSPropertyIDSet.h"
+#include "nsCSSProps.h"
+#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
+#include "nsTArray.h"
+#include <algorithm> // For std::stable_sort
+
+namespace mozilla {
+
+// ------------------------------------------------------------------
+//
+// Internal data types
+//
+// ------------------------------------------------------------------
+
+// This is used while calculating paced spacing. If the keyframe is not pacable,
+// we set its cumulative distance to kNotPaceable, so we can use this to check.
+const double kNotPaceable = -1.0;
+
+// For the aAllowList parameter of AppendStringOrStringSequence and
+// GetPropertyValuesPairs.
+enum class ListAllowance { eDisallow, eAllow };
+
+/**
+ * A comparator to sort nsCSSPropertyID values such that longhands are sorted
+ * before shorthands, and shorthands with fewer components are sorted before
+ * shorthands with more components.
+ *
+ * Using this allows us to prioritize values specified by longhands (or smaller
+ * shorthand subsets) when longhands and shorthands are both specified
+ * on the one keyframe.
+ *
+ * Example orderings that result from this:
+ *
+ * margin-left, margin
+ *
+ * and:
+ *
+ * border-top-color, border-color, border-top, border
+ */
+class PropertyPriorityComparator
+{
+public:
+ PropertyPriorityComparator()
+ : mSubpropertyCountInitialized(false) {}
+
+ bool Equals(nsCSSPropertyID aLhs, nsCSSPropertyID aRhs) const
+ {
+ return aLhs == aRhs;
+ }
+
+ bool LessThan(nsCSSPropertyID aLhs,
+ nsCSSPropertyID aRhs) const
+ {
+ bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs);
+ bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs);
+
+ if (isShorthandLhs) {
+ if (isShorthandRhs) {
+ // First, sort shorthands by the number of longhands they have.
+ uint32_t subpropCountLhs = SubpropertyCount(aLhs);
+ uint32_t subpropCountRhs = SubpropertyCount(aRhs);
+ if (subpropCountLhs != subpropCountRhs) {
+ return subpropCountLhs < subpropCountRhs;
+ }
+ // Otherwise, sort by IDL name below.
+ } else {
+ // Put longhands before shorthands.
+ return false;
+ }
+ } else {
+ if (isShorthandRhs) {
+ // Put longhands before shorthands.
+ return true;
+ }
+ }
+ // For two longhand properties, or two shorthand with the same number
+ // of longhand components, sort by IDL name.
+ return nsCSSProps::PropertyIDLNameSortPosition(aLhs) <
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs);
+ }
+
+ uint32_t SubpropertyCount(nsCSSPropertyID aProperty) const
+ {
+ if (!mSubpropertyCountInitialized) {
+ PodZero(&mSubpropertyCount);
+ mSubpropertyCountInitialized = true;
+ }
+ if (mSubpropertyCount[aProperty] == 0) {
+ uint32_t count = 0;
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
+ p, aProperty, CSSEnabledState::eForAllContent) {
+ ++count;
+ }
+ mSubpropertyCount[aProperty] = count;
+ }
+ return mSubpropertyCount[aProperty];
+ }
+
+private:
+ // Cache of shorthand subproperty counts.
+ mutable RangedArray<
+ uint32_t,
+ eCSSProperty_COUNT_no_shorthands,
+ eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount;
+ mutable bool mSubpropertyCountInitialized;
+};
+
+/**
+ * Adaptor for PropertyPriorityComparator to sort objects which have
+ * a mProperty member.
+ */
+template <typename T>
+class TPropertyPriorityComparator : PropertyPriorityComparator
+{
+public:
+ bool Equals(const T& aLhs, const T& aRhs) const
+ {
+ return PropertyPriorityComparator::Equals(aLhs.mProperty, aRhs.mProperty);
+ }
+ bool LessThan(const T& aLhs, const T& aRhs) const
+ {
+ return PropertyPriorityComparator::LessThan(aLhs.mProperty, aRhs.mProperty);
+ }
+};
+
+/**
+ * Iterator to walk through a PropertyValuePair array using the ordering
+ * provided by PropertyPriorityComparator.
+ */
+class PropertyPriorityIterator
+{
+public:
+ explicit PropertyPriorityIterator(
+ const nsTArray<PropertyValuePair>& aProperties)
+ : mProperties(aProperties)
+ {
+ mSortedPropertyIndices.SetCapacity(mProperties.Length());
+ for (size_t i = 0, len = mProperties.Length(); i < len; ++i) {
+ PropertyAndIndex propertyIndex = { mProperties[i].mProperty, i };
+ mSortedPropertyIndices.AppendElement(propertyIndex);
+ }
+ mSortedPropertyIndices.Sort(PropertyAndIndex::Comparator());
+ }
+
+ class Iter
+ {
+ public:
+ explicit Iter(const PropertyPriorityIterator& aParent)
+ : mParent(aParent)
+ , mIndex(0) { }
+
+ static Iter EndIter(const PropertyPriorityIterator &aParent)
+ {
+ Iter iter(aParent);
+ iter.mIndex = aParent.mSortedPropertyIndices.Length();
+ return iter;
+ }
+
+ bool operator!=(const Iter& aOther) const
+ {
+ return mIndex != aOther.mIndex;
+ }
+
+ Iter& operator++()
+ {
+ MOZ_ASSERT(mIndex + 1 <= mParent.mSortedPropertyIndices.Length(),
+ "Should not seek past end iterator");
+ mIndex++;
+ return *this;
+ }
+
+ const PropertyValuePair& operator*()
+ {
+ MOZ_ASSERT(mIndex < mParent.mSortedPropertyIndices.Length(),
+ "Should not try to dereference an end iterator");
+ return mParent.mProperties[mParent.mSortedPropertyIndices[mIndex].mIndex];
+ }
+
+ private:
+ const PropertyPriorityIterator& mParent;
+ size_t mIndex;
+ };
+
+ Iter begin() { return Iter(*this); }
+ Iter end() { return Iter::EndIter(*this); }
+
+private:
+ struct PropertyAndIndex
+ {
+ nsCSSPropertyID mProperty;
+ size_t mIndex; // Index of mProperty within mProperties
+
+ typedef TPropertyPriorityComparator<PropertyAndIndex> Comparator;
+ };
+
+ const nsTArray<PropertyValuePair>& mProperties;
+ nsTArray<PropertyAndIndex> mSortedPropertyIndices;
+};
+
+/**
+ * A property-values pair obtained from the open-ended properties
+ * discovered on a regular keyframe or property-indexed keyframe object.
+ *
+ * Single values (as required by a regular keyframe, and as also supported
+ * on property-indexed keyframes) are stored as the only element in
+ * mValues.
+ */
+struct PropertyValuesPair
+{
+ nsCSSPropertyID mProperty;
+ nsTArray<nsString> mValues;
+
+ typedef TPropertyPriorityComparator<PropertyValuesPair> Comparator;
+};
+
+/**
+ * An additional property (for a property-values pair) found on a
+ * BaseKeyframe or BasePropertyIndexedKeyframe object.
+ */
+struct AdditionalProperty
+{
+ nsCSSPropertyID mProperty;
+ size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs.
+
+ struct PropertyComparator
+ {
+ bool Equals(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const
+ {
+ return aLhs.mProperty == aRhs.mProperty;
+ }
+ bool LessThan(const AdditionalProperty& aLhs,
+ const AdditionalProperty& aRhs) const
+ {
+ return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ }
+ };
+};
+
+/**
+ * Data for a segment in a keyframe animation of a given property
+ * whose value is a StyleAnimationValue.
+ *
+ * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
+ * to gather data for each individual segment.
+ */
+struct KeyframeValueEntry
+{
+ nsCSSPropertyID mProperty;
+ StyleAnimationValue mValue;
+ float mOffset;
+ Maybe<ComputedTimingFunction> mTimingFunction;
+
+ struct PropertyOffsetComparator
+ {
+ static bool Equals(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs)
+ {
+ return aLhs.mProperty == aRhs.mProperty &&
+ aLhs.mOffset == aRhs.mOffset;
+ }
+ static bool LessThan(const KeyframeValueEntry& aLhs,
+ const KeyframeValueEntry& aRhs)
+ {
+ // First, sort by property IDL name.
+ int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
+ nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
+ if (order != 0) {
+ return order < 0;
+ }
+
+ // Then, by offset.
+ return aLhs.mOffset < aRhs.mOffset;
+ }
+ };
+};
+
+class ComputedOffsetComparator
+{
+public:
+ static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs)
+ {
+ return aLhs.mComputedOffset == aRhs.mComputedOffset;
+ }
+
+ static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs)
+ {
+ return aLhs.mComputedOffset < aRhs.mComputedOffset;
+ }
+};
+
+// ------------------------------------------------------------------
+//
+// Inlined helper methods
+//
+// ------------------------------------------------------------------
+
+inline bool
+IsInvalidValuePair(const PropertyValuePair& aPair, StyleBackendType aBackend)
+{
+ if (aBackend == StyleBackendType::Servo) {
+ return !aPair.mServoDeclarationBlock;
+ }
+
+ // There are three types of values we store as token streams:
+ //
+ // * Shorthand values (where we manually extract the token stream's string
+ // value) and pass that along to various parsing methods
+ // * Longhand values with variable references
+ // * Invalid values
+ //
+ // We can distinguish between the last two cases because for invalid values
+ // we leave the token stream's mPropertyID as eCSSProperty_UNKNOWN.
+ return !nsCSSProps::IsShorthand(aPair.mProperty) &&
+ aPair.mValue.GetUnit() == eCSSUnit_TokenStream &&
+ aPair.mValue.GetTokenStreamValue()->mPropertyID
+ == eCSSProperty_UNKNOWN;
+}
+
+
+// ------------------------------------------------------------------
+//
+// Internal helper method declarations
+//
+// ------------------------------------------------------------------
+
+static void
+GetKeyframeListFromKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv);
+
+static bool
+ConvertKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult);
+
+static bool
+GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult);
+
+static bool
+AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsString>& aValues);
+
+static bool
+AppendValueAsString(JSContext* aCx,
+ nsTArray<nsString>& aValues,
+ JS::Handle<JS::Value> aValue);
+
+static PropertyValuePair
+MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
+ nsCSSParser& aParser, nsIDocument* aDocument);
+
+static bool
+HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
+
+static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
+
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair);
+
+static void
+BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult);
+
+static void
+GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv);
+
+static bool
+RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
+ nsIDocument* aDocument);
+
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+ const Range<Keyframe>& aRangeToAdjust);
+
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange);
+
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+ const Range<double>& aCumulativeDistances);
+
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+ nsCSSPropertyID aProperty,
+ nsStyleContext* aStyleContext);
+
+// ------------------------------------------------------------------
+//
+// Public API
+//
+// ------------------------------------------------------------------
+
+/* static */ nsTArray<Keyframe>
+KeyframeUtils::GetKeyframesFromObject(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JSObject*> aFrames,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(!aRv.Failed());
+
+ nsTArray<Keyframe> keyframes;
+
+ if (!aFrames) {
+ // The argument was explicitly null meaning no keyframes.
+ return keyframes;
+ }
+
+ // At this point we know we have an object. We try to convert it to a
+ // sequence of keyframes first, and if that fails due to not being iterable,
+ // we try to convert it to a property-indexed keyframe.
+ JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return keyframes;
+ }
+
+ if (iter.valueIsIterable()) {
+ GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes, aRv);
+ } else {
+ GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
+ keyframes, aRv);
+ }
+
+ if (aRv.Failed()) {
+ MOZ_ASSERT(keyframes.IsEmpty(),
+ "Should not set any keyframes when there is an error");
+ return keyframes;
+ }
+
+ // We currently don't support additive animation. However, Web Animations
+ // says that if you don't have a keyframe at offset 0 or 1, then you should
+ // synthesize one using an additive zero value when you go to compose style.
+ // Until we implement additive animations we just throw if we encounter any
+ // set of keyframes that would put us in that situation.
+
+ if (RequiresAdditiveAnimation(keyframes, aDocument)) {
+ aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
+ keyframes.Clear();
+ }
+
+ return keyframes;
+}
+
+/* static */ void
+KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
+ SpacingMode aSpacingMode,
+ nsCSSPropertyID aProperty,
+ nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext)
+{
+ if (aKeyframes.IsEmpty()) {
+ return;
+ }
+
+ nsTArray<double> cumulativeDistances;
+ if (aSpacingMode == SpacingMode::paced) {
+ MOZ_ASSERT(IsAnimatableProperty(aProperty),
+ "Paced property should be animatable");
+
+ cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty,
+ aStyleContext);
+ // Reset the computed offsets if using paced spacing.
+ for (Keyframe& keyframe : aKeyframes) {
+ keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet;
+ }
+ }
+
+ // If the first keyframe has 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.
+ const Keyframe* const last = aKeyframes.cend() - 1;
+ 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->mOffset.isNothing() && keyframeB != last) {
+ ++keyframeB;
+ }
+ keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
+
+ // Fill computed offsets in (keyframe A, keyframe B).
+ if (aSpacingMode == SpacingMode::distribute) {
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
+ } else {
+ // a) Find Paced A (first paceable keyframe) and
+ // Paced B (last paceable keyframe) in [keyframe A, keyframe B].
+ RangedPtr<Keyframe> pacedA = keyframeA;
+ while (pacedA < keyframeB &&
+ cumulativeDistances[pacedA - begin] == kNotPaceable) {
+ ++pacedA;
+ }
+ RangedPtr<Keyframe> pacedB = keyframeB;
+ while (pacedB > keyframeA &&
+ cumulativeDistances[pacedB - begin] == kNotPaceable) {
+ --pacedB;
+ }
+ // As spec says, if there is no paceable keyframe
+ // in [keyframe A, keyframe B], we let Paced A and Paced B refer to
+ // keyframe B.
+ if (pacedA > pacedB) {
+ pacedA = pacedB = keyframeB;
+ }
+ // b) Apply distributing offsets in (keyframe A, Paced A] and
+ // [Paced B, keyframe B).
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
+ Range<Keyframe>(keyframeA + 1, pacedA + 1));
+ DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
+ Range<Keyframe>(pacedB, keyframeB));
+ // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B).
+ // We pass the range [Paced A, Paced B] since PaceRange needs the end
+ // points of the range in order to calculate the correct offset.
+ PaceRange(Range<Keyframe>(pacedA, pacedB + 1),
+ Range<double>(&cumulativeDistances[pacedA - begin],
+ pacedB - pacedA + 1));
+ // d) Fill in any computed offsets in (Paced A, Paced B) that are still
+ // not set (e.g. because the keyframe was not paceable, or because the
+ // cumulative distance between paceable properties was zero).
+ 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, end + 1));
+ frame = end;
+ }
+ }
+ keyframeA = keyframeB;
+ }
+}
+
+/* static */ void
+KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
+{
+ nsTArray<ComputedKeyframeValues> emptyArray;
+ ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN,
+ emptyArray, nullptr);
+}
+
+/* static */ nsTArray<ComputedKeyframeValues>
+KeyframeUtils::GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
+ dom::Element* aElement,
+ nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aStyleContext);
+ MOZ_ASSERT(aElement);
+
+ StyleBackendType styleBackend = aElement->OwnerDoc()->GetStyleBackendType();
+
+ const size_t len = aKeyframes.Length();
+ nsTArray<ComputedKeyframeValues> result(len);
+
+ for (const Keyframe& frame : aKeyframes) {
+ nsCSSPropertyIDSet propertiesOnThisKeyframe;
+ ComputedKeyframeValues* computedValues = result.AppendElement();
+ for (const PropertyValuePair& pair :
+ PropertyPriorityIterator(frame.mPropertyValues)) {
+ MOZ_ASSERT(!pair.mServoDeclarationBlock ||
+ styleBackend == StyleBackendType::Servo,
+ "Animation values were parsed using Servo backend but target"
+ " element is not using Servo backend?");
+
+ if (IsInvalidValuePair(pair, styleBackend)) {
+ continue;
+ }
+
+ // Expand each value into the set of longhands and produce
+ // a KeyframeValueEntry for each value.
+ nsTArray<PropertyStyleAnimationValuePair> values;
+
+ if (styleBackend == StyleBackendType::Servo) {
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aStyleContext,
+ *pair.mServoDeclarationBlock, values)) {
+ continue;
+ }
+ } else {
+ // For shorthands, we store the string as a token stream so we need to
+ // extract that first.
+ if (nsCSSProps::IsShorthand(pair.mProperty)) {
+ nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aElement, aStyleContext,
+ tokenStream->mTokenStream, /* aUseSVGMode */ false, values) ||
+ IsComputeValuesFailureKey(pair)) {
+ continue;
+ }
+ } else {
+ if (!StyleAnimationValue::ComputeValues(pair.mProperty,
+ CSSEnabledState::eForAllContent, aElement, aStyleContext,
+ pair.mValue, /* aUseSVGMode */ false, values)) {
+ continue;
+ }
+ MOZ_ASSERT(values.Length() == 1,
+ "Longhand properties should produce a single"
+ " StyleAnimationValue");
+ }
+ }
+
+ for (auto& value : values) {
+ // If we already got a value for this property on the keyframe,
+ // skip this one.
+ if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) {
+ continue;
+ }
+ computedValues->AppendElement(value);
+ propertiesOnThisKeyframe.AddProperty(value.mProperty);
+ }
+ }
+ }
+
+ MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
+ return result;
+}
+
+/* static */ nsTArray<AnimationProperty>
+KeyframeUtils::GetAnimationPropertiesFromKeyframes(
+ const nsTArray<Keyframe>& aKeyframes,
+ const nsTArray<ComputedKeyframeValues>& aComputedValues,
+ nsStyleContext* aStyleContext)
+{
+ MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(),
+ "Array length mismatch");
+
+ nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
+
+ const size_t len = aKeyframes.Length();
+ for (size_t i = 0; i < len; ++i) {
+ const Keyframe& frame = aKeyframes[i];
+ for (auto& value : aComputedValues[i]) {
+ MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Invalid computed offset");
+ KeyframeValueEntry* entry = entries.AppendElement();
+ entry->mOffset = frame.mComputedOffset;
+ entry->mProperty = value.mProperty;
+ entry->mValue = value.mValue;
+ entry->mTimingFunction = frame.mTimingFunction;
+ }
+ }
+
+ nsTArray<AnimationProperty> result;
+ BuildSegmentsFromValueEntries(entries, result);
+ return result;
+}
+
+/* static */ bool
+KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty)
+{
+ if (aProperty == eCSSProperty_UNKNOWN) {
+ return false;
+ }
+
+ if (!nsCSSProps::IsShorthand(aProperty)) {
+ return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
+ }
+
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
+ CSSEnabledState::eForAllContent) {
+ if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// ------------------------------------------------------------------
+//
+// Internal helpers
+//
+// ------------------------------------------------------------------
+
+/**
+ * Converts a JS object to an IDL sequence<Keyframe>.
+ *
+ * @param aCx The JSContext corresponding to |aIterator|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aIterator An already-initialized ForOfIterator for the JS
+ * object to iterate over as a sequence.
+ * @param aResult The array into which the resulting Keyframe objects will be
+ * appended.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void
+GetKeyframeListFromKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(!aRv.Failed());
+ MOZ_ASSERT(aResult.IsEmpty());
+
+ // Convert the object in aIterator to a sequence of keyframes producing
+ // an array of Keyframe objects.
+ if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aResult)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ aResult.Clear();
+ return;
+ }
+
+ // If the sequence<> had zero elements, we won't generate any
+ // keyframes.
+ if (aResult.IsEmpty()) {
+ return;
+ }
+
+ // Check that the keyframes are loosely sorted and with values all
+ // between 0% and 100%.
+ if (!HasValidOffsets(aResult)) {
+ aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
+ aResult.Clear();
+ return;
+ }
+}
+
+/**
+ * Converts a JS object wrapped by the given JS::ForIfIterator to an
+ * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
+ * aResult.
+ */
+static bool
+ConvertKeyframeSequence(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::ForOfIterator& aIterator,
+ nsTArray<Keyframe>& aResult)
+{
+ JS::Rooted<JS::Value> value(aCx);
+ nsCSSParser parser(aDocument->CSSLoader());
+
+ for (;;) {
+ bool done;
+ if (!aIterator.next(&value, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ // Each value found when iterating the object must be an object
+ // or null/undefined (which gets treated as a default {} dictionary
+ // value).
+ if (!value.isObject() && !value.isNullOrUndefined()) {
+ dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT,
+ "Element of sequence<Keyframe> argument");
+ return false;
+ }
+
+ // Convert the JS value into a BaseKeyframe dictionary value.
+ dom::binding_detail::FastBaseKeyframe keyframeDict;
+ if (!keyframeDict.Init(aCx, value,
+ "Element of sequence<Keyframe> argument")) {
+ return false;
+ }
+
+ Keyframe* keyframe = aResult.AppendElement(fallible);
+ if (!keyframe) {
+ return false;
+ }
+ if (!keyframeDict.mOffset.IsNull()) {
+ keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
+ }
+
+ ErrorResult rv;
+ keyframe->mTimingFunction =
+ TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, rv);
+ if (rv.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+
+ // Look for additional property-values pairs on the object.
+ nsTArray<PropertyValuesPair> propertyValuePairs;
+ if (value.isObject()) {
+ JS::Rooted<JSObject*> object(aCx, &value.toObject());
+ if (!GetPropertyValuesPairs(aCx, object,
+ ListAllowance::eDisallow,
+ propertyValuePairs)) {
+ return false;
+ }
+ }
+
+ for (PropertyValuesPair& pair : propertyValuePairs) {
+ MOZ_ASSERT(pair.mValues.Length() == 1);
+ keyframe->mPropertyValues.AppendElement(
+ MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser,
+ aDocument));
+
+ // When we go to convert keyframes into arrays of property values we
+ // call StyleAnimation::ComputeValues. This should normally return true
+ // but in order to test the case where it does not, BaseKeyframeDict
+ // includes a chrome-only member that can be set to indicate that
+ // ComputeValues should fail for shorthand property values on that
+ // keyframe.
+ if (nsCSSProps::IsShorthand(pair.mProperty) &&
+ keyframeDict.mSimulateComputeValuesFailure) {
+ MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Reads the property-values pairs from the specified JS object.
+ *
+ * @param aObject The JS object to look at.
+ * @param aAllowLists If eAllow, values will be converted to
+ * (DOMString or sequence<DOMString); if eDisallow, values
+ * will be converted to DOMString.
+ * @param aResult The array into which the enumerated property-values
+ * pairs will be stored.
+ * @return false on failure or JS exception thrown while interacting
+ * with aObject; true otherwise.
+ */
+static bool
+GetPropertyValuesPairs(JSContext* aCx,
+ JS::Handle<JSObject*> aObject,
+ ListAllowance aAllowLists,
+ nsTArray<PropertyValuesPair>& aResult)
+{
+ nsTArray<AdditionalProperty> properties;
+
+ // Iterate over all the properties on aObject and append an
+ // entry to properties for them.
+ //
+ // We don't compare the jsids that we encounter with those for
+ // the explicit dictionary members, since we know that none
+ // of the CSS property IDL names clash with them.
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, aObject, &ids)) {
+ return false;
+ }
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString propName;
+ if (!propName.init(aCx, ids[i])) {
+ return false;
+ }
+ nsCSSPropertyID property =
+ nsCSSProps::LookupPropertyByIDLName(propName,
+ CSSEnabledState::eForAllContent);
+ if (KeyframeUtils::IsAnimatableProperty(property)) {
+ AdditionalProperty* p = properties.AppendElement();
+ p->mProperty = property;
+ p->mJsidIndex = i;
+ }
+ }
+
+ // Sort the entries by IDL name and then get each value and
+ // convert it either to a DOMString or to a
+ // (DOMString or sequence<DOMString>), depending on aAllowLists,
+ // and build up aResult.
+ properties.Sort(AdditionalProperty::PropertyComparator());
+
+ for (AdditionalProperty& p : properties) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
+ return false;
+ }
+ PropertyValuesPair* pair = aResult.AppendElement();
+ pair->mProperty = p.mProperty;
+ if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
+ pair->mValues)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString, if aAllowLists is eDisallow, or
+ * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
+ * The resulting strings are appended to aValues.
+ */
+static bool
+AppendStringOrStringSequenceToArray(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ListAllowance aAllowLists,
+ nsTArray<nsString>& aValues)
+{
+ if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
+ // The value is an object, and we want to allow lists; convert
+ // aValue to (DOMString or sequence<DOMString>).
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
+ return false;
+ }
+ if (iter.valueIsIterable()) {
+ // If the object is iterable, convert it to sequence<DOMString>.
+ JS::Rooted<JS::Value> element(aCx);
+ for (;;) {
+ bool done;
+ if (!iter.next(&element, &done)) {
+ return false;
+ }
+ if (done) {
+ break;
+ }
+ if (!AppendValueAsString(aCx, aValues, element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ // Either the object is not iterable, or aAllowLists doesn't want
+ // a list; convert it to DOMString.
+ if (!AppendValueAsString(aCx, aValues, aValue)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Converts aValue to DOMString and appends it to aValues.
+ */
+static bool
+AppendValueAsString(JSContext* aCx,
+ nsTArray<nsString>& aValues,
+ JS::Handle<JS::Value> aValue)
+{
+ return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
+ *aValues.AppendElement());
+}
+
+/**
+ * Construct a PropertyValuePair parsing the given string into a suitable
+ * nsCSSValue object.
+ *
+ * @param aProperty The CSS property.
+ * @param aStringValue The property value to parse.
+ * @param aParser The CSS parser object to use.
+ * @param aDocument The document to use when parsing.
+ * @return The constructed PropertyValuePair object.
+ */
+static PropertyValuePair
+MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
+ nsCSSParser& aParser, nsIDocument* aDocument)
+{
+ MOZ_ASSERT(aDocument);
+ PropertyValuePair result;
+
+ result.mProperty = aProperty;
+
+ if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) {
+ nsCString name = nsCSSProps::GetStringValue(aProperty);
+
+ NS_ConvertUTF16toUTF8 value(aStringValue);
+ RefPtr<ThreadSafeURIHolder> base =
+ new ThreadSafeURIHolder(aDocument->GetDocumentURI());
+ RefPtr<ThreadSafeURIHolder> referrer =
+ new ThreadSafeURIHolder(aDocument->GetDocumentURI());
+ RefPtr<ThreadSafePrincipalHolder> principal =
+ new ThreadSafePrincipalHolder(aDocument->NodePrincipal());
+
+ nsCString baseString;
+ aDocument->GetDocumentURI()->GetSpec(baseString);
+
+ RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
+ Servo_ParseProperty(&name, &value, &baseString,
+ base, referrer, principal).Consume();
+
+ if (servoDeclarationBlock) {
+ result.mServoDeclarationBlock = servoDeclarationBlock.forget();
+ return result;
+ }
+ }
+
+ nsCSSValue value;
+ if (!nsCSSProps::IsShorthand(aProperty)) {
+ aParser.ParseLonghandProperty(aProperty,
+ aStringValue,
+ aDocument->GetDocumentURI(),
+ aDocument->GetDocumentURI(),
+ aDocument->NodePrincipal(),
+ value);
+ }
+
+ if (value.GetUnit() == eCSSUnit_Null) {
+ // Either we have a shorthand, or we failed to parse a longhand.
+ // In either case, store the string value as a token stream.
+ nsCSSValueTokenStream* tokenStream = new nsCSSValueTokenStream;
+ tokenStream->mTokenStream = aStringValue;
+
+ // We are about to convert a null value to a token stream value but
+ // by leaving the mPropertyID as unknown, we will be able to
+ // distinguish between invalid values and valid token stream values
+ // (e.g. values with variable references).
+ MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
+ "The property of a token stream should be initialized"
+ " to unknown");
+
+ // By leaving mShorthandPropertyID as unknown, we ensure that when
+ // we call nsCSSValue::AppendToString we get back the string stored
+ // in mTokenStream.
+ MOZ_ASSERT(tokenStream->mShorthandPropertyID == eCSSProperty_UNKNOWN,
+ "The shorthand property of a token stream should be initialized"
+ " to unknown");
+ value.SetTokenStreamValue(tokenStream);
+ } else {
+ // If we succeeded in parsing with Gecko, but not Servo the animation is
+ // not going to work since, for the purposes of animation, we're going to
+ // ignore |mValue| when the backend is Servo.
+ NS_WARNING_ASSERTION(aDocument->GetStyleBackendType() !=
+ StyleBackendType::Servo,
+ "Gecko succeeded in parsing where Servo failed");
+ }
+
+ result.mValue = value;
+
+ return result;
+}
+
+/**
+ * Checks that the given keyframes are loosely ordered (each keyframe's
+ * offset that is not null is greater than or equal to the previous
+ * non-null offset) and that all values are within the range [0.0, 1.0].
+ *
+ * @return true if the keyframes' offsets are correctly ordered and
+ * within range; false otherwise.
+ */
+static bool
+HasValidOffsets(const nsTArray<Keyframe>& aKeyframes)
+{
+ double offset = 0.0;
+ for (const Keyframe& keyframe : aKeyframes) {
+ if (keyframe.mOffset) {
+ double thisOffset = keyframe.mOffset.value();
+ if (thisOffset < offset || thisOffset > 1.0f) {
+ return false;
+ }
+ offset = thisOffset;
+ }
+ }
+ return true;
+}
+
+/**
+ * Takes a property-value pair for a shorthand property and modifies the
+ * value to indicate that when we call StyleAnimationValue::ComputeValues on
+ * that value we should behave as if that function had failed.
+ *
+ * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
+ * a shorthand property.
+ */
+static void
+MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
+{
+ MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
+ "Only shorthand property values can be marked as failure values");
+
+ // We store shorthand values as nsCSSValueTokenStream objects whose mProperty
+ // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream
+ // member contains the shorthand property's value as a string.
+ //
+ // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that
+ // nsCSSValue::AppendToString returns the mTokenStream value, but we can
+ // update mPropertyID to a special value to indicate that this is
+ // a special failure sentinel.
+ nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue();
+ MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
+ "Shorthand value should initially have an unknown property ID");
+ tokenStream->mPropertyID = eCSSPropertyExtra_no_properties;
+}
+
+/**
+ * Returns true if |aPair| is a property-value pair on which we have
+ * previously called MarkAsComputeValuesFailureKey (and hence we should
+ * simulate failure when calling StyleAnimationValue::ComputeValues using its
+ * value).
+ *
+ * @param aPair The property-value pair to test.
+ * @return True if |aPair| represents a failure value.
+ */
+static bool
+IsComputeValuesFailureKey(const PropertyValuePair& aPair)
+{
+ return nsCSSProps::IsShorthand(aPair.mProperty) &&
+ aPair.mValue.GetTokenStreamValue()->mPropertyID ==
+ eCSSPropertyExtra_no_properties;
+}
+
+/**
+ * Builds an array of AnimationProperty objects to represent the keyframe
+ * animation segments in aEntries.
+ */
+static void
+BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
+ nsTArray<AnimationProperty>& aResult)
+{
+ if (aEntries.IsEmpty()) {
+ return;
+ }
+
+ // Sort the KeyframeValueEntry objects so that all entries for a given
+ // property are together, and the entries are sorted by offset otherwise.
+ std::stable_sort(aEntries.begin(), aEntries.end(),
+ &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
+
+ // For a given index i, we want to generate a segment from aEntries[i]
+ // to aEntries[j], if:
+ //
+ // * j > i,
+ // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
+ // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
+ //
+ // That will eliminate runs of same offset/property values where there's no
+ // point generating zero length segments in the middle of the animation.
+ //
+ // Additionally we need to generate a zero length segment at offset 0 and at
+ // offset 1, if we have multiple values for a given property at that offset,
+ // since we need to retain the very first and very last value so they can
+ // be used for reverse and forward filling.
+ //
+ // Typically, for each property in |aEntries|, we expect there to be at least
+ // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
+ // However, since it is possible that when building |aEntries|, the call to
+ // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
+ // Furthermore, since we don't yet implement additive animation and hence
+ // don't have sensible fallback behavior when these values are missing, the
+ // following loop takes care to identify properties that lack a value at
+ // offset 0.0/1.0 and drops those properties from |aResult|.
+
+ nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
+ AnimationProperty* animationProperty = nullptr;
+
+ size_t i = 0, n = aEntries.Length();
+
+ while (i < n) {
+ // Check that the last property ends with an entry at offset 1.
+ if (i + 1 == n) {
+ if (aEntries[i].mOffset != 1.0f && animationProperty) {
+ aResult.RemoveElementAt(aResult.Length() - 1);
+ animationProperty = nullptr;
+ }
+ break;
+ }
+
+ MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
+ aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
+ "Each entry should specify a valid property");
+
+ // Skip properties that don't have an entry with offset 0.
+ if (aEntries[i].mProperty != lastProperty &&
+ aEntries[i].mOffset != 0.0f) {
+ // Since the entries are sorted by offset for a given property, and
+ // since we don't update |lastProperty|, we will keep hitting this
+ // condition until we change property.
+ ++i;
+ continue;
+ }
+
+ // Drop properties that don't end with an entry with offset 1.
+ if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
+ aEntries[i].mOffset != 1.0f) {
+ if (animationProperty) {
+ aResult.RemoveElementAt(aResult.Length() - 1);
+ animationProperty = nullptr;
+ }
+ ++i;
+ continue;
+ }
+
+ // Starting from i, determine the next [i, j] interval from which to
+ // generate a segment.
+ size_t j;
+ if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
+ // We need to generate an initial zero-length segment.
+ MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
+ j = i + 1;
+ while (aEntries[j + 1].mOffset == 0.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else if (aEntries[i].mOffset == 1.0f) {
+ if (aEntries[i + 1].mOffset == 1.0f &&
+ aEntries[i + 1].mProperty == aEntries[i].mProperty) {
+ // We need to generate a final zero-length segment.
+ j = i + 1;
+ while (j + 1 < n &&
+ aEntries[j + 1].mOffset == 1.0f &&
+ aEntries[j + 1].mProperty == aEntries[j].mProperty) {
+ ++j;
+ }
+ } else {
+ // New property.
+ MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
+ animationProperty = nullptr;
+ ++i;
+ continue;
+ }
+ } else {
+ while (aEntries[i].mOffset == aEntries[i + 1].mOffset &&
+ aEntries[i].mProperty == aEntries[i + 1].mProperty) {
+ ++i;
+ }
+ j = i + 1;
+ }
+
+ // If we've moved on to a new property, create a new AnimationProperty
+ // to insert segments into.
+ if (aEntries[i].mProperty != lastProperty) {
+ MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
+ MOZ_ASSERT(!animationProperty);
+ animationProperty = aResult.AppendElement();
+ animationProperty->mProperty = aEntries[i].mProperty;
+ lastProperty = aEntries[i].mProperty;
+ }
+
+ MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
+
+ // Now generate the segment.
+ AnimationPropertySegment* segment =
+ animationProperty->mSegments.AppendElement();
+ segment->mFromKey = aEntries[i].mOffset;
+ segment->mToKey = aEntries[j].mOffset;
+ segment->mFromValue = aEntries[i].mValue;
+ segment->mToValue = aEntries[j].mValue;
+ segment->mTimingFunction = aEntries[i].mTimingFunction;
+
+ i = j;
+ }
+}
+
+/**
+ * Converts a JS object representing a property-indexed keyframe into
+ * an array of Keyframe objects.
+ *
+ * @param aCx The JSContext for |aValue|.
+ * @param aDocument The document to use when parsing CSS properties.
+ * @param aValue The JS object.
+ * @param aResult The array into which the resulting AnimationProperty
+ * objects will be appended.
+ * @param aRv Out param to store any errors thrown by this function.
+ */
+static void
+GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
+ nsIDocument* aDocument,
+ JS::Handle<JS::Value> aValue,
+ nsTArray<Keyframe>& aResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aValue.isObject());
+ MOZ_ASSERT(aResult.IsEmpty());
+ MOZ_ASSERT(!aRv.Failed());
+
+ // Convert the object to a property-indexed keyframe dictionary to
+ // get its explicit dictionary members.
+ dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
+ if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
+ false)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ Maybe<ComputedTimingFunction> easing =
+ TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ // Get all the property--value-list pairs off the object.
+ JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
+ nsTArray<PropertyValuesPair> propertyValuesPairs;
+ if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
+ propertyValuesPairs)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Create a set of keyframes for each property.
+ nsCSSParser parser(aDocument->CSSLoader());
+ nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
+ for (const PropertyValuesPair& pair : propertyValuesPairs) {
+ size_t count = pair.mValues.Length();
+ if (count == 0) {
+ // No animation values for this property.
+ continue;
+ }
+ if (count == 1) {
+ // We don't support additive values and so can't support an
+ // animation that goes from the underlying value to this
+ // specified value. Throw an exception until we do support this.
+ aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
+ return;
+ }
+
+ size_t n = pair.mValues.Length() - 1;
+ size_t i = 0;
+
+ for (const nsString& stringValue : pair.mValues) {
+ double offset = i++ / double(n);
+ Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
+ if (keyframe->mPropertyValues.IsEmpty()) {
+ keyframe->mTimingFunction = easing;
+ keyframe->mComputedOffset = offset;
+ }
+ keyframe->mPropertyValues.AppendElement(
+ MakePropertyValuePair(pair.mProperty, stringValue, parser, aDocument));
+ }
+ }
+
+ aResult.SetCapacity(processedKeyframes.Count());
+ for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) {
+ aResult.AppendElement(Move(*iter.UserData()));
+ }
+
+ aResult.Sort(ComputedOffsetComparator());
+}
+
+/**
+ * Returns true if the supplied set of keyframes has keyframe values for
+ * any property for which it does not also supply a value for the 0% and 100%
+ * offsets. In this case we are supposed to synthesize an additive zero value
+ * but since we don't support additive animation yet we can't support this
+ * case. We try to detect that here so we can throw an exception. The check is
+ * not entirely accurate but should detect most common cases.
+ *
+ * @param aKeyframes The set of keyframes to analyze.
+ * @param aDocument The document to use when parsing keyframes so we can
+ * try to detect where we have an invalid value at 0%/100%.
+ */
+static bool
+RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
+ nsIDocument* aDocument)
+{
+ // We are looking to see if that every property referenced in |aKeyframes|
+ // has a valid property at offset 0.0 and 1.0. The check as to whether a
+ // property is valid or not, however, is not precise. We only check if the
+ // property can be parsed, NOT whether it can also be converted to a
+ // StyleAnimationValue since doing that requires a target element bound to
+ // a document which we might not always have at the point where we want to
+ // perform this check.
+ //
+ // This is only a temporary measure until we implement additive animation.
+ // So as long as this check catches most cases, and we don't do anything
+ // horrible in one of the cases we can't detect, it should be sufficient.
+
+ nsCSSPropertyIDSet properties; // All properties encountered.
+ nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
+ nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value.
+
+ auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
+ properties.AddProperty(aProperty);
+ if (aOffset == 0.0) {
+ propertiesWithFromValue.AddProperty(aProperty);
+ } else if (aOffset == 1.0) {
+ propertiesWithToValue.AddProperty(aProperty);
+ }
+ };
+
+ StyleBackendType styleBackend = aDocument->GetStyleBackendType();
+
+ for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
+ const Keyframe& frame = aKeyframes[i];
+
+ // We won't have called ApplySpacing when this is called so
+ // we can't use frame.mComputedOffset. Instead we do a rough version
+ // of that algorithm that substitutes null offsets with 0.0 for the first
+ // frame, 1.0 for the last frame, and 0.5 for everything else.
+ double computedOffset = i == len - 1
+ ? 1.0
+ : i == 0 ? 0.0 : 0.5;
+ double offsetToUse = frame.mOffset
+ ? frame.mOffset.value()
+ : computedOffset;
+
+ for (const PropertyValuePair& pair : frame.mPropertyValues) {
+ if (IsInvalidValuePair(pair, styleBackend)) {
+ continue;
+ }
+
+ if (nsCSSProps::IsShorthand(pair.mProperty)) {
+ if (styleBackend == StyleBackendType::Gecko) {
+ nsCSSValueTokenStream* tokenStream =
+ pair.mValue.GetTokenStreamValue();
+ nsCSSParser parser(aDocument->CSSLoader());
+ if (!parser.IsValueValidForProperty(pair.mProperty,
+ tokenStream->mTokenStream)) {
+ continue;
+ }
+ }
+ // For the Servo backend, invalid shorthand values are represented by
+ // a null mServoDeclarationBlock member which we skip above in
+ // IsInvalidValuePair.
+ MOZ_ASSERT(styleBackend != StyleBackendType::Servo ||
+ pair.mServoDeclarationBlock);
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
+ prop, pair.mProperty, CSSEnabledState::eForAllContent) {
+ addToPropertySets(*prop, offsetToUse);
+ }
+ } else {
+ addToPropertySets(pair.mProperty, offsetToUse);
+ }
+ }
+ }
+
+ return !propertiesWithFromValue.Equals(properties) ||
+ !propertiesWithToValue.Equals(properties);
+}
+
+/**
+ * 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). The second range, aRangeToAdjust, is passed, so
+ * we can know which keyframe we want to apply to. aRangeToAdjust should be in
+ * the range of aSpacingRange.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ * should apply distribute spacing.
+ * @param aRangeToAdjust The range of keyframes we want to apply to.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange,
+ const Range<Keyframe>& aRangeToAdjust)
+{
+ MOZ_ASSERT(aRangeToAdjust.begin() >= aSpacingRange.begin() &&
+ aRangeToAdjust.end() <= aSpacingRange.end(),
+ "Out of range");
+ const size_t n = aSpacingRange.length() - 1;
+ const double startOffset = aSpacingRange[0].mComputedOffset;
+ const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset;
+ for (auto iter = aRangeToAdjust.begin();
+ iter != aRangeToAdjust.end();
+ ++iter) {
+ size_t index = iter - aSpacingRange.begin();
+ iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
+ }
+}
+
+/**
+ * Overload of DistributeRange to apply distribute spacing to all keyframes in
+ * between the endpoints of the given range.
+ *
+ * @param aSpacingRange The sequence of keyframes between whose endpoints we
+ * should apply distribute spacing.
+ */
+static void
+DistributeRange(const Range<Keyframe>& aSpacingRange)
+{
+ // We don't need to apply distribute spacing to keyframe A and keyframe B.
+ DistributeRange(aSpacingRange,
+ Range<Keyframe>(aSpacingRange.begin() + 1,
+ aSpacingRange.end() - 1));
+}
+
+/**
+ * Apply paced spacing to all paceable keyframes in between the endpoints of the
+ * given range.
+ *
+ * @param aKeyframes The range of keyframes between whose endpoints we should
+ * apply paced spacing. Both endpoints should be paceable, i.e. the
+ * corresponding elements in |aCumulativeDist| should not be kNotPaceable.
+ * Within this function, we refer to the start and end points of this range
+ * as Paced A and Paced B respectively in keeping with the notation used in
+ * the spec.
+ * @param aCumulativeDistances The sequence of cumulative distances of the paced
+ * property as returned by GetCumulativeDistances(). This acts as a
+ * parallel range to |aKeyframes|.
+ */
+static void
+PaceRange(const Range<Keyframe>& aKeyframes,
+ const Range<double>& aCumulativeDistances)
+{
+ MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(),
+ "Range length mismatch");
+
+ const size_t len = aKeyframes.length();
+ // If there is nothing between the end points, there is nothing to space.
+ if (len < 3) {
+ return;
+ }
+
+ const double distA = *(aCumulativeDistances.begin());
+ const double distB = *(aCumulativeDistances.end() - 1);
+ MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable,
+ "Both Paced A and Paced B should be paceable");
+
+ // If the total distance is zero, we should fall back to distribute spacing.
+ // The caller will fill-in any keyframes without a computed offset using
+ // distribute spacing so we can just return here.
+ if (distA == distB) {
+ return;
+ }
+
+ const RangedPtr<Keyframe> pacedA = aKeyframes.begin();
+ const RangedPtr<Keyframe> pacedB = aKeyframes.end() - 1;
+ MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet &&
+ pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet,
+ "Both Paced A and Paced B should have valid computed offsets");
+
+ // Apply computed offset.
+ const double offsetA = pacedA->mComputedOffset;
+ const double diffOffset = pacedB->mComputedOffset - offsetA;
+ const double initialDist = distA;
+ const double totalDist = distB - initialDist;
+ for (auto iter = pacedA + 1; iter != pacedB; ++iter) {
+ size_t k = iter - aKeyframes.begin();
+ if (aCumulativeDistances[k] == kNotPaceable) {
+ continue;
+ }
+
+ double dist = aCumulativeDistances[k] - initialDist;
+ iter->mComputedOffset = offsetA + diffOffset * dist / totalDist;
+ }
+}
+
+/**
+ * Get cumulative distances for the paced property.
+ *
+ * @param aValues The computed values returned by GetComputedKeyframeValues.
+ * @param aPacedProperty The paced property.
+ * @param aStyleContext The style context for computing distance on transform.
+ * @return The cumulative distances for the paced property. The length will be
+ * the same as aValues.
+ */
+static nsTArray<double>
+GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
+ nsCSSPropertyID aPacedProperty,
+ nsStyleContext* aStyleContext)
+{
+ // a) If aPacedProperty is a shorthand property, get its components.
+ // Otherwise, just add the longhand property into the set.
+ size_t pacedPropertyCount = 0;
+ nsCSSPropertyIDSet pacedPropertySet;
+ bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty);
+ if (isShorthand) {
+ CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty,
+ CSSEnabledState::eForAllContent) {
+ pacedPropertySet.AddProperty(*p);
+ ++pacedPropertyCount;
+ }
+ } else {
+ pacedPropertySet.AddProperty(aPacedProperty);
+ pacedPropertyCount = 1;
+ }
+
+ // b) Search each component (shorthand) or the longhand property, and
+ // calculate the cumulative distances of paceable keyframe pairs.
+ const size_t len = aValues.Length();
+ nsTArray<double> cumulativeDistances(len);
+ // cumulativeDistances is a parallel array to |aValues|, so set its length to
+ // the length of |aValues|.
+ cumulativeDistances.SetLength(len);
+ ComputedKeyframeValues prevPacedValues;
+ size_t preIdx = 0;
+ for (size_t i = 0; i < len; ++i) {
+ // Find computed values of the paced property.
+ ComputedKeyframeValues pacedValues;
+ for (const PropertyStyleAnimationValuePair& pair : aValues[i]) {
+ if (pacedPropertySet.HasProperty(pair.mProperty)) {
+ pacedValues.AppendElement(pair);
+ }
+ }
+
+ // Check we have values for all the paceable longhand components.
+ if (pacedValues.Length() != pacedPropertyCount) {
+ // This keyframe is not paceable, assign kNotPaceable and skip it.
+ cumulativeDistances[i] = kNotPaceable;
+ continue;
+ }
+
+ // Sort the pacedValues first, so the order of subproperties of
+ // pacedValues is always the same as that of prevPacedValues.
+ if (isShorthand) {
+ pacedValues.Sort(
+ TPropertyPriorityComparator<PropertyStyleAnimationValuePair>());
+ }
+
+ if (prevPacedValues.IsEmpty()) {
+ // This is the first paceable keyframe so its cumulative distance is 0.0.
+ cumulativeDistances[i] = 0.0;
+ } else {
+ double dist = 0.0;
+ if (isShorthand) {
+ // Apply the distance by the square root of the sum of squares of
+ // longhand component distances.
+ for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) {
+ nsCSSPropertyID prop = prevPacedValues[propIdx].mProperty;
+ MOZ_ASSERT(pacedValues[propIdx].mProperty == prop,
+ "Property mismatch");
+
+ double componentDistance = 0.0;
+ if (StyleAnimationValue::ComputeDistance(
+ prop,
+ prevPacedValues[propIdx].mValue,
+ pacedValues[propIdx].mValue,
+ aStyleContext,
+ componentDistance)) {
+ dist += componentDistance * componentDistance;
+ }
+ }
+ dist = sqrt(dist);
+ } else {
+ // If the property is longhand, we just use the 1st value.
+ // If ComputeDistance() fails, |dist| will remain zero so there will be
+ // no distance between the previous paced value and this value.
+ Unused <<
+ StyleAnimationValue::ComputeDistance(aPacedProperty,
+ prevPacedValues[0].mValue,
+ pacedValues[0].mValue,
+ aStyleContext,
+ dist);
+ }
+ cumulativeDistances[i] = cumulativeDistances[preIdx] + dist;
+ }
+ prevPacedValues.SwapElements(pacedValues);
+ preIdx = i;
+ }
+ return cumulativeDistances;
+}
+
+} // namespace mozilla