Line data Source code
1 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
2 : /* This Source Code Form is subject to the terms of the Mozilla Public
3 : * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 : * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 :
6 : #include "mozilla/KeyframeUtils.h"
7 :
8 : #include "mozilla/AnimationUtils.h"
9 : #include "mozilla/ErrorResult.h"
10 : #include "mozilla/Move.h"
11 : #include "mozilla/RangedArray.h"
12 : #include "mozilla/ServoBindings.h"
13 : #include "mozilla/ServoBindingTypes.h"
14 : #include "mozilla/StyleAnimationValue.h"
15 : #include "mozilla/TimingParams.h"
16 : #include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
17 : #include "mozilla/dom/Element.h"
18 : #include "mozilla/dom/KeyframeEffectBinding.h"
19 : #include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc.
20 : #include "jsapi.h" // For ForOfIterator etc.
21 : #include "nsClassHashtable.h"
22 : #include "nsContentUtils.h" // For GetContextForContent
23 : #include "nsCSSParser.h"
24 : #include "nsCSSPropertyIDSet.h"
25 : #include "nsCSSProps.h"
26 : #include "nsCSSPseudoElements.h" // For CSSPseudoElementType
27 : #include "nsIScriptError.h"
28 : #include "nsStyleContext.h"
29 : #include "nsTArray.h"
30 : #include <algorithm> // For std::stable_sort
31 :
32 : namespace mozilla {
33 :
34 : // ------------------------------------------------------------------
35 : //
36 : // Internal data types
37 : //
38 : // ------------------------------------------------------------------
39 :
40 : // For the aAllowList parameter of AppendStringOrStringSequence and
41 : // GetPropertyValuesPairs.
42 : enum class ListAllowance { eDisallow, eAllow };
43 :
44 : /**
45 : * A comparator to sort nsCSSPropertyID values such that longhands are sorted
46 : * before shorthands, and shorthands with fewer components are sorted before
47 : * shorthands with more components.
48 : *
49 : * Using this allows us to prioritize values specified by longhands (or smaller
50 : * shorthand subsets) when longhands and shorthands are both specified
51 : * on the one keyframe.
52 : *
53 : * Example orderings that result from this:
54 : *
55 : * margin-left, margin
56 : *
57 : * and:
58 : *
59 : * border-top-color, border-color, border-top, border
60 : */
61 : class PropertyPriorityComparator
62 : {
63 : public:
64 16 : PropertyPriorityComparator()
65 16 : : mSubpropertyCountInitialized(false) {}
66 :
67 0 : bool Equals(nsCSSPropertyID aLhs, nsCSSPropertyID aRhs) const
68 : {
69 0 : return aLhs == aRhs;
70 : }
71 :
72 0 : bool LessThan(nsCSSPropertyID aLhs,
73 : nsCSSPropertyID aRhs) const
74 : {
75 0 : bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs);
76 0 : bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs);
77 :
78 0 : if (isShorthandLhs) {
79 0 : if (isShorthandRhs) {
80 : // First, sort shorthands by the number of longhands they have.
81 0 : uint32_t subpropCountLhs = SubpropertyCount(aLhs);
82 0 : uint32_t subpropCountRhs = SubpropertyCount(aRhs);
83 0 : if (subpropCountLhs != subpropCountRhs) {
84 0 : return subpropCountLhs < subpropCountRhs;
85 : }
86 : // Otherwise, sort by IDL name below.
87 : } else {
88 : // Put longhands before shorthands.
89 0 : return false;
90 : }
91 : } else {
92 0 : if (isShorthandRhs) {
93 : // Put longhands before shorthands.
94 0 : return true;
95 : }
96 : }
97 : // For two longhand properties, or two shorthand with the same number
98 : // of longhand components, sort by IDL name.
99 0 : return nsCSSProps::PropertyIDLNameSortPosition(aLhs) <
100 0 : nsCSSProps::PropertyIDLNameSortPosition(aRhs);
101 : }
102 :
103 0 : uint32_t SubpropertyCount(nsCSSPropertyID aProperty) const
104 : {
105 0 : if (!mSubpropertyCountInitialized) {
106 0 : PodZero(&mSubpropertyCount);
107 0 : mSubpropertyCountInitialized = true;
108 : }
109 0 : if (mSubpropertyCount[aProperty] == 0) {
110 0 : uint32_t count = 0;
111 0 : CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
112 : p, aProperty, CSSEnabledState::eForAllContent) {
113 0 : ++count;
114 : }
115 0 : mSubpropertyCount[aProperty] = count;
116 : }
117 0 : return mSubpropertyCount[aProperty];
118 : }
119 :
120 : private:
121 : // Cache of shorthand subproperty counts.
122 : mutable RangedArray<
123 : uint32_t,
124 : eCSSProperty_COUNT_no_shorthands,
125 : eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount;
126 : mutable bool mSubpropertyCountInitialized;
127 : };
128 :
129 : /**
130 : * Adaptor for PropertyPriorityComparator to sort objects which have
131 : * a mProperty member.
132 : */
133 : template <typename T>
134 16 : class TPropertyPriorityComparator : PropertyPriorityComparator
135 : {
136 : public:
137 0 : bool Equals(const T& aLhs, const T& aRhs) const
138 : {
139 0 : return PropertyPriorityComparator::Equals(aLhs.mProperty, aRhs.mProperty);
140 : }
141 0 : bool LessThan(const T& aLhs, const T& aRhs) const
142 : {
143 0 : return PropertyPriorityComparator::LessThan(aLhs.mProperty, aRhs.mProperty);
144 : }
145 : };
146 :
147 : /**
148 : * Iterator to walk through a PropertyValuePair array using the ordering
149 : * provided by PropertyPriorityComparator.
150 : */
151 16 : class PropertyPriorityIterator
152 : {
153 : public:
154 16 : explicit PropertyPriorityIterator(
155 : const nsTArray<PropertyValuePair>& aProperties)
156 16 : : mProperties(aProperties)
157 : {
158 16 : mSortedPropertyIndices.SetCapacity(mProperties.Length());
159 32 : for (size_t i = 0, len = mProperties.Length(); i < len; ++i) {
160 16 : PropertyAndIndex propertyIndex = { mProperties[i].mProperty, i };
161 16 : mSortedPropertyIndices.AppendElement(propertyIndex);
162 : }
163 16 : mSortedPropertyIndices.Sort(PropertyAndIndex::Comparator());
164 16 : }
165 :
166 : class Iter
167 : {
168 : public:
169 32 : explicit Iter(const PropertyPriorityIterator& aParent)
170 32 : : mParent(aParent)
171 32 : , mIndex(0) { }
172 :
173 16 : static Iter EndIter(const PropertyPriorityIterator &aParent)
174 : {
175 16 : Iter iter(aParent);
176 16 : iter.mIndex = aParent.mSortedPropertyIndices.Length();
177 16 : return iter;
178 : }
179 :
180 32 : bool operator!=(const Iter& aOther) const
181 : {
182 32 : return mIndex != aOther.mIndex;
183 : }
184 :
185 16 : Iter& operator++()
186 : {
187 16 : MOZ_ASSERT(mIndex + 1 <= mParent.mSortedPropertyIndices.Length(),
188 : "Should not seek past end iterator");
189 16 : mIndex++;
190 16 : return *this;
191 : }
192 :
193 16 : const PropertyValuePair& operator*()
194 : {
195 16 : MOZ_ASSERT(mIndex < mParent.mSortedPropertyIndices.Length(),
196 : "Should not try to dereference an end iterator");
197 16 : return mParent.mProperties[mParent.mSortedPropertyIndices[mIndex].mIndex];
198 : }
199 :
200 : private:
201 : const PropertyPriorityIterator& mParent;
202 : size_t mIndex;
203 : };
204 :
205 16 : Iter begin() { return Iter(*this); }
206 16 : Iter end() { return Iter::EndIter(*this); }
207 :
208 : private:
209 : struct PropertyAndIndex
210 : {
211 : nsCSSPropertyID mProperty;
212 : size_t mIndex; // Index of mProperty within mProperties
213 :
214 : typedef TPropertyPriorityComparator<PropertyAndIndex> Comparator;
215 : };
216 :
217 : const nsTArray<PropertyValuePair>& mProperties;
218 : nsTArray<PropertyAndIndex> mSortedPropertyIndices;
219 : };
220 :
221 : /**
222 : * A property-values pair obtained from the open-ended properties
223 : * discovered on a regular keyframe or property-indexed keyframe object.
224 : *
225 : * Single values (as required by a regular keyframe, and as also supported
226 : * on property-indexed keyframes) are stored as the only element in
227 : * mValues.
228 : */
229 0 : struct PropertyValuesPair
230 : {
231 : nsCSSPropertyID mProperty;
232 : nsTArray<nsString> mValues;
233 :
234 : typedef TPropertyPriorityComparator<PropertyValuesPair> Comparator;
235 : };
236 :
237 : /**
238 : * An additional property (for a property-values pair) found on a
239 : * BaseKeyframe or BasePropertyIndexedKeyframe object.
240 : */
241 : struct AdditionalProperty
242 : {
243 : nsCSSPropertyID mProperty;
244 : size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs.
245 :
246 : struct PropertyComparator
247 : {
248 0 : bool Equals(const AdditionalProperty& aLhs,
249 : const AdditionalProperty& aRhs) const
250 : {
251 0 : return aLhs.mProperty == aRhs.mProperty;
252 : }
253 0 : bool LessThan(const AdditionalProperty& aLhs,
254 : const AdditionalProperty& aRhs) const
255 : {
256 0 : return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
257 0 : nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
258 : }
259 : };
260 : };
261 :
262 : /**
263 : * Data for a segment in a keyframe animation of a given property
264 : * whose value is a StyleAnimationValue.
265 : *
266 : * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
267 : * to gather data for each individual segment.
268 : */
269 88 : struct KeyframeValueEntry
270 : {
271 : nsCSSPropertyID mProperty;
272 : AnimationValue mValue;
273 :
274 : float mOffset;
275 : Maybe<ComputedTimingFunction> mTimingFunction;
276 : dom::CompositeOperation mComposite;
277 :
278 : struct PropertyOffsetComparator
279 : {
280 : static bool Equals(const KeyframeValueEntry& aLhs,
281 : const KeyframeValueEntry& aRhs)
282 : {
283 : return aLhs.mProperty == aRhs.mProperty &&
284 : aLhs.mOffset == aRhs.mOffset;
285 : }
286 8 : static bool LessThan(const KeyframeValueEntry& aLhs,
287 : const KeyframeValueEntry& aRhs)
288 : {
289 : // First, sort by property IDL name.
290 8 : int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
291 8 : nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
292 8 : if (order != 0) {
293 0 : return order < 0;
294 : }
295 :
296 : // Then, by offset.
297 8 : return aLhs.mOffset < aRhs.mOffset;
298 : }
299 : };
300 : };
301 :
302 : class ComputedOffsetComparator
303 : {
304 : public:
305 0 : static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs)
306 : {
307 0 : return aLhs.mComputedOffset == aRhs.mComputedOffset;
308 : }
309 :
310 0 : static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs)
311 : {
312 0 : return aLhs.mComputedOffset < aRhs.mComputedOffset;
313 : }
314 : };
315 :
316 : // ------------------------------------------------------------------
317 : //
318 : // Internal helper method declarations
319 : //
320 : // ------------------------------------------------------------------
321 :
322 : static void
323 : GetKeyframeListFromKeyframeSequence(JSContext* aCx,
324 : nsIDocument* aDocument,
325 : JS::ForOfIterator& aIterator,
326 : nsTArray<Keyframe>& aResult,
327 : ErrorResult& aRv);
328 :
329 : static bool
330 : ConvertKeyframeSequence(JSContext* aCx,
331 : nsIDocument* aDocument,
332 : JS::ForOfIterator& aIterator,
333 : nsTArray<Keyframe>& aResult);
334 :
335 : static bool
336 : GetPropertyValuesPairs(JSContext* aCx,
337 : JS::Handle<JSObject*> aObject,
338 : ListAllowance aAllowLists,
339 : StyleBackendType aBackend,
340 : nsTArray<PropertyValuesPair>& aResult);
341 :
342 : static bool
343 : AppendStringOrStringSequenceToArray(JSContext* aCx,
344 : JS::Handle<JS::Value> aValue,
345 : ListAllowance aAllowLists,
346 : nsTArray<nsString>& aValues);
347 :
348 : static bool
349 : AppendValueAsString(JSContext* aCx,
350 : nsTArray<nsString>& aValues,
351 : JS::Handle<JS::Value> aValue);
352 :
353 : static Maybe<PropertyValuePair>
354 : MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
355 : nsCSSParser& aParser, nsIDocument* aDocument);
356 :
357 : static bool
358 : HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
359 :
360 : #ifdef DEBUG
361 : static void
362 : MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
363 :
364 : static bool
365 : IsComputeValuesFailureKey(const PropertyValuePair& aPair);
366 : #endif
367 :
368 : static nsTArray<ComputedKeyframeValues>
369 : GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
370 : dom::Element* aElement,
371 : nsStyleContext* aStyleContext);
372 :
373 : static nsTArray<ComputedKeyframeValues>
374 : GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
375 : dom::Element* aElement,
376 : const ServoComputedValues* aComputedValues);
377 :
378 : static void
379 : BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
380 : nsTArray<AnimationProperty>& aResult);
381 :
382 : static void
383 : GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
384 : nsIDocument* aDocument,
385 : JS::Handle<JS::Value> aValue,
386 : nsTArray<Keyframe>& aResult,
387 : ErrorResult& aRv);
388 :
389 : static bool
390 : RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
391 : nsIDocument* aDocument);
392 :
393 : static void
394 : DistributeRange(const Range<Keyframe>& aRange);
395 :
396 : // ------------------------------------------------------------------
397 : //
398 : // Public API
399 : //
400 : // ------------------------------------------------------------------
401 :
402 : /* static */ nsTArray<Keyframe>
403 0 : KeyframeUtils::GetKeyframesFromObject(JSContext* aCx,
404 : nsIDocument* aDocument,
405 : JS::Handle<JSObject*> aFrames,
406 : ErrorResult& aRv)
407 : {
408 0 : MOZ_ASSERT(!aRv.Failed());
409 :
410 0 : nsTArray<Keyframe> keyframes;
411 :
412 0 : if (!aFrames) {
413 : // The argument was explicitly null meaning no keyframes.
414 0 : return keyframes;
415 : }
416 :
417 : // At this point we know we have an object. We try to convert it to a
418 : // sequence of keyframes first, and if that fails due to not being iterable,
419 : // we try to convert it to a property-indexed keyframe.
420 0 : JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
421 0 : JS::ForOfIterator iter(aCx);
422 0 : if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
423 0 : aRv.Throw(NS_ERROR_FAILURE);
424 0 : return keyframes;
425 : }
426 :
427 0 : if (iter.valueIsIterable()) {
428 0 : GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes, aRv);
429 : } else {
430 0 : GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
431 0 : keyframes, aRv);
432 : }
433 :
434 0 : if (aRv.Failed()) {
435 0 : MOZ_ASSERT(keyframes.IsEmpty(),
436 : "Should not set any keyframes when there is an error");
437 0 : return keyframes;
438 : }
439 :
440 0 : if (!AnimationUtils::IsCoreAPIEnabled() &&
441 0 : RequiresAdditiveAnimation(keyframes, aDocument)) {
442 0 : keyframes.Clear();
443 0 : aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
444 : }
445 :
446 0 : return keyframes;
447 : }
448 :
449 : /* static */ void
450 2 : KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes)
451 : {
452 2 : if (aKeyframes.IsEmpty()) {
453 0 : return;
454 : }
455 :
456 : // If the first keyframe has an unspecified offset, fill it in with 0%.
457 : // If there is only a single keyframe, then it gets 100%.
458 2 : if (aKeyframes.Length() > 1) {
459 2 : Keyframe& firstElement = aKeyframes[0];
460 2 : firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
461 : // We will fill in the last keyframe's offset below
462 : } else {
463 0 : Keyframe& lastElement = aKeyframes.LastElement();
464 0 : lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
465 : }
466 :
467 : // Fill in remaining missing offsets.
468 2 : const Keyframe* const last = &aKeyframes.LastElement();
469 2 : const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
470 2 : RangedPtr<Keyframe> keyframeA = begin;
471 6 : while (keyframeA != last) {
472 : // Find keyframe A and keyframe B *between* which we will apply spacing.
473 2 : RangedPtr<Keyframe> keyframeB = keyframeA + 1;
474 2 : while (keyframeB->mOffset.isNothing() && keyframeB != last) {
475 0 : ++keyframeB;
476 : }
477 2 : keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
478 :
479 : // Fill computed offsets in (keyframe A, keyframe B).
480 2 : DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
481 2 : keyframeA = keyframeB;
482 : }
483 : }
484 :
485 : template<typename StyleType>
486 : /* static */ nsTArray<AnimationProperty>
487 8 : KeyframeUtils::GetAnimationPropertiesFromKeyframes(
488 : const nsTArray<Keyframe>& aKeyframes,
489 : dom::Element* aElement,
490 : StyleType* aStyle,
491 : dom::CompositeOperation aEffectComposite)
492 : {
493 : const nsTArray<ComputedKeyframeValues> computedValues =
494 16 : GetComputedKeyframeValues(aKeyframes, aElement, aStyle);
495 8 : MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
496 : "Array length mismatch");
497 :
498 16 : nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
499 :
500 8 : const size_t len = aKeyframes.Length();
501 24 : for (size_t i = 0; i < len; ++i) {
502 16 : const Keyframe& frame = aKeyframes[i];
503 32 : for (auto& value : computedValues[i]) {
504 16 : MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
505 : "Invalid computed offset");
506 16 : KeyframeValueEntry* entry = entries.AppendElement();
507 16 : entry->mOffset = frame.mComputedOffset;
508 16 : entry->mProperty = value.mProperty;
509 16 : entry->mValue = value.mValue;
510 16 : entry->mTimingFunction = frame.mTimingFunction;
511 16 : entry->mComposite =
512 0 : frame.mComposite ? frame.mComposite.value() : aEffectComposite;
513 : }
514 : }
515 :
516 8 : nsTArray<AnimationProperty> result;
517 8 : BuildSegmentsFromValueEntries(entries, result);
518 16 : return result;
519 : }
520 :
521 : /* static */ bool
522 0 : KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty,
523 : StyleBackendType aBackend)
524 : {
525 : // Regardless of the backend type, treat the 'display' property as not
526 : // animatable. (The Servo backend will report it as being animatable, since
527 : // it is in fact animatable by SMIL.)
528 0 : if (aProperty == eCSSProperty_display) {
529 0 : return false;
530 : }
531 :
532 0 : if (aBackend == StyleBackendType::Servo) {
533 0 : return Servo_Property_IsAnimatable(aProperty);
534 : }
535 :
536 0 : if (aProperty == eCSSProperty_UNKNOWN) {
537 0 : return false;
538 : }
539 :
540 0 : if (!nsCSSProps::IsShorthand(aProperty)) {
541 0 : return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
542 : }
543 :
544 0 : CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
545 : CSSEnabledState::eForAllContent) {
546 0 : if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
547 0 : return true;
548 : }
549 : }
550 :
551 0 : return false;
552 : }
553 :
554 : /* static */ already_AddRefed<RawServoDeclarationBlock>
555 0 : KeyframeUtils::ParseProperty(nsCSSPropertyID aProperty,
556 : const nsAString& aValue,
557 : nsIDocument* aDocument)
558 : {
559 0 : MOZ_ASSERT(aDocument);
560 :
561 0 : NS_ConvertUTF16toUTF8 value(aValue);
562 : // FIXME this is using the wrong base uri (bug 1343919)
563 0 : RefPtr<URLExtraData> data = new URLExtraData(aDocument->GetDocumentURI(),
564 0 : aDocument->GetDocumentURI(),
565 0 : aDocument->NodePrincipal());
566 0 : return Servo_ParseProperty(aProperty,
567 : &value,
568 : data,
569 : ParsingMode::Default,
570 : aDocument->GetCompatibilityMode(),
571 0 : aDocument->CSSLoader()).Consume();
572 : }
573 :
574 : // ------------------------------------------------------------------
575 : //
576 : // Internal helpers
577 : //
578 : // ------------------------------------------------------------------
579 :
580 : /**
581 : * Converts a JS object to an IDL sequence<Keyframe>.
582 : *
583 : * @param aCx The JSContext corresponding to |aIterator|.
584 : * @param aDocument The document to use when parsing CSS properties.
585 : * @param aIterator An already-initialized ForOfIterator for the JS
586 : * object to iterate over as a sequence.
587 : * @param aResult The array into which the resulting Keyframe objects will be
588 : * appended.
589 : * @param aRv Out param to store any errors thrown by this function.
590 : */
591 : static void
592 0 : GetKeyframeListFromKeyframeSequence(JSContext* aCx,
593 : nsIDocument* aDocument,
594 : JS::ForOfIterator& aIterator,
595 : nsTArray<Keyframe>& aResult,
596 : ErrorResult& aRv)
597 : {
598 0 : MOZ_ASSERT(!aRv.Failed());
599 0 : MOZ_ASSERT(aResult.IsEmpty());
600 :
601 : // Convert the object in aIterator to a sequence of keyframes producing
602 : // an array of Keyframe objects.
603 0 : if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aResult)) {
604 0 : aResult.Clear();
605 0 : aRv.Throw(NS_ERROR_FAILURE);
606 0 : return;
607 : }
608 :
609 : // If the sequence<> had zero elements, we won't generate any
610 : // keyframes.
611 0 : if (aResult.IsEmpty()) {
612 0 : return;
613 : }
614 :
615 : // Check that the keyframes are loosely sorted and with values all
616 : // between 0% and 100%.
617 0 : if (!HasValidOffsets(aResult)) {
618 0 : aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
619 0 : aResult.Clear();
620 0 : return;
621 : }
622 : }
623 :
624 : /**
625 : * Converts a JS object wrapped by the given JS::ForIfIterator to an
626 : * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
627 : * aResult.
628 : */
629 : static bool
630 0 : ConvertKeyframeSequence(JSContext* aCx,
631 : nsIDocument* aDocument,
632 : JS::ForOfIterator& aIterator,
633 : nsTArray<Keyframe>& aResult)
634 : {
635 0 : JS::Rooted<JS::Value> value(aCx);
636 0 : nsCSSParser parser(aDocument->CSSLoader());
637 :
638 : for (;;) {
639 : bool done;
640 0 : if (!aIterator.next(&value, &done)) {
641 0 : return false;
642 : }
643 0 : if (done) {
644 0 : break;
645 : }
646 : // Each value found when iterating the object must be an object
647 : // or null/undefined (which gets treated as a default {} dictionary
648 : // value).
649 0 : if (!value.isObject() && !value.isNullOrUndefined()) {
650 : dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT,
651 0 : "Element of sequence<Keyframe> argument");
652 0 : return false;
653 : }
654 :
655 : // Convert the JS value into a BaseKeyframe dictionary value.
656 0 : dom::binding_detail::FastBaseKeyframe keyframeDict;
657 0 : if (!keyframeDict.Init(aCx, value,
658 : "Element of sequence<Keyframe> argument")) {
659 0 : return false;
660 : }
661 :
662 0 : Keyframe* keyframe = aResult.AppendElement(fallible);
663 0 : if (!keyframe) {
664 0 : return false;
665 : }
666 0 : if (!keyframeDict.mOffset.IsNull()) {
667 0 : keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
668 : }
669 :
670 0 : if (keyframeDict.mComposite.WasPassed()) {
671 0 : keyframe->mComposite.emplace(keyframeDict.mComposite.Value());
672 : }
673 :
674 0 : ErrorResult rv;
675 : keyframe->mTimingFunction =
676 0 : TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, rv);
677 0 : if (rv.MaybeSetPendingException(aCx)) {
678 0 : return false;
679 : }
680 :
681 : // Look for additional property-values pairs on the object.
682 0 : nsTArray<PropertyValuesPair> propertyValuePairs;
683 0 : if (value.isObject()) {
684 0 : JS::Rooted<JSObject*> object(aCx, &value.toObject());
685 0 : if (!GetPropertyValuesPairs(aCx, object,
686 : ListAllowance::eDisallow,
687 : aDocument->GetStyleBackendType(),
688 : propertyValuePairs)) {
689 0 : return false;
690 : }
691 : }
692 :
693 0 : for (PropertyValuesPair& pair : propertyValuePairs) {
694 0 : MOZ_ASSERT(pair.mValues.Length() == 1);
695 :
696 : Maybe<PropertyValuePair> valuePair =
697 0 : MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser,
698 0 : aDocument);
699 0 : if (!valuePair) {
700 0 : continue;
701 : }
702 0 : keyframe->mPropertyValues.AppendElement(Move(valuePair.ref()));
703 :
704 : #ifdef DEBUG
705 : // When we go to convert keyframes into arrays of property values we
706 : // call StyleAnimation::ComputeValues. This should normally return true
707 : // but in order to test the case where it does not, BaseKeyframeDict
708 : // includes a chrome-only member that can be set to indicate that
709 : // ComputeValues should fail for shorthand property values on that
710 : // keyframe.
711 0 : if (nsCSSProps::IsShorthand(pair.mProperty) &&
712 0 : keyframeDict.mSimulateComputeValuesFailure) {
713 0 : MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
714 : }
715 : #endif
716 : }
717 0 : }
718 :
719 0 : return true;
720 : }
721 :
722 : /**
723 : * Reads the property-values pairs from the specified JS object.
724 : *
725 : * @param aObject The JS object to look at.
726 : * @param aAllowLists If eAllow, values will be converted to
727 : * (DOMString or sequence<DOMString); if eDisallow, values
728 : * will be converted to DOMString.
729 : * @param aBackend The style backend in use. Used to determine which properties
730 : * are animatable since only animatable properties are read.
731 : * @param aResult The array into which the enumerated property-values
732 : * pairs will be stored.
733 : * @return false on failure or JS exception thrown while interacting
734 : * with aObject; true otherwise.
735 : */
736 : static bool
737 0 : GetPropertyValuesPairs(JSContext* aCx,
738 : JS::Handle<JSObject*> aObject,
739 : ListAllowance aAllowLists,
740 : StyleBackendType aBackend,
741 : nsTArray<PropertyValuesPair>& aResult)
742 : {
743 0 : nsTArray<AdditionalProperty> properties;
744 :
745 : // Iterate over all the properties on aObject and append an
746 : // entry to properties for them.
747 : //
748 : // We don't compare the jsids that we encounter with those for
749 : // the explicit dictionary members, since we know that none
750 : // of the CSS property IDL names clash with them.
751 0 : JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
752 0 : if (!JS_Enumerate(aCx, aObject, &ids)) {
753 0 : return false;
754 : }
755 0 : for (size_t i = 0, n = ids.length(); i < n; i++) {
756 0 : nsAutoJSString propName;
757 0 : if (!propName.init(aCx, ids[i])) {
758 0 : return false;
759 : }
760 : nsCSSPropertyID property =
761 : nsCSSProps::LookupPropertyByIDLName(propName,
762 0 : CSSEnabledState::eForAllContent);
763 0 : if (KeyframeUtils::IsAnimatableProperty(property, aBackend)) {
764 0 : AdditionalProperty* p = properties.AppendElement();
765 0 : p->mProperty = property;
766 0 : p->mJsidIndex = i;
767 : }
768 : }
769 :
770 : // Sort the entries by IDL name and then get each value and
771 : // convert it either to a DOMString or to a
772 : // (DOMString or sequence<DOMString>), depending on aAllowLists,
773 : // and build up aResult.
774 0 : properties.Sort(AdditionalProperty::PropertyComparator());
775 :
776 0 : for (AdditionalProperty& p : properties) {
777 0 : JS::Rooted<JS::Value> value(aCx);
778 0 : if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
779 0 : return false;
780 : }
781 0 : PropertyValuesPair* pair = aResult.AppendElement();
782 0 : pair->mProperty = p.mProperty;
783 0 : if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
784 : pair->mValues)) {
785 0 : return false;
786 : }
787 : }
788 :
789 0 : return true;
790 : }
791 :
792 : /**
793 : * Converts aValue to DOMString, if aAllowLists is eDisallow, or
794 : * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
795 : * The resulting strings are appended to aValues.
796 : */
797 : static bool
798 0 : AppendStringOrStringSequenceToArray(JSContext* aCx,
799 : JS::Handle<JS::Value> aValue,
800 : ListAllowance aAllowLists,
801 : nsTArray<nsString>& aValues)
802 : {
803 0 : if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
804 : // The value is an object, and we want to allow lists; convert
805 : // aValue to (DOMString or sequence<DOMString>).
806 0 : JS::ForOfIterator iter(aCx);
807 0 : if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
808 0 : return false;
809 : }
810 0 : if (iter.valueIsIterable()) {
811 : // If the object is iterable, convert it to sequence<DOMString>.
812 0 : JS::Rooted<JS::Value> element(aCx);
813 : for (;;) {
814 : bool done;
815 0 : if (!iter.next(&element, &done)) {
816 0 : return false;
817 : }
818 0 : if (done) {
819 0 : break;
820 : }
821 0 : if (!AppendValueAsString(aCx, aValues, element)) {
822 0 : return false;
823 : }
824 0 : }
825 0 : return true;
826 : }
827 : }
828 :
829 : // Either the object is not iterable, or aAllowLists doesn't want
830 : // a list; convert it to DOMString.
831 0 : if (!AppendValueAsString(aCx, aValues, aValue)) {
832 0 : return false;
833 : }
834 :
835 0 : return true;
836 : }
837 :
838 : /**
839 : * Converts aValue to DOMString and appends it to aValues.
840 : */
841 : static bool
842 0 : AppendValueAsString(JSContext* aCx,
843 : nsTArray<nsString>& aValues,
844 : JS::Handle<JS::Value> aValue)
845 : {
846 : return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
847 0 : *aValues.AppendElement());
848 : }
849 :
850 : static void
851 0 : ReportInvalidPropertyValueToConsole(nsCSSPropertyID aProperty,
852 : const nsAString& aInvalidPropertyValue,
853 : nsIDocument* aDoc)
854 : {
855 0 : const nsString& invalidValue = PromiseFlatString(aInvalidPropertyValue);
856 : const NS_ConvertASCIItoUTF16 propertyName(
857 0 : nsCSSProps::GetStringValue(aProperty));
858 0 : const char16_t* params[] = { invalidValue.get(), propertyName.get() };
859 0 : nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
860 0 : NS_LITERAL_CSTRING("Animation"),
861 : aDoc,
862 : nsContentUtils::eDOM_PROPERTIES,
863 : "InvalidKeyframePropertyValue",
864 0 : params, ArrayLength(params));
865 0 : }
866 :
867 : /**
868 : * Construct a PropertyValuePair parsing the given string into a suitable
869 : * nsCSSValue object.
870 : *
871 : * @param aProperty The CSS property.
872 : * @param aStringValue The property value to parse.
873 : * @param aParser The CSS parser object to use.
874 : * @param aDocument The document to use when parsing.
875 : * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
876 : * an invalid property value.
877 : */
878 : static Maybe<PropertyValuePair>
879 0 : MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
880 : nsCSSParser& aParser, nsIDocument* aDocument)
881 : {
882 0 : MOZ_ASSERT(aDocument);
883 0 : Maybe<PropertyValuePair> result;
884 :
885 0 : if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) {
886 : RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
887 0 : KeyframeUtils::ParseProperty(aProperty, aStringValue, aDocument);
888 :
889 0 : if (servoDeclarationBlock) {
890 0 : result.emplace(aProperty, Move(servoDeclarationBlock));
891 : } else {
892 0 : ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
893 : }
894 0 : return result;
895 : }
896 :
897 0 : nsCSSValue value;
898 0 : if (!nsCSSProps::IsShorthand(aProperty)) {
899 0 : aParser.ParseLonghandProperty(aProperty,
900 : aStringValue,
901 : aDocument->GetDocumentURI(),
902 : aDocument->GetDocumentURI(),
903 : aDocument->NodePrincipal(),
904 0 : value);
905 0 : if (value.GetUnit() == eCSSUnit_Null) {
906 : // Invalid property value, so return Nothing.
907 0 : ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
908 0 : return result;
909 : }
910 : }
911 :
912 0 : if (value.GetUnit() == eCSSUnit_Null) {
913 : // If we have a shorthand, store the string value as a token stream.
914 0 : nsCSSValueTokenStream* tokenStream = new nsCSSValueTokenStream;
915 0 : tokenStream->mTokenStream = aStringValue;
916 :
917 : // We are about to convert a null value to a token stream value but
918 : // by leaving the mPropertyID as unknown, we will be able to
919 : // distinguish between shorthand values and valid token stream values
920 : // (e.g. values with variable references).
921 0 : MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
922 : "The property of a token stream should be initialized"
923 : " to unknown");
924 :
925 : // By leaving mShorthandPropertyID as unknown, we ensure that when
926 : // we call nsCSSValue::AppendToString we get back the string stored
927 : // in mTokenStream.
928 0 : MOZ_ASSERT(tokenStream->mShorthandPropertyID == eCSSProperty_UNKNOWN,
929 : "The shorthand property of a token stream should be initialized"
930 : " to unknown");
931 0 : value.SetTokenStreamValue(tokenStream);
932 : }
933 :
934 0 : result.emplace(aProperty, Move(value));
935 0 : return result;
936 : }
937 :
938 : /**
939 : * Checks that the given keyframes are loosely ordered (each keyframe's
940 : * offset that is not null is greater than or equal to the previous
941 : * non-null offset) and that all values are within the range [0.0, 1.0].
942 : *
943 : * @return true if the keyframes' offsets are correctly ordered and
944 : * within range; false otherwise.
945 : */
946 : static bool
947 0 : HasValidOffsets(const nsTArray<Keyframe>& aKeyframes)
948 : {
949 0 : double offset = 0.0;
950 0 : for (const Keyframe& keyframe : aKeyframes) {
951 0 : if (keyframe.mOffset) {
952 0 : double thisOffset = keyframe.mOffset.value();
953 0 : if (thisOffset < offset || thisOffset > 1.0f) {
954 0 : return false;
955 : }
956 0 : offset = thisOffset;
957 : }
958 : }
959 0 : return true;
960 : }
961 :
962 : #ifdef DEBUG
963 : /**
964 : * Takes a property-value pair for a shorthand property and modifies the
965 : * value to indicate that when we call StyleAnimationValue::ComputeValues on
966 : * that value we should behave as if that function had failed.
967 : *
968 : * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
969 : * a shorthand property.
970 : */
971 : static void
972 0 : MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
973 : {
974 0 : MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
975 : "Only shorthand property values can be marked as failure values");
976 :
977 0 : aPair.mSimulateComputeValuesFailure = true;
978 0 : }
979 :
980 : /**
981 : * Returns true if |aPair| is a property-value pair on which we have
982 : * previously called MarkAsComputeValuesFailureKey (and hence we should
983 : * simulate failure when calling StyleAnimationValue::ComputeValues using its
984 : * value).
985 : *
986 : * @param aPair The property-value pair to test.
987 : * @return True if |aPair| represents a failure value.
988 : */
989 : static bool
990 0 : IsComputeValuesFailureKey(const PropertyValuePair& aPair)
991 : {
992 0 : return nsCSSProps::IsShorthand(aPair.mProperty) &&
993 0 : aPair.mSimulateComputeValuesFailure;
994 : }
995 : #endif
996 :
997 : /**
998 : * Calculate the StyleAnimationValues of properties of each keyframe.
999 : * This involves expanding shorthand properties into longhand properties,
1000 : * removing the duplicated properties for each keyframe, and creating an
1001 : * array of |property:computed value| pairs for each keyframe.
1002 : *
1003 : * These computed values are used when computing the final set of
1004 : * per-property animation values (see GetAnimationPropertiesFromKeyframes).
1005 : *
1006 : * @param aKeyframes The input keyframes.
1007 : * @param aElement The context element.
1008 : * @param aStyleContext The style context to use when computing values.
1009 : * @return The set of ComputedKeyframeValues. The length will be the same as
1010 : * aFrames.
1011 : */
1012 : static nsTArray<ComputedKeyframeValues>
1013 8 : GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
1014 : dom::Element* aElement,
1015 : nsStyleContext* aStyleContext)
1016 : {
1017 8 : MOZ_ASSERT(aStyleContext);
1018 8 : MOZ_ASSERT(aElement);
1019 :
1020 8 : const size_t len = aKeyframes.Length();
1021 8 : nsTArray<ComputedKeyframeValues> result(len);
1022 :
1023 24 : for (const Keyframe& frame : aKeyframes) {
1024 16 : nsCSSPropertyIDSet propertiesOnThisKeyframe;
1025 16 : ComputedKeyframeValues* computedValues = result.AppendElement();
1026 48 : for (const PropertyValuePair& pair :
1027 48 : PropertyPriorityIterator(frame.mPropertyValues)) {
1028 16 : MOZ_ASSERT(!pair.mServoDeclarationBlock,
1029 : "Animation values were parsed using Servo backend but target"
1030 : " element is not using Servo backend?");
1031 :
1032 : // Expand each value into the set of longhands and produce
1033 : // a KeyframeValueEntry for each value.
1034 32 : nsTArray<PropertyStyleAnimationValuePair> values;
1035 :
1036 : // For shorthands, we store the string as a token stream so we need to
1037 : // extract that first.
1038 16 : if (nsCSSProps::IsShorthand(pair.mProperty)) {
1039 0 : nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
1040 0 : if (!StyleAnimationValue::ComputeValues(pair.mProperty,
1041 : CSSEnabledState::eForAllContent, aElement, aStyleContext,
1042 : tokenStream->mTokenStream, /* aUseSVGMode */ false, values)) {
1043 0 : continue;
1044 : }
1045 :
1046 : #ifdef DEBUG
1047 0 : if (IsComputeValuesFailureKey(pair)) {
1048 0 : continue;
1049 : }
1050 : #endif
1051 16 : } else if (pair.mValue.GetUnit() == eCSSUnit_Null) {
1052 : // An uninitialized nsCSSValue represents the underlying value which
1053 : // we represent as an uninitialized AnimationValue so we just leave
1054 : // neutralPair->mValue as-is.
1055 0 : PropertyStyleAnimationValuePair* neutralPair = values.AppendElement();
1056 0 : neutralPair->mProperty = pair.mProperty;
1057 : } else {
1058 16 : if (!StyleAnimationValue::ComputeValues(pair.mProperty,
1059 : CSSEnabledState::eForAllContent, aElement, aStyleContext,
1060 : pair.mValue, /* aUseSVGMode */ false, values)) {
1061 0 : continue;
1062 : }
1063 16 : MOZ_ASSERT(values.Length() == 1,
1064 : "Longhand properties should produce a single"
1065 : " StyleAnimationValue");
1066 : }
1067 :
1068 32 : for (auto& value : values) {
1069 : // If we already got a value for this property on the keyframe,
1070 : // skip this one.
1071 16 : if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) {
1072 0 : continue;
1073 : }
1074 16 : propertiesOnThisKeyframe.AddProperty(value.mProperty);
1075 16 : computedValues->AppendElement(Move(value));
1076 : }
1077 : }
1078 : }
1079 :
1080 8 : MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
1081 8 : return result;
1082 : }
1083 :
1084 : /**
1085 : * The variation of the above function. This is for Servo backend.
1086 : */
1087 : static nsTArray<ComputedKeyframeValues>
1088 0 : GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
1089 : dom::Element* aElement,
1090 : const ServoComputedValues* aComputedValues)
1091 : {
1092 0 : MOZ_ASSERT(aElement);
1093 0 : MOZ_ASSERT(aElement->IsStyledByServo());
1094 :
1095 0 : nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
1096 0 : MOZ_ASSERT(presContext);
1097 :
1098 0 : return presContext->StyleSet()->AsServo()
1099 0 : ->GetComputedKeyframeValuesFor(aKeyframes, aElement, aComputedValues);
1100 : }
1101 :
1102 : static void
1103 0 : AppendInitialSegment(AnimationProperty* aAnimationProperty,
1104 : const KeyframeValueEntry& aFirstEntry)
1105 : {
1106 : AnimationPropertySegment* segment =
1107 0 : aAnimationProperty->mSegments.AppendElement();
1108 0 : segment->mFromKey = 0.0f;
1109 0 : segment->mToKey = aFirstEntry.mOffset;
1110 0 : segment->mToValue = aFirstEntry.mValue;
1111 0 : segment->mToComposite = aFirstEntry.mComposite;
1112 0 : }
1113 :
1114 : static void
1115 0 : AppendFinalSegment(AnimationProperty* aAnimationProperty,
1116 : const KeyframeValueEntry& aLastEntry)
1117 : {
1118 : AnimationPropertySegment* segment =
1119 0 : aAnimationProperty->mSegments.AppendElement();
1120 0 : segment->mFromKey = aLastEntry.mOffset;
1121 0 : segment->mFromValue = aLastEntry.mValue;
1122 0 : segment->mFromComposite = aLastEntry.mComposite;
1123 0 : segment->mToKey = 1.0f;
1124 0 : segment->mTimingFunction = aLastEntry.mTimingFunction;
1125 0 : }
1126 :
1127 : // Returns a newly created AnimationProperty if one was created to fill-in the
1128 : // missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
1129 : // becase we don't support additive animation).
1130 : static AnimationProperty*
1131 0 : HandleMissingInitialKeyframe(nsTArray<AnimationProperty>& aResult,
1132 : const KeyframeValueEntry& aEntry)
1133 : {
1134 0 : MOZ_ASSERT(aEntry.mOffset != 0.0f,
1135 : "The offset of the entry should not be 0.0");
1136 :
1137 : // If the preference of the core Web Animations API is not enabled, don't fill
1138 : // in the missing keyframe since the missing keyframe requires support for
1139 : // additive animation which is guarded by this pref.
1140 0 : if (!AnimationUtils::IsCoreAPIEnabled()){
1141 0 : return nullptr;
1142 : }
1143 :
1144 0 : AnimationProperty* result = aResult.AppendElement();
1145 0 : result->mProperty = aEntry.mProperty;
1146 :
1147 0 : AppendInitialSegment(result, aEntry);
1148 :
1149 0 : return result;
1150 : }
1151 :
1152 : static void
1153 0 : HandleMissingFinalKeyframe(nsTArray<AnimationProperty>& aResult,
1154 : const KeyframeValueEntry& aEntry,
1155 : AnimationProperty* aCurrentAnimationProperty)
1156 : {
1157 0 : MOZ_ASSERT(aEntry.mOffset != 1.0f,
1158 : "The offset of the entry should not be 1.0");
1159 :
1160 : // If the preference of the core Web Animations API is not enabled, don't fill
1161 : // in the missing keyframe since the missing keyframe requires support for
1162 : // additive animation which is guarded by this pref.
1163 0 : if (!AnimationUtils::IsCoreAPIEnabled()){
1164 : // If we have already appended a new entry for the property so we have to
1165 : // remove it.
1166 0 : if (aCurrentAnimationProperty) {
1167 0 : aResult.RemoveElementAt(aResult.Length() - 1);
1168 : }
1169 0 : return;
1170 : }
1171 :
1172 : // If |aCurrentAnimationProperty| is nullptr, that means this is the first
1173 : // entry for the property, we have to append a new AnimationProperty for this
1174 : // property.
1175 0 : if (!aCurrentAnimationProperty) {
1176 0 : aCurrentAnimationProperty = aResult.AppendElement();
1177 0 : aCurrentAnimationProperty->mProperty = aEntry.mProperty;
1178 :
1179 : // If we have only one entry whose offset is neither 1 nor 0 for this
1180 : // property, we need to append the initial segment as well.
1181 0 : if (aEntry.mOffset != 0.0f) {
1182 0 : AppendInitialSegment(aCurrentAnimationProperty, aEntry);
1183 : }
1184 : }
1185 0 : AppendFinalSegment(aCurrentAnimationProperty, aEntry);
1186 : }
1187 :
1188 : /**
1189 : * Builds an array of AnimationProperty objects to represent the keyframe
1190 : * animation segments in aEntries.
1191 : */
1192 : static void
1193 8 : BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
1194 : nsTArray<AnimationProperty>& aResult)
1195 : {
1196 8 : if (aEntries.IsEmpty()) {
1197 0 : return;
1198 : }
1199 :
1200 : // Sort the KeyframeValueEntry objects so that all entries for a given
1201 : // property are together, and the entries are sorted by offset otherwise.
1202 16 : std::stable_sort(aEntries.begin(), aEntries.end(),
1203 8 : &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
1204 :
1205 : // For a given index i, we want to generate a segment from aEntries[i]
1206 : // to aEntries[j], if:
1207 : //
1208 : // * j > i,
1209 : // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
1210 : // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
1211 : //
1212 : // That will eliminate runs of same offset/property values where there's no
1213 : // point generating zero length segments in the middle of the animation.
1214 : //
1215 : // Additionally we need to generate a zero length segment at offset 0 and at
1216 : // offset 1, if we have multiple values for a given property at that offset,
1217 : // since we need to retain the very first and very last value so they can
1218 : // be used for reverse and forward filling.
1219 : //
1220 : // Typically, for each property in |aEntries|, we expect there to be at least
1221 : // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
1222 : // However, since it is possible that when building |aEntries|, the call to
1223 : // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
1224 : // Furthermore, if additive animation is disabled, the following loop takes
1225 : // care to identify properties that lack a value at offset 0.0/1.0 and drops
1226 : // those properties from |aResult|.
1227 :
1228 8 : nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
1229 8 : AnimationProperty* animationProperty = nullptr;
1230 :
1231 8 : size_t i = 0, n = aEntries.Length();
1232 :
1233 24 : while (i < n) {
1234 : // If we've reached the end of the array of entries, synthesize a final (and
1235 : // initial) segment if necessary.
1236 16 : if (i + 1 == n) {
1237 8 : if (aEntries[i].mOffset != 1.0f) {
1238 0 : HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
1239 8 : } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
1240 : // If the last entry with offset 1 and no animation property, that means
1241 : // it is the only entry for this property so append a single segment
1242 : // from 0 offset to |aEntry[i].offset|.
1243 0 : Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
1244 : }
1245 8 : animationProperty = nullptr;
1246 8 : break;
1247 : }
1248 :
1249 8 : MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
1250 : aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
1251 : "Each entry should specify a valid property");
1252 :
1253 : // No keyframe for this property at offset 0.
1254 16 : if (aEntries[i].mProperty != lastProperty &&
1255 8 : aEntries[i].mOffset != 0.0f) {
1256 : // If we don't support additive animation we can't fill in the missing
1257 : // keyframes and we should just skip this property altogether. Since the
1258 : // entries are sorted by offset for a given property, and since we don't
1259 : // update |lastProperty|, we will keep hitting this condition until we
1260 : // change property.
1261 0 : animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
1262 0 : if (animationProperty) {
1263 0 : lastProperty = aEntries[i].mProperty;
1264 : } else {
1265 : // Skip this entry if we did not handle the missing entry.
1266 0 : ++i;
1267 0 : continue;
1268 : }
1269 : }
1270 :
1271 : // Skip this entry if the next entry has the same offset except for initial
1272 : // and final ones. We will handle missing keyframe in the next loop
1273 : // if the property is changed on the next entry.
1274 24 : if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
1275 8 : aEntries[i].mOffset == aEntries[i + 1].mOffset &&
1276 8 : aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
1277 0 : ++i;
1278 0 : continue;
1279 : }
1280 :
1281 : // No keyframe for this property at offset 1.
1282 8 : if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
1283 0 : aEntries[i].mOffset != 1.0f) {
1284 0 : HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
1285 : // Move on to new property.
1286 0 : animationProperty = nullptr;
1287 0 : ++i;
1288 0 : continue;
1289 : }
1290 :
1291 : // Starting from i + 1, determine the next [i, j] interval from which to
1292 : // generate a segment. Basically, j is i + 1, but there are some special
1293 : // cases for offset 0 and 1, so we need to handle them specifically.
1294 : // Note: From this moment, we make sure [i + 1] is valid and
1295 : // there must be an initial entry (i.e. mOffset = 0.0) and
1296 : // a final entry (i.e. mOffset = 1.0). Besides, all the entries
1297 : // with the same offsets except for initial/final ones are filtered
1298 : // out already.
1299 8 : size_t j = i + 1;
1300 8 : if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
1301 : // We need to generate an initial zero-length segment.
1302 0 : MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
1303 0 : while (j + 1 < n &&
1304 0 : aEntries[j + 1].mOffset == 0.0f &&
1305 0 : aEntries[j + 1].mProperty == aEntries[j].mProperty) {
1306 0 : ++j;
1307 : }
1308 8 : } else if (aEntries[i].mOffset == 1.0f) {
1309 0 : if (aEntries[i + 1].mOffset == 1.0f &&
1310 0 : aEntries[i + 1].mProperty == aEntries[i].mProperty) {
1311 : // We need to generate a final zero-length segment.
1312 0 : while (j + 1 < n &&
1313 0 : aEntries[j + 1].mOffset == 1.0f &&
1314 0 : aEntries[j + 1].mProperty == aEntries[j].mProperty) {
1315 0 : ++j;
1316 : }
1317 : } else {
1318 : // New property.
1319 0 : MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
1320 0 : animationProperty = nullptr;
1321 0 : ++i;
1322 0 : continue;
1323 : }
1324 : }
1325 :
1326 : // If we've moved on to a new property, create a new AnimationProperty
1327 : // to insert segments into.
1328 8 : if (aEntries[i].mProperty != lastProperty) {
1329 8 : MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
1330 8 : MOZ_ASSERT(!animationProperty);
1331 8 : animationProperty = aResult.AppendElement();
1332 8 : animationProperty->mProperty = aEntries[i].mProperty;
1333 8 : lastProperty = aEntries[i].mProperty;
1334 : }
1335 :
1336 8 : MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
1337 :
1338 : // Now generate the segment.
1339 : AnimationPropertySegment* segment =
1340 8 : animationProperty->mSegments.AppendElement();
1341 8 : segment->mFromKey = aEntries[i].mOffset;
1342 8 : segment->mToKey = aEntries[j].mOffset;
1343 8 : segment->mFromValue = aEntries[i].mValue;
1344 8 : segment->mToValue = aEntries[j].mValue;
1345 8 : segment->mTimingFunction = aEntries[i].mTimingFunction;
1346 8 : segment->mFromComposite = aEntries[i].mComposite;
1347 8 : segment->mToComposite = aEntries[j].mComposite;
1348 :
1349 8 : i = j;
1350 : }
1351 : }
1352 :
1353 : /**
1354 : * Converts a JS object representing a property-indexed keyframe into
1355 : * an array of Keyframe objects.
1356 : *
1357 : * @param aCx The JSContext for |aValue|.
1358 : * @param aDocument The document to use when parsing CSS properties.
1359 : * @param aValue The JS object.
1360 : * @param aResult The array into which the resulting AnimationProperty
1361 : * objects will be appended.
1362 : * @param aRv Out param to store any errors thrown by this function.
1363 : */
1364 : static void
1365 0 : GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
1366 : nsIDocument* aDocument,
1367 : JS::Handle<JS::Value> aValue,
1368 : nsTArray<Keyframe>& aResult,
1369 : ErrorResult& aRv)
1370 : {
1371 0 : MOZ_ASSERT(aValue.isObject());
1372 0 : MOZ_ASSERT(aResult.IsEmpty());
1373 0 : MOZ_ASSERT(!aRv.Failed());
1374 :
1375 : // Convert the object to a property-indexed keyframe dictionary to
1376 : // get its explicit dictionary members.
1377 0 : dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
1378 0 : if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
1379 : false)) {
1380 0 : aRv.Throw(NS_ERROR_FAILURE);
1381 0 : return;
1382 : }
1383 :
1384 0 : Maybe<dom::CompositeOperation> composite;
1385 0 : if (keyframeDict.mComposite.WasPassed()) {
1386 0 : composite.emplace(keyframeDict.mComposite.Value());
1387 : }
1388 :
1389 : Maybe<ComputedTimingFunction> easing =
1390 0 : TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv);
1391 0 : if (aRv.Failed()) {
1392 0 : return;
1393 : }
1394 :
1395 : // Get all the property--value-list pairs off the object.
1396 0 : JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
1397 0 : nsTArray<PropertyValuesPair> propertyValuesPairs;
1398 0 : if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
1399 : aDocument->GetStyleBackendType(),
1400 : propertyValuesPairs)) {
1401 0 : aRv.Throw(NS_ERROR_FAILURE);
1402 0 : return;
1403 : }
1404 :
1405 : // Create a set of keyframes for each property.
1406 0 : nsCSSParser parser(aDocument->CSSLoader());
1407 0 : nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
1408 0 : for (const PropertyValuesPair& pair : propertyValuesPairs) {
1409 0 : size_t count = pair.mValues.Length();
1410 0 : if (count == 0) {
1411 : // No animation values for this property.
1412 0 : continue;
1413 : }
1414 :
1415 : // If we only have one value, we should animate from the underlying value
1416 : // using additive animation--however, we don't support additive animation
1417 : // when the core animation API pref is switched off.
1418 0 : if ((!AnimationUtils::IsCoreAPIEnabled()) &&
1419 : count == 1) {
1420 0 : aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
1421 0 : return;
1422 : }
1423 :
1424 0 : size_t n = pair.mValues.Length() - 1;
1425 0 : size_t i = 0;
1426 :
1427 0 : for (const nsString& stringValue : pair.mValues) {
1428 : // For single-valued lists, the single value should be added to a
1429 : // keyframe with offset 1.
1430 0 : double offset = n ? i++ / double(n) : 1;
1431 0 : Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
1432 0 : if (keyframe->mPropertyValues.IsEmpty()) {
1433 0 : keyframe->mTimingFunction = easing;
1434 0 : keyframe->mComposite = composite;
1435 0 : keyframe->mComputedOffset = offset;
1436 : }
1437 :
1438 : Maybe<PropertyValuePair> valuePair =
1439 0 : MakePropertyValuePair(pair.mProperty, stringValue, parser, aDocument);
1440 0 : if (!valuePair) {
1441 0 : continue;
1442 : }
1443 0 : keyframe->mPropertyValues.AppendElement(Move(valuePair.ref()));
1444 : }
1445 : }
1446 :
1447 0 : aResult.SetCapacity(processedKeyframes.Count());
1448 0 : for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) {
1449 0 : aResult.AppendElement(Move(*iter.UserData()));
1450 : }
1451 :
1452 0 : aResult.Sort(ComputedOffsetComparator());
1453 : }
1454 :
1455 : /**
1456 : * Returns true if the supplied set of keyframes has keyframe values for
1457 : * any property for which it does not also supply a value for the 0% and 100%
1458 : * offsets. In this case we are supposed to synthesize an additive zero value
1459 : * but since we don't support additive animation yet we can't support this
1460 : * case. We try to detect that here so we can throw an exception. The check is
1461 : * not entirely accurate but should detect most common cases.
1462 : *
1463 : * @param aKeyframes The set of keyframes to analyze.
1464 : * @param aDocument The document to use when parsing keyframes so we can
1465 : * try to detect where we have an invalid value at 0%/100%.
1466 : */
1467 : static bool
1468 0 : RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
1469 : nsIDocument* aDocument)
1470 : {
1471 : // We are looking to see if that every property referenced in |aKeyframes|
1472 : // has a valid property at offset 0.0 and 1.0. The check as to whether a
1473 : // property is valid or not, however, is not precise. We only check if the
1474 : // property can be parsed, NOT whether it can also be converted to a
1475 : // StyleAnimationValue since doing that requires a target element bound to
1476 : // a document which we might not always have at the point where we want to
1477 : // perform this check.
1478 : //
1479 : // This is only a temporary measure until we implement additive animation.
1480 : // So as long as this check catches most cases, and we don't do anything
1481 : // horrible in one of the cases we can't detect, it should be sufficient.
1482 :
1483 0 : nsCSSPropertyIDSet properties; // All properties encountered.
1484 0 : nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
1485 0 : nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value.
1486 :
1487 0 : auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
1488 0 : properties.AddProperty(aProperty);
1489 0 : if (aOffset == 0.0) {
1490 0 : propertiesWithFromValue.AddProperty(aProperty);
1491 0 : } else if (aOffset == 1.0) {
1492 0 : propertiesWithToValue.AddProperty(aProperty);
1493 : }
1494 0 : };
1495 :
1496 0 : StyleBackendType styleBackend = aDocument->GetStyleBackendType();
1497 :
1498 0 : for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
1499 0 : const Keyframe& frame = aKeyframes[i];
1500 :
1501 : // We won't have called DistributeKeyframes when this is called so
1502 : // we can't use frame.mComputedOffset. Instead we do a rough version
1503 : // of that algorithm that substitutes null offsets with 0.0 for the first
1504 : // frame, 1.0 for the last frame, and 0.5 for everything else.
1505 0 : double computedOffset = i == len - 1
1506 0 : ? 1.0
1507 0 : : i == 0 ? 0.0 : 0.5;
1508 : double offsetToUse = frame.mOffset
1509 0 : ? frame.mOffset.value()
1510 0 : : computedOffset;
1511 :
1512 0 : for (const PropertyValuePair& pair : frame.mPropertyValues) {
1513 0 : if (nsCSSProps::IsShorthand(pair.mProperty)) {
1514 0 : if (styleBackend == StyleBackendType::Gecko) {
1515 : nsCSSValueTokenStream* tokenStream =
1516 0 : pair.mValue.GetTokenStreamValue();
1517 0 : nsCSSParser parser(aDocument->CSSLoader());
1518 0 : if (!parser.IsValueValidForProperty(pair.mProperty,
1519 : tokenStream->mTokenStream)) {
1520 0 : continue;
1521 : }
1522 : }
1523 :
1524 0 : MOZ_ASSERT(styleBackend != StyleBackendType::Servo ||
1525 : pair.mServoDeclarationBlock);
1526 0 : CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
1527 : prop, pair.mProperty, CSSEnabledState::eForAllContent) {
1528 0 : addToPropertySets(*prop, offsetToUse);
1529 : }
1530 : } else {
1531 0 : addToPropertySets(pair.mProperty, offsetToUse);
1532 : }
1533 : }
1534 : }
1535 :
1536 0 : return !propertiesWithFromValue.Equals(properties) ||
1537 0 : !propertiesWithToValue.Equals(properties);
1538 : }
1539 :
1540 : /**
1541 : * Distribute the offsets of all keyframes in between the endpoints of the
1542 : * given range.
1543 : *
1544 : * @param aRange The sequence of keyframes between whose endpoints we should
1545 : * distribute offsets.
1546 : */
1547 : static void
1548 2 : DistributeRange(const Range<Keyframe>& aRange)
1549 : {
1550 4 : const Range<Keyframe> rangeToAdjust = Range<Keyframe>(aRange.begin() + 1,
1551 6 : aRange.end() - 1);
1552 2 : const size_t n = aRange.length() - 1;
1553 2 : const double startOffset = aRange[0].mComputedOffset;
1554 2 : const double diffOffset = aRange[n].mComputedOffset - startOffset;
1555 2 : for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
1556 0 : size_t index = iter - aRange.begin();
1557 0 : iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
1558 : }
1559 2 : }
1560 :
1561 : } // namespace mozilla
|