Backed out 2 changesets (bug 1539720) for causing caret related failures. CLOSED...
[gecko.git] / dom / animation / KeyframeUtils.cpp
blobf8a81e4a4e27c24b14804cd84ff0372f27ec3a0b
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/ComputedStyle.h"
16 #include "mozilla/ErrorResult.h"
17 #include "mozilla/RangedArray.h"
18 #include "mozilla/ServoBindingTypes.h"
19 #include "mozilla/ServoBindings.h"
20 #include "mozilla/ServoCSSParser.h"
21 #include "mozilla/StaticPrefs_dom.h"
22 #include "mozilla/StyleAnimationValue.h"
23 #include "mozilla/TimingParams.h"
24 #include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
25 #include "mozilla/dom/BindingCallContext.h"
26 #include "mozilla/dom/Document.h" // For Document::AreWebAnimationsImplicitKeyframesEnabled
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 "nsTArray.h"
40 using mozilla::dom::Nullable;
42 namespace mozilla {
44 // ------------------------------------------------------------------
46 // Internal data types
48 // ------------------------------------------------------------------
50 // For the aAllowList parameter of AppendStringOrStringSequence and
51 // GetPropertyValuesPairs.
52 enum class ListAllowance { eDisallow, eAllow };
54 /**
55 * A property-values pair obtained from the open-ended properties
56 * discovered on a regular keyframe or property-indexed keyframe object.
58 * Single values (as required by a regular keyframe, and as also supported
59 * on property-indexed keyframes) are stored as the only element in
60 * mValues.
62 struct PropertyValuesPair {
63 nsCSSPropertyID mProperty;
64 nsTArray<nsCString> mValues;
67 /**
68 * An additional property (for a property-values pair) found on a
69 * BaseKeyframe or BasePropertyIndexedKeyframe object.
71 struct AdditionalProperty {
72 nsCSSPropertyID mProperty;
73 size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs.
75 struct PropertyComparator {
76 bool Equals(const AdditionalProperty& aLhs,
77 const AdditionalProperty& aRhs) const {
78 return aLhs.mProperty == aRhs.mProperty;
80 bool LessThan(const AdditionalProperty& aLhs,
81 const AdditionalProperty& aRhs) const {
82 return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
83 nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
88 /**
89 * Data for a segment in a keyframe animation of a given property
90 * whose value is a StyleAnimationValue.
92 * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
93 * to gather data for each individual segment.
95 struct KeyframeValueEntry {
96 nsCSSPropertyID mProperty;
97 AnimationValue mValue;
99 float mOffset;
100 Maybe<StyleComputedTimingFunction> mTimingFunction;
101 dom::CompositeOperation mComposite;
103 struct PropertyOffsetComparator {
104 static bool Equals(const KeyframeValueEntry& aLhs,
105 const KeyframeValueEntry& aRhs) {
106 return aLhs.mProperty == aRhs.mProperty && aLhs.mOffset == aRhs.mOffset;
108 static bool LessThan(const KeyframeValueEntry& aLhs,
109 const KeyframeValueEntry& aRhs) {
110 // First, sort by property IDL name.
111 int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
112 nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
113 if (order != 0) {
114 return order < 0;
117 // Then, by offset.
118 return aLhs.mOffset < aRhs.mOffset;
123 class ComputedOffsetComparator {
124 public:
125 static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) {
126 return aLhs.mComputedOffset == aRhs.mComputedOffset;
129 static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
130 return aLhs.mComputedOffset < aRhs.mComputedOffset;
134 // ------------------------------------------------------------------
136 // Internal helper method declarations
138 // ------------------------------------------------------------------
140 static void GetKeyframeListFromKeyframeSequence(
141 JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
142 nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv);
144 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
145 JS::ForOfIterator& aIterator,
146 const char* aContext,
147 nsTArray<Keyframe>& aResult);
149 static bool GetPropertyValuesPairs(JSContext* aCx,
150 JS::Handle<JSObject*> aObject,
151 ListAllowance aAllowLists,
152 nsTArray<PropertyValuesPair>& aResult);
154 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
155 JS::Handle<JS::Value> aValue,
156 ListAllowance aAllowLists,
157 nsTArray<nsCString>& aValues);
159 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
160 JS::Handle<JS::Value> aValue);
162 static Maybe<PropertyValuePair> MakePropertyValuePair(
163 nsCSSPropertyID aProperty, const nsACString& aStringValue,
164 dom::Document* aDocument);
166 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
168 #ifdef DEBUG
169 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
171 #endif
173 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
174 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
175 PseudoStyleType aPseudoType, const ComputedStyle* aComputedValues);
177 static void BuildSegmentsFromValueEntries(
178 nsTArray<KeyframeValueEntry>& aEntries,
179 nsTArray<AnimationProperty>& aResult);
181 static void GetKeyframeListFromPropertyIndexedKeyframe(
182 JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
183 nsTArray<Keyframe>& aResult, ErrorResult& aRv);
185 static bool HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
186 dom::Document* aDocument);
188 static void DistributeRange(const Range<Keyframe>& aRange);
190 // ------------------------------------------------------------------
192 // Public API
194 // ------------------------------------------------------------------
196 /* static */
197 nsTArray<Keyframe> KeyframeUtils::GetKeyframesFromObject(
198 JSContext* aCx, dom::Document* aDocument, JS::Handle<JSObject*> aFrames,
199 const char* aContext, ErrorResult& aRv) {
200 MOZ_ASSERT(!aRv.Failed());
202 nsTArray<Keyframe> keyframes;
204 if (!aFrames) {
205 // The argument was explicitly null meaning no keyframes.
206 return keyframes;
209 // At this point we know we have an object. We try to convert it to a
210 // sequence of keyframes first, and if that fails due to not being iterable,
211 // we try to convert it to a property-indexed keyframe.
212 JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
213 JS::ForOfIterator iter(aCx);
214 if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
215 aRv.Throw(NS_ERROR_FAILURE);
216 return keyframes;
219 if (iter.valueIsIterable()) {
220 GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes,
221 aContext, aRv);
222 } else {
223 GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
224 keyframes, aRv);
227 if (aRv.Failed()) {
228 MOZ_ASSERT(keyframes.IsEmpty(),
229 "Should not set any keyframes when there is an error");
230 return keyframes;
233 if (!dom::Document::AreWebAnimationsImplicitKeyframesEnabled(aCx, nullptr) &&
234 HasImplicitKeyframeValues(keyframes, aDocument)) {
235 keyframes.Clear();
236 aRv.ThrowNotSupportedError(
237 "Animation to or from an underlying value is not yet supported");
240 return keyframes;
243 /* static */
244 void KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes) {
245 if (aKeyframes.IsEmpty()) {
246 return;
249 // If the first keyframe has an unspecified offset, fill it in with 0%.
250 // If there is only a single keyframe, then it gets 100%.
251 if (aKeyframes.Length() > 1) {
252 Keyframe& firstElement = aKeyframes[0];
253 firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
254 // We will fill in the last keyframe's offset below
255 } else {
256 Keyframe& lastElement = aKeyframes.LastElement();
257 lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
260 // Fill in remaining missing offsets.
261 const Keyframe* const last = &aKeyframes.LastElement();
262 const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
263 RangedPtr<Keyframe> keyframeA = begin;
264 while (keyframeA != last) {
265 // Find keyframe A and keyframe B *between* which we will apply spacing.
266 RangedPtr<Keyframe> keyframeB = keyframeA + 1;
267 while (keyframeB->mOffset.isNothing() && keyframeB != last) {
268 ++keyframeB;
270 keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
272 // Fill computed offsets in (keyframe A, keyframe B).
273 DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
274 keyframeA = keyframeB;
278 /* static */
279 nsTArray<AnimationProperty> KeyframeUtils::GetAnimationPropertiesFromKeyframes(
280 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
281 PseudoStyleType aPseudoType, const ComputedStyle* aStyle,
282 dom::CompositeOperation aEffectComposite) {
283 nsTArray<AnimationProperty> result;
285 const nsTArray<ComputedKeyframeValues> computedValues =
286 GetComputedKeyframeValues(aKeyframes, aElement, aPseudoType, aStyle);
287 if (computedValues.IsEmpty()) {
288 // In rare cases GetComputedKeyframeValues might fail and return an empty
289 // array, in which case we likewise return an empty array from here.
290 return result;
293 MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
294 "Array length mismatch");
296 nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
298 const size_t len = aKeyframes.Length();
299 for (size_t i = 0; i < len; ++i) {
300 const Keyframe& frame = aKeyframes[i];
301 for (auto& value : computedValues[i]) {
302 MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
303 "Invalid computed offset");
304 KeyframeValueEntry* entry = entries.AppendElement();
305 entry->mOffset = frame.mComputedOffset;
306 entry->mProperty = value.mProperty;
307 entry->mValue = value.mValue;
308 entry->mTimingFunction = frame.mTimingFunction;
309 // The following assumes that CompositeOperation is a strict subset of
310 // CompositeOperationOrAuto.
311 entry->mComposite =
312 frame.mComposite == dom::CompositeOperationOrAuto::Auto
313 ? aEffectComposite
314 : static_cast<dom::CompositeOperation>(frame.mComposite);
318 BuildSegmentsFromValueEntries(entries, result);
319 return result;
322 /* static */
323 bool KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty) {
324 // Regardless of the backend type, treat the 'display' property as not
325 // animatable. (Servo will report it as being animatable, since it is
326 // in fact animatable by SMIL.)
327 if (aProperty == eCSSProperty_display) {
328 return false;
330 return Servo_Property_IsAnimatable(aProperty);
333 // ------------------------------------------------------------------
335 // Internal helpers
337 // ------------------------------------------------------------------
340 * Converts a JS object to an IDL sequence<Keyframe>.
342 * @param aCx The JSContext corresponding to |aIterator|.
343 * @param aDocument The document to use when parsing CSS properties.
344 * @param aIterator An already-initialized ForOfIterator for the JS
345 * object to iterate over as a sequence.
346 * @param aResult The array into which the resulting Keyframe objects will be
347 * appended.
348 * @param aContext The context string to prepend to thrown exceptions.
349 * @param aRv Out param to store any errors thrown by this function.
351 static void GetKeyframeListFromKeyframeSequence(
352 JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
353 nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv) {
354 MOZ_ASSERT(!aRv.Failed());
355 MOZ_ASSERT(aResult.IsEmpty());
357 // Convert the object in aIterator to a sequence of keyframes producing
358 // an array of Keyframe objects.
359 if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aContext, aResult)) {
360 aResult.Clear();
361 aRv.NoteJSContextException(aCx);
362 return;
365 // If the sequence<> had zero elements, we won't generate any
366 // keyframes.
367 if (aResult.IsEmpty()) {
368 return;
371 // Check that the keyframes are loosely sorted and with values all
372 // between 0% and 100%.
373 if (!HasValidOffsets(aResult)) {
374 aResult.Clear();
375 aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
376 return;
381 * Converts a JS object wrapped by the given JS::ForIfIterator to an
382 * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
383 * aResult.
385 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
386 JS::ForOfIterator& aIterator,
387 const char* aContext,
388 nsTArray<Keyframe>& aResult) {
389 JS::Rooted<JS::Value> value(aCx);
390 // Parsing errors should only be reported after we have finished iterating
391 // through all values. If we have any early returns while iterating, we should
392 // ignore parsing errors.
393 IgnoredErrorResult parseEasingResult;
395 for (;;) {
396 bool done;
397 if (!aIterator.next(&value, &done)) {
398 return false;
400 if (done) {
401 break;
403 // Each value found when iterating the object must be an object
404 // or null/undefined (which gets treated as a default {} dictionary
405 // value).
406 if (!value.isObject() && !value.isNullOrUndefined()) {
407 dom::ThrowErrorMessage<dom::MSG_NOT_OBJECT>(
408 aCx, aContext, "Element of sequence<Keyframe> argument");
409 return false;
412 // Convert the JS value into a BaseKeyframe dictionary value.
413 dom::binding_detail::FastBaseKeyframe keyframeDict;
414 dom::BindingCallContext callCx(aCx, aContext);
415 if (!keyframeDict.Init(callCx, value,
416 "Element of sequence<Keyframe> argument")) {
417 // This may happen if the value type of the member of BaseKeyframe is
418 // invalid. e.g. `offset` only accept a double value, so if we provide a
419 // string, we enter this branch.
420 // Besides, keyframeDict.Init() should throw a Type Error message already,
421 // so we don't have to do it again.
422 return false;
425 Keyframe* keyframe = aResult.AppendElement(fallible);
426 if (!keyframe) {
427 return false;
430 if (!keyframeDict.mOffset.IsNull()) {
431 keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
434 if (StaticPrefs::dom_animations_api_compositing_enabled()) {
435 keyframe->mComposite = keyframeDict.mComposite;
438 // Look for additional property-values pairs on the object.
439 nsTArray<PropertyValuesPair> propertyValuePairs;
440 if (value.isObject()) {
441 JS::Rooted<JSObject*> object(aCx, &value.toObject());
442 if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eDisallow,
443 propertyValuePairs)) {
444 return false;
448 if (!parseEasingResult.Failed()) {
449 keyframe->mTimingFunction =
450 TimingParams::ParseEasing(keyframeDict.mEasing, parseEasingResult);
451 // Even if the above fails, we still need to continue reading off all the
452 // properties since checking the validity of easing should be treated as
453 // a separate step that happens *after* all the other processing in this
454 // loop since (since it is never likely to be handled by WebIDL unlike the
455 // rest of this loop).
458 for (PropertyValuesPair& pair : propertyValuePairs) {
459 MOZ_ASSERT(pair.mValues.Length() == 1);
461 Maybe<PropertyValuePair> valuePair =
462 MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument);
463 if (!valuePair) {
464 continue;
466 keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
468 #ifdef DEBUG
469 // When we go to convert keyframes into arrays of property values we
470 // call StyleAnimation::ComputeValues. This should normally return true
471 // but in order to test the case where it does not, BaseKeyframeDict
472 // includes a chrome-only member that can be set to indicate that
473 // ComputeValues should fail for shorthand property values on that
474 // keyframe.
475 if (nsCSSProps::IsShorthand(pair.mProperty) &&
476 keyframeDict.mSimulateComputeValuesFailure) {
477 MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
479 #endif
483 // Throw any errors we encountered while parsing 'easing' properties.
484 if (parseEasingResult.MaybeSetPendingException(aCx)) {
485 return false;
488 return true;
492 * Reads the property-values pairs from the specified JS object.
494 * @param aObject The JS object to look at.
495 * @param aAllowLists If eAllow, values will be converted to
496 * (DOMString or sequence<DOMString); if eDisallow, values
497 * will be converted to DOMString.
498 * @param aResult The array into which the enumerated property-values
499 * pairs will be stored.
500 * @return false on failure or JS exception thrown while interacting
501 * with aObject; true otherwise.
503 static bool GetPropertyValuesPairs(JSContext* aCx,
504 JS::Handle<JSObject*> aObject,
505 ListAllowance aAllowLists,
506 nsTArray<PropertyValuesPair>& aResult) {
507 nsTArray<AdditionalProperty> properties;
509 // Iterate over all the properties on aObject and append an
510 // entry to properties for them.
512 // We don't compare the jsids that we encounter with those for
513 // the explicit dictionary members, since we know that none
514 // of the CSS property IDL names clash with them.
515 JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
516 if (!JS_Enumerate(aCx, aObject, &ids)) {
517 return false;
519 for (size_t i = 0, n = ids.length(); i < n; i++) {
520 nsAutoJSCString propName;
521 if (!propName.init(aCx, ids[i])) {
522 return false;
525 // Basically, we have to handle "cssOffset" and "cssFloat" specially here:
526 // "cssOffset" => eCSSProperty_offset
527 // "cssFloat" => eCSSProperty_float
528 // This means if the attribute is the string "cssOffset"/"cssFloat", we use
529 // CSS "offset"/"float" property.
530 // https://drafts.csswg.org/web-animations/#property-name-conversion
531 nsCSSPropertyID property = nsCSSPropertyID::eCSSProperty_UNKNOWN;
532 if (propName.EqualsLiteral("cssOffset")) {
533 property = nsCSSPropertyID::eCSSProperty_offset;
534 } else if (propName.EqualsLiteral("cssFloat")) {
535 property = nsCSSPropertyID::eCSSProperty_float;
536 } else if (!propName.EqualsLiteral("offset") &&
537 !propName.EqualsLiteral("float")) {
538 property = nsCSSProps::LookupPropertyByIDLName(
539 propName, CSSEnabledState::ForAllContent);
542 if (KeyframeUtils::IsAnimatableProperty(property)) {
543 AdditionalProperty* p = properties.AppendElement();
544 p->mProperty = property;
545 p->mJsidIndex = i;
549 // Sort the entries by IDL name and then get each value and
550 // convert it either to a DOMString or to a
551 // (DOMString or sequence<DOMString>), depending on aAllowLists,
552 // and build up aResult.
553 properties.Sort(AdditionalProperty::PropertyComparator());
555 for (AdditionalProperty& p : properties) {
556 JS::Rooted<JS::Value> value(aCx);
557 if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
558 return false;
560 PropertyValuesPair* pair = aResult.AppendElement();
561 pair->mProperty = p.mProperty;
562 if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
563 pair->mValues)) {
564 return false;
568 return true;
572 * Converts aValue to DOMString, if aAllowLists is eDisallow, or
573 * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
574 * The resulting strings are appended to aValues.
576 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
577 JS::Handle<JS::Value> aValue,
578 ListAllowance aAllowLists,
579 nsTArray<nsCString>& aValues) {
580 if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
581 // The value is an object, and we want to allow lists; convert
582 // aValue to (DOMString or sequence<DOMString>).
583 JS::ForOfIterator iter(aCx);
584 if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
585 return false;
587 if (iter.valueIsIterable()) {
588 // If the object is iterable, convert it to sequence<DOMString>.
589 JS::Rooted<JS::Value> element(aCx);
590 for (;;) {
591 bool done;
592 if (!iter.next(&element, &done)) {
593 return false;
595 if (done) {
596 break;
598 if (!AppendValueAsString(aCx, aValues, element)) {
599 return false;
602 return true;
606 // Either the object is not iterable, or aAllowLists doesn't want
607 // a list; convert it to DOMString.
608 if (!AppendValueAsString(aCx, aValues, aValue)) {
609 return false;
612 return true;
616 * Converts aValue to DOMString and appends it to aValues.
618 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
619 JS::Handle<JS::Value> aValue) {
620 return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
621 *aValues.AppendElement());
624 static void ReportInvalidPropertyValueToConsole(
625 nsCSSPropertyID aProperty, const nsACString& aInvalidPropertyValue,
626 dom::Document* aDoc) {
627 AutoTArray<nsString, 2> params;
628 params.AppendElement(NS_ConvertUTF8toUTF16(aInvalidPropertyValue));
629 CopyASCIItoUTF16(nsCSSProps::GetStringValue(aProperty),
630 *params.AppendElement());
631 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Animation"_ns,
632 aDoc, nsContentUtils::eDOM_PROPERTIES,
633 "InvalidKeyframePropertyValue", params);
637 * Construct a PropertyValuePair parsing the given string into a suitable
638 * nsCSSValue object.
640 * @param aProperty The CSS property.
641 * @param aStringValue The property value to parse.
642 * @param aDocument The document to use when parsing.
643 * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
644 * an invalid property value.
646 static Maybe<PropertyValuePair> MakePropertyValuePair(
647 nsCSSPropertyID aProperty, const nsACString& aStringValue,
648 dom::Document* aDocument) {
649 MOZ_ASSERT(aDocument);
650 Maybe<PropertyValuePair> result;
652 ServoCSSParser::ParsingEnvironment env =
653 ServoCSSParser::GetParsingEnvironment(aDocument);
654 RefPtr<StyleLockedDeclarationBlock> servoDeclarationBlock =
655 ServoCSSParser::ParseProperty(aProperty, aStringValue, env,
656 StyleParsingMode::DEFAULT);
658 if (servoDeclarationBlock) {
659 result.emplace(aProperty, std::move(servoDeclarationBlock));
660 } else {
661 ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
663 return result;
667 * Checks that the given keyframes are loosely ordered (each keyframe's
668 * offset that is not null is greater than or equal to the previous
669 * non-null offset) and that all values are within the range [0.0, 1.0].
671 * @return true if the keyframes' offsets are correctly ordered and
672 * within range; false otherwise.
674 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes) {
675 double offset = 0.0;
676 for (const Keyframe& keyframe : aKeyframes) {
677 if (keyframe.mOffset) {
678 double thisOffset = keyframe.mOffset.value();
679 if (thisOffset < offset || thisOffset > 1.0f) {
680 return false;
682 offset = thisOffset;
685 return true;
688 #ifdef DEBUG
690 * Takes a property-value pair for a shorthand property and modifies the
691 * value to indicate that when we call StyleAnimationValue::ComputeValues on
692 * that value we should behave as if that function had failed.
694 * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
695 * a shorthand property.
697 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair) {
698 MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
699 "Only shorthand property values can be marked as failure values");
701 aPair.mSimulateComputeValuesFailure = true;
704 #endif
707 * The variation of the above function. This is for Servo backend.
709 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
710 const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
711 PseudoStyleType aPseudoType, const ComputedStyle* aComputedStyle) {
712 MOZ_ASSERT(aElement);
714 nsTArray<ComputedKeyframeValues> result;
716 nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
717 if (!presContext) {
718 // This has been reported to happen with some combinations of content
719 // (particularly involving resize events and layout flushes? See bug 1407898
720 // and bug 1408420) but no reproducible steps have been found.
721 // For now we just return an empty array.
722 return result;
725 result = presContext->StyleSet()->GetComputedKeyframeValuesFor(
726 aKeyframes, aElement, aPseudoType, aComputedStyle);
727 return result;
730 static void AppendInitialSegment(AnimationProperty* aAnimationProperty,
731 const KeyframeValueEntry& aFirstEntry) {
732 AnimationPropertySegment* segment =
733 aAnimationProperty->mSegments.AppendElement();
734 segment->mFromKey = 0.0f;
735 segment->mToKey = aFirstEntry.mOffset;
736 segment->mToValue = aFirstEntry.mValue;
737 segment->mToComposite = aFirstEntry.mComposite;
740 static void AppendFinalSegment(AnimationProperty* aAnimationProperty,
741 const KeyframeValueEntry& aLastEntry) {
742 AnimationPropertySegment* segment =
743 aAnimationProperty->mSegments.AppendElement();
744 segment->mFromKey = aLastEntry.mOffset;
745 segment->mFromValue = aLastEntry.mValue;
746 segment->mFromComposite = aLastEntry.mComposite;
747 segment->mToKey = 1.0f;
748 segment->mTimingFunction = aLastEntry.mTimingFunction;
751 // Returns a newly created AnimationProperty if one was created to fill-in the
752 // missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
753 // becase we don't support implicit keyframes).
754 static AnimationProperty* HandleMissingInitialKeyframe(
755 nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry) {
756 MOZ_ASSERT(aEntry.mOffset != 0.0f,
757 "The offset of the entry should not be 0.0");
759 // If the preference for implicit keyframes is not enabled, don't fill in the
760 // missing keyframe.
761 if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
762 return nullptr;
765 AnimationProperty* result = aResult.AppendElement();
766 result->mProperty = aEntry.mProperty;
768 AppendInitialSegment(result, aEntry);
770 return result;
773 static void HandleMissingFinalKeyframe(
774 nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry,
775 AnimationProperty* aCurrentAnimationProperty) {
776 MOZ_ASSERT(aEntry.mOffset != 1.0f,
777 "The offset of the entry should not be 1.0");
779 // If the preference for implicit keyframes is not enabled, don't fill
780 // in the missing keyframe.
781 if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) {
782 // If we have already appended a new entry for the property so we have to
783 // remove it.
784 if (aCurrentAnimationProperty) {
785 aResult.RemoveLastElement();
787 return;
790 // If |aCurrentAnimationProperty| is nullptr, that means this is the first
791 // entry for the property, we have to append a new AnimationProperty for this
792 // property.
793 if (!aCurrentAnimationProperty) {
794 aCurrentAnimationProperty = aResult.AppendElement();
795 aCurrentAnimationProperty->mProperty = aEntry.mProperty;
797 // If we have only one entry whose offset is neither 1 nor 0 for this
798 // property, we need to append the initial segment as well.
799 if (aEntry.mOffset != 0.0f) {
800 AppendInitialSegment(aCurrentAnimationProperty, aEntry);
803 AppendFinalSegment(aCurrentAnimationProperty, aEntry);
807 * Builds an array of AnimationProperty objects to represent the keyframe
808 * animation segments in aEntries.
810 static void BuildSegmentsFromValueEntries(
811 nsTArray<KeyframeValueEntry>& aEntries,
812 nsTArray<AnimationProperty>& aResult) {
813 if (aEntries.IsEmpty()) {
814 return;
817 // Sort the KeyframeValueEntry objects so that all entries for a given
818 // property are together, and the entries are sorted by offset otherwise.
819 std::stable_sort(aEntries.begin(), aEntries.end(),
820 &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
822 // For a given index i, we want to generate a segment from aEntries[i]
823 // to aEntries[j], if:
825 // * j > i,
826 // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
827 // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
829 // That will eliminate runs of same offset/property values where there's no
830 // point generating zero length segments in the middle of the animation.
832 // Additionally we need to generate a zero length segment at offset 0 and at
833 // offset 1, if we have multiple values for a given property at that offset,
834 // since we need to retain the very first and very last value so they can
835 // be used for reverse and forward filling.
837 // Typically, for each property in |aEntries|, we expect there to be at least
838 // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
839 // However, since it is possible that when building |aEntries|, the call to
840 // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
841 // Furthermore, if additive animation is disabled, the following loop takes
842 // care to identify properties that lack a value at offset 0.0/1.0 and drops
843 // those properties from |aResult|.
845 nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
846 AnimationProperty* animationProperty = nullptr;
848 size_t i = 0, n = aEntries.Length();
850 while (i < n) {
851 // If we've reached the end of the array of entries, synthesize a final (and
852 // initial) segment if necessary.
853 if (i + 1 == n) {
854 if (aEntries[i].mOffset != 1.0f) {
855 HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
856 } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
857 // If the last entry with offset 1 and no animation property, that means
858 // it is the only entry for this property so append a single segment
859 // from 0 offset to |aEntry[i].offset|.
860 Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
862 animationProperty = nullptr;
863 break;
866 MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
867 aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
868 "Each entry should specify a valid property");
870 // No keyframe for this property at offset 0.
871 if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) {
872 // If we don't support additive animation we can't fill in the missing
873 // keyframes and we should just skip this property altogether. Since the
874 // entries are sorted by offset for a given property, and since we don't
875 // update |lastProperty|, we will keep hitting this condition until we
876 // change property.
877 animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
878 if (animationProperty) {
879 lastProperty = aEntries[i].mProperty;
880 } else {
881 // Skip this entry if we did not handle the missing entry.
882 ++i;
883 continue;
887 // Skip this entry if the next entry has the same offset except for initial
888 // and final ones. We will handle missing keyframe in the next loop
889 // if the property is changed on the next entry.
890 if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
891 aEntries[i].mOffset == aEntries[i + 1].mOffset &&
892 aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
893 ++i;
894 continue;
897 // No keyframe for this property at offset 1.
898 if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
899 aEntries[i].mOffset != 1.0f) {
900 HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
901 // Move on to new property.
902 animationProperty = nullptr;
903 ++i;
904 continue;
907 // Starting from i + 1, determine the next [i, j] interval from which to
908 // generate a segment. Basically, j is i + 1, but there are some special
909 // cases for offset 0 and 1, so we need to handle them specifically.
910 // Note: From this moment, we make sure [i + 1] is valid and
911 // there must be an initial entry (i.e. mOffset = 0.0) and
912 // a final entry (i.e. mOffset = 1.0). Besides, all the entries
913 // with the same offsets except for initial/final ones are filtered
914 // out already.
915 size_t j = i + 1;
916 if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
917 // We need to generate an initial zero-length segment.
918 MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
919 while (j + 1 < n && aEntries[j + 1].mOffset == 0.0f &&
920 aEntries[j + 1].mProperty == aEntries[j].mProperty) {
921 ++j;
923 } else if (aEntries[i].mOffset == 1.0f) {
924 if (aEntries[i + 1].mOffset == 1.0f &&
925 aEntries[i + 1].mProperty == aEntries[i].mProperty) {
926 // We need to generate a final zero-length segment.
927 while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f &&
928 aEntries[j + 1].mProperty == aEntries[j].mProperty) {
929 ++j;
931 } else {
932 // New property.
933 MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
934 animationProperty = nullptr;
935 ++i;
936 continue;
940 // If we've moved on to a new property, create a new AnimationProperty
941 // to insert segments into.
942 if (aEntries[i].mProperty != lastProperty) {
943 MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
944 MOZ_ASSERT(!animationProperty);
945 animationProperty = aResult.AppendElement();
946 animationProperty->mProperty = aEntries[i].mProperty;
947 lastProperty = aEntries[i].mProperty;
950 MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
952 // Now generate the segment.
953 AnimationPropertySegment* segment =
954 animationProperty->mSegments.AppendElement();
955 segment->mFromKey = aEntries[i].mOffset;
956 segment->mToKey = aEntries[j].mOffset;
957 segment->mFromValue = aEntries[i].mValue;
958 segment->mToValue = aEntries[j].mValue;
959 segment->mTimingFunction = aEntries[i].mTimingFunction;
960 segment->mFromComposite = aEntries[i].mComposite;
961 segment->mToComposite = aEntries[j].mComposite;
963 i = j;
968 * Converts a JS object representing a property-indexed keyframe into
969 * an array of Keyframe objects.
971 * @param aCx The JSContext for |aValue|.
972 * @param aDocument The document to use when parsing CSS properties.
973 * @param aValue The JS object.
974 * @param aResult The array into which the resulting AnimationProperty
975 * objects will be appended.
976 * @param aRv Out param to store any errors thrown by this function.
978 static void GetKeyframeListFromPropertyIndexedKeyframe(
979 JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
980 nsTArray<Keyframe>& aResult, ErrorResult& aRv) {
981 MOZ_ASSERT(aValue.isObject());
982 MOZ_ASSERT(aResult.IsEmpty());
983 MOZ_ASSERT(!aRv.Failed());
985 // Convert the object to a property-indexed keyframe dictionary to
986 // get its explicit dictionary members.
987 dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
988 // XXXbz Pass in the method name from callers and set up a BindingCallContext?
989 if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument")) {
990 aRv.Throw(NS_ERROR_FAILURE);
991 return;
994 // Get all the property--value-list pairs off the object.
995 JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
996 nsTArray<PropertyValuesPair> propertyValuesPairs;
997 if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
998 propertyValuesPairs)) {
999 aRv.Throw(NS_ERROR_FAILURE);
1000 return;
1003 // Create a set of keyframes for each property.
1004 nsTHashMap<nsFloatHashKey, Keyframe> processedKeyframes;
1005 for (const PropertyValuesPair& pair : propertyValuesPairs) {
1006 size_t count = pair.mValues.Length();
1007 if (count == 0) {
1008 // No animation values for this property.
1009 continue;
1012 // If we only have one value, we should animate from the underlying value
1013 // but not if the pref for supporting implicit keyframes is disabled.
1014 if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled() &&
1015 count == 1) {
1016 aRv.ThrowNotSupportedError(
1017 "Animation to or from an underlying value is not yet supported");
1018 return;
1021 size_t n = pair.mValues.Length() - 1;
1022 size_t i = 0;
1024 for (const nsCString& stringValue : pair.mValues) {
1025 // For single-valued lists, the single value should be added to a
1026 // keyframe with offset 1.
1027 double offset = n ? i++ / double(n) : 1;
1028 Keyframe& keyframe = processedKeyframes.LookupOrInsert(offset);
1029 if (keyframe.mPropertyValues.IsEmpty()) {
1030 keyframe.mComputedOffset = offset;
1033 Maybe<PropertyValuePair> valuePair =
1034 MakePropertyValuePair(pair.mProperty, stringValue, aDocument);
1035 if (!valuePair) {
1036 continue;
1038 keyframe.mPropertyValues.AppendElement(std::move(valuePair.ref()));
1042 aResult.SetCapacity(processedKeyframes.Count());
1043 std::transform(processedKeyframes.begin(), processedKeyframes.end(),
1044 MakeBackInserter(aResult), [](auto& entry) {
1045 return std::move(*entry.GetModifiableData());
1048 aResult.Sort(ComputedOffsetComparator());
1050 // Fill in any specified offsets
1052 // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of
1053 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1054 const FallibleTArray<Nullable<double>>* offsets = nullptr;
1055 AutoTArray<Nullable<double>, 1> singleOffset;
1056 auto& offset = keyframeDict.mOffset;
1057 if (offset.IsDouble()) {
1058 singleOffset.AppendElement(offset.GetAsDouble());
1059 // dom::Sequence is a fallible but AutoTArray is infallible and we need to
1060 // point to one or the other. Fortunately, fallible and infallible array
1061 // types can be implicitly converted provided they are const.
1062 const FallibleTArray<Nullable<double>>& asFallibleArray = singleOffset;
1063 offsets = &asFallibleArray;
1064 } else if (offset.IsDoubleOrNullSequence()) {
1065 offsets = &offset.GetAsDoubleOrNullSequence();
1067 // If offset.IsNull() is true, then we want to leave the mOffset member of
1068 // each keyframe with its initialized value of null. By leaving |offsets|
1069 // as nullptr here, we skip updating mOffset below.
1071 size_t offsetsToFill =
1072 offsets ? std::min(offsets->Length(), aResult.Length()) : 0;
1073 for (size_t i = 0; i < offsetsToFill; i++) {
1074 if (!offsets->ElementAt(i).IsNull()) {
1075 aResult[i].mOffset.emplace(offsets->ElementAt(i).Value());
1079 // Check that the keyframes are loosely sorted and that any specified offsets
1080 // are between 0.0 and 1.0 inclusive.
1082 // This corresponds to steps 6-7 of
1083 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1085 // In the spec, TypeErrors arising from invalid offsets and easings are thrown
1086 // at the end of the procedure since it assumes we initially store easing
1087 // values as strings and then later parse them.
1089 // However, we will parse easing members immediately when we process them
1090 // below. In order to maintain the relative order in which TypeErrors are
1091 // thrown according to the spec, namely exceptions arising from invalid
1092 // offsets are thrown before exceptions arising from invalid easings, we check
1093 // the offsets here.
1094 if (!HasValidOffsets(aResult)) {
1095 aResult.Clear();
1096 aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
1097 return;
1100 // Fill in any easings.
1102 // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of
1103 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1104 FallibleTArray<Maybe<StyleComputedTimingFunction>> easings;
1105 auto parseAndAppendEasing = [&](const nsACString& easingString,
1106 ErrorResult& aRv) {
1107 auto easing = TimingParams::ParseEasing(easingString, aRv);
1108 if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) {
1109 aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
1113 auto& easing = keyframeDict.mEasing;
1114 if (easing.IsUTF8String()) {
1115 parseAndAppendEasing(easing.GetAsUTF8String(), aRv);
1116 if (aRv.Failed()) {
1117 aResult.Clear();
1118 return;
1120 } else {
1121 for (const auto& easingString : easing.GetAsUTF8StringSequence()) {
1122 parseAndAppendEasing(easingString, aRv);
1123 if (aRv.Failed()) {
1124 aResult.Clear();
1125 return;
1130 // If |easings| is empty, then we are supposed to fill it in with the value
1131 // "linear" and then repeat the list as necessary.
1133 // However, for Keyframe.mTimingFunction we represent "linear" as a None
1134 // value. Since we have not assigned 'mTimingFunction' for any of the
1135 // keyframes in |aResult| they will already have their initial None value
1136 // (i.e. linear). As a result, if |easings| is empty, we don't need to do
1137 // anything.
1138 if (!easings.IsEmpty()) {
1139 for (size_t i = 0; i < aResult.Length(); i++) {
1140 aResult[i].mTimingFunction = easings[i % easings.Length()];
1144 // Fill in any composite operations.
1146 // This corresponds to step 5, "Otherwise," branch, substep 12 of
1147 // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
1148 if (StaticPrefs::dom_animations_api_compositing_enabled()) {
1149 const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
1150 AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
1151 auto& composite = keyframeDict.mComposite;
1152 if (composite.IsCompositeOperationOrAuto()) {
1153 singleCompositeOp.AppendElement(
1154 composite.GetAsCompositeOperationOrAuto());
1155 const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
1156 singleCompositeOp;
1157 compositeOps = &asFallibleArray;
1158 } else if (composite.IsCompositeOperationOrAutoSequence()) {
1159 compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
1162 // Fill in and repeat as needed.
1163 if (compositeOps && !compositeOps->IsEmpty()) {
1164 size_t length = compositeOps->Length();
1165 for (size_t i = 0; i < aResult.Length(); i++) {
1166 aResult[i].mComposite = compositeOps->ElementAt(i % length);
1173 * Returns true if the supplied set of keyframes has keyframe values for
1174 * any property for which it does not also supply a value for the 0% and 100%
1175 * offsets. The check is not entirely accurate but should detect most common
1176 * cases.
1178 * @param aKeyframes The set of keyframes to analyze.
1179 * @param aDocument The document to use when parsing keyframes so we can
1180 * try to detect where we have an invalid value at 0%/100%.
1182 static bool HasImplicitKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
1183 dom::Document* aDocument) {
1184 // We are looking to see if that every property referenced in |aKeyframes|
1185 // has a valid property at offset 0.0 and 1.0. The check as to whether a
1186 // property is valid or not, however, is not precise. We only check if the
1187 // property can be parsed, NOT whether it can also be converted to a
1188 // StyleAnimationValue since doing that requires a target element bound to
1189 // a document which we might not always have at the point where we want to
1190 // perform this check.
1192 // This is only a temporary measure until we ship implicit keyframes and
1193 // remove the corresponding pref.
1194 // So as long as this check catches most cases, and we don't do anything
1195 // horrible in one of the cases we can't detect, it should be sufficient.
1197 nsCSSPropertyIDSet properties; // All properties encountered.
1198 nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
1199 nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value.
1201 auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
1202 properties.AddProperty(aProperty);
1203 if (aOffset == 0.0) {
1204 propertiesWithFromValue.AddProperty(aProperty);
1205 } else if (aOffset == 1.0) {
1206 propertiesWithToValue.AddProperty(aProperty);
1210 for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
1211 const Keyframe& frame = aKeyframes[i];
1213 // We won't have called DistributeKeyframes when this is called so
1214 // we can't use frame.mComputedOffset. Instead we do a rough version
1215 // of that algorithm that substitutes null offsets with 0.0 for the first
1216 // frame, 1.0 for the last frame, and 0.5 for everything else.
1217 double computedOffset = i == len - 1 ? 1.0 : i == 0 ? 0.0 : 0.5;
1218 double offsetToUse = frame.mOffset ? frame.mOffset.value() : computedOffset;
1220 for (const PropertyValuePair& pair : frame.mPropertyValues) {
1221 if (nsCSSProps::IsShorthand(pair.mProperty)) {
1222 MOZ_ASSERT(pair.mServoDeclarationBlock);
1223 CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(prop, pair.mProperty,
1224 CSSEnabledState::ForAllContent) {
1225 addToPropertySets(*prop, offsetToUse);
1227 } else {
1228 addToPropertySets(pair.mProperty, offsetToUse);
1233 return !propertiesWithFromValue.Equals(properties) ||
1234 !propertiesWithToValue.Equals(properties);
1238 * Distribute the offsets of all keyframes in between the endpoints of the
1239 * given range.
1241 * @param aRange The sequence of keyframes between whose endpoints we should
1242 * distribute offsets.
1244 static void DistributeRange(const Range<Keyframe>& aRange) {
1245 const Range<Keyframe> rangeToAdjust =
1246 Range<Keyframe>(aRange.begin() + 1, aRange.end() - 1);
1247 const size_t n = aRange.length() - 1;
1248 const double startOffset = aRange[0].mComputedOffset;
1249 const double diffOffset = aRange[n].mComputedOffset - startOffset;
1250 for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
1251 size_t index = iter - aRange.begin();
1252 iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
1256 } // namespace mozilla