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