/* 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/ServoBindingTypes.h" #include "mozilla/ServoComputedValuesWithParent.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 "nsContentUtils.h" // For GetContextForContent #include "nsCSSParser.h" #include "nsCSSPropertyIDSet.h" #include "nsCSSProps.h" #include "nsCSSPseudoElements.h" // For CSSPseudoElementType #include "nsStyleContext.h" #include "nsTArray.h" #include // 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 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& 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 Comparator; }; const nsTArray& mProperties; nsTArray 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 mValues; typedef TPropertyPriorityComparator 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; AnimationValue mValue; float mOffset; Maybe mTimingFunction; dom::CompositeOperation mComposite; 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& aResult, ErrorResult& aRv); static bool ConvertKeyframeSequence(JSContext* aCx, nsIDocument* aDocument, JS::ForOfIterator& aIterator, nsTArray& aResult); static bool GetPropertyValuesPairs(JSContext* aCx, JS::Handle aObject, ListAllowance aAllowLists, nsTArray& aResult); static bool AppendStringOrStringSequenceToArray(JSContext* aCx, JS::Handle aValue, ListAllowance aAllowLists, nsTArray& aValues); static bool AppendValueAsString(JSContext* aCx, nsTArray& aValues, JS::Handle aValue); static PropertyValuePair MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue, nsCSSParser& aParser, nsIDocument* aDocument); static bool HasValidOffsets(const nsTArray& aKeyframes); static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair); static bool IsComputeValuesFailureKey(const PropertyValuePair& aPair); static void BuildSegmentsFromValueEntries(nsTArray& aEntries, nsTArray& aResult); static void GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx, nsIDocument* aDocument, JS::Handle aValue, nsTArray& aResult, ErrorResult& aRv); static bool RequiresAdditiveAnimation(const nsTArray& aKeyframes, nsIDocument* aDocument); static void DistributeRange(const Range& aSpacingRange, const Range& aRangeToAdjust); static void DistributeRange(const Range& aSpacingRange); static void PaceRange(const Range& aKeyframes, const Range& aCumulativeDistances); static nsTArray GetCumulativeDistances(const nsTArray& aValues, nsCSSPropertyID aProperty, nsStyleContext* aStyleContext); // ------------------------------------------------------------------ // // Public API // // ------------------------------------------------------------------ /* static */ nsTArray KeyframeUtils::GetKeyframesFromObject(JSContext* aCx, nsIDocument* aDocument, JS::Handle aFrames, ErrorResult& aRv) { MOZ_ASSERT(!aRv.Failed()); nsTArray 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 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; } if (!AnimationUtils::IsCoreAPIEnabled() && RequiresAdditiveAnimation(keyframes, aDocument)) { keyframes.Clear(); aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); } return keyframes; } /* static */ void KeyframeUtils::ApplySpacing(nsTArray& aKeyframes, SpacingMode aSpacingMode, nsCSSPropertyID aProperty, nsTArray& aComputedValues, nsStyleContext* aStyleContext) { if (aKeyframes.IsEmpty()) { return; } nsTArray 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.LastElement(); const RangedPtr begin(aKeyframes.Elements(), aKeyframes.Length()); RangedPtr keyframeA = begin; while (keyframeA != last) { // Find keyframe A and keyframe B *between* which we will apply spacing. RangedPtr 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(keyframeA, keyframeB + 1)); } else { // a) Find Paced A (first paceable keyframe) and // Paced B (last paceable keyframe) in [keyframe A, keyframe B]. RangedPtr pacedA = keyframeA; while (pacedA < keyframeB && cumulativeDistances[pacedA - begin] == kNotPaceable) { ++pacedA; } RangedPtr 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(keyframeA, keyframeB + 1), Range(keyframeA + 1, pacedA + 1)); DistributeRange(Range(keyframeA, keyframeB + 1), Range(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(pacedA, pacedB + 1), Range(&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 frame = pacedA + 1; frame < pacedB; ++frame) { if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) { continue; } RangedPtr start = frame - 1; RangedPtr end = frame + 1; while (end < pacedB && end->mComputedOffset == Keyframe::kComputedOffsetNotSet) { ++end; } DistributeRange(Range(start, end + 1)); frame = end; } } keyframeA = keyframeB; } } /* static */ void KeyframeUtils::ApplyDistributeSpacing(nsTArray& aKeyframes) { nsTArray emptyArray; ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN, emptyArray, nullptr); } /* static */ nsTArray KeyframeUtils::GetComputedKeyframeValues( const nsTArray& aKeyframes, dom::Element* aElement, const ServoComputedValuesWithParent& aServoValues) { MOZ_ASSERT(aElement); MOZ_ASSERT(aElement->IsStyledByServo()); nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement); MOZ_ASSERT(presContext); return presContext->StyleSet()->AsServo() ->GetComputedKeyframeValuesFor(aKeyframes, aElement, aServoValues); } /* static */ nsTArray KeyframeUtils::GetComputedKeyframeValues(const nsTArray& aKeyframes, dom::Element* aElement, nsStyleContext* aStyleContext) { MOZ_ASSERT(aStyleContext); MOZ_ASSERT(aElement); const size_t len = aKeyframes.Length(); nsTArray result(len); for (const Keyframe& frame : aKeyframes) { nsCSSPropertyIDSet propertiesOnThisKeyframe; ComputedKeyframeValues* computedValues = result.AppendElement(); for (const PropertyValuePair& pair : PropertyPriorityIterator(frame.mPropertyValues)) { MOZ_ASSERT(!pair.mServoDeclarationBlock, "Animation values were parsed using Servo backend but target" " element is not using Servo backend?"); if (IsInvalidValuePair(pair, StyleBackendType::Gecko)) { continue; } // Expand each value into the set of longhands and produce // a KeyframeValueEntry for each value. nsTArray values; // 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 (pair.mValue.GetUnit() == eCSSUnit_Null) { // An uninitialized nsCSSValue represents the underlying value which // we represent as an uninitialized AnimationValue so we just leave // neutralPair->mValue as-is. PropertyStyleAnimationValuePair* neutralPair = values.AppendElement(); neutralPair->mProperty = pair.mProperty; } 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; } propertiesOnThisKeyframe.AddProperty(value.mProperty); computedValues->AppendElement(Move(value)); } } } MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch"); return result; } /* static */ nsTArray KeyframeUtils::GetAnimationPropertiesFromKeyframes( const nsTArray& aKeyframes, const nsTArray& aComputedValues, dom::CompositeOperation aEffectComposite) { MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(), "Array length mismatch"); nsTArray 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; entry->mComposite = frame.mComposite ? frame.mComposite.value() : aEffectComposite; } } nsTArray 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; } /* static */ already_AddRefed KeyframeUtils::ParseProperty(nsCSSPropertyID aProperty, const nsAString& aValue, nsIDocument* aDocument) { MOZ_ASSERT(aDocument); NS_ConvertUTF16toUTF8 value(aValue); // FIXME this is using the wrong base uri (bug 1343919) RefPtr data = new URLExtraData(aDocument->GetDocumentURI(), aDocument->GetDocumentURI(), aDocument->NodePrincipal()); return Servo_ParseProperty(aProperty, &value, data).Consume(); } // ------------------------------------------------------------------ // // Internal helpers // // ------------------------------------------------------------------ /** * Converts a JS object to an IDL sequence. * * @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& 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)) { aResult.Clear(); aRv.Throw(NS_ERROR_FAILURE); 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(); aResult.Clear(); return; } } /** * Converts a JS object wrapped by the given JS::ForIfIterator to an * IDL sequence and stores the resulting Keyframe objects in * aResult. */ static bool ConvertKeyframeSequence(JSContext* aCx, nsIDocument* aDocument, JS::ForOfIterator& aIterator, nsTArray& aResult) { JS::Rooted 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 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 argument")) { return false; } Keyframe* keyframe = aResult.AppendElement(fallible); if (!keyframe) { return false; } if (!keyframeDict.mOffset.IsNull()) { keyframe->mOffset.emplace(keyframeDict.mOffset.Value()); } if (keyframeDict.mComposite.WasPassed()) { keyframe->mComposite.emplace(keyframeDict.mComposite.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 propertyValuePairs; if (value.isObject()) { JS::Rooted 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 aObject, ListAllowance aAllowLists, nsTArray& aResult) { nsTArray 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 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), depending on aAllowLists, // and build up aResult. properties.Sort(AdditionalProperty::PropertyComparator()); for (AdditionalProperty& p : properties) { JS::Rooted 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) if aAllowLists is aAllow. * The resulting strings are appended to aValues. */ static bool AppendStringOrStringSequenceToArray(JSContext* aCx, JS::Handle aValue, ListAllowance aAllowLists, nsTArray& aValues) { if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) { // The value is an object, and we want to allow lists; convert // aValue to (DOMString or sequence). 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. JS::Rooted 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& aValues, JS::Handle 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) { RefPtr servoDeclarationBlock = KeyframeUtils::ParseProperty(aProperty, aStringValue, aDocument); 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); } 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& 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; } static void AppendInitialSegment(AnimationProperty* aAnimationProperty, const KeyframeValueEntry& aFirstEntry) { AnimationPropertySegment* segment = aAnimationProperty->mSegments.AppendElement(); segment->mFromKey = 0.0f; segment->mToKey = aFirstEntry.mOffset; segment->mToValue = aFirstEntry.mValue; segment->mToComposite = aFirstEntry.mComposite; } static void AppendFinalSegment(AnimationProperty* aAnimationProperty, const KeyframeValueEntry& aLastEntry) { AnimationPropertySegment* segment = aAnimationProperty->mSegments.AppendElement(); segment->mFromKey = aLastEntry.mOffset; segment->mFromValue = aLastEntry.mValue; segment->mFromComposite = aLastEntry.mComposite; segment->mToKey = 1.0f; segment->mTimingFunction = aLastEntry.mTimingFunction; } // Returns a newly created AnimationProperty if one was created to fill-in the // missing keyframe, nullptr otherwise (if we decided not to fill the keyframe // becase we don't support additive animation). static AnimationProperty* HandleMissingInitialKeyframe(nsTArray& aResult, const KeyframeValueEntry& aEntry) { MOZ_ASSERT(aEntry.mOffset != 0.0f, "The offset of the entry should not be 0.0"); // If the preference of the core Web Animations API is not enabled, don't fill // in the missing keyframe since the missing keyframe requires support for // additive animation which is guarded by this pref. if (!AnimationUtils::IsCoreAPIEnabled()){ return nullptr; } AnimationProperty* result = aResult.AppendElement(); result->mProperty = aEntry.mProperty; AppendInitialSegment(result, aEntry); return result; } static void HandleMissingFinalKeyframe(nsTArray& aResult, const KeyframeValueEntry& aEntry, AnimationProperty* aCurrentAnimationProperty) { MOZ_ASSERT(aEntry.mOffset != 1.0f, "The offset of the entry should not be 1.0"); // If the preference of the core Web Animations API is not enabled, don't fill // in the missing keyframe since the missing keyframe requires support for // additive animation which is guarded by this pref. if (!AnimationUtils::IsCoreAPIEnabled()){ // If we have already appended a new entry for the property so we have to // remove it. if (aCurrentAnimationProperty) { aResult.RemoveElementAt(aResult.Length() - 1); } return; } // If |aCurrentAnimationProperty| is nullptr, that means this is the first // entry for the property, we have to append a new AnimationProperty for this // property. if (!aCurrentAnimationProperty) { aCurrentAnimationProperty = aResult.AppendElement(); aCurrentAnimationProperty->mProperty = aEntry.mProperty; // If we have only one entry whose offset is neither 1 nor 0 for this // property, we need to append the initial segment as well. if (aEntry.mOffset != 0.0f) { AppendInitialSegment(aCurrentAnimationProperty, aEntry); } } AppendFinalSegment(aCurrentAnimationProperty, aEntry); } /** * Builds an array of AnimationProperty objects to represent the keyframe * animation segments in aEntries. */ static void BuildSegmentsFromValueEntries(nsTArray& aEntries, nsTArray& 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, if additive animation is disabled, 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) { // If we've reached the end of the array of entries, synthesize a final (and // initial) segment if necessary. if (i + 1 == n) { if (aEntries[i].mOffset != 1.0f) { HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty); } else if (aEntries[i].mOffset == 1.0f && !animationProperty) { // If the last entry with offset 1 and no animation property, that means // it is the only entry for this property so append a single segment // from 0 offset to |aEntry[i].offset|. Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]); } animationProperty = nullptr; break; } MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN && aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN, "Each entry should specify a valid property"); // No keyframe for this property at offset 0. if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) { // If we don't support additive animation we can't fill in the missing // keyframes and we should just skip this property altogether. 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. animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]); if (animationProperty) { lastProperty = aEntries[i].mProperty; } else { // Skip this entry if we did not handle the missing entry. ++i; continue; } } // Skip this entry if the next entry has the same offset except for initial // and final ones. We will handle missing keyframe in the next loop // if the property is changed on the next entry. if (aEntries[i].mProperty == aEntries[i + 1].mProperty && aEntries[i].mOffset == aEntries[i + 1].mOffset && aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) { ++i; continue; } // No keyframe for this property at offset 1. if (aEntries[i].mProperty != aEntries[i + 1].mProperty && aEntries[i].mOffset != 1.0f) { HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty); // Move on to new property. animationProperty = nullptr; ++i; continue; } // Starting from i + 1, determine the next [i, j] interval from which to // generate a segment. Basically, j is i + 1, but there are some special // cases for offset 0 and 1, so we need to handle them specifically. // Note: From this moment, we make sure [i + 1] is valid and // there must be an initial entry (i.e. mOffset = 0.0) and // a final entry (i.e. mOffset = 1.0). Besides, all the entries // with the same offsets except for initial/final ones are filtered // out already. size_t j = i + 1; 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); while (j + 1 < n && 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. 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; } } // 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; segment->mFromComposite = aEntries[i].mComposite; segment->mToComposite = aEntries[j].mComposite; 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 aValue, nsTArray& 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 composite; if (keyframeDict.mComposite.WasPassed()) { composite.emplace(keyframeDict.mComposite.Value()); } Maybe easing = TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv); if (aRv.Failed()) { return; } // Get all the property--value-list pairs off the object. JS::Rooted object(aCx, &aValue.toObject()); nsTArray 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 processedKeyframes; for (const PropertyValuesPair& pair : propertyValuesPairs) { size_t count = pair.mValues.Length(); if (count == 0) { // No animation values for this property. continue; } // If we only have one value, we should animate from the underlying value // using additive animation--however, we don't support additive animation // when the core animation API pref is switched off. if ((!AnimationUtils::IsCoreAPIEnabled()) && count == 1) { 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) { // For single-valued lists, the single value should be added to a // keyframe with offset 1. double offset = n ? i++ / double(n) : 1; Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset); if (keyframe->mPropertyValues.IsEmpty()) { keyframe->mTimingFunction = easing; keyframe->mComposite = composite; 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& 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& aSpacingRange, const Range& 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& aSpacingRange) { // We don't need to apply distribute spacing to keyframe A and keyframe B. DistributeRange(aSpacingRange, Range(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& aKeyframes, const Range& 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 pacedA = aKeyframes.begin(); const RangedPtr 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 GetCumulativeDistances(const nsTArray& 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 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()); } 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.mGecko, pacedValues[propIdx].mValue.mGecko, 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.mGecko, pacedValues[0].mValue.mGecko, aStyleContext, dist); } cumulativeDistances[i] = cumulativeDistances[preIdx] + dist; } prevPacedValues.SwapElements(pacedValues); preIdx = i; } return cumulativeDistances; } } // namespace mozilla