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