Backed out 8 changesets (bug 1873776) for causing vendor failures. CLOSED TREE
[gecko.git] / dom / animation / KeyframeUtils.cpp
blob329273fd2244fec89f6e111ef272ac4189a5014a
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/KeyframeUtils.h"
9 #include <algorithm> // For std::stable_sort, std::min
10 #include <utility>
12 #include "jsapi.h" // For most JSAPI
13 #include "js/ForOfIterator.h" // For JS::ForOfIterator
14 #include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetProperty, JS_GetPropertyById
15 #include "mozilla/AnimatedPropertyID.h"
16 #include "mozilla/ComputedStyle.h"
17 #include "mozilla/ErrorResult.h"
18 #include "mozilla/RangedArray.h"
19 #include "mozilla/ServoBindingTypes.h"
20 #include "mozilla/ServoBindings.h"
21 #include "mozilla/ServoCSSParser.h"
22 #include "mozilla/StaticPrefs_dom.h"
23 #include "mozilla/StyleAnimationValue.h"
24 #include "mozilla/TimingParams.h"
25 #include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
26 #include "mozilla/dom/BindingCallContext.h"
27 #include "mozilla/dom/Element.h"
28 #include "mozilla/dom/KeyframeEffect.h" // For PropertyValuesPair etc.
29 #include "mozilla/dom/KeyframeEffectBinding.h"
30 #include "mozilla/dom/Nullable.h"
31 #include "nsCSSPropertyIDSet.h"
32 #include "nsCSSProps.h"
33 #include "nsCSSPseudoElements.h" // For PseudoStyleType
34 #include "nsClassHashtable.h"
35 #include "nsContentUtils.h" // For GetContextForContent
36 #include "nsIScriptError.h"
37 #include "nsPresContextInlines.h"
38 #include "nsString.h"
39 #include "nsTArray.h"
41 using mozilla::dom::Nullable;
43 namespace mozilla {
45 // ------------------------------------------------------------------
47 // Internal data types
49 // ------------------------------------------------------------------
51 // For the aAllowList parameter of AppendStringOrStringSequence and
52 // GetPropertyValuesPairs.
53 enum class ListAllowance { eDisallow, eAllow };
55 /**
56 * A property-values pair obtained from the open-ended properties
57 * discovered on a regular keyframe or property-indexed keyframe object.
59 * Single values (as required by a regular keyframe, and as also supported
60 * on property-indexed keyframes) are stored as the only element in
61 * mValues.
63 struct PropertyValuesPair {
64 PropertyValuesPair() : mProperty(eCSSProperty_UNKNOWN) {}
66 AnimatedPropertyID mProperty;
67 nsTArray<nsCString> mValues;
70 /**
71 * An additional property (for a property-values pair) found on a
72 * BaseKeyframe or BasePropertyIndexedKeyframe object.
74 struct AdditionalProperty {
75 AnimatedPropertyID mProperty;
76 size_t mJsidIndex = 0; // Index into |ids| in GetPropertyValuesPairs.
78 struct PropertyComparator {
79 bool Equals(const AdditionalProperty& aLhs,
80 const AdditionalProperty& aRhs) const {
81 return aLhs.mProperty == aRhs.mProperty;
83 bool LessThan(const AdditionalProperty& aLhs,
84 const AdditionalProperty& aRhs) const {
85 bool customLhs =
86 aLhs.mProperty.mID == nsCSSPropertyID::eCSSPropertyExtra_variable;
87 bool customRhs =
88 aRhs.mProperty.mID == nsCSSPropertyID::eCSSPropertyExtra_variable;
89 if (!customLhs && !customRhs) {
90 // Compare by IDL names.
91 return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty.mID) <
92 nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty.mID);
94 if (customLhs && customRhs) {
95 // Compare by custom property names.
96 return nsDependentAtomString(aLhs.mProperty.mCustomName) <
97 nsDependentAtomString(aRhs.mProperty.mCustomName);
99 // Custom properties should be ordered before normal CSS properties, as if
100 // the custom property name starts with `--`.
101 // <https://drafts.csswg.org/web-animations-1/#idl-attribute-name-to-animation-property-name>
102 return !customLhs && customRhs;
108 * Data for a segment in a keyframe animation of a given property
109 * whose value is a StyleAnimationValue.
111 * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
112 * to gather data for each individual segment.
114 struct KeyframeValueEntry {
115 KeyframeValueEntry()
116 : mProperty(eCSSProperty_UNKNOWN), mOffset(), mComposite() {}
118 AnimatedPropertyID mProperty;
119 AnimationValue mValue;
121 float mOffset;
122 Maybe<StyleComputedTimingFunction> mTimingFunction;
123 dom::CompositeOperation mComposite;
125 struct PropertyOffsetComparator {
126 static bool Equals(const KeyframeValueEntry& aLhs,
127 const KeyframeValueEntry& aRhs) {
128 return aLhs.mProperty == aRhs.mProperty && aLhs.mOffset == aRhs.mOffset;
130 static bool LessThan(const KeyframeValueEntry& aLhs,
131 const KeyframeValueEntry& aRhs) {
132 // First, sort by property name.
133 bool customLhs =
134 aLhs.mProperty.mID == nsCSSPropertyID::eCSSPropertyExtra_variable;
135 bool customRhs =
136 aRhs.mProperty.mID == nsCSSPropertyID::eCSSPropertyExtra_variable;
137 if (!customLhs && !customRhs) {
138 // Compare by IDL names.
139 int32_t order =
140 nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty.mID) -
141 nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty.mID);
142 if (order != 0) {
143 return order < 0;
145 } else if (customLhs && customRhs) {
146 // Compare by custom property names.
147 int order = Compare(nsDependentAtomString(aLhs.mProperty.mCustomName),
148 nsDependentAtomString(aRhs.mProperty.mCustomName));
149 if (order != 0) {
150 return order < 0;
152 } else {
153 return !customLhs && customRhs;
156 // Then, by offset.
157 return aLhs.mOffset < aRhs.mOffset;
162 class ComputedOffsetComparator {
163 public:
164 static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) {
165 return aLhs.mComputedOffset == aRhs.mComputedOffset;
168 static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
169 return aLhs.mComputedOffset < aRhs.mComputedOffset;
173 // ------------------------------------------------------------------
175 // Internal helper method declarations
177 // ------------------------------------------------------------------
179 static void GetKeyframeListFromKeyframeSequence(
180 JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
181 nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv);
183 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
184 JS::ForOfIterator& aIterator,
185 const char* aContext,
186 nsTArray<Keyframe>& aResult);
188 static bool GetPropertyValuesPairs(JSContext* aCx,
189 JS::Handle<JSObject*> aObject,
190 ListAllowance aAllowLists,
191 nsTArray<PropertyValuesPair>& aResult);
193 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
194 JS::Handle<JS::Value> aValue,
195 ListAllowance aAllowLists,
196 nsTArray<nsCString>& aValues);
198 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
199 JS::Handle<JS::Value> aValue);
201 static Maybe<PropertyValuePair> MakePropertyValuePair(
202 const AnimatedPropertyID& aProperty, const nsACString& aStringValue,
203 dom::Document* aDocument);
205 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
207 #ifdef DEBUG
208 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
210 #endif
212 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
213 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
214 PseudoStyleType aPseudoType, const ComputedStyle* aComputedValues);
216 static void BuildSegmentsFromValueEntries(
217 nsTArray<KeyframeValueEntry>& aEntries,
218 nsTArray<AnimationProperty>& aResult);
220 static void GetKeyframeListFromPropertyIndexedKeyframe(
221 JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
222 nsTArray<Keyframe>& aResult, ErrorResult& aRv);
224 static void DistributeRange(const Range<Keyframe>& aRange);
226 // ------------------------------------------------------------------
228 // Public API
230 // ------------------------------------------------------------------
232 /* static */
233 nsTArray<Keyframe> KeyframeUtils::GetKeyframesFromObject(
234 JSContext* aCx, dom::Document* aDocument, JS::Handle<JSObject*> aFrames,
235 const char* aContext, ErrorResult& aRv) {
236 MOZ_ASSERT(!aRv.Failed());
238 nsTArray<Keyframe> keyframes;
240 if (!aFrames) {
241 // The argument was explicitly null meaning no keyframes.
242 return keyframes;
245 // At this point we know we have an object. We try to convert it to a
246 // sequence of keyframes first, and if that fails due to not being iterable,
247 // we try to convert it to a property-indexed keyframe.
248 JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
249 JS::ForOfIterator iter(aCx);
250 if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
251 aRv.Throw(NS_ERROR_FAILURE);
252 return keyframes;
255 if (iter.valueIsIterable()) {
256 GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes,
257 aContext, aRv);
258 } else {
259 GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
260 keyframes, aRv);
263 if (aRv.Failed()) {
264 MOZ_ASSERT(keyframes.IsEmpty(),
265 "Should not set any keyframes when there is an error");
266 return keyframes;
269 return keyframes;
272 /* static */
273 void KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes) {
274 if (aKeyframes.IsEmpty()) {
275 return;
278 // If the first keyframe has an unspecified offset, fill it in with 0%.
279 // If there is only a single keyframe, then it gets 100%.
280 if (aKeyframes.Length() > 1) {
281 Keyframe& firstElement = aKeyframes[0];
282 firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
283 // We will fill in the last keyframe's offset below
284 } else {
285 Keyframe& lastElement = aKeyframes.LastElement();
286 lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
289 // Fill in remaining missing offsets.
290 const Keyframe* const last = &aKeyframes.LastElement();
291 const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
292 RangedPtr<Keyframe> keyframeA = begin;
293 while (keyframeA != last) {
294 // Find keyframe A and keyframe B *between* which we will apply spacing.
295 RangedPtr<Keyframe> keyframeB = keyframeA + 1;
296 while (keyframeB->mOffset.isNothing() && keyframeB != last) {
297 ++keyframeB;
299 keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
301 // Fill computed offsets in (keyframe A, keyframe B).
302 DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
303 keyframeA = keyframeB;
307 /* static */
308 nsTArray<AnimationProperty> KeyframeUtils::GetAnimationPropertiesFromKeyframes(
309 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
310 PseudoStyleType aPseudoType, const ComputedStyle* aStyle,
311 dom::CompositeOperation aEffectComposite) {
312 nsTArray<AnimationProperty> result;
314 const nsTArray<ComputedKeyframeValues> computedValues =
315 GetComputedKeyframeValues(aKeyframes, aElement, aPseudoType, aStyle);
316 if (computedValues.IsEmpty()) {
317 // In rare cases GetComputedKeyframeValues might fail and return an empty
318 // array, in which case we likewise return an empty array from here.
319 return result;
322 MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
323 "Array length mismatch");
325 nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
327 const size_t len = aKeyframes.Length();
328 for (size_t i = 0; i < len; ++i) {
329 const Keyframe& frame = aKeyframes[i];
330 for (auto& value : computedValues[i]) {
331 MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
332 "Invalid computed offset");
333 KeyframeValueEntry* entry = entries.AppendElement();
334 entry->mOffset = frame.mComputedOffset;
335 entry->mProperty = value.mProperty;
336 entry->mValue = value.mValue;
337 entry->mTimingFunction = frame.mTimingFunction;
338 // The following assumes that CompositeOperation is a strict subset of
339 // CompositeOperationOrAuto.
340 entry->mComposite =
341 frame.mComposite == dom::CompositeOperationOrAuto::Auto
342 ? aEffectComposite
343 : static_cast<dom::CompositeOperation>(frame.mComposite);
347 BuildSegmentsFromValueEntries(entries, result);
348 return result;
351 /* static */
352 bool KeyframeUtils::IsAnimatableProperty(const AnimatedPropertyID& aProperty) {
353 // Regardless of the backend type, treat the 'display' property as not
354 // animatable. (Servo will report it as being animatable, since it is
355 // in fact animatable by SMIL.)
356 if (aProperty.mID == eCSSProperty_display) {
357 return false;
359 return Servo_Property_IsAnimatable(&aProperty);
362 // ------------------------------------------------------------------
364 // Internal helpers
366 // ------------------------------------------------------------------
369 * Converts a JS object to an IDL sequence<Keyframe>.
371 * @param aCx The JSContext corresponding to |aIterator|.
372 * @param aDocument The document to use when parsing CSS properties.
373 * @param aIterator An already-initialized ForOfIterator for the JS
374 * object to iterate over as a sequence.
375 * @param aResult The array into which the resulting Keyframe objects will be
376 * appended.
377 * @param aContext The context string to prepend to thrown exceptions.
378 * @param aRv Out param to store any errors thrown by this function.
380 static void GetKeyframeListFromKeyframeSequence(
381 JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
382 nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv) {
383 MOZ_ASSERT(!aRv.Failed());
384 MOZ_ASSERT(aResult.IsEmpty());
386 // Convert the object in aIterator to a sequence of keyframes producing
387 // an array of Keyframe objects.
388 if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aContext, aResult)) {
389 aResult.Clear();
390 aRv.NoteJSContextException(aCx);
391 return;
394 // If the sequence<> had zero elements, we won't generate any
395 // keyframes.
396 if (aResult.IsEmpty()) {
397 return;
400 // Check that the keyframes are loosely sorted and with values all
401 // between 0% and 100%.
402 if (!HasValidOffsets(aResult)) {
403 aResult.Clear();
404 aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
405 return;
410 * Converts a JS object wrapped by the given JS::ForIfIterator to an
411 * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
412 * aResult.
414 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
415 JS::ForOfIterator& aIterator,
416 const char* aContext,
417 nsTArray<Keyframe>& aResult) {
418 JS::Rooted<JS::Value> value(aCx);
419 // Parsing errors should only be reported after we have finished iterating
420 // through all values. If we have any early returns while iterating, we should
421 // ignore parsing errors.
422 IgnoredErrorResult parseEasingResult;
424 for (;;) {
425 bool done;
426 if (!aIterator.next(&value, &done)) {
427 return false;
429 if (done) {
430 break;
432 // Each value found when iterating the object must be an object
433 // or null/undefined (which gets treated as a default {} dictionary
434 // value).
435 if (!value.isObject() && !value.isNullOrUndefined()) {
436 dom::ThrowErrorMessage<dom::MSG_NOT_OBJECT>(
437 aCx, aContext, "Element of sequence<Keyframe> argument");
438 return false;
441 // Convert the JS value into a BaseKeyframe dictionary value.
442 dom::binding_detail::FastBaseKeyframe keyframeDict;
443 dom::BindingCallContext callCx(aCx, aContext);
444 if (!keyframeDict.Init(callCx, value,
445 "Element of sequence<Keyframe> argument")) {
446 // This may happen if the value type of the member of BaseKeyframe is
447 // invalid. e.g. `offset` only accept a double value, so if we provide a
448 // string, we enter this branch.
449 // Besides, keyframeDict.Init() should throw a Type Error message already,
450 // so we don't have to do it again.
451 return false;
454 Keyframe* keyframe = aResult.AppendElement(fallible);
455 if (!keyframe) {
456 return false;
459 if (!keyframeDict.mOffset.IsNull()) {
460 keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
463 if (StaticPrefs::dom_animations_api_compositing_enabled()) {
464 keyframe->mComposite = keyframeDict.mComposite;
467 // Look for additional property-values pairs on the object.
468 nsTArray<PropertyValuesPair> propertyValuePairs;
469 if (value.isObject()) {
470 JS::Rooted<JSObject*> object(aCx, &value.toObject());
471 if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eDisallow,
472 propertyValuePairs)) {
473 return false;
477 if (!parseEasingResult.Failed()) {
478 keyframe->mTimingFunction =
479 TimingParams::ParseEasing(keyframeDict.mEasing, parseEasingResult);
480 // Even if the above fails, we still need to continue reading off all the
481 // properties since checking the validity of easing should be treated as
482 // a separate step that happens *after* all the other processing in this
483 // loop since (since it is never likely to be handled by WebIDL unlike the
484 // rest of this loop).
487 for (PropertyValuesPair& pair : propertyValuePairs) {
488 MOZ_ASSERT(pair.mValues.Length() == 1);
490 Maybe<PropertyValuePair> valuePair =
491 MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument);
492 if (!valuePair) {
493 continue;
495 keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
497 #ifdef DEBUG
498 // When we go to convert keyframes into arrays of property values we
499 // call StyleAnimation::ComputeValues. This should normally return true
500 // but in order to test the case where it does not, BaseKeyframeDict
501 // includes a chrome-only member that can be set to indicate that
502 // ComputeValues should fail for shorthand property values on that
503 // keyframe.
504 if (nsCSSProps::IsShorthand(pair.mProperty.mID) &&
505 keyframeDict.mSimulateComputeValuesFailure) {
506 MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
508 #endif
512 // Throw any errors we encountered while parsing 'easing' properties.
513 if (parseEasingResult.MaybeSetPendingException(aCx)) {
514 return false;
517 return true;
521 * Reads the property-values pairs from the specified JS object.
523 * @param aObject The JS object to look at.
524 * @param aAllowLists If eAllow, values will be converted to
525 * (DOMString or sequence<DOMString); if eDisallow, values
526 * will be converted to DOMString.
527 * @param aResult The array into which the enumerated property-values
528 * pairs will be stored.
529 * @return false on failure or JS exception thrown while interacting
530 * with aObject; true otherwise.
532 static bool GetPropertyValuesPairs(JSContext* aCx,
533 JS::Handle<JSObject*> aObject,
534 ListAllowance aAllowLists,
535 nsTArray<PropertyValuesPair>& aResult) {
536 nsTArray<AdditionalProperty> properties;
538 // Iterate over all the properties on aObject and append an
539 // entry to properties for them.
541 // We don't compare the jsids that we encounter with those for
542 // the explicit dictionary members, since we know that none
543 // of the CSS property IDL names clash with them.
544 JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
545 if (!JS_Enumerate(aCx, aObject, &ids)) {
546 return false;
548 for (size_t i = 0, n = ids.length(); i < n; i++) {
549 nsAutoJSCString propName;
550 if (!propName.init(aCx, ids[i])) {
551 return false;
554 // Basically, we have to handle "cssOffset" and "cssFloat" specially here:
555 // "cssOffset" => eCSSProperty_offset
556 // "cssFloat" => eCSSProperty_float
557 // This means if the attribute is the string "cssOffset"/"cssFloat", we use
558 // CSS "offset"/"float" property.
559 // https://drafts.csswg.org/web-animations/#property-name-conversion
560 nsCSSPropertyID propertyID = nsCSSPropertyID::eCSSProperty_UNKNOWN;
561 if (nsCSSProps::IsCustomPropertyName(propName)) {
562 propertyID = eCSSPropertyExtra_variable;
563 } else if (propName.EqualsLiteral("cssOffset")) {
564 propertyID = nsCSSPropertyID::eCSSProperty_offset;
565 } else if (propName.EqualsLiteral("cssFloat")) {
566 propertyID = nsCSSPropertyID::eCSSProperty_float;
567 } else if (!propName.EqualsLiteral("offset") &&
568 !propName.EqualsLiteral("float")) {
569 propertyID = nsCSSProps::LookupPropertyByIDLName(
570 propName, CSSEnabledState::ForAllContent);
573 // TODO(zrhoffman, bug 1811897) Add test coverage for removing the `--`
574 // prefix here.
575 AnimatedPropertyID property =
576 propertyID == eCSSPropertyExtra_variable
577 ? AnimatedPropertyID(
578 NS_Atomize(Substring(propName, 2, propName.Length() - 2)))
579 : AnimatedPropertyID(propertyID);
581 if (KeyframeUtils::IsAnimatableProperty(property)) {
582 properties.AppendElement(AdditionalProperty{std::move(property), i});
586 // Sort the entries by IDL name and then get each value and
587 // convert it either to a DOMString or to a
588 // (DOMString or sequence<DOMString>), depending on aAllowLists,
589 // and build up aResult.
590 properties.Sort(AdditionalProperty::PropertyComparator());
592 for (AdditionalProperty& p : properties) {
593 JS::Rooted<JS::Value> value(aCx);
594 if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
595 return false;
597 PropertyValuesPair* pair = aResult.AppendElement();
598 pair->mProperty = p.mProperty;
599 if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
600 pair->mValues)) {
601 return false;
605 return true;
609 * Converts aValue to DOMString, if aAllowLists is eDisallow, or
610 * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
611 * The resulting strings are appended to aValues.
613 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
614 JS::Handle<JS::Value> aValue,
615 ListAllowance aAllowLists,
616 nsTArray<nsCString>& aValues) {
617 if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
618 // The value is an object, and we want to allow lists; convert
619 // aValue to (DOMString or sequence<DOMString>).
620 JS::ForOfIterator iter(aCx);
621 if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
622 return false;
624 if (iter.valueIsIterable()) {
625 // If the object is iterable, convert it to sequence<DOMString>.
626 JS::Rooted<JS::Value> element(aCx);
627 for (;;) {
628 bool done;
629 if (!iter.next(&element, &done)) {
630 return false;
632 if (done) {
633 break;
635 if (!AppendValueAsString(aCx, aValues, element)) {
636 return false;
639 return true;
643 // Either the object is not iterable, or aAllowLists doesn't want
644 // a list; convert it to DOMString.
645 if (!AppendValueAsString(aCx, aValues, aValue)) {
646 return false;
649 return true;
653 * Converts aValue to DOMString and appends it to aValues.
655 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
656 JS::Handle<JS::Value> aValue) {
657 return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
658 *aValues.AppendElement());
661 static void ReportInvalidPropertyValueToConsole(
662 const AnimatedPropertyID& aProperty,
663 const nsACString& aInvalidPropertyValue, dom::Document* aDoc) {
664 AutoTArray<nsString, 2> params;
665 params.AppendElement(NS_ConvertUTF8toUTF16(aInvalidPropertyValue));
666 aProperty.ToString(*params.AppendElement());
667 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Animation"_ns,
668 aDoc, nsContentUtils::eDOM_PROPERTIES,
669 "InvalidKeyframePropertyValue", params);
673 * Construct a PropertyValuePair parsing the given string into a suitable
674 * nsCSSValue object.
676 * @param aProperty The CSS property.
677 * @param aStringValue The property value to parse.
678 * @param aDocument The document to use when parsing.
679 * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
680 * an invalid property value.
682 static Maybe<PropertyValuePair> MakePropertyValuePair(
683 const AnimatedPropertyID& aProperty, const nsACString& aStringValue,
684 dom::Document* aDocument) {
685 MOZ_ASSERT(aDocument);
686 Maybe<PropertyValuePair> result;
688 ServoCSSParser::ParsingEnvironment env =
689 ServoCSSParser::GetParsingEnvironment(aDocument);
690 RefPtr<StyleLockedDeclarationBlock> servoDeclarationBlock =
691 ServoCSSParser::ParseProperty(aProperty, aStringValue, env,
692 StyleParsingMode::DEFAULT);
694 if (servoDeclarationBlock) {
695 result.emplace(aProperty, std::move(servoDeclarationBlock));
696 } else {
697 ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
699 return result;
703 * Checks that the given keyframes are loosely ordered (each keyframe's
704 * offset that is not null is greater than or equal to the previous
705 * non-null offset) and that all values are within the range [0.0, 1.0].
707 * @return true if the keyframes' offsets are correctly ordered and
708 * within range; false otherwise.
710 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes) {
711 double offset = 0.0;
712 for (const Keyframe& keyframe : aKeyframes) {
713 if (keyframe.mOffset) {
714 double thisOffset = keyframe.mOffset.value();
715 if (thisOffset < offset || thisOffset > 1.0f) {
716 return false;
718 offset = thisOffset;
721 return true;
724 #ifdef DEBUG
726 * Takes a property-value pair for a shorthand property and modifies the
727 * value to indicate that when we call StyleAnimationValue::ComputeValues on
728 * that value we should behave as if that function had failed.
730 * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
731 * a shorthand property.
733 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair) {
734 MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty.mID),
735 "Only shorthand property values can be marked as failure values");
737 aPair.mSimulateComputeValuesFailure = true;
740 #endif
743 * The variation of the above function. This is for Servo backend.
745 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
746 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
747 PseudoStyleType aPseudoType, const ComputedStyle* aComputedStyle) {
748 MOZ_ASSERT(aElement);
750 nsTArray<ComputedKeyframeValues> result;
752 nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
753 if (!presContext) {
754 // This has been reported to happen with some combinations of content
755 // (particularly involving resize events and layout flushes? See bug 1407898
756 // and bug 1408420) but no reproducible steps have been found.
757 // For now we just return an empty array.
758 return result;
761 result = presContext->StyleSet()->GetComputedKeyframeValuesFor(
762 aKeyframes, aElement, aPseudoType, aComputedStyle);
763 return result;
766 static void AppendInitialSegment(AnimationProperty* aAnimationProperty,
767 const KeyframeValueEntry& aFirstEntry) {
768 AnimationPropertySegment* segment =
769 aAnimationProperty->mSegments.AppendElement();
770 segment->mFromKey = 0.0f;
771 segment->mToKey = aFirstEntry.mOffset;
772 segment->mToValue = aFirstEntry.mValue;
773 segment->mToComposite = aFirstEntry.mComposite;
776 static void AppendFinalSegment(AnimationProperty* aAnimationProperty,
777 const KeyframeValueEntry& aLastEntry) {
778 AnimationPropertySegment* segment =
779 aAnimationProperty->mSegments.AppendElement();
780 segment->mFromKey = aLastEntry.mOffset;
781 segment->mFromValue = aLastEntry.mValue;
782 segment->mFromComposite = aLastEntry.mComposite;
783 segment->mToKey = 1.0f;
784 segment->mTimingFunction = aLastEntry.mTimingFunction;
787 // Returns a newly created AnimationProperty if one was created to fill-in the
788 // missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
789 // becase we don't support implicit keyframes).
790 static AnimationProperty* HandleMissingInitialKeyframe(
791 nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry) {
792 MOZ_ASSERT(aEntry.mOffset != 0.0f,
793 "The offset of the entry should not be 0.0");
795 AnimationProperty* result = aResult.AppendElement();
796 result->mProperty = aEntry.mProperty;
798 AppendInitialSegment(result, aEntry);
800 return result;
803 static void HandleMissingFinalKeyframe(
804 nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry,
805 AnimationProperty* aCurrentAnimationProperty) {
806 MOZ_ASSERT(aEntry.mOffset != 1.0f,
807 "The offset of the entry should not be 1.0");
809 // If |aCurrentAnimationProperty| is nullptr, that means this is the first
810 // entry for the property, we have to append a new AnimationProperty for this
811 // property.
812 if (!aCurrentAnimationProperty) {
813 aCurrentAnimationProperty = aResult.AppendElement();
814 aCurrentAnimationProperty->mProperty = aEntry.mProperty;
816 // If we have only one entry whose offset is neither 1 nor 0 for this
817 // property, we need to append the initial segment as well.
818 if (aEntry.mOffset != 0.0f) {
819 AppendInitialSegment(aCurrentAnimationProperty, aEntry);
822 AppendFinalSegment(aCurrentAnimationProperty, aEntry);
826 * Builds an array of AnimationProperty objects to represent the keyframe
827 * animation segments in aEntries.
829 static void BuildSegmentsFromValueEntries(
830 nsTArray<KeyframeValueEntry>& aEntries,
831 nsTArray<AnimationProperty>& aResult) {
832 if (aEntries.IsEmpty()) {
833 return;
836 // Sort the KeyframeValueEntry objects so that all entries for a given
837 // property are together, and the entries are sorted by offset otherwise.
838 std::stable_sort(aEntries.begin(), aEntries.end(),
839 &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
841 // For a given index i, we want to generate a segment from aEntries[i]
842 // to aEntries[j], if:
844 // * j > i,
845 // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
846 // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
848 // That will eliminate runs of same offset/property values where there's no
849 // point generating zero length segments in the middle of the animation.
851 // Additionally we need to generate a zero length segment at offset 0 and at
852 // offset 1, if we have multiple values for a given property at that offset,
853 // since we need to retain the very first and very last value so they can
854 // be used for reverse and forward filling.
856 // Typically, for each property in |aEntries|, we expect there to be at least
857 // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
858 // However, since it is possible that when building |aEntries|, the call to
859 // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
860 // Furthermore, if additive animation is disabled, the following loop takes
861 // care to identify properties that lack a value at offset 0.0/1.0 and drops
862 // those properties from |aResult|.
864 AnimatedPropertyID lastProperty(eCSSProperty_UNKNOWN);
865 AnimationProperty* animationProperty = nullptr;
867 size_t i = 0, n = aEntries.Length();
869 while (i < n) {
870 // If we've reached the end of the array of entries, synthesize a final (and
871 // initial) segment if necessary.
872 if (i + 1 == n) {
873 if (aEntries[i].mOffset != 1.0f) {
874 HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
875 } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
876 // If the last entry with offset 1 and no animation property, that means
877 // it is the only entry for this property so append a single segment
878 // from 0 offset to |aEntry[i].offset|.
879 Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
881 animationProperty = nullptr;
882 break;
885 MOZ_ASSERT(
886 aEntries[i].mProperty.IsValid() && aEntries[i + 1].mProperty.IsValid(),
887 "Each entry should specify a valid property");
889 // No keyframe for this property at offset 0.
890 if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) {
891 // If we don't support additive animation we can't fill in the missing
892 // keyframes and we should just skip this property altogether. Since the
893 // entries are sorted by offset for a given property, and since we don't
894 // update |lastProperty|, we will keep hitting this condition until we
895 // change property.
896 animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
897 if (animationProperty) {
898 lastProperty = aEntries[i].mProperty;
899 } else {
900 // Skip this entry if we did not handle the missing entry.
901 ++i;
902 continue;
906 // Skip this entry if the next entry has the same offset except for initial
907 // and final ones. We will handle missing keyframe in the next loop
908 // if the property is changed on the next entry.
909 if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
910 aEntries[i].mOffset == aEntries[i + 1].mOffset &&
911 aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
912 ++i;
913 continue;
916 // No keyframe for this property at offset 1.
917 if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
918 aEntries[i].mOffset != 1.0f) {
919 HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
920 // Move on to new property.
921 animationProperty = nullptr;
922 ++i;
923 continue;
926 // Starting from i + 1, determine the next [i, j] interval from which to
927 // generate a segment. Basically, j is i + 1, but there are some special
928 // cases for offset 0 and 1, so we need to handle them specifically.
929 // Note: From this moment, we make sure [i + 1] is valid and
930 // there must be an initial entry (i.e. mOffset = 0.0) and
931 // a final entry (i.e. mOffset = 1.0). Besides, all the entries
932 // with the same offsets except for initial/final ones are filtered
933 // out already.
934 size_t j = i + 1;
935 if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
936 // We need to generate an initial zero-length segment.
937 MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
938 while (j + 1 < n && aEntries[j + 1].mOffset == 0.0f &&
939 aEntries[j + 1].mProperty == aEntries[j].mProperty) {
940 ++j;
942 } else if (aEntries[i].mOffset == 1.0f) {
943 if (aEntries[i + 1].mOffset == 1.0f &&
944 aEntries[i + 1].mProperty == aEntries[i].mProperty) {
945 // We need to generate a final zero-length segment.
946 while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f &&
947 aEntries[j + 1].mProperty == aEntries[j].mProperty) {
948 ++j;
950 } else {
951 // New property.
952 MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
953 animationProperty = nullptr;
954 ++i;
955 continue;
959 // If we've moved on to a new property, create a new AnimationProperty
960 // to insert segments into.
961 if (aEntries[i].mProperty != lastProperty) {
962 MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
963 MOZ_ASSERT(!animationProperty);
964 animationProperty = aResult.AppendElement();
965 animationProperty->mProperty = aEntries[i].mProperty;
966 lastProperty = aEntries[i].mProperty;
969 MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
971 // Now generate the segment.
972 AnimationPropertySegment* segment =
973 animationProperty->mSegments.AppendElement();
974 segment->mFromKey = aEntries[i].mOffset;
975 segment->mToKey = aEntries[j].mOffset;
976 segment->mFromValue = aEntries[i].mValue;
977 segment->mToValue = aEntries[j].mValue;
978 segment->mTimingFunction = aEntries[i].mTimingFunction;
979 segment->mFromComposite = aEntries[i].mComposite;
980 segment->mToComposite = aEntries[j].mComposite;
982 i = j;
987 * Converts a JS object representing a property-indexed keyframe into
988 * an array of Keyframe objects.
990 * @param aCx The JSContext for |aValue|.
991 * @param aDocument The document to use when parsing CSS properties.
992 * @param aValue The JS object.
993 * @param aResult The array into which the resulting AnimationProperty
994 * objects will be appended.
995 * @param aRv Out param to store any errors thrown by this function.
997 static void GetKeyframeListFromPropertyIndexedKeyframe(
998 JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
999 nsTArray<Keyframe>& aResult, ErrorResult& aRv) {
1000 MOZ_ASSERT(aValue.isObject());
1001 MOZ_ASSERT(aResult.IsEmpty());
1002 MOZ_ASSERT(!aRv.Failed());
1004 // Convert the object to a property-indexed keyframe dictionary to
1005 // get its explicit dictionary members.
1006 dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
1007 // XXXbz Pass in the method name from callers and set up a BindingCallContext?
1008 if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument")) {
1009 aRv.Throw(NS_ERROR_FAILURE);
1010 return;
1013 // Get all the property--value-list pairs off the object.
1014 JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
1015 nsTArray<PropertyValuesPair> propertyValuesPairs;
1016 if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
1017 propertyValuesPairs)) {
1018 aRv.Throw(NS_ERROR_FAILURE);
1019 return;
1022 // Create a set of keyframes for each property.
1023 nsTHashMap<nsFloatHashKey, Keyframe> processedKeyframes;
1024 for (const PropertyValuesPair& pair : propertyValuesPairs) {
1025 size_t count = pair.mValues.Length();
1026 if (count == 0) {
1027 // No animation values for this property.
1028 continue;
1031 size_t n = pair.mValues.Length() - 1;
1032 size_t i = 0;
1034 for (const nsCString& stringValue : pair.mValues) {
1035 // For single-valued lists, the single value should be added to a
1036 // keyframe with offset 1.
1037 double offset = n ? i++ / double(n) : 1;
1038 Keyframe& keyframe = processedKeyframes.LookupOrInsert(offset);
1039 if (keyframe.mPropertyValues.IsEmpty()) {
1040 keyframe.mComputedOffset = offset;
1043 Maybe<PropertyValuePair> valuePair =
1044 MakePropertyValuePair(pair.mProperty, stringValue, aDocument);
1045 if (!valuePair) {
1046 continue;
1048 keyframe.mPropertyValues.AppendElement(std::move(valuePair.ref()));
1052 aResult.SetCapacity(processedKeyframes.Count());
1053 std::transform(processedKeyframes.begin(), processedKeyframes.end(),
1054 MakeBackInserter(aResult), [](auto& entry) {
1055 return std::move(*entry.GetModifiableData());
1058 aResult.Sort(ComputedOffsetComparator());
1060 // Fill in any specified offsets
1062 // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of
1063 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1064 const FallibleTArray<Nullable<double>>* offsets = nullptr;
1065 AutoTArray<Nullable<double>, 1> singleOffset;
1066 auto& offset = keyframeDict.mOffset;
1067 if (offset.IsDouble()) {
1068 singleOffset.AppendElement(offset.GetAsDouble());
1069 // dom::Sequence is a fallible but AutoTArray is infallible and we need to
1070 // point to one or the other. Fortunately, fallible and infallible array
1071 // types can be implicitly converted provided they are const.
1072 const FallibleTArray<Nullable<double>>& asFallibleArray = singleOffset;
1073 offsets = &asFallibleArray;
1074 } else if (offset.IsDoubleOrNullSequence()) {
1075 offsets = &offset.GetAsDoubleOrNullSequence();
1077 // If offset.IsNull() is true, then we want to leave the mOffset member of
1078 // each keyframe with its initialized value of null. By leaving |offsets|
1079 // as nullptr here, we skip updating mOffset below.
1081 size_t offsetsToFill =
1082 offsets ? std::min(offsets->Length(), aResult.Length()) : 0;
1083 for (size_t i = 0; i < offsetsToFill; i++) {
1084 if (!offsets->ElementAt(i).IsNull()) {
1085 aResult[i].mOffset.emplace(offsets->ElementAt(i).Value());
1089 // Check that the keyframes are loosely sorted and that any specified offsets
1090 // are between 0.0 and 1.0 inclusive.
1092 // This corresponds to steps 6-7 of
1093 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1095 // In the spec, TypeErrors arising from invalid offsets and easings are thrown
1096 // at the end of the procedure since it assumes we initially store easing
1097 // values as strings and then later parse them.
1099 // However, we will parse easing members immediately when we process them
1100 // below. In order to maintain the relative order in which TypeErrors are
1101 // thrown according to the spec, namely exceptions arising from invalid
1102 // offsets are thrown before exceptions arising from invalid easings, we check
1103 // the offsets here.
1104 if (!HasValidOffsets(aResult)) {
1105 aResult.Clear();
1106 aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
1107 return;
1110 // Fill in any easings.
1112 // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of
1113 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1114 FallibleTArray<Maybe<StyleComputedTimingFunction>> easings;
1115 auto parseAndAppendEasing = [&](const nsACString& easingString,
1116 ErrorResult& aRv) {
1117 auto easing = TimingParams::ParseEasing(easingString, aRv);
1118 if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) {
1119 aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
1123 auto& easing = keyframeDict.mEasing;
1124 if (easing.IsUTF8String()) {
1125 parseAndAppendEasing(easing.GetAsUTF8String(), aRv);
1126 if (aRv.Failed()) {
1127 aResult.Clear();
1128 return;
1130 } else {
1131 for (const auto& easingString : easing.GetAsUTF8StringSequence()) {
1132 parseAndAppendEasing(easingString, aRv);
1133 if (aRv.Failed()) {
1134 aResult.Clear();
1135 return;
1140 // If |easings| is empty, then we are supposed to fill it in with the value
1141 // "linear" and then repeat the list as necessary.
1143 // However, for Keyframe.mTimingFunction we represent "linear" as a None
1144 // value. Since we have not assigned 'mTimingFunction' for any of the
1145 // keyframes in |aResult| they will already have their initial None value
1146 // (i.e. linear). As a result, if |easings| is empty, we don't need to do
1147 // anything.
1148 if (!easings.IsEmpty()) {
1149 for (size_t i = 0; i < aResult.Length(); i++) {
1150 aResult[i].mTimingFunction = easings[i % easings.Length()];
1154 // Fill in any composite operations.
1156 // This corresponds to step 5, "Otherwise," branch, substep 12 of
1157 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1158 if (StaticPrefs::dom_animations_api_compositing_enabled()) {
1159 const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
1160 AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
1161 auto& composite = keyframeDict.mComposite;
1162 if (composite.IsCompositeOperationOrAuto()) {
1163 singleCompositeOp.AppendElement(
1164 composite.GetAsCompositeOperationOrAuto());
1165 const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
1166 singleCompositeOp;
1167 compositeOps = &asFallibleArray;
1168 } else if (composite.IsCompositeOperationOrAutoSequence()) {
1169 compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
1172 // Fill in and repeat as needed.
1173 if (compositeOps && !compositeOps->IsEmpty()) {
1174 size_t length = compositeOps->Length();
1175 for (size_t i = 0; i < aResult.Length(); i++) {
1176 aResult[i].mComposite = compositeOps->ElementAt(i % length);
1183 * Distribute the offsets of all keyframes in between the endpoints of the
1184 * given range.
1186 * @param aRange The sequence of keyframes between whose endpoints we should
1187 * distribute offsets.
1189 static void DistributeRange(const Range<Keyframe>& aRange) {
1190 const Range<Keyframe> rangeToAdjust =
1191 Range<Keyframe>(aRange.begin() + 1, aRange.end() - 1);
1192 const size_t n = aRange.length() - 1;
1193 const double startOffset = aRange[0].mComputedOffset;
1194 const double diffOffset = aRange[n].mComputedOffset - startOffset;
1195 for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
1196 size_t index = iter - aRange.begin();
1197 iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
1201 } // namespace mozilla