From 9ae14fcec8f236467a8235d64c0042cd5e6ef681 Mon Sep 17 00:00:00 2001 From: roytam1 Date: Fri, 29 Mar 2024 09:42:12 +0800 Subject: [PATCH] import changes from `dev' branch of rmottola/Arctic-Fox: - Bug 1003204: Removed CommonUtils.exceptionStr() in toolkit/ r=gfritzsche (d1c2efa08f) - Bug 1243936 - Convert remaining callsites within devtools/ and toolkit/ to use channel.open2() (54f398eb47) - Bug 1187270 - Add Telemetry session ID to crash annotations, r=gfritzsche (6c08170c5a) - Bug 1249219 - Part 4: Use NonOwningAnimationTarget as the returned value of some animation target getters. r=birtles (df788abe39) - Bug 1249219 - Part 5: Add a wrapper of AnimationAdded/Changed/Removed. r=birtles (58cf3a3ce2) - Bug 1249219 - Part 6: Support pseudo elements in Animation Mutation Observer. r=heycam (d10c901821) - Bug 1249219 - Part 7: Test. r=birtles (e2b78422be) - add back some utils of Bug 751291 used in tests (8fd2cc847f) - Bug 1253470 - Part 1: Produce console warnings for invalid duration. r=birtles (a0491eeab4) - Bug 1253470 - Part 2: Produce console warnings for invalid iterationStart. r=birtles (6a910650c9) - Bug 1253470 - Part 3: Produce console warnings for invalid iterations. r=birtles (e3210e754e) - Bug 1253470 - Part 4: Produce console warnings for invalid easing. r=birtles (fc1868b3c0) - Bug 1245748 - Move ComputedTiming to a separate file; r=heycam (22a76e4f03) - Bug 1245748 - Rename Keyframe-related IDL types to match changes to Web Animations spec; r=heycam, r=bz (e79338bafd) - Bug 1245748 - Update handling of 'composite' dictionary members to match changes to the spec; r=heycam, r=bz (d9cc71cde8) - Bug 1245748 - Define the Keyframe type for storing specified keyframes; r=heycam (a429e2bf46) - Bug 1245748 - Add missing includes to TimingParams.{cpp,h}; r=heycam (3e1e121c6f) - Bug 1245748 - Move keyframe handling code to a separate KeyframeUtils class; r=heycam (e359f26244) - Bug 1245748 - Add KeyframeUtils::GetKeyframesFromObject; r=heycam (eda69445d7) - Bug 1245748 - Add nsStyleContext parameter to StyleAnimationValue::ComputeValue(s); r=heycam (2c22b9926c) - Bug 1245748 - Add a variant of StyleAnimationValue::ComputeValues that takes an nsCSSValue; r=heycam (12386559dd) - Bug 1245748 - Split PropertyPriorityComparator into a separate (reusable) class; r=heycam (132394bf45) - Bug 1245748 - Add PropertyPriorityIterator; r=heycam (bfef46fd12) - Bug 1245748 - Add GetAnimationPropertiesFromKeyframes; r=heycam (4681ac8407) - Bug 1245748 - Add ApplyDistributeSpacing for Keyframe objects; r=heycam (9c0bc885c9) - Bug 1245748 - Use Keyframe-based utility functions when constructing KeyframeEffect(ReadOnly); r=heycam (e0b7460548) - Bug 1229859 - Introduce new import-globals-from eslint rule to import globals from other modules; r=Mossop (10075a136c) --- dom/animation/Animation.cpp | 18 +- dom/animation/AnimationUtils.cpp | 61 - dom/animation/ComputedTiming.h | 74 + dom/animation/KeyframeEffect.cpp | 945 +-------- dom/animation/KeyframeEffect.h | 113 +- dom/animation/KeyframeUtils.cpp | 1713 +++++++++++++++++ dom/animation/KeyframeUtils.h | 104 + dom/animation/TimingParams.cpp | 76 +- dom/animation/TimingParams.h | 26 +- dom/animation/moz.build | 3 + .../test/chrome/test_animation_observers.html | 163 +- .../file_keyframeeffect-getframes.html | 58 +- .../file_keyframeeffect-getframes.html | 12 +- dom/base/nsDOMMutationObserver.cpp | 26 +- dom/base/nsDOMMutationObserver.h | 2 + dom/base/nsDOMWindowUtils.cpp | 16 +- dom/base/nsNodeUtils.cpp | 73 +- dom/base/nsNodeUtils.h | 30 +- dom/bindings/Errors.msg | 2 + dom/smil/nsSMILCSSValueType.cpp | 14 +- dom/webidl/BaseKeyframeTypes.webidl | 35 + dom/webidl/Keyframe.webidl | 25 - dom/webidl/PropertyIndexedKeyframes.webidl | 18 - dom/webidl/moz.build | 3 +- layout/style/StyleAnimationValue.cpp | 263 +-- layout/style/StyleAnimationValue.h | 30 +- services/crypto/modules/utils.js | 30 + .../docs/import-globals-from.rst | 18 + .../lib/rules/import-globals-from.js | 78 + .../keyframe-effect/constructor.html | 272 +-- .../keyframe-effect/getComputedTiming.html | 14 +- .../addoncompat/RemoteAddonsParent.jsm | 2 +- toolkit/components/crashes/CrashManager.jsm | 1 - .../components/telemetry/TelemetrySession.jsm | 16 + toolkit/crashreporter/nsExceptionHandler.cpp | 24 + toolkit/crashreporter/nsExceptionHandler.h | 1 + .../test/unit/test_crashreporter_crash.js | 5 + .../test/unit/test_event_files.js | 6 + toolkit/devtools/server/actors/settings.js | 2 +- toolkit/modules/Sqlite.jsm | 29 +- toolkit/xre/nsAppRunner.cpp | 7 + xpcom/system/nsICrashReporter.idl | 7 + 42 files changed, 2888 insertions(+), 1527 deletions(-) create mode 100644 dom/animation/ComputedTiming.h create mode 100644 dom/animation/KeyframeUtils.cpp create mode 100644 dom/animation/KeyframeUtils.h create mode 100644 dom/webidl/BaseKeyframeTypes.webidl delete mode 100644 dom/webidl/Keyframe.webidl delete mode 100644 dom/webidl/PropertyIndexedKeyframes.webidl create mode 100644 testing/eslint-plugin-mozilla/docs/import-globals-from.rst create mode 100644 testing/eslint-plugin-mozilla/lib/rules/import-globals-from.js diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index b113aae924..b46189f770 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -11,6 +11,7 @@ #include "mozilla/AutoRestore.h" #include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher #include "mozilla/Maybe.h" // For Maybe +#include "mozilla/NonOwningAnimationTarget.h" #include "nsAnimationManager.h" // For CSSAnimation #include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch #include "nsIDocument.h" // For nsIDocument @@ -58,13 +59,14 @@ namespace { explicit AutoMutationBatchForAnimation(const Animation& aAnimation MOZ_GUARD_OBJECT_NOTIFIER_PARAM) { MOZ_GUARD_OBJECT_NOTIFIER_INIT; - Element* targetElement = nsNodeUtils::GetTargetForAnimation(&aAnimation); - if (!targetElement) { + Maybe target = + nsNodeUtils::GetTargetForAnimation(&aAnimation); + if (!target) { return; } // For mutation observers, we use the OwnerDoc. - nsIDocument* doc = targetElement->OwnerDoc(); + nsIDocument* doc = target->mElement->OwnerDoc(); if (!doc) { return; } @@ -1106,16 +1108,14 @@ Animation::PostUpdate() return; } - Element* targetElement; - CSSPseudoElementType targetPseudoType; - mEffect->GetTarget(targetElement, targetPseudoType); - if (!targetElement) { + Maybe target = mEffect->GetTarget(); + if (!target) { return; } presContext->EffectCompositor() - ->RequestRestyle(targetElement, - targetPseudoType, + ->RequestRestyle(target->mElement, + target->mPseudoType, EffectCompositor::RestyleType::Layer, CascadeLevel()); } diff --git a/dom/animation/AnimationUtils.cpp b/dom/animation/AnimationUtils.cpp index bab8bfa1f6..26679b80b1 100644 --- a/dom/animation/AnimationUtils.cpp +++ b/dom/animation/AnimationUtils.cpp @@ -6,15 +6,12 @@ #include "AnimationUtils.h" -#include "nsCSSParser.h" // For nsCSSParser #include "nsDebug.h" #include "nsIAtom.h" #include "nsIContent.h" #include "nsIDocument.h" #include "nsGlobalWindow.h" #include "nsString.h" -#include "mozilla/Attributes.h" -#include "mozilla/ComputedTimingFunction.h" // ComputedTimingFunction #include "xpcpublic.h" // For xpc::NativeGlobal namespace mozilla { @@ -39,64 +36,6 @@ AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage, printf_stderr("%s", aMessage.get()); } -/* static */ Maybe -AnimationUtils::ParseEasing(const nsAString& aEasing, - nsIDocument* aDocument) -{ - MOZ_ASSERT(aDocument); - - nsCSSValue value; - nsCSSParser parser; - parser.ParseLonghandProperty(eCSSProperty_animation_timing_function, - aEasing, - aDocument->GetDocumentURI(), - aDocument->GetDocumentURI(), - aDocument->NodePrincipal(), - value); - - switch (value.GetUnit()) { - case eCSSUnit_List: { - const nsCSSValueList* list = value.GetListValue(); - if (list->mNext) { - // don't support a list of timing functions - break; - } - switch (list->mValue.GetUnit()) { - case eCSSUnit_Enumerated: - // Return Nothing() if "linear" is passed in. - if (list->mValue.GetIntValue() == - NS_STYLE_TRANSITION_TIMING_FUNCTION_LINEAR) { - return Nothing(); - } - MOZ_FALLTHROUGH; - case eCSSUnit_Cubic_Bezier: - case eCSSUnit_Steps: { - nsTimingFunction timingFunction; - nsRuleNode::ComputeTimingFunction(list->mValue, timingFunction); - ComputedTimingFunction computedTimingFunction; - computedTimingFunction.Init(timingFunction); - return Some(computedTimingFunction); - } - default: - MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function list " - "item unit"); - break; - } - break; - } - case eCSSUnit_Null: - case eCSSUnit_Inherit: - case eCSSUnit_Initial: - case eCSSUnit_Unset: - case eCSSUnit_TokenStream: - break; - default: - MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function unit"); - break; - } - return Nothing(); -} - /* static */ nsIDocument* AnimationUtils::GetCurrentRealmDocument(JSContext* aCx) { diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h new file mode 100644 index 0000000000..13b902f0d4 --- /dev/null +++ b/dom/animation/ComputedTiming.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_ComputedTiming_h +#define mozilla_ComputedTiming_h + +#include "mozilla/dom/Nullable.h" +#include "mozilla/StickyTimeDuration.h" + +// X11 has a #define for None +#ifdef None +#undef None +#endif +#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // FillMode + +namespace mozilla { + +/** + * Stores the results of calculating the timing properties of an animation + * at a given sample time. + */ +struct ComputedTiming +{ + // The total duration of the animation including all iterations. + // Will equal StickyTimeDuration::Forever() if the animation repeats + // indefinitely. + StickyTimeDuration mActiveDuration; + // The effect end time in local time (i.e. an offset from the effect's + // start time). Will equal StickyTimeDuration::Forever() if the animation + // plays indefinitely. + StickyTimeDuration mEndTime; + // Progress towards the end of the current iteration. If the effect is + // being sampled backwards, this will go from 1.0 to 0.0. + // Will be null if the animation is neither animating nor + // filling at the sampled time. + Nullable mProgress; + // Zero-based iteration index (meaningless if mProgress is null). + uint64_t mCurrentIteration = 0; + // Unlike TimingParams::mIterations, this value is + // guaranteed to be in the range [0, Infinity]. + double mIterations = 1.0; + double mIterationStart = 0.0; + StickyTimeDuration mDuration; + + // This is the computed fill mode so it is never auto + dom::FillMode mFill = dom::FillMode::None; + bool FillsForwards() const { + MOZ_ASSERT(mFill != dom::FillMode::Auto, + "mFill should not be Auto in ComputedTiming."); + return mFill == dom::FillMode::Both || + mFill == dom::FillMode::Forwards; + } + bool FillsBackwards() const { + MOZ_ASSERT(mFill != dom::FillMode::Auto, + "mFill should not be Auto in ComputedTiming."); + return mFill == dom::FillMode::Both || + mFill == dom::FillMode::Backwards; + } + + enum class AnimationPhase { + Null, // Not sampled (null sample time) + Before, // Sampled prior to the start of the active interval + Active, // Sampled within the active interval + After // Sampled after (or at) the end of the active interval + }; + AnimationPhase mPhase = AnimationPhase::Null; +}; + +} // namespace mozilla + +#endif // mozilla_ComputedTiming_h diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp index 663e0c143e..bb9f788e6c 100644 --- a/dom/animation/KeyframeEffect.cpp +++ b/dom/animation/KeyframeEffect.cpp @@ -8,20 +8,18 @@ #include "mozilla/dom/AnimatableBinding.h" #include "mozilla/dom/KeyframeEffectBinding.h" -#include "mozilla/dom/PropertyIndexedKeyframesBinding.h" #include "mozilla/AnimationUtils.h" #include "mozilla/EffectCompositor.h" #include "mozilla/FloatingPoint.h" #include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt +#include "mozilla/KeyframeUtils.h" #include "mozilla/StyleAnimationValue.h" #include "Layers.h" // For Layer -#include "nsCSSParser.h" +#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement #include "nsCSSPropertySet.h" #include "nsCSSProps.h" // For nsCSSProps::PropHasFlags -#include "nsCSSPseudoElements.h" -#include "nsCSSValue.h" +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType #include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch -#include "nsStyleUtil.h" #include // For std::max namespace mozilla { @@ -708,13 +706,32 @@ KeyframeEffectReadOnly::ConstructKeyframeEffect( return nullptr; } - InfallibleTArray animationProperties; - BuildAnimationPropertyList(aGlobal.Context(), targetElement, pseudoType, - aFrames, animationProperties, aRv); - + nsTArray keyframes = + KeyframeUtils::GetKeyframesFromObject(aGlobal.Context(), aFrames, aRv); if (aRv.Failed()) { return nullptr; } + KeyframeUtils::ApplyDistributeSpacing(keyframes); + + RefPtr styleContext; + nsIPresShell* shell = doc->GetShell(); + if (shell && targetElement) { + nsIAtom* pseudo = + pseudoType < CSSPseudoElementType::Count ? + nsCSSPseudoElements::GetPseudoAtom(pseudoType) : nullptr; + styleContext = + nsComputedDOMStyle::GetStyleContextForElement(targetElement, pseudo, + shell); + } + + nsTArray animationProperties; + if (styleContext) { + animationProperties = + KeyframeUtils::GetAnimationPropertiesFromKeyframes(styleContext, + targetElement, + pseudoType, + keyframes); + } RefPtr effect = new KeyframeEffectType(targetElement->OwnerDoc(), targetElement, @@ -806,7 +823,7 @@ enum class ValuePosition /** * A single value in a keyframe animation, used by GetFrames to produce a - * minimal set of Keyframe objects. + * minimal set of keyframe objects. */ struct OrderedKeyframeValueEntry : KeyframeValue { @@ -868,898 +885,6 @@ struct OrderedKeyframeValueEntry : KeyframeValue }; }; -/** - * Data for a segment in a keyframe animation of a given property - * whose value is a StyleAnimationValue. - * - * KeyframeValueEntry is used in BuildAnimationPropertyListFromKeyframeSequence - * to gather data for each individual segment described by an author-supplied - * an IDL sequence value so that they can be parsed into mProperties. - */ -struct KeyframeValueEntry : KeyframeValue -{ - float mOffset; - Maybe mTimingFunction; - - struct PropertyOffsetComparator - { - static bool Equals(const KeyframeValueEntry& aLhs, - const KeyframeValueEntry& aRhs) - { - return aLhs.mProperty == aRhs.mProperty && - aLhs.mOffset == aRhs.mOffset; - } - static bool LessThan(const KeyframeValueEntry& aLhs, - const KeyframeValueEntry& aRhs) - { - // First, sort by property IDL name. - int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) - - nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); - if (order != 0) { - return order < 0; - } - - // Then, by offset. - return aLhs.mOffset < aRhs.mOffset; - } - }; -}; - -/** - * A property-values pair obtained from the open-ended properties - * discovered on a Keyframe or PropertyIndexedKeyframes object. - * - * Single values (as required by Keyframe, and as also supported - * on PropertyIndexedKeyframes) are stored as the only element in - * mValues. - */ -struct PropertyValuesPair -{ - nsCSSProperty mProperty; - nsTArray mValues; - - class PropertyPriorityComparator - { - public: - PropertyPriorityComparator() - : mSubpropertyCountInitialized(false) {} - - bool Equals(const PropertyValuesPair& aLhs, - const PropertyValuesPair& aRhs) const - { - return aLhs.mProperty == aRhs.mProperty; - } - - bool LessThan(const PropertyValuesPair& aLhs, - const PropertyValuesPair& aRhs) const - { - bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs.mProperty); - bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs.mProperty); - - if (isShorthandLhs) { - if (isShorthandRhs) { - // First, sort shorthands by the number of longhands they have. - uint32_t subpropCountLhs = SubpropertyCount(aLhs.mProperty); - uint32_t subpropCountRhs = SubpropertyCount(aRhs.mProperty); - if (subpropCountLhs != subpropCountRhs) { - return subpropCountLhs < subpropCountRhs; - } - // Otherwise, sort by IDL name below. - } else { - // Put longhands before shorthands. - return false; - } - } else { - if (isShorthandRhs) { - // Put longhands before shorthands. - return true; - } - } - // For two longhand properties, or two shorthand with the same number - // of longhand components, sort by IDL name. - return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) < - nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); - } - - uint32_t SubpropertyCount(nsCSSProperty aProperty) const - { - if (!mSubpropertyCountInitialized) { - PodZero(&mSubpropertyCount); - mSubpropertyCountInitialized = true; - } - if (mSubpropertyCount[aProperty] == 0) { - uint32_t count = 0; - CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( - p, aProperty, nsCSSProps::eEnabledForAllContent) { - ++count; - } - mSubpropertyCount[aProperty] = count; - } - return mSubpropertyCount[aProperty]; - } - - private: - // Cache of shorthand subproperty counts. - mutable RangedArray< - uint32_t, - eCSSProperty_COUNT_no_shorthands, - eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount; - mutable bool mSubpropertyCountInitialized; - }; -}; - -/** - * The result of parsing a JS object as a Keyframe dictionary - * and getting its property-value pairs from its open-ended - * properties. - */ -struct OffsetIndexedKeyframe -{ - binding_detail::FastKeyframe mKeyframeDict; - nsTArray mPropertyValuePairs; -}; - -/** - * An additional property (for a property-values pair) found on a Keyframe - * or PropertyIndexedKeyframes object. - */ -struct AdditionalProperty -{ - nsCSSProperty mProperty; - size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs. - - struct PropertyComparator - { - bool Equals(const AdditionalProperty& aLhs, - const AdditionalProperty& aRhs) const - { - return aLhs.mProperty == aRhs.mProperty; - } - bool LessThan(const AdditionalProperty& aLhs, - const AdditionalProperty& aRhs) const - { - return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) < - nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); - } - }; -}; - -/** - * Converts aValue to DOMString and appends it to aValues. - */ -static bool -AppendValueAsString(JSContext* aCx, - nsTArray& aValues, - JS::Handle aValue) -{ - return ConvertJSValueToString(aCx, aValue, eStringify, eStringify, - *aValues.AppendElement()); -} - -// For the aAllowList parameter of AppendStringOrStringSequence and -// GetPropertyValuesPairs. -enum class ListAllowance { eDisallow, eAllow }; - -/** - * Converts aValue to DOMString, if aAllowLists is eDisallow, or - * to (DOMString or sequence) if aAllowLists is aAllow. - * The resulting strings are appended to aValues. - */ -static bool -AppendStringOrStringSequenceToArray(JSContext* aCx, - JS::Handle aValue, - ListAllowance aAllowLists, - nsTArray& aValues) -{ - if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) { - // The value is an object, and we want to allow lists; convert - // aValue to (DOMString or sequence). - JS::ForOfIterator iter(aCx); - if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) { - return false; - } - if (iter.valueIsIterable()) { - // If the object is iterable, convert it to sequence. - JS::Rooted element(aCx); - for (;;) { - bool done; - if (!iter.next(&element, &done)) { - return false; - } - if (done) { - break; - } - if (!AppendValueAsString(aCx, aValues, element)) { - return false; - } - } - return true; - } - } - - // Either the object is not iterable, or aAllowLists doesn't want - // a list; convert it to DOMString. - if (!AppendValueAsString(aCx, aValues, aValue)) { - return false; - } - - return true; -} - -/** - * Reads the property-values pairs from the specified JS object. - * - * @param aObject The JS object to look at. - * @param aAllowLists If eAllow, values will be converted to - * (DOMString or sequence aObject, - ListAllowance aAllowLists, - nsTArray& aResult) -{ - nsTArray properties; - - // Iterate over all the properties on aObject and append an - // entry to properties for them. - // - // We don't compare the jsids that we encounter with those for - // the explicit dictionary members, since we know that none - // of the CSS property IDL names clash with them. - JS::Rooted ids(aCx, JS::IdVector(aCx)); - if (!JS_Enumerate(aCx, aObject, &ids)) { - return false; - } - for (size_t i = 0, n = ids.length(); i < n; i++) { - nsAutoJSString propName; - if (!propName.init(aCx, ids[i])) { - return false; - } - nsCSSProperty property = - nsCSSProps::LookupPropertyByIDLName(propName, - nsCSSProps::eEnabledForAllContent); - if (property != eCSSProperty_UNKNOWN && - (nsCSSProps::IsShorthand(property) || - nsCSSProps::kAnimTypeTable[property] != eStyleAnimType_None)) { - // Only need to check for longhands being animatable, as the - // StyleAnimationValue::ComputeValues calls later on will check for - // a shorthand's components being animatable. - AdditionalProperty* p = properties.AppendElement(); - p->mProperty = property; - p->mJsidIndex = i; - } - } - - // Sort the entries by IDL name and then get each value and - // convert it either to a DOMString or to a - // (DOMString or sequence), depending on aAllowLists, - // and build up aResult. - properties.Sort(AdditionalProperty::PropertyComparator()); - - for (AdditionalProperty& p : properties) { - JS::Rooted value(aCx); - if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) { - return false; - } - PropertyValuesPair* pair = aResult.AppendElement(); - pair->mProperty = p.mProperty; - if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists, - pair->mValues)) { - return false; - } - } - - return true; -} - -/** - * Converts a JS object wrapped by the given JS::ForIfIterator to an - * IDL sequence and stores the resulting OffsetIndexedKeyframe - * objects in aResult. - */ -static bool -ConvertKeyframeSequence(JSContext* aCx, - JS::ForOfIterator& aIterator, - nsTArray& aResult) -{ - JS::Rooted value(aCx); - for (;;) { - bool done; - if (!aIterator.next(&value, &done)) { - return false; - } - if (done) { - break; - } - // Each value found when iterating the object must be an object - // or null/undefined (which gets treated as a default {} dictionary - // value). - if (!value.isObject() && !value.isNullOrUndefined()) { - ThrowErrorMessage(aCx, MSG_NOT_OBJECT, - "Element of sequence argument"); - return false; - } - // Convert the JS value into a Keyframe dictionary value. - OffsetIndexedKeyframe* keyframe = aResult.AppendElement(); - if (!keyframe->mKeyframeDict.Init( - aCx, value, "Element of sequence argument")) { - return false; - } - // Look for additional property-values pairs on the object. - if (value.isObject()) { - JS::Rooted object(aCx, &value.toObject()); - if (!GetPropertyValuesPairs(aCx, object, - ListAllowance::eDisallow, - keyframe->mPropertyValuePairs)) { - return false; - } - } - } - return true; -} - -/** - * Checks that the given keyframes are loosely ordered (each keyframe's - * offset that is not null is greater than or equal to the previous - * non-null offset) and that all values are within the range [0.0, 1.0]. - * - * @return true if the keyframes' offsets are correctly ordered and - * within range; false otherwise. - */ -static bool -HasValidOffsets(const nsTArray& aKeyframes) -{ - double offset = 0.0; - for (const OffsetIndexedKeyframe& keyframe : aKeyframes) { - if (!keyframe.mKeyframeDict.mOffset.IsNull()) { - double thisOffset = keyframe.mKeyframeDict.mOffset.Value(); - if (thisOffset < offset || thisOffset > 1.0f) { - return false; - } - offset = thisOffset; - } - } - return true; -} - -/** - * Fills in any null offsets for the given keyframes by applying the - * "distribute" spacing algorithm. - * - * http://w3c.github.io/web-animations/#distribute-keyframe-spacing-mode - */ -static void -ApplyDistributeSpacing(nsTArray& aKeyframes) -{ - // If the first or last keyframes have an unspecified offset, - // fill them in with 0% and 100%. If there is only a single keyframe, - // then it gets 100%. - if (aKeyframes.LastElement().mKeyframeDict.mOffset.IsNull()) { - aKeyframes.LastElement().mKeyframeDict.mOffset.SetValue(1.0); - } - if (aKeyframes[0].mKeyframeDict.mOffset.IsNull()) { - aKeyframes[0].mKeyframeDict.mOffset.SetValue(0.0); - } - - // Fill in remaining missing offsets. - size_t i = 0; - while (i < aKeyframes.Length() - 1) { - MOZ_ASSERT(!aKeyframes[i].mKeyframeDict.mOffset.IsNull()); - double start = aKeyframes[i].mKeyframeDict.mOffset.Value(); - size_t j = i + 1; - while (aKeyframes[j].mKeyframeDict.mOffset.IsNull()) { - ++j; - } - double end = aKeyframes[j].mKeyframeDict.mOffset.Value(); - size_t n = j - i; - for (size_t k = 1; k < n; ++k) { - double offset = start + double(k) / n * (end - start); - aKeyframes[i + k].mKeyframeDict.mOffset.SetValue(offset); - } - i = j; - } -} - -/** - * Splits out each property's keyframe animation segment information - * from the OffsetIndexedKeyframe objects into an array of KeyframeValueEntry. - * - * The easing string value in OffsetIndexedKeyframe objects is parsed - * into a ComputedTimingFunction value in the corresponding KeyframeValueEntry - * objects. - * - * @param aTarget The target of the animation. - * @param aPseudoType The pseudo type of the target if it is a pseudo element. - * @param aKeyframes The keyframes to read. - * @param aResult The array to append the resulting KeyframeValueEntry - * objects to. - */ -static void -GenerateValueEntries(Element* aTarget, - CSSPseudoElementType aPseudoType, - nsTArray& aKeyframes, - nsTArray& aResult, - ErrorResult& aRv) -{ - nsCSSPropertySet properties; // All properties encountered. - nsCSSPropertySet propertiesWithFromValue; // Those with a defined 0% value. - nsCSSPropertySet propertiesWithToValue; // Those with a defined 100% value. - - for (OffsetIndexedKeyframe& keyframe : aKeyframes) { - float offset = float(keyframe.mKeyframeDict.mOffset.Value()); - Maybe easing = - AnimationUtils::ParseEasing(keyframe.mKeyframeDict.mEasing, - aTarget->OwnerDoc()); - // We ignore keyframe.mKeyframeDict.mComposite since we don't support - // composite modes on keyframes yet. - - // keyframe.mPropertyValuePairs is currently sorted by CSS property IDL - // name, since that was the order we read the properties from the JS - // object. Re-sort the list so that longhand properties appear before - // shorthands, and with shorthands all appearing in increasing order of - // number of components. For two longhand properties, or two shorthands - // with the same number of components, sort by IDL name. - // - // Example orderings that result from this: - // - // margin-left, margin - // - // and: - // - // border-top-color, border-color, border-top, border - // - // This allows us to prioritize values specified by longhands (or smaller - // shorthand subsets) when longhands and shorthands are both specified - // on the one keyframe. - keyframe.mPropertyValuePairs.Sort( - PropertyValuesPair::PropertyPriorityComparator()); - - nsCSSPropertySet propertiesOnThisKeyframe; - for (const PropertyValuesPair& pair : keyframe.mPropertyValuePairs) { - MOZ_ASSERT(pair.mValues.Length() == 1, - "ConvertKeyframeSequence should have parsed single " - "DOMString values from the property-values pairs"); - // Parse the property's string value and produce a KeyframeValueEntry (or - // more than one, for shorthands) for it. - nsTArray values; - if (StyleAnimationValue::ComputeValues(pair.mProperty, - nsCSSProps::eEnabledForAllContent, - aTarget, - aPseudoType, - pair.mValues[0], - /* aUseSVGMode */ false, - values)) { - for (auto& value : values) { - // If we already got a value for this property on the keyframe, - // skip this one. - if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) { - continue; - } - - KeyframeValueEntry* entry = aResult.AppendElement(); - entry->mOffset = offset; - entry->mProperty = value.mProperty; - entry->mValue = value.mValue; - entry->mTimingFunction = easing; - - if (offset == 0.0) { - propertiesWithFromValue.AddProperty(value.mProperty); - } else if (offset == 1.0) { - propertiesWithToValue.AddProperty(value.mProperty); - } - propertiesOnThisKeyframe.AddProperty(value.mProperty); - properties.AddProperty(value.mProperty); - } - } - } - } - - // We don't support additive segments and so can't support missing properties - // using their underlying value in 0% and 100% keyframes. Throw an exception - // until we do support this. - if (!propertiesWithFromValue.Equals(properties) || - !propertiesWithToValue.Equals(properties)) { - aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); - return; - } -} - -/** - * Builds an array of AnimationProperty objects to represent the keyframe - * animation segments in aEntries. - */ -static void -BuildSegmentsFromValueEntries(nsTArray& aEntries, - nsTArray& aResult) -{ - if (aEntries.IsEmpty()) { - return; - } - - // Sort the KeyframeValueEntry objects so that all entries for a given - // property are together, and the entries are sorted by offset otherwise. - std::stable_sort(aEntries.begin(), aEntries.end(), - &KeyframeValueEntry::PropertyOffsetComparator::LessThan); - - MOZ_ASSERT(aEntries[0].mOffset == 0.0f); - MOZ_ASSERT(aEntries.LastElement().mOffset == 1.0f); - - // For a given index i, we want to generate a segment from aEntries[i] - // to aEntries[j], if: - // - // * j > i, - // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and - // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s. - // - // That will eliminate runs of same offset/property values where there's no - // point generating zero length segments in the middle of the animation. - // - // Additionally we need to generate a zero length segment at offset 0 and at - // offset 1, if we have multiple values for a given property at that offset, - // since we need to retain the very first and very last value so they can - // be used for reverse and forward filling. - - nsCSSProperty lastProperty = eCSSProperty_UNKNOWN; - AnimationProperty* animationProperty = nullptr; - - size_t i = 0, n = aEntries.Length(); - - while (i + 1 < n) { - // Starting from i, determine the next [i, j] interval from which to - // generate a segment. - size_t j; - if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) { - // We need to generate an initial zero-length segment. - MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty); - j = i + 1; - while (aEntries[j + 1].mOffset == 0.0f) { - MOZ_ASSERT(aEntries[j].mProperty == aEntries[j + 1].mProperty); - ++j; - } - } else if (aEntries[i].mOffset == 1.0f) { - if (aEntries[i + 1].mOffset == 1.0f) { - // We need to generate a final zero-length segment. - MOZ_ASSERT(aEntries[i].mProperty == aEntries[i].mProperty); - j = i + 1; - while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f) { - MOZ_ASSERT(aEntries[j].mProperty == aEntries[j + 1].mProperty); - ++j; - } - } else { - // New property. - MOZ_ASSERT(aEntries[i + 1].mOffset == 0.0f); - MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty); - ++i; - continue; - } - } else { - while (aEntries[i].mOffset == aEntries[i + 1].mOffset && - aEntries[i].mProperty == aEntries[i + 1].mProperty) { - ++i; - } - j = i + 1; - } - - // If we've moved on to a new property, create a new AnimationProperty - // to insert segments into. - if (aEntries[i].mProperty != lastProperty) { - MOZ_ASSERT(aEntries[i].mOffset == 0.0f); - animationProperty = aResult.AppendElement(); - animationProperty->mProperty = aEntries[i].mProperty; - lastProperty = aEntries[i].mProperty; - } - - MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer."); - - // Now generate the segment. - AnimationPropertySegment* segment = - animationProperty->mSegments.AppendElement(); - segment->mFromKey = aEntries[i].mOffset; - segment->mToKey = aEntries[j].mOffset; - segment->mFromValue = aEntries[i].mValue; - segment->mToValue = aEntries[j].mValue; - segment->mTimingFunction = aEntries[i].mTimingFunction; - - i = j; - } -} - -/** - * Converts a JS object to an IDL sequence and builds an - * array of AnimationProperty objects for the keyframe animation - * that it specifies. - * - * @param aTarget The target of the animation. - * @param aIterator An already-initialized ForOfIterator for the JS - * object to iterate over as a sequence. - * @param aResult The array into which the resulting AnimationProperty - * objects will be appended. - */ -static void -BuildAnimationPropertyListFromKeyframeSequence( - JSContext* aCx, - Element* aTarget, - CSSPseudoElementType aPseudoType, - JS::ForOfIterator& aIterator, - nsTArray& aResult, - ErrorResult& aRv) -{ - // Convert the object in aIterator to sequence, producing - // an array of OffsetIndexedKeyframe objects. - AutoTArray keyframes; - if (!ConvertKeyframeSequence(aCx, aIterator, keyframes)) { - aRv.Throw(NS_ERROR_FAILURE); - return; - } - - // If the sequence<> had zero elements, we won't generate any - // keyframes. - if (keyframes.IsEmpty()) { - return; - } - - // Check that the keyframes are loosely sorted and with values all - // between 0% and 100%. - if (!HasValidOffsets(keyframes)) { - aRv.ThrowTypeError(); - return; - } - - // Fill in 0%/100% values if the first/element keyframes don't have - // a specified offset, and evenly space those that have a missing - // offset. (We don't support paced spacing yet.) - ApplyDistributeSpacing(keyframes); - - // Convert the OffsetIndexedKeyframes into a list of KeyframeValueEntry - // objects. - nsTArray entries; - GenerateValueEntries(aTarget, aPseudoType, keyframes, entries, aRv); - if (aRv.Failed()) { - return; - } - - // Finally, build an array of AnimationProperty objects in aResult - // corresponding to the entries. - BuildSegmentsFromValueEntries(entries, aResult); -} - -/** - * Converts a JS object to an IDL PropertyIndexedKeyframes and builds an - * array of AnimationProperty objects for the keyframe animation - * that it specifies. - * - * @param aTarget The target of the animation. - * @param aValue The JS object. - * @param aResult The array into which the resulting AnimationProperty - * objects will be appended. - */ -static void -BuildAnimationPropertyListFromPropertyIndexedKeyframes( - JSContext* aCx, - Element* aTarget, - CSSPseudoElementType aPseudoType, - JS::Handle aValue, - InfallibleTArray& aResult, - ErrorResult& aRv) -{ - MOZ_ASSERT(aValue.isObject()); - - // Convert the object to a PropertyIndexedKeyframes dictionary to - // get its explicit dictionary members. - binding_detail::FastPropertyIndexedKeyframes keyframes; - if (!keyframes.Init(aCx, aValue, "PropertyIndexedKeyframes argument", - false)) { - aRv.Throw(NS_ERROR_FAILURE); - return; - } - - Maybe easing = - AnimationUtils::ParseEasing(keyframes.mEasing, aTarget->OwnerDoc()); - - // We ignore easing.mComposite since we don't support composite modes on - // keyframes yet. - - // Get all the property--value-list pairs off the object. - JS::Rooted object(aCx, &aValue.toObject()); - nsTArray propertyValuesPairs; - if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow, - propertyValuesPairs)) { - aRv.Throw(NS_ERROR_FAILURE); - return; - } - - // We must keep track of which properties we've already generated - // an AnimationProperty since the author could have specified both a - // shorthand and one of its component longhands on the - // PropertyIndexedKeyframes. - nsCSSPropertySet properties; - - // Create AnimationProperty objects for each PropertyValuesPair, applying - // the "distribute" spacing algorithm to the segments. - for (const PropertyValuesPair& pair : propertyValuesPairs) { - size_t count = pair.mValues.Length(); - if (count == 0) { - // No animation values for this property. - continue; - } - if (count == 1) { - // We don't support additive segments and so can't support an - // animation that goes from the underlying value to this - // specified value. Throw an exception until we do support this. - aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); - return; - } - - // If we find an invalid value, we don't create a segment for it, but - // we adjust the surrounding segments so that the timing of the segments - // is the same as if we did support it. For example, animating with - // values ["red", "green", "yellow", "invalid", "blue"] will generate - // segments with this timing: - // - // 0.00 -> 0.25 : red -> green - // 0.25 -> 0.50 : green -> yellow - // 0.50 -> 1.00 : yellow -> blue - // - // With future spec clarifications we might decide to preserve the invalid - // value on the segment and make the animation code deal with the invalid - // value instead. - nsTArray fromValues; - float fromKey = 0.0f; - if (!StyleAnimationValue::ComputeValues(pair.mProperty, - nsCSSProps::eEnabledForAllContent, - aTarget, - aPseudoType, - pair.mValues[0], - /* aUseSVGMode */ false, - fromValues)) { - // We need to throw for an invalid first value, since that would imply an - // additive animation, which we don't support yet. - aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); - return; - } - - if (fromValues.IsEmpty()) { - // All longhand components of a shorthand pair.mProperty must be disabled. - continue; - } - - // Create AnimationProperty objects for each property that had a - // value computed. When pair.mProperty is a longhand, it is just - // that property. When pair.mProperty is a shorthand, we'll have - // one property per longhand component. - nsTArray animationPropertyIndexes; - animationPropertyIndexes.SetLength(fromValues.Length()); - for (size_t i = 0, n = fromValues.Length(); i < n; ++i) { - nsCSSProperty p = fromValues[i].mProperty; - bool found = false; - if (properties.HasProperty(p)) { - // We have already dealt with this property. Look up and - // overwrite the old AnimationProperty object. - for (size_t j = 0, m = aResult.Length(); j < m; ++j) { - if (aResult[j].mProperty == p) { - aResult[j].mSegments.Clear(); - animationPropertyIndexes[i] = j; - found = true; - break; - } - } - MOZ_ASSERT(found, "properties is inconsistent with aResult"); - } - if (!found) { - // This is the first time we've encountered this property. - animationPropertyIndexes[i] = aResult.Length(); - AnimationProperty* animationProperty = aResult.AppendElement(); - animationProperty->mProperty = p; - properties.AddProperty(p); - } - } - - double portion = 1.0 / (count - 1); - for (size_t i = 0; i < count - 1; ++i) { - nsTArray toValues; - float toKey = (i + 1) * portion; - if (!StyleAnimationValue::ComputeValues(pair.mProperty, - nsCSSProps::eEnabledForAllContent, - aTarget, - aPseudoType, - pair.mValues[i + 1], - /* aUseSVGMode */ false, - toValues)) { - if (i + 1 == count - 1) { - // We need to throw for an invalid last value, since that would - // imply an additive animation, which we don't support yet. - aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); - return; - } - // Otherwise, skip the segment. - continue; - } - MOZ_ASSERT(toValues.Length() == fromValues.Length(), - "should get the same number of properties as the last time " - "we called ComputeValues for pair.mProperty"); - for (size_t j = 0, n = toValues.Length(); j < n; ++j) { - size_t index = animationPropertyIndexes[j]; - AnimationPropertySegment* segment = - aResult[index].mSegments.AppendElement(); - segment->mFromKey = fromKey; - segment->mFromValue = fromValues[j].mValue; - segment->mToKey = toKey; - segment->mToValue = toValues[j].mValue; - segment->mTimingFunction = easing; - } - fromValues = Move(toValues); - fromKey = toKey; - } - } -} - -/** - * Converts a JS value to an IDL - * (PropertyIndexedKeyframes or sequence) value and builds an - * array of AnimationProperty objects for the keyframe animation - * that it specifies. - * - * @param aTarget The target of the animation, used to resolve style - * for a property's underlying value if needed. - * @param aFrames The JS value, provided as an optional IDL |object?| value, - * that is the keyframe list specification. - * @param aResult The array into which the resulting AnimationProperty - * objects will be appended. - */ -/* static */ void -KeyframeEffectReadOnly::BuildAnimationPropertyList( - JSContext* aCx, - Element* aTarget, - CSSPseudoElementType aPseudoType, - JS::Handle aFrames, - InfallibleTArray& aResult, - ErrorResult& aRv) -{ - MOZ_ASSERT(aResult.IsEmpty()); - - // A frame list specification in the IDL is: - // - // (PropertyIndexedKeyframes or sequence or SharedKeyframeList) - // - // We don't support SharedKeyframeList yet, but we do the other two. We - // manually implement the parts of JS-to-IDL union conversion algorithm - // from the Web IDL spec, since we have to represent this an object? so - // we can look at the open-ended set of properties on a - // PropertyIndexedKeyframes or Keyframe. - - if (!aFrames) { - // The argument was explicitly null. In this case, the default dictionary - // value for PropertyIndexedKeyframes would result in no keyframes. - return; - } - - // At this point we know we have an object. We try to convert it to a - // sequence first, and if that fails due to not being iterable, - // we try to convert it to PropertyIndexedKeyframes. - JS::Rooted objectValue(aCx, JS::ObjectValue(*aFrames)); - JS::ForOfIterator iter(aCx); - if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) { - aRv.Throw(NS_ERROR_FAILURE); - return; - } - - if (iter.valueIsIterable()) { - BuildAnimationPropertyListFromKeyframeSequence(aCx, aTarget, aPseudoType, - iter, aResult, aRv); - } else { - BuildAnimationPropertyListFromPropertyIndexedKeyframes(aCx, aTarget, - aPseudoType, - objectValue, aResult, - aRv); - } -} - /* static */ already_AddRefed KeyframeEffectReadOnly::Constructor( const GlobalObject& aGlobal, @@ -1943,8 +1068,8 @@ KeyframeEffectReadOnly::GetFrames(JSContext*& aCx, OrderedKeyframeValueEntry* entry = &entries[i]; OrderedKeyframeValueEntry* previousEntry = nullptr; - // Create a JS object with the explicit ComputedKeyframe dictionary members. - ComputedKeyframe keyframeDict; + // Create a JS object with the BaseComputedKeyframe dictionary members. + BaseComputedKeyframe keyframeDict; keyframeDict.mOffset.SetValue(entry->mOffset); keyframeDict.mComputedOffset.Construct(entry->mOffset); if (entry->mTimingFunction && entry->mTimingFunction->isSome()) { @@ -1952,7 +1077,6 @@ KeyframeEffectReadOnly::GetFrames(JSContext*& aCx, keyframeDict.mEasing.Truncate(); entry->mTimingFunction->value().AppendToString(keyframeDict.mEasing); } - keyframeDict.mComposite.SetValue(CompositeOperation::Replace); JS::Rooted keyframeJSValue(aCx); if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) { @@ -2318,15 +1442,8 @@ KeyframeEffect::Constructor( void KeyframeEffect::NotifySpecifiedTimingUpdated() { - nsIDocument* doc = nullptr; - // Bug 1249219: - // We don't support animation mutation observers on pseudo-elements yet. - if (mTarget && - mPseudoType == CSSPseudoElementType::NotPseudo) { - doc = mTarget->OwnerDoc(); - } - - nsAutoAnimationMutationBatch mb(doc); + // Use the same document for a pseudo element and its parent element. + nsAutoAnimationMutationBatch mb(mTarget->OwnerDoc()); if (mAnimation) { mAnimation->NotifyEffectTimingUpdated(); diff --git a/dom/animation/KeyframeEffect.h b/dom/animation/KeyframeEffect.h index 8071a9fc8e..32afff0e43 100644 --- a/dom/animation/KeyframeEffect.h +++ b/dom/animation/KeyframeEffect.h @@ -8,14 +8,20 @@ #define mozilla_dom_KeyframeEffect_h #include "nsAutoPtr.h" +#include "nsCSSProperty.h" +#include "nsCSSValue.h" #include "nsCycleCollectionParticipant.h" #include "nsIDocument.h" +#include "nsTArray.h" #include "nsWrapperCache.h" #include "mozilla/AnimationPerformanceWarning.h" #include "mozilla/Attributes.h" -#include "mozilla/ComputedTimingFunction.h" // ComputedTimingFunction -#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords -#include "mozilla/OwningNonNull.h" // OwningNonNull<...> +#include "mozilla/ComputedTiming.h" +#include "mozilla/ComputedTimingFunction.h" +#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords +#include "mozilla/Maybe.h" +#include "mozilla/NonOwningAnimationTarget.h" +#include "mozilla/OwningNonNull.h" // OwningNonNull<...> #include "mozilla/StickyTimeDuration.h" #include "mozilla/StyleAnimationValue.h" #include "mozilla/TimeStamp.h" @@ -23,10 +29,8 @@ #include "mozilla/dom/AnimationEffectReadOnly.h" #include "mozilla/dom/AnimationEffectTimingReadOnly.h" #include "mozilla/dom/Element.h" -#include "mozilla/dom/KeyframeBinding.h" #include "mozilla/dom/Nullable.h" - struct JSContext; class nsCSSPropertySet; class nsIContent; @@ -50,54 +54,39 @@ struct AnimationPropertyDetails; } /** - * Stores the results of calculating the timing properties of an animation - * at a given sample time. + * A property-value pair specified on a keyframe. */ -struct ComputedTiming +struct PropertyValuePair { - // The total duration of the animation including all iterations. - // Will equal StickyTimeDuration::Forever() if the animation repeats - // indefinitely. - StickyTimeDuration mActiveDuration; - // The effect end time in local time (i.e. an offset from the effect's - // start time). Will equal StickyTimeDuration::Forever() if the animation - // plays indefinitely. - StickyTimeDuration mEndTime; - // Progress towards the end of the current iteration. If the effect is - // being sampled backwards, this will go from 1.0 to 0.0. - // Will be null if the animation is neither animating nor - // filling at the sampled time. - Nullable mProgress; - // Zero-based iteration index (meaningless if mProgress is null). - uint64_t mCurrentIteration = 0; - // Unlike TimingParams::mIterations, this value is - // guaranteed to be in the range [0, Infinity]. - double mIterations = 1.0; - double mIterationStart = 0.0; - StickyTimeDuration mDuration; + nsCSSProperty mProperty; + // The specified value for the property. For shorthand properties or invalid + // property values, we store the specified property value as a token stream + // (string). + nsCSSValue mValue; +}; - // This is the computed fill mode so it is never auto - dom::FillMode mFill = dom::FillMode::None; - bool FillsForwards() const { - MOZ_ASSERT(mFill != dom::FillMode::Auto, - "mFill should not be Auto in ComputedTiming."); - return mFill == dom::FillMode::Both || - mFill == dom::FillMode::Forwards; - } - bool FillsBackwards() const { - MOZ_ASSERT(mFill != dom::FillMode::Auto, - "mFill should not be Auto in ComputedTiming."); - return mFill == dom::FillMode::Both || - mFill == dom::FillMode::Backwards; - } - - enum class AnimationPhase { - Null, // Not sampled (null sample time) - Before, // Sampled prior to the start of the active interval - Active, // Sampled within the active interval - After // Sampled after (or at) the end of the active interval - }; - AnimationPhase mPhase = AnimationPhase::Null; +/** + * A single keyframe. + * + * This is the canonical form in which keyframe effects are stored and + * corresponds closely to the type of objects returned via the getFrames() API. + * + * Before computing an output animation value, however, we flatten these frames + * down to a series of per-property value arrays where we also resolve any + * overlapping shorthands/longhands, convert specified CSS values to computed + * values, etc. + * + * When the target element or style context changes, however, we rebuild these + * per-property arrays from the original list of keyframes objects. As a result, + * these objects represent the master definition of the effect's values. + */ +struct Keyframe +{ + Maybe mOffset; + double mComputedOffset = 0.0; + Maybe mTimingFunction; // Nothing() here means + // "linear" + nsTArray mPropertyValues; }; struct AnimationPropertySegment @@ -206,20 +195,20 @@ public: ErrorResult& aRv); void GetTarget(Nullable& aRv) const; + Maybe GetTarget() const + { + Maybe result; + if (mTarget) { + result.emplace(mTarget, mPseudoType); + } + return result; + } void GetFrames(JSContext*& aCx, nsTArray& aResult, ErrorResult& aRv); void GetProperties(nsTArray& aProperties, ErrorResult& aRv) const; - // Temporary workaround to return both the target element and pseudo-type - // until we implement PseudoElement (bug 1174575). - void GetTarget(Element*& aTarget, - CSSPseudoElementType& aPseudoType) const { - aTarget = mTarget; - aPseudoType = mPseudoType; - } - IterationCompositeOperation IterationComposite() const; CompositeOperation Composite() const; void GetSpacing(nsString& aRetVal) const { @@ -359,14 +348,6 @@ protected: // owning Animation's timing. void UpdateTargetRegistration(); - static void BuildAnimationPropertyList( - JSContext* aCx, - Element* aTarget, - CSSPseudoElementType aPseudoType, - JS::Handle aFrames, - InfallibleTArray& aResult, - ErrorResult& aRv); - nsCOMPtr mTarget; RefPtr mAnimation; diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp new file mode 100644 index 0000000000..5f342ed4d9 --- /dev/null +++ b/dom/animation/KeyframeUtils.cpp @@ -0,0 +1,1713 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/KeyframeUtils.h" + +#include "mozilla/AnimationUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Move.h" +#include "mozilla/TimingParams.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc. +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "jsapi.h" // For ForOfIterator etc. +#include "nsClassHashtable.h" +#include "nsCSSParser.h" +#include "nsCSSProps.h" +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType +#include "nsTArray.h" +#include // For std::stable_sort + +// TODO: Remove once we drop LookupStyleContext +#include "nsComputedDOMStyle.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" + +namespace mozilla { + +// ------------------------------------------------------------------ +// +// Internal data types +// +// ------------------------------------------------------------------ + +// For the aAllowList parameter of AppendStringOrStringSequence and +// GetPropertyValuesPairs. +enum class ListAllowance { eDisallow, eAllow }; + +/** + * A comparator to sort nsCSSProperty values such that longhands are sorted + * before shorthands, and shorthands with less components are sorted before + * shorthands with more components. + * + * Using this allows us to prioritize values specified by longhands (or smaller + * shorthand subsets) when longhands and shorthands are both specified + * on the one keyframe. + * + * Example orderings that result from this: + * + * margin-left, margin + * + * and: + * + * border-top-color, border-color, border-top, border + */ +class PropertyPriorityComparator +{ +public: + PropertyPriorityComparator() + : mSubpropertyCountInitialized(false) {} + + bool Equals(nsCSSProperty aLhs, nsCSSProperty aRhs) const + { + return aLhs == aRhs; + } + + bool LessThan(nsCSSProperty aLhs, + nsCSSProperty aRhs) const + { + bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs); + bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs); + + if (isShorthandLhs) { + if (isShorthandRhs) { + // First, sort shorthands by the number of longhands they have. + uint32_t subpropCountLhs = SubpropertyCount(aLhs); + uint32_t subpropCountRhs = SubpropertyCount(aRhs); + if (subpropCountLhs != subpropCountRhs) { + return subpropCountLhs < subpropCountRhs; + } + // Otherwise, sort by IDL name below. + } else { + // Put longhands before shorthands. + return false; + } + } else { + if (isShorthandRhs) { + // Put longhands before shorthands. + return true; + } + } + // For two longhand properties, or two shorthand with the same number + // of longhand components, sort by IDL name. + return nsCSSProps::PropertyIDLNameSortPosition(aLhs) < + nsCSSProps::PropertyIDLNameSortPosition(aRhs); + } + + uint32_t SubpropertyCount(nsCSSProperty aProperty) const + { + if (!mSubpropertyCountInitialized) { + PodZero(&mSubpropertyCount); + mSubpropertyCountInitialized = true; + } + if (mSubpropertyCount[aProperty] == 0) { + uint32_t count = 0; + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( + p, aProperty, nsCSSProps::eEnabledForAllContent) { + ++count; + } + mSubpropertyCount[aProperty] = count; + } + return mSubpropertyCount[aProperty]; + } + +private: + // Cache of shorthand subproperty counts. + mutable RangedArray< + uint32_t, + eCSSProperty_COUNT_no_shorthands, + eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount; + mutable bool mSubpropertyCountInitialized; +}; + +/** + * Adaptor for PropertyPriorityComparator to sort objects which have + * a mProperty member. + */ +template +class TPropertyPriorityComparator : PropertyPriorityComparator +{ +public: + bool Equals(const T& aLhs, const T& aRhs) const + { + return PropertyPriorityComparator::Equals(aLhs.mProperty, aRhs.mProperty); + } + bool LessThan(const T& aLhs, const T& aRhs) const + { + return PropertyPriorityComparator::LessThan(aLhs.mProperty, aRhs.mProperty); + } +}; + +/** + * Iterator to walk through a PropertyValuePair array using the ordering + * provided by PropertyPriorityComparator. + */ +class PropertyPriorityIterator +{ +public: + explicit PropertyPriorityIterator( + const nsTArray& aProperties) + : mProperties(aProperties) + { + mSortedPropertyIndices.SetCapacity(mProperties.Length()); + for (size_t i = 0, len = mProperties.Length(); i < len; ++i) { + PropertyAndIndex propertyIndex = { mProperties[i].mProperty, i }; + mSortedPropertyIndices.AppendElement(propertyIndex); + } + mSortedPropertyIndices.Sort(PropertyAndIndex::Comparator()); + } + + class Iter + { + public: + explicit Iter(const PropertyPriorityIterator& aParent) + : mParent(aParent) + , mIndex(0) { } + + static Iter EndIter(const PropertyPriorityIterator &aParent) + { + Iter iter(aParent); + iter.mIndex = aParent.mSortedPropertyIndices.Length(); + return iter; + } + + bool operator!=(const Iter& aOther) const + { + return mIndex != aOther.mIndex; + } + + Iter& operator++() + { + MOZ_ASSERT(mIndex + 1 <= mParent.mSortedPropertyIndices.Length(), + "Should not seek past end iterator"); + mIndex++; + return *this; + } + + const PropertyValuePair& operator*() + { + MOZ_ASSERT(mIndex < mParent.mSortedPropertyIndices.Length(), + "Should not try to dereference an end iterator"); + return mParent.mProperties[mParent.mSortedPropertyIndices[mIndex].mIndex]; + } + + private: + const PropertyPriorityIterator& mParent; + size_t mIndex; + }; + + Iter begin() { return Iter(*this); } + Iter end() { return Iter::EndIter(*this); } + +private: + struct PropertyAndIndex + { + nsCSSProperty mProperty; + size_t mIndex; // Index of mProperty within mProperties + + typedef TPropertyPriorityComparator Comparator; + }; + + const nsTArray& mProperties; + nsTArray mSortedPropertyIndices; +}; + +/** + * A property-values pair obtained from the open-ended properties + * discovered on a regular keyframe or property-indexed keyframe object. + * + * Single values (as required by a regular keyframe, and as also supported + * on property-indexed keyframes) are stored as the only element in + * mValues. + */ +struct PropertyValuesPair +{ + nsCSSProperty mProperty; + nsTArray mValues; + + typedef TPropertyPriorityComparator Comparator; +}; + +/** + * The result of parsing a JS object as a BaseKeyframe dictionary + * and getting its property-value pairs from its open-ended + * properties. + */ +struct OffsetIndexedKeyframe +{ + dom::binding_detail::FastBaseKeyframe mKeyframeDict; + nsTArray mPropertyValuePairs; +}; + +/** + * An additional property (for a property-values pair) found on a + * BaseKeyframe or BasePropertyIndexedKeyframe object. + */ +struct AdditionalProperty +{ + nsCSSProperty mProperty; + size_t mJsidIndex; // Index into |ids| in GetPropertyValuesPairs. + + struct PropertyComparator + { + bool Equals(const AdditionalProperty& aLhs, + const AdditionalProperty& aRhs) const + { + return aLhs.mProperty == aRhs.mProperty; + } + bool LessThan(const AdditionalProperty& aLhs, + const AdditionalProperty& aRhs) const + { + return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) < + nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); + } + }; +}; + +/** + * A property and StyleAnimationValue pair. + */ +struct KeyframeValue +{ + nsCSSProperty mProperty; + StyleAnimationValue mValue; +}; + +/** + * Data for a segment in a keyframe animation of a given property + * whose value is a StyleAnimationValue. + * + * KeyframeValueEntry is used in BuildAnimationPropertyListFromKeyframeSequence + * to gather data for each individual segment described by an author-supplied + * an IDL sequence value so that they can be parsed into mProperties. + */ +struct KeyframeValueEntry : KeyframeValue +{ + float mOffset; + Maybe mTimingFunction; + + struct PropertyOffsetComparator + { + static bool Equals(const KeyframeValueEntry& aLhs, + const KeyframeValueEntry& aRhs) + { + return aLhs.mProperty == aRhs.mProperty && + aLhs.mOffset == aRhs.mOffset; + } + static bool LessThan(const KeyframeValueEntry& aLhs, + const KeyframeValueEntry& aRhs) + { + // First, sort by property IDL name. + int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) - + nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); + if (order != 0) { + return order < 0; + } + + // Then, by offset. + return aLhs.mOffset < aRhs.mOffset; + } + }; +}; + +class ComputedOffsetComparator +{ +public: + static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) + { + return aLhs.mComputedOffset == aRhs.mComputedOffset; + } + + static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) + { + return aLhs.mComputedOffset < aRhs.mComputedOffset; + } +}; + + +// ------------------------------------------------------------------ +// +// Internal helper method declarations +// +// ------------------------------------------------------------------ + +static void +BuildAnimationPropertyListFromKeyframeSequence( + JSContext* aCx, + Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::ForOfIterator& aIterator, + nsTArray& aResult, + ErrorResult& aRv); + +static void +GetKeyframeListFromKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult, + ErrorResult& aRv); + +static bool +ConvertKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult); + +static bool +ConvertKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult); + +static bool +GetPropertyValuesPairs(JSContext* aCx, + JS::Handle aObject, + ListAllowance aAllowLists, + nsTArray& aResult); + +static bool +AppendStringOrStringSequenceToArray(JSContext* aCx, + JS::Handle aValue, + ListAllowance aAllowLists, + nsTArray& aValues); + +static bool +AppendValueAsString(JSContext* aCx, + nsTArray& aValues, + JS::Handle aValue); + +static PropertyValuePair +MakePropertyValuePair(nsCSSProperty aProperty, const nsAString& aStringValue, + nsCSSParser& aParser, nsIDocument* aDocument); + +static bool +HasValidOffsets(const nsTArray& aKeyframes); + +static bool +HasValidOffsets(const nsTArray& aKeyframes); + +static void +ApplyDistributeSpacing(nsTArray& aKeyframes); + +static void +GenerateValueEntries(Element* aTarget, + CSSPseudoElementType aPseudoType, + nsTArray& aKeyframes, + nsTArray& aResult, + ErrorResult& aRv); + +static void +BuildSegmentsFromValueEntries(nsTArray& aEntries, + nsTArray& aResult); + +static void +BuildAnimationPropertyListFromPropertyIndexedKeyframes( + JSContext* aCx, + Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::Handle aValue, + InfallibleTArray& aResult, + ErrorResult& aRv); + +static void +GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx, + JS::Handle aValue, + nsTArray& aResult, + ErrorResult& aRv); + +static bool +RequiresAdditiveAnimation(const nsTArray& aKeyframes, + nsIDocument* aDocument); + + +// TODO: This is only temporary until we remove the call sites for this. +already_AddRefed +LookupStyleContext(dom::Element* aElement, CSSPseudoElementType aPseudoType); + +// ------------------------------------------------------------------ +// +// Public API +// +// ------------------------------------------------------------------ + +/* static */ void +KeyframeUtils::BuildAnimationPropertyList( + JSContext* aCx, + Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::Handle aFrames, + InfallibleTArray& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aResult.IsEmpty()); + + // See the description of frame lists in the spec: + // + // https://w3c.github.io/web-animations/#processing-a-frames-argument + // + // We don't support SharedKeyframeList yet, but we do support the other + // types of arguments. We manually implement the parts of JS-to-IDL union + // conversion algorithm from the Web IDL spec, since we have to represent + // this as an object? so we can look at the open-ended set of properties + // on these objects. + + if (!aFrames) { + // The argument was explicitly null. In this case, the default dictionary + // value for PropertyIndexedKeyframe would result in no keyframes. + return; + } + + // At this point we know we have an object. We try to convert it to a + // sequence first, and if that fails due to not being iterable, + // we try to convert it to PropertyIndexedKeyframe. + JS::Rooted objectValue(aCx, JS::ObjectValue(*aFrames)); + JS::ForOfIterator iter(aCx); + if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (iter.valueIsIterable()) { + BuildAnimationPropertyListFromKeyframeSequence(aCx, aTarget, aPseudoType, + iter, aResult, aRv); + } else { + BuildAnimationPropertyListFromPropertyIndexedKeyframes(aCx, aTarget, + aPseudoType, + objectValue, aResult, + aRv); + } +} + +/* static */ nsTArray +KeyframeUtils::GetKeyframesFromObject(JSContext* aCx, + JS::Handle aFrames, + ErrorResult& aRv) +{ + MOZ_ASSERT(!aRv.Failed()); + + nsTArray keyframes; + + if (!aFrames) { + // The argument was explicitly null meaning no keyframes. + return keyframes; + } + + // At this point we know we have an object. We try to convert it to a + // sequence of keyframes first, and if that fails due to not being iterable, + // we try to convert it to a property-indexed keyframe. + JS::Rooted objectValue(aCx, JS::ObjectValue(*aFrames)); + JS::ForOfIterator iter(aCx); + if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) { + aRv.Throw(NS_ERROR_FAILURE); + return keyframes; + } + + if (iter.valueIsIterable()) { + GetKeyframeListFromKeyframeSequence(aCx, iter, keyframes, aRv); + } else { + GetKeyframeListFromPropertyIndexedKeyframe(aCx, objectValue, keyframes, + aRv); + } + + if (aRv.Failed()) { + MOZ_ASSERT(keyframes.IsEmpty(), + "Should not set any keyframes when there is an error"); + return keyframes; + } + + // We currently don't support additive animation. However, Web Animations + // says that if you don't have a keyframe at offset 0 or 1, then you should + // synthesize one using an additive zero value when you go to compose style. + // Until we implement additive animations we just throw if we encounter any + // set of keyframes that would put us in that situation. + + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + keyframes.Clear(); + return keyframes; + } + + if (RequiresAdditiveAnimation(keyframes, doc)) { + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + keyframes.Clear(); + } + + return keyframes; +} + +/* static */ void +KeyframeUtils::ApplyDistributeSpacing(nsTArray& aKeyframes) +{ + if (aKeyframes.IsEmpty()) { + return; + } + + // If the first or last keyframes have an unspecified offset, + // fill them in with 0% and 100%. If there is only a single keyframe, + // then it gets 100%. + Keyframe& lastElement = aKeyframes.LastElement(); + lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0); + if (aKeyframes.Length() > 1) { + Keyframe& firstElement = aKeyframes[0]; + firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0); + } + + // Fill in remaining missing offsets. + size_t i = 0; + while (i < aKeyframes.Length() - 1) { + double start = aKeyframes[i].mComputedOffset; + size_t j = i + 1; + while (aKeyframes[j].mOffset.isNothing() && j < aKeyframes.Length() - 1) { + ++j; + } + double end = aKeyframes[j].mOffset.valueOr(1.0); + size_t n = j - i; + for (size_t k = 1; k < n; ++k) { + double offset = start + double(k) / n * (end - start); + aKeyframes[i + k].mComputedOffset = offset; + } + i = j; + aKeyframes[j].mComputedOffset = end; + } +} + +/* static */ nsTArray +KeyframeUtils::GetAnimationPropertiesFromKeyframes( + nsStyleContext* aStyleContext, + dom::Element* aElement, + CSSPseudoElementType aPseudoType, + const nsTArray& aFrames) +{ + nsTArray entries; + + for (const Keyframe& frame : aFrames) { + nsCSSPropertySet propertiesOnThisKeyframe; + for (const PropertyValuePair& pair : + PropertyPriorityIterator(frame.mPropertyValues)) { + // We currently store invalid longhand values on keyframes as a token + // stream so if we see one of them, just keep moving. + if (!nsCSSProps::IsShorthand(pair.mProperty) && + pair.mValue.GetUnit() == eCSSUnit_TokenStream) { + continue; + } + + // Expand each value into the set of longhands and produce + // a KeyframeValueEntry for each value. + nsTArray values; + + // For shorthands, we store the string as a token stream so we need to + // extract that first. + if (nsCSSProps::IsShorthand(pair.mProperty)) { + nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue(); + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + nsCSSProps::eEnabledForAllContent, aElement, aStyleContext, + tokenStream->mTokenStream, /* aUseSVGMode */ false, values)) { + continue; + } + } else { + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + nsCSSProps::eEnabledForAllContent, aElement, aStyleContext, + pair.mValue, /* aUseSVGMode */ false, values)) { + continue; + } + MOZ_ASSERT(values.Length() == 1, + "Longhand properties should produce a single" + " StyleAnimationValue"); + + // 'visibility' requires special handling that is unique to CSS + // Transitions/CSS Animations/Web Animations (i.e. not SMIL) so we + // apply that here. + // + // Bug 1259285 - Move this code to StyleAnimationValue + if (pair.mProperty == eCSSProperty_visibility) { + MOZ_ASSERT(values[0].mValue.GetUnit() == + StyleAnimationValue::eUnit_Enumerated, + "unexpected unit"); + values[0].mValue.SetIntValue(values[0].mValue.GetIntValue(), + StyleAnimationValue::eUnit_Visibility); + } + } + + for (auto& value : values) { + // If we already got a value for this property on the keyframe, + // skip this one. + if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) { + continue; + } + + KeyframeValueEntry* entry = entries.AppendElement(); + entry->mOffset = frame.mComputedOffset; + entry->mProperty = value.mProperty; + entry->mValue = value.mValue; + entry->mTimingFunction = frame.mTimingFunction; + + propertiesOnThisKeyframe.AddProperty(value.mProperty); + } + } + } + + nsTArray result; + BuildSegmentsFromValueEntries(entries, result); + + return result; +} + + +// ------------------------------------------------------------------ +// +// Internal helpers +// +// ------------------------------------------------------------------ + +/** + * Converts a JS object to an IDL sequence and builds an + * array of AnimationProperty objects for the keyframe animation + * that it specifies. + * + * @param aTarget The target of the animation. + * @param aIterator An already-initialized ForOfIterator for the JS + * object to iterate over as a sequence. + * @param aResult The array into which the resulting AnimationProperty + * objects will be appended. + */ +static void +BuildAnimationPropertyListFromKeyframeSequence( + JSContext* aCx, + Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::ForOfIterator& aIterator, + nsTArray& aResult, + ErrorResult& aRv) +{ + // Convert the object in aIterator to sequence, producing + // an array of OffsetIndexedKeyframe objects. + AutoTArray keyframes; + if (!ConvertKeyframeSequence(aCx, aIterator, keyframes)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // If the sequence<> had zero elements, we won't generate any + // keyframes. + if (keyframes.IsEmpty()) { + return; + } + + // Check that the keyframes are loosely sorted and with values all + // between 0% and 100%. + if (!HasValidOffsets(keyframes)) { + aRv.ThrowTypeError(); + return; + } + + // Fill in 0%/100% values if the first/element keyframes don't have + // a specified offset, and evenly space those that have a missing + // offset. (We don't support paced spacing yet.) + ApplyDistributeSpacing(keyframes); + + // Convert the OffsetIndexedKeyframes into a list of KeyframeValueEntry + // objects. + nsTArray entries; + GenerateValueEntries(aTarget, aPseudoType, keyframes, entries, aRv); + if (aRv.Failed()) { + return; + } + + // Finally, build an array of AnimationProperty objects in aResult + // corresponding to the entries. + BuildSegmentsFromValueEntries(entries, aResult); +} + +/** + * Converts a JS object to an IDL sequence. + * + * @param aCx The JSContext corresponding to |aIterator|. + * @param aIterator An already-initialized ForOfIterator for the JS + * object to iterate over as a sequence. + * @param aResult The array into which the resulting Keyframe objects will be + * appended. + * @param aRv Out param to store any errors thrown by this function. + */ +static void +GetKeyframeListFromKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(!aRv.Failed()); + MOZ_ASSERT(aResult.IsEmpty()); + + // Convert the object in aIterator to a sequence of keyframes producing + // an array of Keyframe objects. + if (!ConvertKeyframeSequence(aCx, aIterator, aResult)) { + aRv.Throw(NS_ERROR_FAILURE); + aResult.Clear(); + return; + } + + // If the sequence<> had zero elements, we won't generate any + // keyframes. + if (aResult.IsEmpty()) { + return; + } + + // Check that the keyframes are loosely sorted and with values all + // between 0% and 100%. + if (!HasValidOffsets(aResult)) { + aRv.ThrowTypeError(); + aResult.Clear(); + return; + } +} + +/** + * Converts a JS object wrapped by the given JS::ForIfIterator to an + * IDL sequence and stores the resulting OffsetIndexedKeyframe + * objects in aResult. + */ +static bool +ConvertKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult) +{ + JS::Rooted value(aCx); + for (;;) { + bool done; + if (!aIterator.next(&value, &done)) { + return false; + } + if (done) { + break; + } + // Each value found when iterating the object must be an object + // or null/undefined (which gets treated as a default {} dictionary + // value). + if (!value.isObject() && !value.isNullOrUndefined()) { + dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT, + "Element of sequence argument"); + return false; + } + // Convert the JS value into a BaseKeyframe dictionary value. + OffsetIndexedKeyframe* keyframe = aResult.AppendElement(); + if (!keyframe->mKeyframeDict.Init( + aCx, value, "Element of sequence argument")) { + return false; + } + // Look for additional property-values pairs on the object. + if (value.isObject()) { + JS::Rooted object(aCx, &value.toObject()); + if (!GetPropertyValuesPairs(aCx, object, + ListAllowance::eDisallow, + keyframe->mPropertyValuePairs)) { + return false; + } + } + } + return true; +} + +/** + * Converts a JS object wrapped by the given JS::ForIfIterator to an + * IDL sequence and stores the resulting Keyframe objects in + * aResult. + */ +static bool +ConvertKeyframeSequence(JSContext* aCx, + JS::ForOfIterator& aIterator, + nsTArray& aResult) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx); + if (!doc) { + return false; + } + + JS::Rooted value(aCx); + nsCSSParser parser(doc->CSSLoader()); + + for (;;) { + bool done; + if (!aIterator.next(&value, &done)) { + return false; + } + if (done) { + break; + } + // Each value found when iterating the object must be an object + // or null/undefined (which gets treated as a default {} dictionary + // value). + if (!value.isObject() && !value.isNullOrUndefined()) { + dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT, + "Element of sequence argument"); + return false; + } + + // Convert the JS value into a BaseKeyframe dictionary value. + dom::binding_detail::FastBaseKeyframe keyframeDict; + if (!keyframeDict.Init(aCx, value, + "Element of sequence argument")) { + return false; + } + + Keyframe* keyframe = aResult.AppendElement(fallible); + if (!keyframe) { + return false; + } + if (!keyframeDict.mOffset.IsNull()) { + keyframe->mOffset.emplace(keyframeDict.mOffset.Value()); + } + + ErrorResult rv; + keyframe->mTimingFunction = + TimingParams::ParseEasing(keyframeDict.mEasing, doc, rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Look for additional property-values pairs on the object. + nsTArray propertyValuePairs; + if (value.isObject()) { + JS::Rooted object(aCx, &value.toObject()); + if (!GetPropertyValuesPairs(aCx, object, + ListAllowance::eDisallow, + propertyValuePairs)) { + return false; + } + } + + for (PropertyValuesPair& pair : propertyValuePairs) { + MOZ_ASSERT(pair.mValues.Length() == 1); + keyframe->mPropertyValues.AppendElement( + MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser, doc)); + } + } + + return true; +} + +/** + * Reads the property-values pairs from the specified JS object. + * + * @param aObject The JS object to look at. + * @param aAllowLists If eAllow, values will be converted to + * (DOMString or sequence aObject, + ListAllowance aAllowLists, + nsTArray& aResult) +{ + nsTArray properties; + + // Iterate over all the properties on aObject and append an + // entry to properties for them. + // + // We don't compare the jsids that we encounter with those for + // the explicit dictionary members, since we know that none + // of the CSS property IDL names clash with them. + JS::Rooted ids(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, aObject, &ids)) { + return false; + } + for (size_t i = 0, n = ids.length(); i < n; i++) { + nsAutoJSString propName; + if (!propName.init(aCx, ids[i])) { + return false; + } + nsCSSProperty property = + nsCSSProps::LookupPropertyByIDLName(propName, + nsCSSProps::eEnabledForAllContent); + if (property != eCSSProperty_UNKNOWN && + (nsCSSProps::IsShorthand(property) || + nsCSSProps::kAnimTypeTable[property] != eStyleAnimType_None)) { + // Only need to check for longhands being animatable, as the + // StyleAnimationValue::ComputeValues calls later on will check for + // a shorthand's components being animatable. + AdditionalProperty* p = properties.AppendElement(); + p->mProperty = property; + p->mJsidIndex = i; + } + } + + // Sort the entries by IDL name and then get each value and + // convert it either to a DOMString or to a + // (DOMString or sequence), depending on aAllowLists, + // and build up aResult. + properties.Sort(AdditionalProperty::PropertyComparator()); + + for (AdditionalProperty& p : properties) { + JS::Rooted value(aCx); + if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) { + return false; + } + PropertyValuesPair* pair = aResult.AppendElement(); + pair->mProperty = p.mProperty; + if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists, + pair->mValues)) { + return false; + } + } + + return true; +} + +/** + * Converts aValue to DOMString, if aAllowLists is eDisallow, or + * to (DOMString or sequence) if aAllowLists is aAllow. + * The resulting strings are appended to aValues. + */ +static bool +AppendStringOrStringSequenceToArray(JSContext* aCx, + JS::Handle aValue, + ListAllowance aAllowLists, + nsTArray& aValues) +{ + if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) { + // The value is an object, and we want to allow lists; convert + // aValue to (DOMString or sequence). + JS::ForOfIterator iter(aCx); + if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) { + return false; + } + if (iter.valueIsIterable()) { + // If the object is iterable, convert it to sequence. + JS::Rooted element(aCx); + for (;;) { + bool done; + if (!iter.next(&element, &done)) { + return false; + } + if (done) { + break; + } + if (!AppendValueAsString(aCx, aValues, element)) { + return false; + } + } + return true; + } + } + + // Either the object is not iterable, or aAllowLists doesn't want + // a list; convert it to DOMString. + if (!AppendValueAsString(aCx, aValues, aValue)) { + return false; + } + + return true; +} + +/** + * Converts aValue to DOMString and appends it to aValues. + */ +static bool +AppendValueAsString(JSContext* aCx, + nsTArray& aValues, + JS::Handle aValue) +{ + return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify, + *aValues.AppendElement()); +} + +/** + * Construct a PropertyValuePair parsing the given string into a suitable + * nsCSSValue object. + * + * @param aProperty The CSS property. + * @param aStringValue The property value to parse. + * @param aParser The CSS parser object to use. + * @param aDocument The document to use when parsing. + * @return The constructed PropertyValuePair object. + */ +static PropertyValuePair +MakePropertyValuePair(nsCSSProperty aProperty, const nsAString& aStringValue, + nsCSSParser& aParser, nsIDocument* aDocument) +{ + MOZ_ASSERT(aDocument); + + nsCSSValue value; + if (!nsCSSProps::IsShorthand(aProperty)) { + aParser.ParseLonghandProperty(aProperty, + aStringValue, + aDocument->GetDocumentURI(), + aDocument->GetDocumentURI(), + aDocument->NodePrincipal(), + value); + } + + if (value.GetUnit() == eCSSUnit_Null) { + // Either we have a shorthand, or we failed to parse a longhand. + // In either case, store the string value as a token stream. + nsCSSValueTokenStream* tokenStream = new nsCSSValueTokenStream; + tokenStream->mTokenStream = aStringValue; + // By leaving mShorthandPropertyID as unknown, we ensure that when + // we call nsCSSValue::AppendToString we get back the string stored + // in mTokenStream. + MOZ_ASSERT(tokenStream->mShorthandPropertyID == eCSSProperty_UNKNOWN, + "The shorthand property of a token stream should be initialized" + " to unknown"); + value.SetTokenStreamValue(tokenStream); + } + + return { aProperty, value }; +} + +/** + * Checks that the given keyframes are loosely ordered (each keyframe's + * offset that is not null is greater than or equal to the previous + * non-null offset) and that all values are within the range [0.0, 1.0]. + * + * @return true if the keyframes' offsets are correctly ordered and + * within range; false otherwise. + */ +static bool +HasValidOffsets(const nsTArray& aKeyframes) +{ + double offset = 0.0; + for (const OffsetIndexedKeyframe& keyframe : aKeyframes) { + if (!keyframe.mKeyframeDict.mOffset.IsNull()) { + double thisOffset = keyframe.mKeyframeDict.mOffset.Value(); + if (thisOffset < offset || thisOffset > 1.0f) { + return false; + } + offset = thisOffset; + } + } + return true; +} + +/** + * Checks that the given keyframes are loosely ordered (each keyframe's + * offset that is not null is greater than or equal to the previous + * non-null offset) and that all values are within the range [0.0, 1.0]. + * + * @return true if the keyframes' offsets are correctly ordered and + * within range; false otherwise. + */ +static bool +HasValidOffsets(const nsTArray& aKeyframes) +{ + double offset = 0.0; + for (const Keyframe& keyframe : aKeyframes) { + if (keyframe.mOffset) { + double thisOffset = keyframe.mOffset.value(); + if (thisOffset < offset || thisOffset > 1.0f) { + return false; + } + offset = thisOffset; + } + } + return true; +} + +/** + * Fills in any null offsets for the given keyframes by applying the + * "distribute" spacing algorithm. + * + * http://w3c.github.io/web-animations/#distribute-keyframe-spacing-mode + */ +static void +ApplyDistributeSpacing(nsTArray& aKeyframes) +{ + // If the first or last keyframes have an unspecified offset, + // fill them in with 0% and 100%. If there is only a single keyframe, + // then it gets 100%. + if (aKeyframes.LastElement().mKeyframeDict.mOffset.IsNull()) { + aKeyframes.LastElement().mKeyframeDict.mOffset.SetValue(1.0); + } + if (aKeyframes[0].mKeyframeDict.mOffset.IsNull()) { + aKeyframes[0].mKeyframeDict.mOffset.SetValue(0.0); + } + + // Fill in remaining missing offsets. + size_t i = 0; + while (i < aKeyframes.Length() - 1) { + MOZ_ASSERT(!aKeyframes[i].mKeyframeDict.mOffset.IsNull()); + double start = aKeyframes[i].mKeyframeDict.mOffset.Value(); + size_t j = i + 1; + while (aKeyframes[j].mKeyframeDict.mOffset.IsNull()) { + ++j; + } + double end = aKeyframes[j].mKeyframeDict.mOffset.Value(); + size_t n = j - i; + for (size_t k = 1; k < n; ++k) { + double offset = start + double(k) / n * (end - start); + aKeyframes[i + k].mKeyframeDict.mOffset.SetValue(offset); + } + i = j; + } +} + +/** + * Splits out each property's keyframe animation segment information + * from the OffsetIndexedKeyframe objects into an array of KeyframeValueEntry. + * + * The easing string value in OffsetIndexedKeyframe objects is parsed + * into a ComputedTimingFunction value in the corresponding KeyframeValueEntry + * objects. + * + * @param aTarget The target of the animation. + * @param aPseudoType The pseudo type of the target if it is a pseudo element. + * @param aKeyframes The keyframes to read. + * @param aResult The array to append the resulting KeyframeValueEntry + * objects to. + */ +static void +GenerateValueEntries(Element* aTarget, + CSSPseudoElementType aPseudoType, + nsTArray& aKeyframes, + nsTArray& aResult, + ErrorResult& aRv) +{ + RefPtr styleContext = + LookupStyleContext(aTarget, aPseudoType); + if (!styleContext) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCSSPropertySet properties; // All properties encountered. + nsCSSPropertySet propertiesWithFromValue; // Those with a defined 0% value. + nsCSSPropertySet propertiesWithToValue; // Those with a defined 100% value. + + for (OffsetIndexedKeyframe& keyframe : aKeyframes) { + Maybe easing = + TimingParams::ParseEasing(keyframe.mKeyframeDict.mEasing, + aTarget->OwnerDoc(), aRv); + if (aRv.Failed()) { + return; + } + float offset = float(keyframe.mKeyframeDict.mOffset.Value()); + // We ignore keyframe.mKeyframeDict.mComposite since we don't support + // composite modes on keyframes yet. + + // keyframe.mPropertyValuePairs is currently sorted by CSS property IDL + // name, since that was the order we read the properties from the JS + // object. Re-sort the list so that longhand properties appear before + // shorthands, and with shorthands all appearing in increasing order of + // number of components. For two longhand properties, or two shorthands + // with the same number of components, sort by IDL name. + // + // @see PropertyPriorityComparator. + keyframe.mPropertyValuePairs.Sort(PropertyValuesPair::Comparator()); + + nsCSSPropertySet propertiesOnThisKeyframe; + for (const PropertyValuesPair& pair : keyframe.mPropertyValuePairs) { + MOZ_ASSERT(pair.mValues.Length() == 1, + "ConvertKeyframeSequence should have parsed single " + "DOMString values from the property-values pairs"); + // Parse the property's string value and produce a KeyframeValueEntry (or + // more than one, for shorthands) for it. + nsTArray values; + if (StyleAnimationValue::ComputeValues(pair.mProperty, + nsCSSProps::eEnabledForAllContent, + aTarget, + styleContext, + pair.mValues[0], + /* aUseSVGMode */ false, + values)) { + for (auto& value : values) { + // If we already got a value for this property on the keyframe, + // skip this one. + if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) { + continue; + } + + KeyframeValueEntry* entry = aResult.AppendElement(); + entry->mOffset = offset; + entry->mProperty = value.mProperty; + entry->mValue = value.mValue; + entry->mTimingFunction = easing; + + if (offset == 0.0) { + propertiesWithFromValue.AddProperty(value.mProperty); + } else if (offset == 1.0) { + propertiesWithToValue.AddProperty(value.mProperty); + } + propertiesOnThisKeyframe.AddProperty(value.mProperty); + properties.AddProperty(value.mProperty); + } + } + } + } + + // We don't support additive segments and so can't support missing properties + // using their underlying value in 0% and 100% keyframes. Throw an exception + // until we do support this. + if (!propertiesWithFromValue.Equals(properties) || + !propertiesWithToValue.Equals(properties)) { + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + return; + } +} + +/** + * Builds an array of AnimationProperty objects to represent the keyframe + * animation segments in aEntries. + */ +static void +BuildSegmentsFromValueEntries(nsTArray& aEntries, + nsTArray& aResult) +{ + if (aEntries.IsEmpty()) { + return; + } + + // Sort the KeyframeValueEntry objects so that all entries for a given + // property are together, and the entries are sorted by offset otherwise. + std::stable_sort(aEntries.begin(), aEntries.end(), + &KeyframeValueEntry::PropertyOffsetComparator::LessThan); + + MOZ_ASSERT(aEntries[0].mOffset == 0.0f); + MOZ_ASSERT(aEntries.LastElement().mOffset == 1.0f); + + // For a given index i, we want to generate a segment from aEntries[i] + // to aEntries[j], if: + // + // * j > i, + // * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and + // * aEntries[j - 1]'s offset/property is different from aEntries[j]'s. + // + // That will eliminate runs of same offset/property values where there's no + // point generating zero length segments in the middle of the animation. + // + // Additionally we need to generate a zero length segment at offset 0 and at + // offset 1, if we have multiple values for a given property at that offset, + // since we need to retain the very first and very last value so they can + // be used for reverse and forward filling. + + nsCSSProperty lastProperty = eCSSProperty_UNKNOWN; + AnimationProperty* animationProperty = nullptr; + + size_t i = 0, n = aEntries.Length(); + + while (i + 1 < n) { + // Starting from i, determine the next [i, j] interval from which to + // generate a segment. + size_t j; + if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) { + // We need to generate an initial zero-length segment. + MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty); + j = i + 1; + while (aEntries[j + 1].mOffset == 0.0f) { + MOZ_ASSERT(aEntries[j].mProperty == aEntries[j + 1].mProperty); + ++j; + } + } else if (aEntries[i].mOffset == 1.0f) { + if (aEntries[i + 1].mOffset == 1.0f) { + // We need to generate a final zero-length segment. + MOZ_ASSERT(aEntries[i].mProperty == aEntries[i].mProperty); + j = i + 1; + while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f) { + MOZ_ASSERT(aEntries[j].mProperty == aEntries[j + 1].mProperty); + ++j; + } + } else { + // New property. + MOZ_ASSERT(aEntries[i + 1].mOffset == 0.0f); + MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty); + ++i; + continue; + } + } else { + while (aEntries[i].mOffset == aEntries[i + 1].mOffset && + aEntries[i].mProperty == aEntries[i + 1].mProperty) { + ++i; + } + j = i + 1; + } + + // If we've moved on to a new property, create a new AnimationProperty + // to insert segments into. + if (aEntries[i].mProperty != lastProperty) { + MOZ_ASSERT(aEntries[i].mOffset == 0.0f); + animationProperty = aResult.AppendElement(); + animationProperty->mProperty = aEntries[i].mProperty; + lastProperty = aEntries[i].mProperty; + } + + MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer."); + + // Now generate the segment. + AnimationPropertySegment* segment = + animationProperty->mSegments.AppendElement(); + segment->mFromKey = aEntries[i].mOffset; + segment->mToKey = aEntries[j].mOffset; + segment->mFromValue = aEntries[i].mValue; + segment->mToValue = aEntries[j].mValue; + segment->mTimingFunction = aEntries[i].mTimingFunction; + + i = j; + } +} + +/** + * Converts a JS object to an IDL PropertyIndexedKeyframe and builds an + * array of AnimationProperty objects for the keyframe animation + * that it specifies. + * + * @param aTarget The target of the animation. + * @param aValue The JS object. + * @param aResult The array into which the resulting AnimationProperty + * objects will be appended. + */ +static void +BuildAnimationPropertyListFromPropertyIndexedKeyframes( + JSContext* aCx, + Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::Handle aValue, + InfallibleTArray& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aValue.isObject()); + + RefPtr styleContext = + LookupStyleContext(aTarget, aPseudoType); + if (!styleContext) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Convert the object to a PropertyIndexedKeyframe dictionary to + // get its explicit dictionary members. + dom::binding_detail::FastBasePropertyIndexedKeyframe keyframes; + if (!keyframes.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument", + false)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + Maybe easing = + TimingParams::ParseEasing(keyframes.mEasing, aTarget->OwnerDoc(), aRv); + + // We ignore easing.mComposite since we don't support composite modes on + // keyframes yet. + + // Get all the property--value-list pairs off the object. + JS::Rooted object(aCx, &aValue.toObject()); + nsTArray propertyValuesPairs; + if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow, + propertyValuesPairs)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // We must keep track of which properties we've already generated + // an AnimationProperty since the author could have specified both a + // shorthand and one of its component longhands on the + // PropertyIndexedKeyframe. + nsCSSPropertySet properties; + + // Create AnimationProperty objects for each PropertyValuesPair, applying + // the "distribute" spacing algorithm to the segments. + for (const PropertyValuesPair& pair : propertyValuesPairs) { + size_t count = pair.mValues.Length(); + if (count == 0) { + // No animation values for this property. + continue; + } + if (count == 1) { + // We don't support additive segments and so can't support an + // animation that goes from the underlying value to this + // specified value. Throw an exception until we do support this. + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + return; + } + + // If we find an invalid value, we don't create a segment for it, but + // we adjust the surrounding segments so that the timing of the segments + // is the same as if we did support it. For example, animating with + // values ["red", "green", "yellow", "invalid", "blue"] will generate + // segments with this timing: + // + // 0.00 -> 0.25 : red -> green + // 0.25 -> 0.50 : green -> yellow + // 0.50 -> 1.00 : yellow -> blue + // + // With future spec clarifications we might decide to preserve the invalid + // value on the segment and make the animation code deal with the invalid + // value instead. + nsTArray fromValues; + float fromKey = 0.0f; + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + nsCSSProps::eEnabledForAllContent, + aTarget, + styleContext, + pair.mValues[0], + /* aUseSVGMode */ false, + fromValues)) { + // We need to throw for an invalid first value, since that would imply an + // additive animation, which we don't support yet. + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + return; + } + + if (fromValues.IsEmpty()) { + // All longhand components of a shorthand pair.mProperty must be disabled. + continue; + } + + // Create AnimationProperty objects for each property that had a + // value computed. When pair.mProperty is a longhand, it is just + // that property. When pair.mProperty is a shorthand, we'll have + // one property per longhand component. + nsTArray animationPropertyIndexes; + animationPropertyIndexes.SetLength(fromValues.Length()); + for (size_t i = 0, n = fromValues.Length(); i < n; ++i) { + nsCSSProperty p = fromValues[i].mProperty; + bool found = false; + if (properties.HasProperty(p)) { + // We have already dealt with this property. Look up and + // overwrite the old AnimationProperty object. + for (size_t j = 0, m = aResult.Length(); j < m; ++j) { + if (aResult[j].mProperty == p) { + aResult[j].mSegments.Clear(); + animationPropertyIndexes[i] = j; + found = true; + break; + } + } + MOZ_ASSERT(found, "properties is inconsistent with aResult"); + } + if (!found) { + // This is the first time we've encountered this property. + animationPropertyIndexes[i] = aResult.Length(); + AnimationProperty* animationProperty = aResult.AppendElement(); + animationProperty->mProperty = p; + properties.AddProperty(p); + } + } + + double portion = 1.0 / (count - 1); + for (size_t i = 0; i < count - 1; ++i) { + nsTArray toValues; + float toKey = (i + 1) * portion; + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + nsCSSProps::eEnabledForAllContent, + aTarget, + styleContext, + pair.mValues[i + 1], + /* aUseSVGMode */ false, + toValues)) { + if (i + 1 == count - 1) { + // We need to throw for an invalid last value, since that would + // imply an additive animation, which we don't support yet. + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + return; + } + // Otherwise, skip the segment. + continue; + } + MOZ_ASSERT(toValues.Length() == fromValues.Length(), + "should get the same number of properties as the last time " + "we called ComputeValues for pair.mProperty"); + for (size_t j = 0, n = toValues.Length(); j < n; ++j) { + size_t index = animationPropertyIndexes[j]; + AnimationPropertySegment* segment = + aResult[index].mSegments.AppendElement(); + segment->mFromKey = fromKey; + segment->mFromValue = fromValues[j].mValue; + segment->mToKey = toKey; + segment->mToValue = toValues[j].mValue; + segment->mTimingFunction = easing; + } + fromValues = Move(toValues); + fromKey = toKey; + } + } +} + +/** + * Converts a JS object representing a property-indexed keyframe into + * an array of Keyframe objects. + * + * @param aCx The JSContext for |aValue|. + * @param aValue The JS object. + * @param aResult The array into which the resulting AnimationProperty + * objects will be appended. + * @param aRv Out param to store any errors thrown by this function. + */ +static void +GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx, + JS::Handle aValue, + nsTArray& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aValue.isObject()); + MOZ_ASSERT(aResult.IsEmpty()); + MOZ_ASSERT(!aRv.Failed()); + + // Convert the object to a property-indexed keyframe dictionary to + // get its explicit dictionary members. + dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict; + if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument", + false)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Get the document to use for parsing CSS properties. + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + Maybe easing = + TimingParams::ParseEasing(keyframeDict.mEasing, doc, aRv); + if (aRv.Failed()) { + return; + } + + // Get all the property--value-list pairs off the object. + JS::Rooted object(aCx, &aValue.toObject()); + nsTArray propertyValuesPairs; + if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow, + propertyValuesPairs)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Create a set of keyframes for each property. + nsCSSParser parser(doc->CSSLoader()); + nsClassHashtable processedKeyframes; + for (const PropertyValuesPair& pair : propertyValuesPairs) { + size_t count = pair.mValues.Length(); + if (count == 0) { + // No animation values for this property. + continue; + } + if (count == 1) { + // We don't support additive values and so can't support an + // animation that goes from the underlying value to this + // specified value. Throw an exception until we do support this. + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + return; + } + + size_t n = pair.mValues.Length() - 1; + size_t i = 0; + + for (const nsString& stringValue : pair.mValues) { + double offset = i++ / double(n); + Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset); + if (keyframe->mPropertyValues.IsEmpty()) { + keyframe->mTimingFunction = easing; + keyframe->mComputedOffset = offset; + } + keyframe->mPropertyValues.AppendElement( + MakePropertyValuePair(pair.mProperty, stringValue, parser, doc)); + } + } + + aResult.SetCapacity(processedKeyframes.Count()); + for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) { + aResult.AppendElement(Move(*iter.UserData())); + } + + aResult.Sort(ComputedOffsetComparator()); +} + +/** + * Returns true if the supplied set of keyframes has keyframe values for + * any property for which it does not also supply a value for the 0% and 100% + * offsets. In this case we are supposed to synthesize an additive zero value + * but since we don't support additive animation yet we can't support this + * case. We try to detect that here so we can throw an exception. The check is + * not entirely accurate but should detect most common cases. + * + * @param aKeyframes The set of keyframes to analyze. + * @param aDocument The document to use when parsing keyframes so we can + * try to detect where we have an invalid value at 0%/100%. + */ +static bool +RequiresAdditiveAnimation(const nsTArray& aKeyframes, + nsIDocument* aDocument) +{ + // We are looking to see if that every property referenced in |aKeyframes| + // has a valid property at offset 0.0 and 1.0. The check as to whether a + // property is valid or not, however, is not precise. We only check if the + // property can be parsed, NOT whether it can also be converted to a + // StyleAnimationValue since doing that requires a target element bound to + // a document which we might not always have at the point where we want to + // perform this check. + // + // This is only a temporary measure until we implement additive animation. + // So as long as this check catches most cases, and we don't do anything + // horrible in one of the cases we can't detect, it should be sufficient. + + nsCSSPropertySet properties; // All properties encountered. + nsCSSPropertySet propertiesWithFromValue; // Those with a defined 0% value. + nsCSSPropertySet propertiesWithToValue; // Those with a defined 100% value. + + auto addToPropertySets = [&](nsCSSProperty aProperty, double aOffset) { + properties.AddProperty(aProperty); + if (aOffset == 0.0) { + propertiesWithFromValue.AddProperty(aProperty); + } else if (aOffset == 1.0) { + propertiesWithToValue.AddProperty(aProperty); + } + }; + + for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) { + const Keyframe& frame = aKeyframes[i]; + + // We won't have called ApplyDistributeSpacing when this is called so + // we can't use frame.mComputedOffset. Instead we do a rough version + // of that algorithm that substitutes null offsets with 0.0 for the first + // frame, 1.0 for the last frame, and 0.5 for everything else. + double computedOffset = i == len - 1 + ? 1.0 + : i == 0 ? 0.0 : 0.5; + double offsetToUse = frame.mOffset + ? frame.mOffset.value() + : computedOffset; + + for (const PropertyValuePair& pair : frame.mPropertyValues) { + if (nsCSSProps::IsShorthand(pair.mProperty)) { + nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue(); + nsCSSParser parser(aDocument->CSSLoader()); + if (!parser.IsValueValidForProperty(pair.mProperty, + tokenStream->mTokenStream)) { + continue; + } + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( + prop, pair.mProperty, nsCSSProps::eEnabledForAllContent) { + addToPropertySets(*prop, offsetToUse); + } + } else { + if (pair.mValue.GetUnit() == eCSSUnit_TokenStream) { + continue; + } + addToPropertySets(pair.mProperty, offsetToUse); + } + } + } + + return !propertiesWithFromValue.Equals(properties) || + !propertiesWithToValue.Equals(properties); +} + +already_AddRefed +LookupStyleContext(dom::Element* aElement, CSSPseudoElementType aPseudoType) +{ + nsIDocument* doc = aElement->GetCurrentDoc(); + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + return nullptr; + } + + nsIAtom* pseudo = + aPseudoType < CSSPseudoElementType::Count ? + nsCSSPseudoElements::GetPseudoAtom(aPseudoType) : nullptr; + return nsComputedDOMStyle::GetStyleContextForElement(aElement, pseudo, shell); +} + +} // namespace mozilla diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h new file mode 100644 index 0000000000..c57f65e405 --- /dev/null +++ b/dom/animation/KeyframeUtils.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_KeyframeUtils_h +#define mozilla_KeyframeUtils_h + +#include "nsTArrayForwardDeclare.h" // For nsTArray +#include "js/RootingAPI.h" // For JS::Handle + +struct JSContext; +class JSObject; + +namespace mozilla { +struct AnimationProperty; +enum class CSSPseudoElementType : uint8_t; +class ErrorResult; +struct Keyframe; + +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + + +namespace mozilla { + +/** + * Utility methods for processing keyframes. + */ +class KeyframeUtils +{ +public: + /** + * Converts a JS value to a property-indexed keyframe or a sequence of + * regular keyframes and builds an array of AnimationProperty objects for the + * keyframe animation that it specifies. + * + * @param aTarget The target of the animation, used to resolve style + * for a property's underlying value if needed. + * @param aFrames The JS value, provided as an optional IDL |object?| value, + * that is the keyframe list specification. + * @param aResult The array into which the resulting AnimationProperty + * objects will be appended. + */ + static void + BuildAnimationPropertyList(JSContext* aCx, Element* aTarget, + CSSPseudoElementType aPseudoType, + JS::Handle aFrames, + InfallibleTArray& aResult, + ErrorResult& aRv); + + /** + * Converts a JS value representing a property-indexed keyframe or a sequence + * of keyframes to an array of Keyframe objects. + * + * @param aCx The JSContext that corresponds to |aFrames|. + * @param aFrames The JS value, provided as an optional IDL |object?| value, + * that is the keyframe list specification. + * @param aRv (out) Out-param to hold any error returned by this function. + * Must be initially empty. + * @return The set of processed keyframes. If an error occurs, aRv will be + * filled-in with the appropriate error code and an empty array will be + * returned. + */ + static nsTArray + GetKeyframesFromObject(JSContext* aCx, + JS::Handle aFrames, + ErrorResult& aRv); + + /** + * Fills in the mComputedOffset member of each keyframe in the given array + * using the "distribute" spacing algorithm. + * + * http://w3c.github.io/web-animations/#distribute-keyframe-spacing-mode + * + * @param keyframes The set of keyframes to adjust. + */ + static void ApplyDistributeSpacing(nsTArray& aKeyframes); + + /** + * Converts an array of Keyframe objects into an array of AnimationProperty + * objects. This involves expanding shorthand properties into longhand + * properties, creating an array of computed values for each longhand + * property and determining the offset and timing function to use for each + * value. + * + * @param aStyleContext The style context to use when computing values. + * @param aFrames The input keyframes. + * @return The set of animation properties. If an error occurs, the returned + * array will be empty. + */ + static nsTArray + GetAnimationPropertiesFromKeyframes(nsStyleContext* aStyleContext, + dom::Element* aElement, + CSSPseudoElementType aPseudoType, + const nsTArray& aFrames); +}; + +} // namespace mozilla + +#endif // mozilla_KeyframeUtils_h diff --git a/dom/animation/TimingParams.cpp b/dom/animation/TimingParams.cpp index 0092bfeefc..79603ca486 100644 --- a/dom/animation/TimingParams.cpp +++ b/dom/animation/TimingParams.cpp @@ -6,7 +6,12 @@ #include "mozilla/TimingParams.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/dom/AnimatableBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "nsCSSParser.h" // For nsCSSParser #include "nsIDocument.h" +#include "nsRuleNode.h" namespace mozilla { @@ -60,6 +65,15 @@ TimingParamsFromOptionsUnion(const OptionsType& aOptions, if (aRv.Failed()) { return result; } + TimingParams::ValidateIterations(timing.mIterations, aRv); + if (aRv.Failed()) { + return result; + } + Maybe easing = + TimingParams::ParseEasing(timing.mEasing, aDocument, aRv); + if (aRv.Failed()) { + return result; + } result.mDuration = duration; result.mDelay = TimeDuration::FromMilliseconds(timing.mDelay); @@ -68,7 +82,7 @@ TimingParamsFromOptionsUnion(const OptionsType& aOptions, result.mIterationStart = timing.mIterationStart; result.mDirection = timing.mDirection; result.mFill = timing.mFill; - result.mFunction = AnimationUtils::ParseEasing(timing.mEasing, aDocument); + result.mFunction = easing; } return result; } @@ -91,6 +105,66 @@ TimingParams::FromOptionsUnion( return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv); } +/* static */ Maybe +TimingParams::ParseEasing(const nsAString& aEasing, + nsIDocument* aDocument, + ErrorResult& aRv) +{ + MOZ_ASSERT(aDocument); + + nsCSSValue value; + nsCSSParser parser; + parser.ParseLonghandProperty(eCSSProperty_animation_timing_function, + aEasing, + aDocument->GetDocumentURI(), + aDocument->GetDocumentURI(), + aDocument->NodePrincipal(), + value); + + switch (value.GetUnit()) { + case eCSSUnit_List: { + const nsCSSValueList* list = value.GetListValue(); + if (list->mNext) { + // don't support a list of timing functions + break; + } + switch (list->mValue.GetUnit()) { + case eCSSUnit_Enumerated: + // Return Nothing() if "linear" is passed in. + if (list->mValue.GetIntValue() == + NS_STYLE_TRANSITION_TIMING_FUNCTION_LINEAR) { + return Nothing(); + } + MOZ_FALLTHROUGH; + case eCSSUnit_Cubic_Bezier: + case eCSSUnit_Steps: { + nsTimingFunction timingFunction; + nsRuleNode::ComputeTimingFunction(list->mValue, timingFunction); + ComputedTimingFunction computedTimingFunction; + computedTimingFunction.Init(timingFunction); + return Some(computedTimingFunction); + } + default: + MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function list " + "item unit"); + break; + } + break; + } + case eCSSUnit_Inherit: + case eCSSUnit_Initial: + case eCSSUnit_Unset: + case eCSSUnit_TokenStream: + case eCSSUnit_Null: + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function unit"); + break; + } + aRv.ThrowTypeError(); + return Nothing(); +} + bool TimingParams::operator==(const TimingParams& aOther) const { diff --git a/dom/animation/TimingParams.h b/dom/animation/TimingParams.h index 7c3a4320e6..3be6f80ca7 100644 --- a/dom/animation/TimingParams.h +++ b/dom/animation/TimingParams.h @@ -7,10 +7,12 @@ #ifndef mozilla_TimingParams_h #define mozilla_TimingParams_h +#include "nsStringFwd.h" #include "mozilla/dom/Nullable.h" #include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString #include "mozilla/ComputedTimingFunction.h" #include "mozilla/Maybe.h" +#include "mozilla/StickyTimeDuration.h" #include "mozilla/TimeStamp.h" // for TimeDuration // X11 has a #define for None @@ -53,12 +55,13 @@ struct TimingParams double durationInMs = aDuration.GetAsUnrestrictedDouble(); if (durationInMs >= 0) { result.emplace(StickyTimeDuration::FromMilliseconds(durationInMs)); - return result; + } else { + aRv.ThrowTypeError( + NS_LITERAL_STRING("duration")); } - } else if (aDuration.GetAsString().EqualsLiteral("auto")) { - return result; + } else if (!aDuration.GetAsString().EqualsLiteral("auto")) { + aRv.ThrowTypeError(); } - aRv.Throw(NS_ERROR_DOM_TYPE_ERR); return result; } @@ -66,10 +69,23 @@ struct TimingParams ErrorResult& aRv) { if (aIterationStart < 0) { - aRv.Throw(NS_ERROR_DOM_TYPE_ERR); + aRv.ThrowTypeError( + NS_LITERAL_STRING("iterationStart")); } } + static void ValidateIterations(double aIterations, ErrorResult& aRv) + { + if (IsNaN(aIterations) || aIterations < 0) { + aRv.ThrowTypeError( + NS_LITERAL_STRING("iterations")); + } + } + + static Maybe ParseEasing(const nsAString& aEasing, + nsIDocument* aDocument, + ErrorResult& aRv); + // mDuration.isNothing() represents the "auto" value Maybe mDuration; TimeDuration mDelay; // Initializes to zero diff --git a/dom/animation/moz.build b/dom/animation/moz.build index 8dcb1c1ad0..4bd6b91e43 100644 --- a/dom/animation/moz.build +++ b/dom/animation/moz.build @@ -23,9 +23,11 @@ EXPORTS.mozilla += [ 'AnimationPerformanceWarning.h', 'AnimationUtils.h', 'AnimValuesStyleRule.h', + 'ComputedTiming.h', 'ComputedTimingFunction.h', 'EffectCompositor.h', 'EffectSet.h', + 'KeyframeUtils.h', 'NonOwningAnimationTarget.h', 'PendingAnimationTracker.h', 'PseudoElementHashEntry.h', @@ -47,6 +49,7 @@ UNIFIED_SOURCES += [ 'EffectCompositor.cpp', 'EffectSet.cpp', 'KeyframeEffect.cpp', + 'KeyframeUtils.cpp', 'PendingAnimationTracker.cpp', 'TimingParams.cpp', ] diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html index a6735396ee..1113bad9b2 100644 --- a/dom/animation/test/chrome/test_animation_observers.html +++ b/dom/animation/test/chrome/test_animation_observers.html @@ -14,6 +14,10 @@ background-color: yellow; line-height: 16px; } +.init::before { + content: ""; + animation: anim 100s; +}