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