From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/animation/AnimValuesStyleRule.cpp | 110 ++ dom/animation/AnimValuesStyleRule.h | 58 + dom/animation/Animation.cpp | 1426 +++++++++++++++++ dom/animation/Animation.h | 453 ++++++ dom/animation/AnimationComparator.h | 32 + dom/animation/AnimationEffectReadOnly.cpp | 343 ++++ dom/animation/AnimationEffectReadOnly.h | 102 ++ dom/animation/AnimationEffectTiming.cpp | 152 ++ dom/animation/AnimationEffectTiming.h | 49 + dom/animation/AnimationEffectTimingReadOnly.cpp | 51 + dom/animation/AnimationEffectTimingReadOnly.h | 63 + dom/animation/AnimationPerformanceWarning.cpp | 79 + dom/animation/AnimationPerformanceWarning.h | 79 + dom/animation/AnimationTarget.h | 78 + dom/animation/AnimationTimeline.cpp | 63 + dom/animation/AnimationTimeline.h | 125 ++ dom/animation/AnimationUtils.cpp | 81 + dom/animation/AnimationUtils.h | 74 + dom/animation/CSSPseudoElement.cpp | 123 ++ dom/animation/CSSPseudoElement.h | 91 ++ dom/animation/ComputedTiming.h | 78 + dom/animation/ComputedTimingFunction.cpp | 194 +++ dom/animation/ComputedTimingFunction.h | 65 + dom/animation/DocumentTimeline.cpp | 283 ++++ dom/animation/DocumentTimeline.h | 111 ++ dom/animation/EffectCompositor.cpp | 920 +++++++++++ dom/animation/EffectCompositor.h | 307 ++++ dom/animation/EffectSet.cpp | 177 +++ dom/animation/EffectSet.h | 261 +++ dom/animation/KeyframeEffect.cpp | 211 +++ dom/animation/KeyframeEffect.h | 82 + dom/animation/KeyframeEffectParams.cpp | 169 ++ dom/animation/KeyframeEffectParams.h | 68 + dom/animation/KeyframeEffectReadOnly.cpp | 1430 +++++++++++++++++ dom/animation/KeyframeEffectReadOnly.h | 439 ++++++ dom/animation/KeyframeUtils.cpp | 1667 ++++++++++++++++++++ dom/animation/KeyframeUtils.h | 151 ++ dom/animation/PendingAnimationTracker.cpp | 124 ++ dom/animation/PendingAnimationTracker.h | 84 + dom/animation/PseudoElementHashEntry.h | 58 + dom/animation/TimingParams.cpp | 182 +++ dom/animation/TimingParams.h | 130 ++ dom/animation/moz.build | 67 + dom/animation/test/chrome.ini | 17 + dom/animation/test/chrome/file_animate_xrays.html | 19 + dom/animation/test/chrome/test_animate_xrays.html | 31 + .../test/chrome/test_animation_observers.html | 1177 ++++++++++++++ .../chrome/test_animation_performance_warning.html | 957 +++++++++++ .../test/chrome/test_animation_properties.html | 993 ++++++++++++ .../test_generated_content_getAnimations.html | 83 + .../test/chrome/test_observers_for_sync_api.html | 854 ++++++++++ dom/animation/test/chrome/test_restyles.html | 815 ++++++++++ .../test/chrome/test_running_on_compositor.html | 966 ++++++++++++ dom/animation/test/crashtests/1216842-1.html | 35 + dom/animation/test/crashtests/1216842-2.html | 35 + dom/animation/test/crashtests/1216842-3.html | 27 + dom/animation/test/crashtests/1216842-4.html | 27 + dom/animation/test/crashtests/1216842-5.html | 38 + dom/animation/test/crashtests/1216842-6.html | 38 + dom/animation/test/crashtests/1239889-1.html | 12 + dom/animation/test/crashtests/1244595-1.html | 3 + dom/animation/test/crashtests/1272475-1.html | 20 + dom/animation/test/crashtests/1272475-2.html | 20 + dom/animation/test/crashtests/1277272-1-inner.html | 19 + dom/animation/test/crashtests/1277272-1.html | 26 + dom/animation/test/crashtests/1278485-1.html | 26 + dom/animation/test/crashtests/1290535-1.html | 20 + dom/animation/test/crashtests/crashtests.list | 13 + .../test/css-animations/file_animation-cancel.html | 154 ++ .../file_animation-computed-timing.html | 566 +++++++ .../css-animations/file_animation-currenttime.html | 345 ++++ .../test/css-animations/file_animation-finish.html | 97 ++ .../css-animations/file_animation-finished.html | 93 ++ .../test/css-animations/file_animation-id.html | 24 + .../css-animations/file_animation-pausing.html | 165 ++ .../css-animations/file_animation-playstate.html | 71 + .../test/css-animations/file_animation-ready.html | 149 ++ .../css-animations/file_animation-reverse.html | 29 + .../css-animations/file_animation-starttime.html | 383 +++++ .../file_animations-dynamic-changes.html | 154 ++ .../file_cssanimation-animationname.html | 37 + .../file_document-get-animations.html | 276 ++++ .../test/css-animations/file_effect-target.html | 54 + .../file_element-get-animations.html | 445 ++++++ .../file_keyframeeffect-getkeyframes.html | 672 ++++++++ .../file_pseudoElement-get-animations.html | 70 + .../test/css-animations/test_animation-cancel.html | 15 + .../test_animation-computed-timing.html | 16 + .../css-animations/test_animation-currenttime.html | 15 + .../test/css-animations/test_animation-finish.html | 15 + .../css-animations/test_animation-finished.html | 15 + .../test/css-animations/test_animation-id.html | 15 + .../css-animations/test_animation-pausing.html | 15 + .../css-animations/test_animation-playstate.html | 15 + .../test/css-animations/test_animation-ready.html | 15 + .../css-animations/test_animation-reverse.html | 15 + .../css-animations/test_animation-starttime.html | 15 + .../test_animations-dynamic-changes.html | 15 + .../test_cssanimation-animationname.html | 15 + .../test_document-get-animations.html | 15 + .../test/css-animations/test_effect-target.html | 15 + .../test_element-get-animations.html | 15 + .../test_keyframeeffect-getkeyframes.html | 15 + .../test_pseudoElement-get-animations.html | 14 + .../css-transitions/file_animation-cancel.html | 165 ++ .../file_animation-computed-timing.html | 315 ++++ .../file_animation-currenttime.html | 307 ++++ .../css-transitions/file_animation-finished.html | 61 + .../css-transitions/file_animation-pausing.html | 50 + .../test/css-transitions/file_animation-ready.html | 96 ++ .../css-transitions/file_animation-starttime.html | 284 ++++ .../css-transitions/file_csstransition-events.html | 223 +++ .../file_csstransition-transitionproperty.html | 24 + .../file_document-get-animations.html | 93 ++ .../test/css-transitions/file_effect-target.html | 66 + .../file_element-get-animations.html | 147 ++ .../file_keyframeeffect-getkeyframes.html | 95 ++ .../file_pseudoElement-get-animations.html | 45 + .../test/css-transitions/file_setting-effect.html | 91 ++ .../css-transitions/test_animation-cancel.html | 14 + .../test_animation-computed-timing.html | 16 + .../test_animation-currenttime.html | 14 + .../css-transitions/test_animation-finished.html | 14 + .../css-transitions/test_animation-pausing.html | 14 + .../test/css-transitions/test_animation-ready.html | 14 + .../css-transitions/test_animation-starttime.html | 14 + .../css-transitions/test_csstransition-events.html | 14 + .../test_csstransition-transitionproperty.html | 14 + .../test_document-get-animations.html | 15 + .../test/css-transitions/test_effect-target.html | 14 + .../test_element-get-animations.html | 14 + .../test_keyframeeffect-getkeyframes.html | 14 + .../test_pseudoElement-get-animations.html | 14 + .../test/css-transitions/test_setting-effect.html | 14 + .../document-timeline/file_document-timeline.html | 135 ++ .../document-timeline/test_document-timeline.html | 14 + .../test_request_animation_frame.html | 27 + dom/animation/test/mochitest.ini | 111 ++ .../test/mozilla/file_cubic_bezier_limits.html | 167 ++ .../test/mozilla/file_deferred_start.html | 121 ++ .../mozilla/file_disable_animations_api_core.html | 30 + .../test/mozilla/file_disabled_properties.html | 73 + .../test/mozilla/file_discrete-animations.html | 170 ++ .../file_document-timeline-origin-time-range.html | 30 + dom/animation/test/mozilla/file_hide_and_show.html | 162 ++ .../test/mozilla/file_partial_keyframes.html | 41 + dom/animation/test/mozilla/file_set-easing.html | 34 + .../test/mozilla/file_spacing_property_order.html | 33 + .../test/mozilla/file_spacing_transform.html | 240 +++ .../test/mozilla/file_transform_limits.html | 55 + .../file_transition_finish_on_compositor.html | 67 + .../mozilla/file_underlying-discrete-value.html | 192 +++ .../test/mozilla/test_cubic_bezier_limits.html | 14 + .../test/mozilla/test_deferred_start.html | 14 + .../mozilla/test_disable_animations_api_core.html | 14 + .../test/mozilla/test_disabled_properties.html | 14 + .../test/mozilla/test_discrete-animations.html | 18 + .../test_document-timeline-origin-time-range.html | 14 + dom/animation/test/mozilla/test_hide_and_show.html | 14 + .../test/mozilla/test_partial_keyframes.html | 14 + dom/animation/test/mozilla/test_set-easing.html | 14 + .../test/mozilla/test_spacing_property_order.html | 14 + .../test/mozilla/test_spacing_transform.html | 14 + .../test/mozilla/test_transform_limits.html | 14 + .../test_transition_finish_on_compositor.html | 14 + .../mozilla/test_underlying-discrete-value.html | 15 + .../file_animation-seeking-with-current-time.html | 121 ++ .../file_animation-seeking-with-start-time.html | 121 ++ .../test/style/file_animation-setting-effect.html | 125 ++ .../test/style/file_animation-setting-spacing.html | 111 ++ .../test_animation-seeking-with-current-time.html | 15 + .../test_animation-seeking-with-start-time.html | 15 + .../test/style/test_animation-setting-effect.html | 15 + .../test/style/test_animation-setting-spacing.html | 14 + dom/animation/test/testcommon.js | 216 +++ 175 files changed, 26146 insertions(+) create mode 100644 dom/animation/AnimValuesStyleRule.cpp create mode 100644 dom/animation/AnimValuesStyleRule.h create mode 100644 dom/animation/Animation.cpp create mode 100644 dom/animation/Animation.h create mode 100644 dom/animation/AnimationComparator.h create mode 100644 dom/animation/AnimationEffectReadOnly.cpp create mode 100644 dom/animation/AnimationEffectReadOnly.h create mode 100644 dom/animation/AnimationEffectTiming.cpp create mode 100644 dom/animation/AnimationEffectTiming.h create mode 100644 dom/animation/AnimationEffectTimingReadOnly.cpp create mode 100644 dom/animation/AnimationEffectTimingReadOnly.h create mode 100644 dom/animation/AnimationPerformanceWarning.cpp create mode 100644 dom/animation/AnimationPerformanceWarning.h create mode 100644 dom/animation/AnimationTarget.h create mode 100644 dom/animation/AnimationTimeline.cpp create mode 100644 dom/animation/AnimationTimeline.h create mode 100644 dom/animation/AnimationUtils.cpp create mode 100644 dom/animation/AnimationUtils.h create mode 100644 dom/animation/CSSPseudoElement.cpp create mode 100644 dom/animation/CSSPseudoElement.h create mode 100644 dom/animation/ComputedTiming.h create mode 100644 dom/animation/ComputedTimingFunction.cpp create mode 100644 dom/animation/ComputedTimingFunction.h create mode 100644 dom/animation/DocumentTimeline.cpp create mode 100644 dom/animation/DocumentTimeline.h create mode 100644 dom/animation/EffectCompositor.cpp create mode 100644 dom/animation/EffectCompositor.h create mode 100644 dom/animation/EffectSet.cpp create mode 100644 dom/animation/EffectSet.h create mode 100644 dom/animation/KeyframeEffect.cpp create mode 100644 dom/animation/KeyframeEffect.h create mode 100644 dom/animation/KeyframeEffectParams.cpp create mode 100644 dom/animation/KeyframeEffectParams.h create mode 100644 dom/animation/KeyframeEffectReadOnly.cpp create mode 100644 dom/animation/KeyframeEffectReadOnly.h create mode 100644 dom/animation/KeyframeUtils.cpp create mode 100644 dom/animation/KeyframeUtils.h create mode 100644 dom/animation/PendingAnimationTracker.cpp create mode 100644 dom/animation/PendingAnimationTracker.h create mode 100644 dom/animation/PseudoElementHashEntry.h create mode 100644 dom/animation/TimingParams.cpp create mode 100644 dom/animation/TimingParams.h create mode 100644 dom/animation/moz.build create mode 100644 dom/animation/test/chrome.ini create mode 100644 dom/animation/test/chrome/file_animate_xrays.html create mode 100644 dom/animation/test/chrome/test_animate_xrays.html create mode 100644 dom/animation/test/chrome/test_animation_observers.html create mode 100644 dom/animation/test/chrome/test_animation_performance_warning.html create mode 100644 dom/animation/test/chrome/test_animation_properties.html create mode 100644 dom/animation/test/chrome/test_generated_content_getAnimations.html create mode 100644 dom/animation/test/chrome/test_observers_for_sync_api.html create mode 100644 dom/animation/test/chrome/test_restyles.html create mode 100644 dom/animation/test/chrome/test_running_on_compositor.html create mode 100644 dom/animation/test/crashtests/1216842-1.html create mode 100644 dom/animation/test/crashtests/1216842-2.html create mode 100644 dom/animation/test/crashtests/1216842-3.html create mode 100644 dom/animation/test/crashtests/1216842-4.html create mode 100644 dom/animation/test/crashtests/1216842-5.html create mode 100644 dom/animation/test/crashtests/1216842-6.html create mode 100644 dom/animation/test/crashtests/1239889-1.html create mode 100644 dom/animation/test/crashtests/1244595-1.html create mode 100644 dom/animation/test/crashtests/1272475-1.html create mode 100644 dom/animation/test/crashtests/1272475-2.html create mode 100644 dom/animation/test/crashtests/1277272-1-inner.html create mode 100644 dom/animation/test/crashtests/1277272-1.html create mode 100644 dom/animation/test/crashtests/1278485-1.html create mode 100644 dom/animation/test/crashtests/1290535-1.html create mode 100644 dom/animation/test/crashtests/crashtests.list create mode 100644 dom/animation/test/css-animations/file_animation-cancel.html create mode 100644 dom/animation/test/css-animations/file_animation-computed-timing.html create mode 100644 dom/animation/test/css-animations/file_animation-currenttime.html create mode 100644 dom/animation/test/css-animations/file_animation-finish.html create mode 100644 dom/animation/test/css-animations/file_animation-finished.html create mode 100644 dom/animation/test/css-animations/file_animation-id.html create mode 100644 dom/animation/test/css-animations/file_animation-pausing.html create mode 100644 dom/animation/test/css-animations/file_animation-playstate.html create mode 100644 dom/animation/test/css-animations/file_animation-ready.html create mode 100644 dom/animation/test/css-animations/file_animation-reverse.html create mode 100644 dom/animation/test/css-animations/file_animation-starttime.html create mode 100644 dom/animation/test/css-animations/file_animations-dynamic-changes.html create mode 100644 dom/animation/test/css-animations/file_cssanimation-animationname.html create mode 100644 dom/animation/test/css-animations/file_document-get-animations.html create mode 100644 dom/animation/test/css-animations/file_effect-target.html create mode 100644 dom/animation/test/css-animations/file_element-get-animations.html create mode 100644 dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html create mode 100644 dom/animation/test/css-animations/file_pseudoElement-get-animations.html create mode 100644 dom/animation/test/css-animations/test_animation-cancel.html create mode 100644 dom/animation/test/css-animations/test_animation-computed-timing.html create mode 100644 dom/animation/test/css-animations/test_animation-currenttime.html create mode 100644 dom/animation/test/css-animations/test_animation-finish.html create mode 100644 dom/animation/test/css-animations/test_animation-finished.html create mode 100644 dom/animation/test/css-animations/test_animation-id.html create mode 100644 dom/animation/test/css-animations/test_animation-pausing.html create mode 100644 dom/animation/test/css-animations/test_animation-playstate.html create mode 100644 dom/animation/test/css-animations/test_animation-ready.html create mode 100644 dom/animation/test/css-animations/test_animation-reverse.html create mode 100644 dom/animation/test/css-animations/test_animation-starttime.html create mode 100644 dom/animation/test/css-animations/test_animations-dynamic-changes.html create mode 100644 dom/animation/test/css-animations/test_cssanimation-animationname.html create mode 100644 dom/animation/test/css-animations/test_document-get-animations.html create mode 100644 dom/animation/test/css-animations/test_effect-target.html create mode 100644 dom/animation/test/css-animations/test_element-get-animations.html create mode 100644 dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html create mode 100644 dom/animation/test/css-animations/test_pseudoElement-get-animations.html create mode 100644 dom/animation/test/css-transitions/file_animation-cancel.html create mode 100644 dom/animation/test/css-transitions/file_animation-computed-timing.html create mode 100644 dom/animation/test/css-transitions/file_animation-currenttime.html create mode 100644 dom/animation/test/css-transitions/file_animation-finished.html create mode 100644 dom/animation/test/css-transitions/file_animation-pausing.html create mode 100644 dom/animation/test/css-transitions/file_animation-ready.html create mode 100644 dom/animation/test/css-transitions/file_animation-starttime.html create mode 100644 dom/animation/test/css-transitions/file_csstransition-events.html create mode 100644 dom/animation/test/css-transitions/file_csstransition-transitionproperty.html create mode 100644 dom/animation/test/css-transitions/file_document-get-animations.html create mode 100644 dom/animation/test/css-transitions/file_effect-target.html create mode 100644 dom/animation/test/css-transitions/file_element-get-animations.html create mode 100644 dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html create mode 100644 dom/animation/test/css-transitions/file_pseudoElement-get-animations.html create mode 100644 dom/animation/test/css-transitions/file_setting-effect.html create mode 100644 dom/animation/test/css-transitions/test_animation-cancel.html create mode 100644 dom/animation/test/css-transitions/test_animation-computed-timing.html create mode 100644 dom/animation/test/css-transitions/test_animation-currenttime.html create mode 100644 dom/animation/test/css-transitions/test_animation-finished.html create mode 100644 dom/animation/test/css-transitions/test_animation-pausing.html create mode 100644 dom/animation/test/css-transitions/test_animation-ready.html create mode 100644 dom/animation/test/css-transitions/test_animation-starttime.html create mode 100644 dom/animation/test/css-transitions/test_csstransition-events.html create mode 100644 dom/animation/test/css-transitions/test_csstransition-transitionproperty.html create mode 100644 dom/animation/test/css-transitions/test_document-get-animations.html create mode 100644 dom/animation/test/css-transitions/test_effect-target.html create mode 100644 dom/animation/test/css-transitions/test_element-get-animations.html create mode 100644 dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html create mode 100644 dom/animation/test/css-transitions/test_pseudoElement-get-animations.html create mode 100644 dom/animation/test/css-transitions/test_setting-effect.html create mode 100644 dom/animation/test/document-timeline/file_document-timeline.html create mode 100644 dom/animation/test/document-timeline/test_document-timeline.html create mode 100644 dom/animation/test/document-timeline/test_request_animation_frame.html create mode 100644 dom/animation/test/mochitest.ini create mode 100644 dom/animation/test/mozilla/file_cubic_bezier_limits.html create mode 100644 dom/animation/test/mozilla/file_deferred_start.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_core.html create mode 100644 dom/animation/test/mozilla/file_disabled_properties.html create mode 100644 dom/animation/test/mozilla/file_discrete-animations.html create mode 100644 dom/animation/test/mozilla/file_document-timeline-origin-time-range.html create mode 100644 dom/animation/test/mozilla/file_hide_and_show.html create mode 100644 dom/animation/test/mozilla/file_partial_keyframes.html create mode 100644 dom/animation/test/mozilla/file_set-easing.html create mode 100644 dom/animation/test/mozilla/file_spacing_property_order.html create mode 100644 dom/animation/test/mozilla/file_spacing_transform.html create mode 100644 dom/animation/test/mozilla/file_transform_limits.html create mode 100644 dom/animation/test/mozilla/file_transition_finish_on_compositor.html create mode 100644 dom/animation/test/mozilla/file_underlying-discrete-value.html create mode 100644 dom/animation/test/mozilla/test_cubic_bezier_limits.html create mode 100644 dom/animation/test/mozilla/test_deferred_start.html create mode 100644 dom/animation/test/mozilla/test_disable_animations_api_core.html create mode 100644 dom/animation/test/mozilla/test_disabled_properties.html create mode 100644 dom/animation/test/mozilla/test_discrete-animations.html create mode 100644 dom/animation/test/mozilla/test_document-timeline-origin-time-range.html create mode 100644 dom/animation/test/mozilla/test_hide_and_show.html create mode 100644 dom/animation/test/mozilla/test_partial_keyframes.html create mode 100644 dom/animation/test/mozilla/test_set-easing.html create mode 100644 dom/animation/test/mozilla/test_spacing_property_order.html create mode 100644 dom/animation/test/mozilla/test_spacing_transform.html create mode 100644 dom/animation/test/mozilla/test_transform_limits.html create mode 100644 dom/animation/test/mozilla/test_transition_finish_on_compositor.html create mode 100644 dom/animation/test/mozilla/test_underlying-discrete-value.html create mode 100644 dom/animation/test/style/file_animation-seeking-with-current-time.html create mode 100644 dom/animation/test/style/file_animation-seeking-with-start-time.html create mode 100644 dom/animation/test/style/file_animation-setting-effect.html create mode 100644 dom/animation/test/style/file_animation-setting-spacing.html create mode 100644 dom/animation/test/style/test_animation-seeking-with-current-time.html create mode 100644 dom/animation/test/style/test_animation-seeking-with-start-time.html create mode 100644 dom/animation/test/style/test_animation-setting-effect.html create mode 100644 dom/animation/test/style/test_animation-setting-spacing.html create mode 100644 dom/animation/test/testcommon.js (limited to 'dom/animation') diff --git a/dom/animation/AnimValuesStyleRule.cpp b/dom/animation/AnimValuesStyleRule.cpp new file mode 100644 index 000000000..dbdffd5b5 --- /dev/null +++ b/dom/animation/AnimValuesStyleRule.cpp @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +#include "AnimValuesStyleRule.h" +#include "nsRuleData.h" +#include "nsStyleContext.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(AnimValuesStyleRule, nsIStyleRule) + +void +AnimValuesStyleRule::MapRuleInfoInto(nsRuleData* aRuleData) +{ + nsStyleContext *contextParent = aRuleData->mStyleContext->GetParent(); + if (contextParent && contextParent->HasPseudoElementData()) { + // Don't apply transitions or animations to things inside of + // pseudo-elements. + // FIXME (Bug 522599): Add tests for this. + + // Prevent structs from being cached on the rule node since we're inside + // a pseudo-element, as we could determine cacheability differently + // when walking the rule tree for a style context that is not inside + // a pseudo-element. Note that nsRuleNode::GetStyle##name_ and GetStyleData + // will never look at cached structs when we're animating things inside + // a pseduo-element, so that we don't incorrectly return a struct that + // is only appropriate for non-pseudo-elements. + aRuleData->mConditions.SetUncacheable(); + return; + } + + for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) { + nsCSSPropertyID property = static_cast(iter.Key()); + if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID( + nsCSSProps::kSIDTable[property])) { + nsCSSValue *prop = aRuleData->ValueFor(property); + if (prop->GetUnit() == eCSSUnit_Null) { + DebugOnly ok = + StyleAnimationValue::UncomputeValue(property, iter.Data(), + *prop); + MOZ_ASSERT(ok, "could not store computed value"); + } + } + } +} + +bool +AnimValuesStyleRule::MightMapInheritedStyleData() +{ + return mStyleBits & NS_STYLE_INHERITED_STRUCT_MASK; +} + +bool +AnimValuesStyleRule::GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty, + nsCSSValue* aValue) +{ + MOZ_ASSERT(false, "GetDiscretelyAnimatedCSSValue is not implemented yet"); + return false; +} + +void +AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty, + const StyleAnimationValue &aValue) +{ + MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN, + "Unexpected css property"); + mAnimationValues.Put(aProperty, aValue); + mStyleBits |= + nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]); +} + +void +AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty, + StyleAnimationValue&& aValue) +{ + MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN, + "Unexpected css property"); + mAnimationValues.Put(aProperty, Move(aValue)); + mStyleBits |= + nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]); +} + +#ifdef DEBUG +void +AnimValuesStyleRule::List(FILE* out, int32_t aIndent) const +{ + nsAutoCString str; + for (int32_t index = aIndent; --index >= 0; ) { + str.AppendLiteral(" "); + } + str.AppendLiteral("[anim values] { "); + for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) { + nsCSSPropertyID property = static_cast(iter.Key()); + str.Append(nsCSSProps::GetStringValue(property)); + str.AppendLiteral(": "); + nsAutoString value; + Unused << + StyleAnimationValue::UncomputeValue(property, iter.Data(), value); + AppendUTF16toUTF8(value, str); + str.AppendLiteral("; "); + } + str.AppendLiteral("}\n"); + fprintf_stderr(out, "%s", str.get()); +} +#endif + +} // namespace mozilla diff --git a/dom/animation/AnimValuesStyleRule.h b/dom/animation/AnimValuesStyleRule.h new file mode 100644 index 000000000..3562014b9 --- /dev/null +++ b/dom/animation/AnimValuesStyleRule.h @@ -0,0 +1,58 @@ +/* -*- 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_AnimValuesStyleRule_h +#define mozilla_AnimValuesStyleRule_h + +#include "mozilla/StyleAnimationValue.h" +#include "nsCSSPropertyID.h" +#include "nsCSSPropertyIDSet.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" // For nsUint32HashKey +#include "nsIStyleRule.h" +#include "nsISupportsImpl.h" // For NS_DECL_ISUPPORTS +#include "nsRuleNode.h" // For nsCachedStyleData +#include "nsTArray.h" // For nsTArray + +namespace mozilla { + +/** + * A style rule that maps property-StyleAnimationValue pairs. + */ +class AnimValuesStyleRule final : public nsIStyleRule +{ +public: + AnimValuesStyleRule() + : mStyleBits(0) {} + + // nsISupports implementation + NS_DECL_ISUPPORTS + + // nsIStyleRule implementation + void MapRuleInfoInto(nsRuleData* aRuleData) override; + bool MightMapInheritedStyleData() override; + bool GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty, + nsCSSValue* aValue) override; +#ifdef DEBUG + void List(FILE* out = stdout, int32_t aIndent = 0) const override; +#endif + + // For the following functions, it there is already a value for |aProperty| it + // will be replaced with |aValue|. + void AddValue(nsCSSPropertyID aProperty, const StyleAnimationValue &aValue); + void AddValue(nsCSSPropertyID aProperty, StyleAnimationValue&& aValue); + +private: + ~AnimValuesStyleRule() {} + + nsDataHashtable mAnimationValues; + + uint32_t mStyleBits; +}; + +} // namespace mozilla + +#endif // mozilla_AnimValuesStyleRule_h diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp new file mode 100644 index 000000000..6dd583ed1 --- /dev/null +++ b/dom/animation/Animation.cpp @@ -0,0 +1,1426 @@ +/* -*- 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/. */ + +#include "Animation.h" +#include "AnimationUtils.h" +#include "mozilla/dom/AnimationBinding.h" +#include "mozilla/dom/AnimationPlaybackEvent.h" +#include "mozilla/dom/DocumentTimeline.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher +#include "mozilla/Maybe.h" // For Maybe +#include "nsAnimationManager.h" // For CSSAnimation +#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch +#include "nsIDocument.h" // For nsIDocument +#include "nsIPresShell.h" // For nsIPresShell +#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr +#include "nsTransitionManager.h" // For CSSTransition +#include "PendingAnimationTracker.h" // For PendingAnimationTracker + +namespace mozilla { +namespace dom { + +// Static members +uint64_t Animation::sNextAnimationIndex = 0; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper, + mTimeline, + mEffect, + mReady, + mFinished) + +NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Animation) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +JSObject* +Animation::WrapObject(JSContext* aCx, JS::Handle aGivenProto) +{ + return dom::AnimationBinding::Wrap(aCx, this, aGivenProto); +} + +// --------------------------------------------------------------------------- +// +// Utility methods +// +// --------------------------------------------------------------------------- + +namespace { + // A wrapper around nsAutoAnimationMutationBatch that looks up the + // appropriate document from the supplied animation. + class MOZ_RAII AutoMutationBatchForAnimation { + public: + explicit AutoMutationBatchForAnimation(const Animation& aAnimation + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + Maybe target = + nsNodeUtils::GetTargetForAnimation(&aAnimation); + if (!target) { + return; + } + + // For mutation observers, we use the OwnerDoc. + nsIDocument* doc = target->mElement->OwnerDoc(); + if (!doc) { + return; + } + + mAutoBatch.emplace(doc); + } + + private: + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER + Maybe mAutoBatch; + }; +} + +// --------------------------------------------------------------------------- +// +// Animation interface: +// +// --------------------------------------------------------------------------- +/* static */ already_AddRefed +Animation::Constructor(const GlobalObject& aGlobal, + AnimationEffectReadOnly* aEffect, + const Optional& aTimeline, + ErrorResult& aRv) +{ + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr animation = new Animation(global); + + AnimationTimeline* timeline; + if (aTimeline.WasPassed()) { + timeline = aTimeline.Value(); + } else { + nsIDocument* document = + AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!document) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + timeline = document->Timeline(); + } + + animation->SetTimelineNoUpdate(timeline); + animation->SetEffectNoUpdate(aEffect); + + return animation.forget(); +} + +void +Animation::SetId(const nsAString& aId) +{ + if (mId == aId) { + return; + } + mId = aId; + nsNodeUtils::AnimationChanged(this); +} + +void +Animation::SetEffect(AnimationEffectReadOnly* aEffect) +{ + SetEffectNoUpdate(aEffect); + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#setting-the-target-effect +void +Animation::SetEffectNoUpdate(AnimationEffectReadOnly* aEffect) +{ + RefPtr kungFuDeathGrip(this); + + if (mEffect == aEffect) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + bool wasRelevant = mIsRelevant; + + if (mEffect) { + if (!aEffect) { + // If the new effect is null, call ResetPendingTasks before clearing + // mEffect since ResetPendingTasks needs it to get the appropriate + // PendingAnimationTracker. + ResetPendingTasks(); + } + + // We need to notify observers now because once we set mEffect to null + // we won't be able to find the target element to notify. + if (mIsRelevant) { + nsNodeUtils::AnimationRemoved(this); + } + + // Break links with the old effect and then drop it. + RefPtr oldEffect = mEffect; + mEffect = nullptr; + oldEffect->SetAnimation(nullptr); + + // The following will not do any notification because mEffect is null. + UpdateRelevance(); + } + + if (aEffect) { + // Break links from the new effect to its previous animation, if any. + RefPtr newEffect = aEffect; + Animation* prevAnim = aEffect->GetAnimation(); + if (prevAnim) { + prevAnim->SetEffect(nullptr); + } + + // Create links with the new effect. SetAnimation(this) will also update + // mIsRelevant of this animation, and then notify mutation observer if + // needed by calling Animation::UpdateRelevance(), so we don't need to + // call it again. + mEffect = newEffect; + mEffect->SetAnimation(this); + + // Notify possible add or change. + // If the target is different, the change notification will be ignored by + // AutoMutationBatchForAnimation. + if (wasRelevant && mIsRelevant) { + nsNodeUtils::AnimationChanged(this); + } + + // Reschedule pending pause or pending play tasks. + // If we have a pending animation, it will either be registered + // in the pending animation tracker and have a null pending ready time, + // or, after it has been painted, it will be removed from the tracker + // and assigned a pending ready time. + // After updating the effect we'll typically need to repaint so if we've + // already been assigned a pending ready time, we should clear it and put + // the animation back in the tracker. + if (!mPendingReadyTime.IsNull()) { + mPendingReadyTime.SetNull(); + + nsIDocument* doc = GetRenderedDocument(); + if (doc) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + if (mPendingState == PendingState::PlayPending) { + tracker->AddPlayPending(*this); + } else { + tracker->AddPausePending(*this); + } + } + } + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); +} + +void +Animation::SetTimeline(AnimationTimeline* aTimeline) +{ + SetTimelineNoUpdate(aTimeline); + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#setting-the-timeline +void +Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) +{ + if (mTimeline == aTimeline) { + return; + } + + RefPtr oldTimeline = mTimeline; + if (oldTimeline) { + oldTimeline->RemoveAnimation(this); + } + + mTimeline = aTimeline; + if (!mStartTime.IsNull()) { + mHoldTime.SetNull(); + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); +} + +// https://w3c.github.io/web-animations/#set-the-animation-start-time +void +Animation::SetStartTime(const Nullable& aNewStartTime) +{ + if (aNewStartTime == mStartTime) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + Nullable timelineTime; + if (mTimeline) { + // The spec says to check if the timeline is active (has a resolved time) + // before using it here, but we don't need to since it's harmless to set + // the already null time to null. + timelineTime = mTimeline->GetCurrentTime(); + } + if (timelineTime.IsNull() && !aNewStartTime.IsNull()) { + mHoldTime.SetNull(); + } + + Nullable previousCurrentTime = GetCurrentTime(); + mStartTime = aNewStartTime; + if (!aNewStartTime.IsNull()) { + if (mPlaybackRate != 0.0) { + mHoldTime.SetNull(); + } + } else { + mHoldTime = previousCurrentTime; + } + + CancelPendingTasks(); + if (mReady) { + // We may have already resolved mReady, but in that case calling + // MaybeResolve is a no-op, so that's okay. + mReady->MaybeResolve(this); + } + + UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#current-time +Nullable +Animation::GetCurrentTime() const +{ + Nullable result; + if (!mHoldTime.IsNull()) { + result = mHoldTime; + return result; + } + + if (mTimeline && !mStartTime.IsNull()) { + Nullable timelineTime = mTimeline->GetCurrentTime(); + if (!timelineTime.IsNull()) { + result.SetValue((timelineTime.Value() - mStartTime.Value()) + .MultDouble(mPlaybackRate)); + } + } + return result; +} + +// https://w3c.github.io/web-animations/#set-the-current-time +void +Animation::SetCurrentTime(const TimeDuration& aSeekTime) +{ + // Return early if the current time has not changed. However, if we + // are pause-pending, then setting the current time to any value + // including the current value has the effect of aborting the + // pause so we should not return early in that case. + if (mPendingState != PendingState::PausePending && + Nullable(aSeekTime) == GetCurrentTime()) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + SilentlySetCurrentTime(aSeekTime); + + if (mPendingState == PendingState::PausePending) { + // Finish the pause operation + mHoldTime.SetValue(aSeekTime); + mStartTime.SetNull(); + + if (mReady) { + mReady->MaybeResolve(this); + } + CancelPendingTasks(); + } + + UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#set-the-animation-playback-rate +void +Animation::SetPlaybackRate(double aPlaybackRate) +{ + if (aPlaybackRate == mPlaybackRate) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + Nullable previousTime = GetCurrentTime(); + mPlaybackRate = aPlaybackRate; + if (!previousTime.IsNull()) { + SetCurrentTime(previousTime.Value()); + } + + // In the case where GetCurrentTime() returns the same result before and + // after updating mPlaybackRate, SetCurrentTime will return early since, + // as far as it can tell, nothing has changed. + // As a result, we need to perform the following updates here: + // - update timing (since, if the sign of the playback rate has changed, our + // finished state may have changed), + // - dispatch a change notification for the changed playback rate, and + // - update the playback rate on animations on layers. + UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#play-state +AnimationPlayState +Animation::PlayState() const +{ + if (mPendingState != PendingState::NotPending) { + return AnimationPlayState::Pending; + } + + Nullable currentTime = GetCurrentTime(); + if (currentTime.IsNull()) { + return AnimationPlayState::Idle; + } + + if (mStartTime.IsNull()) { + return AnimationPlayState::Paused; + } + + if ((mPlaybackRate > 0.0 && currentTime.Value() >= EffectEnd()) || + (mPlaybackRate < 0.0 && currentTime.Value() <= TimeDuration())) { + return AnimationPlayState::Finished; + } + + return AnimationPlayState::Running; +} + +Promise* +Animation::GetReady(ErrorResult& aRv) +{ + nsCOMPtr global = GetOwnerGlobal(); + if (!mReady && global) { + mReady = Promise::Create(global, aRv); // Lazily create on demand + } + if (!mReady) { + aRv.Throw(NS_ERROR_FAILURE); + } else if (PlayState() != AnimationPlayState::Pending) { + mReady->MaybeResolve(this); + } + return mReady; +} + +Promise* +Animation::GetFinished(ErrorResult& aRv) +{ + nsCOMPtr global = GetOwnerGlobal(); + if (!mFinished && global) { + mFinished = Promise::Create(global, aRv); // Lazily create on demand + } + if (!mFinished) { + aRv.Throw(NS_ERROR_FAILURE); + } else if (mFinishedIsResolved) { + MaybeResolveFinishedPromise(); + } + return mFinished; +} + +void +Animation::Cancel() +{ + CancelNoUpdate(); + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#finish-an-animation +void +Animation::Finish(ErrorResult& aRv) +{ + if (mPlaybackRate == 0 || + (mPlaybackRate > 0 && EffectEnd() == TimeDuration::Forever())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + AutoMutationBatchForAnimation mb(*this); + + // Seek to the end + TimeDuration limit = + mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0); + bool didChange = GetCurrentTime() != Nullable(limit); + SilentlySetCurrentTime(limit); + + // If we are paused or play-pending we need to fill in the start time in + // order to transition to the finished state. + // + // We only do this, however, if we have an active timeline. If we have an + // inactive timeline we can't transition into the finished state just like + // we can't transition to the running state (this finished state is really + // a substate of the running state). + if (mStartTime.IsNull() && + mTimeline && + !mTimeline->GetCurrentTime().IsNull()) { + mStartTime.SetValue(mTimeline->GetCurrentTime().Value() - + limit.MultDouble(1.0 / mPlaybackRate)); + didChange = true; + } + + // If we just resolved the start time for a pause or play-pending + // animation, we need to clear the task. We don't do this as a branch of + // the above however since we can have a play-pending animation with a + // resolved start time if we aborted a pause operation. + if (!mStartTime.IsNull() && + (mPendingState == PendingState::PlayPending || + mPendingState == PendingState::PausePending)) { + if (mPendingState == PendingState::PausePending) { + mHoldTime.SetNull(); + } + CancelPendingTasks(); + didChange = true; + if (mReady) { + mReady->MaybeResolve(this); + } + } + UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Sync); + if (didChange && IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } + PostUpdate(); +} + +void +Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior) +{ + PlayNoUpdate(aRv, aLimitBehavior); + PostUpdate(); +} + +void +Animation::Pause(ErrorResult& aRv) +{ + PauseNoUpdate(aRv); + PostUpdate(); +} + +// https://w3c.github.io/web-animations/#reverse-an-animation +void +Animation::Reverse(ErrorResult& aRv) +{ + if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mPlaybackRate == 0.0) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + SilentlySetPlaybackRate(-mPlaybackRate); + Play(aRv, LimitBehavior::AutoRewind); + + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } +} + +// --------------------------------------------------------------------------- +// +// JS wrappers for Animation interface: +// +// --------------------------------------------------------------------------- + +Nullable +Animation::GetStartTimeAsDouble() const +{ + return AnimationUtils::TimeDurationToDouble(mStartTime); +} + +void +Animation::SetStartTimeAsDouble(const Nullable& aStartTime) +{ + return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime)); +} + +Nullable +Animation::GetCurrentTimeAsDouble() const +{ + return AnimationUtils::TimeDurationToDouble(GetCurrentTime()); +} + +void +Animation::SetCurrentTimeAsDouble(const Nullable& aCurrentTime, + ErrorResult& aRv) +{ + if (aCurrentTime.IsNull()) { + if (!GetCurrentTime().IsNull()) { + aRv.Throw(NS_ERROR_DOM_TYPE_ERR); + } + return; + } + + return SetCurrentTime(TimeDuration::FromMilliseconds(aCurrentTime.Value())); +} + +// --------------------------------------------------------------------------- + +void +Animation::Tick() +{ + // Finish pending if we have a pending ready time, but only if we also + // have an active timeline. + if (mPendingState != PendingState::NotPending && + !mPendingReadyTime.IsNull() && + mTimeline && + !mTimeline->GetCurrentTime().IsNull()) { + // Even though mPendingReadyTime is initialized using TimeStamp::Now() + // during the *previous* tick of the refresh driver, it can still be + // ahead of the *current* timeline time when we are using the + // vsync timer so we need to clamp it to the timeline time. + mPendingReadyTime.SetValue(std::min(mTimeline->GetCurrentTime().Value(), + mPendingReadyTime.Value())); + FinishPendingAt(mPendingReadyTime.Value()); + mPendingReadyTime.SetNull(); + } + + if (IsPossiblyOrphanedPendingAnimation()) { + MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTime().IsNull(), + "Orphaned pending animations should have an active timeline"); + FinishPendingAt(mTimeline->GetCurrentTime().Value()); + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + + if (!mEffect) { + return; + } + + // Update layers if we are newly finished. + KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect && + !keyframeEffect->Properties().IsEmpty() && + !mFinishedAtLastComposeStyle && + PlayState() == AnimationPlayState::Finished) { + PostUpdate(); + } +} + +void +Animation::TriggerOnNextTick(const Nullable& aReadyTime) +{ + // Normally we expect the play state to be pending but it's possible that, + // due to the handling of possibly orphaned animations in Tick(), this + // animation got started whilst still being in another document's pending + // animation map. + if (PlayState() != AnimationPlayState::Pending) { + return; + } + + // If aReadyTime.IsNull() we'll detect this in Tick() where we check for + // orphaned animations and trigger this animation anyway + mPendingReadyTime = aReadyTime; +} + +void +Animation::TriggerNow() +{ + // Normally we expect the play state to be pending but when an animation + // is cancelled and its rendered document can't be reached, we can end up + // with the animation still in a pending player tracker even after it is + // no longer pending. + if (PlayState() != AnimationPlayState::Pending) { + return; + } + + // If we don't have an active timeline we can't trigger the animation. + // However, this is a test-only method that we don't expect to be used in + // conjunction with animations without an active timeline so generate + // a warning if we do find ourselves in that situation. + if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { + NS_WARNING("Failed to trigger an animation with an active timeline"); + return; + } + + FinishPendingAt(mTimeline->GetCurrentTime().Value()); +} + +Nullable +Animation::GetCurrentOrPendingStartTime() const +{ + Nullable result; + + if (!mStartTime.IsNull()) { + result = mStartTime; + return result; + } + + if (mPendingReadyTime.IsNull() || mHoldTime.IsNull()) { + return result; + } + + // Calculate the equivalent start time from the pending ready time. + result = StartTimeFromReadyTime(mPendingReadyTime.Value()); + + return result; +} + +TimeDuration +Animation::StartTimeFromReadyTime(const TimeDuration& aReadyTime) const +{ + MOZ_ASSERT(!mHoldTime.IsNull(), "Hold time should be set in order to" + " convert a ready time to a start time"); + if (mPlaybackRate == 0) { + return aReadyTime; + } + return aReadyTime - mHoldTime.Value().MultDouble(1 / mPlaybackRate); +} + +TimeStamp +Animation::AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const +{ + // Initializes to null. Return the same object every time to benefit from + // return-value-optimization. + TimeStamp result; + + // We *don't* check for mTimeline->TracksWallclockTime() here because that + // method only tells us if the timeline times can be converted to + // TimeStamps that can be compared to TimeStamp::Now() or not, *not* + // whether the timelines can be converted to TimeStamp values at all. + // + // Furthermore, we want to be able to use this method when the refresh driver + // is under test control (in which case TracksWallclockTime() will return + // false). + // + // Once we introduce timelines that are not time-based we will need to + // differentiate between them here and determine how to sort their events. + if (!mTimeline) { + return result; + } + + // Check the time is convertible to a timestamp + if (aTime == TimeDuration::Forever() || + mPlaybackRate == 0.0 || + mStartTime.IsNull()) { + return result; + } + + // Invert the standard relation: + // animation time = (timeline time - start time) * playback rate + TimeDuration timelineTime = + TimeDuration(aTime).MultDouble(1.0 / mPlaybackRate) + mStartTime.Value(); + + result = mTimeline->ToTimeStamp(timelineTime); + return result; +} + +TimeStamp +Animation::ElapsedTimeToTimeStamp( + const StickyTimeDuration& aElapsedTime) const +{ + return AnimationTimeToTimeStamp(aElapsedTime + + mEffect->SpecifiedTiming().mDelay); +} + + +// https://w3c.github.io/web-animations/#silently-set-the-current-time +void +Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) +{ + if (!mHoldTime.IsNull() || + mStartTime.IsNull() || + !mTimeline || + mTimeline->GetCurrentTime().IsNull() || + mPlaybackRate == 0.0) { + mHoldTime.SetValue(aSeekTime); + if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { + mStartTime.SetNull(); + } + } else { + mStartTime.SetValue(mTimeline->GetCurrentTime().Value() - + (aSeekTime.MultDouble(1 / mPlaybackRate))); + } + + mPreviousCurrentTime.SetNull(); +} + +void +Animation::SilentlySetPlaybackRate(double aPlaybackRate) +{ + Nullable previousTime = GetCurrentTime(); + mPlaybackRate = aPlaybackRate; + if (!previousTime.IsNull()) { + SilentlySetCurrentTime(previousTime.Value()); + } +} + +// https://w3c.github.io/web-animations/#cancel-an-animation +void +Animation::CancelNoUpdate() +{ + ResetPendingTasks(); + + if (mFinished) { + mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + ResetFinishedPromise(); + + DispatchPlaybackEvent(NS_LITERAL_STRING("cancel")); + + mHoldTime.SetNull(); + mStartTime.SetNull(); + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + + if (mTimeline) { + mTimeline->RemoveAnimation(this); + } +} + +void +Animation::UpdateRelevance() +{ + bool wasRelevant = mIsRelevant; + mIsRelevant = HasCurrentEffect() || IsInEffect(); + + // Notify animation observers. + if (wasRelevant && !mIsRelevant) { + nsNodeUtils::AnimationRemoved(this); + } else if (!wasRelevant && mIsRelevant) { + nsNodeUtils::AnimationAdded(this); + } +} + +bool +Animation::HasLowerCompositeOrderThan(const Animation& aOther) const +{ + // 0. Object-equality case + if (&aOther == this) { + return false; + } + + // 1. CSS Transitions sort lowest + { + auto asCSSTransitionForSorting = + [] (const Animation& anim) -> const CSSTransition* + { + const CSSTransition* transition = anim.AsCSSTransition(); + return transition && transition->IsTiedToMarkup() ? + transition : + nullptr; + }; + auto thisTransition = asCSSTransitionForSorting(*this); + auto otherTransition = asCSSTransitionForSorting(aOther); + if (thisTransition && otherTransition) { + return thisTransition->HasLowerCompositeOrderThan(*otherTransition); + } + if (thisTransition || otherTransition) { + return thisTransition; + } + } + + // 2. CSS Animations sort next + { + auto asCSSAnimationForSorting = + [] (const Animation& anim) -> const CSSAnimation* + { + const CSSAnimation* animation = anim.AsCSSAnimation(); + return animation && animation->IsTiedToMarkup() ? animation : nullptr; + }; + auto thisAnimation = asCSSAnimationForSorting(*this); + auto otherAnimation = asCSSAnimationForSorting(aOther); + if (thisAnimation && otherAnimation) { + return thisAnimation->HasLowerCompositeOrderThan(*otherAnimation); + } + if (thisAnimation || otherAnimation) { + return thisAnimation; + } + } + + // Subclasses of Animation repurpose mAnimationIndex to implement their + // own brand of composite ordering. However, by this point we should have + // handled any such custom composite ordering so we should now have unique + // animation indices. + MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex, + "Animation indices should be unique"); + + // 3. Finally, generic animations sort by their position in the global + // animation array. + return mAnimationIndex < aOther.mAnimationIndex; +} + +void +Animation::ComposeStyle(RefPtr& aStyleRule, + const nsCSSPropertyIDSet& aPropertiesToSkip) +{ + if (!mEffect) { + return; + } + + if (!IsInEffect()) { + return; + } + + // In order to prevent flicker, there are a few cases where we want to use + // a different time for rendering that would otherwise be returned by + // GetCurrentTime. These are: + // + // (a) For animations that are pausing but which are still running on the + // compositor. In this case we send a layer transaction that removes the + // animation but which also contains the animation values calculated on + // the main thread. To prevent flicker when this occurs we want to ensure + // the timeline time used to calculate the main thread animation values + // does not lag far behind the time used on the compositor. Ideally we + // would like to use the "animation ready time" calculated at the end of + // the layer transaction as the timeline time but it will be too late to + // update the style rule at that point so instead we just use the current + // wallclock time. + // + // (b) For animations that are pausing that we have already taken off the + // compositor. In this case we record a pending ready time but we don't + // apply it until the next tick. However, while waiting for the next tick, + // we should still use the pending ready time as the timeline time. If we + // use the regular timeline time the animation may appear jump backwards + // if the main thread's timeline time lags behind the compositor. + // + // (c) For animations that are play-pending due to an aborted pause operation + // (i.e. a pause operation that was interrupted before we entered the + // paused state). When we cancel a pending pause we might momentarily take + // the animation off the compositor, only to re-add it moments later. In + // that case the compositor might have been ahead of the main thread so we + // should use the current wallclock time to ensure the animation doesn't + // temporarily jump backwards. + // + // To address each of these cases we temporarily tweak the hold time + // immediately before updating the style rule and then restore it immediately + // afterwards. This is purely to prevent visual flicker. Other behavior + // such as dispatching events continues to rely on the regular timeline time. + AnimationPlayState playState = PlayState(); + { + AutoRestore> restoreHoldTime(mHoldTime); + + if (playState == AnimationPlayState::Pending && + mHoldTime.IsNull() && + !mStartTime.IsNull()) { + Nullable timeToUse = mPendingReadyTime; + if (timeToUse.IsNull() && + mTimeline && + mTimeline->TracksWallclockTime()) { + timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now()); + } + if (!timeToUse.IsNull()) { + mHoldTime.SetValue((timeToUse.Value() - mStartTime.Value()) + .MultDouble(mPlaybackRate)); + } + } + + KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect) { + keyframeEffect->ComposeStyle(aStyleRule, aPropertiesToSkip); + } + } + + MOZ_ASSERT(playState == PlayState(), + "Play state should not change during the course of compositing"); + mFinishedAtLastComposeStyle = (playState == AnimationPlayState::Finished); +} + +void +Animation::NotifyEffectTimingUpdated() +{ + MOZ_ASSERT(mEffect, + "We should only update timing effect when we have a target " + "effect"); + UpdateTiming(Animation::SeekFlag::NoSeek, + Animation::SyncNotifyFlag::Async); +} + +// https://w3c.github.io/web-animations/#play-an-animation +void +Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) +{ + AutoMutationBatchForAnimation mb(*this); + + bool abortedPause = mPendingState == PendingState::PausePending; + + Nullable currentTime = GetCurrentTime(); + if (mPlaybackRate > 0.0 && + (currentTime.IsNull() || + (aLimitBehavior == LimitBehavior::AutoRewind && + (currentTime.Value() < TimeDuration() || + currentTime.Value() >= EffectEnd())))) { + mHoldTime.SetValue(TimeDuration(0)); + } else if (mPlaybackRate < 0.0 && + (currentTime.IsNull() || + (aLimitBehavior == LimitBehavior::AutoRewind && + (currentTime.Value() <= TimeDuration() || + currentTime.Value() > EffectEnd())))) { + if (EffectEnd() == TimeDuration::Forever()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mHoldTime.SetValue(TimeDuration(EffectEnd())); + } else if (mPlaybackRate == 0.0 && currentTime.IsNull()) { + mHoldTime.SetValue(TimeDuration(0)); + } + + bool reuseReadyPromise = false; + if (mPendingState != PendingState::NotPending) { + CancelPendingTasks(); + reuseReadyPromise = true; + } + + // If the hold time is null then we're either already playing normally (and + // we can ignore this call) or we aborted a pending pause operation (in which + // case, for consistency, we need to go through the motions of doing an + // asynchronous start even though we already have a resolved start time). + if (mHoldTime.IsNull() && !abortedPause) { + return; + } + + // Clear the start time until we resolve a new one. We do this except + // for the case where we are aborting a pause and don't have a hold time. + // + // If we're aborting a pause and *do* have a hold time (e.g. because + // the animation is finished or we just applied the auto-rewind behavior + // above) we should respect it by clearing the start time. If we *don't* + // have a hold time we should keep the current start time so that the + // the animation continues moving uninterrupted by the aborted pause. + // + // (If we're not aborting a pause, mHoldTime must be resolved by now + // or else we would have returned above.) + if (!mHoldTime.IsNull()) { + mStartTime.SetNull(); + } + + if (!reuseReadyPromise) { + // Clear ready promise. We'll create a new one lazily. + mReady = nullptr; + } + + mPendingState = PendingState::PlayPending; + + nsIDocument* doc = GetRenderedDocument(); + if (doc) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + tracker->AddPlayPending(*this); + } else { + TriggerOnNextTick(Nullable()); + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } +} + +// https://w3c.github.io/web-animations/#pause-an-animation +void +Animation::PauseNoUpdate(ErrorResult& aRv) +{ + if (IsPausedOrPausing()) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + // If we are transitioning from idle, fill in the current time + if (GetCurrentTime().IsNull()) { + if (mPlaybackRate >= 0.0) { + mHoldTime.SetValue(TimeDuration(0)); + } else { + if (EffectEnd() == TimeDuration::Forever()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mHoldTime.SetValue(TimeDuration(EffectEnd())); + } + } + + bool reuseReadyPromise = false; + if (mPendingState == PendingState::PlayPending) { + CancelPendingTasks(); + reuseReadyPromise = true; + } + + if (!reuseReadyPromise) { + // Clear ready promise. We'll create a new one lazily. + mReady = nullptr; + } + + mPendingState = PendingState::PausePending; + + nsIDocument* doc = GetRenderedDocument(); + if (doc) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + tracker->AddPausePending(*this); + } else { + TriggerOnNextTick(Nullable()); + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + nsNodeUtils::AnimationChanged(this); + } +} + +void +Animation::ResumeAt(const TimeDuration& aReadyTime) +{ + // This method is only expected to be called for an animation that is + // waiting to play. We can easily adapt it to handle other states + // but it's currently not necessary. + MOZ_ASSERT(mPendingState == PendingState::PlayPending, + "Expected to resume a play-pending animation"); + MOZ_ASSERT(mHoldTime.IsNull() != mStartTime.IsNull(), + "An animation in the play-pending state should have either a" + " resolved hold time or resolved start time (but not both)"); + + // If we aborted a pending pause operation we will already have a start time + // we should use. In all other cases, we resolve it from the ready time. + if (mStartTime.IsNull()) { + mStartTime = StartTimeFromReadyTime(aReadyTime); + if (mPlaybackRate != 0) { + mHoldTime.SetNull(); + } + } + mPendingState = PendingState::NotPending; + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + + if (mReady) { + mReady->MaybeResolve(this); + } +} + +void +Animation::PauseAt(const TimeDuration& aReadyTime) +{ + MOZ_ASSERT(mPendingState == PendingState::PausePending, + "Expected to pause a pause-pending animation"); + + if (!mStartTime.IsNull() && mHoldTime.IsNull()) { + mHoldTime.SetValue((aReadyTime - mStartTime.Value()) + .MultDouble(mPlaybackRate)); + } + mStartTime.SetNull(); + mPendingState = PendingState::NotPending; + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + + if (mReady) { + mReady->MaybeResolve(this); + } +} + +void +Animation::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) +{ + // We call UpdateFinishedState before UpdateEffect because the former + // can change the current time, which is used by the latter. + UpdateFinishedState(aSeekFlag, aSyncNotifyFlag); + UpdateEffect(); + + if (mTimeline) { + mTimeline->NotifyAnimationUpdated(*this); + } +} + +// https://w3c.github.io/web-animations/#update-an-animations-finished-state +void +Animation::UpdateFinishedState(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) +{ + Nullable currentTime = GetCurrentTime(); + TimeDuration effectEnd = TimeDuration(EffectEnd()); + + if (!mStartTime.IsNull() && + mPendingState == PendingState::NotPending) { + if (mPlaybackRate > 0.0 && + !currentTime.IsNull() && + currentTime.Value() >= effectEnd) { + if (aSeekFlag == SeekFlag::DidSeek) { + mHoldTime = currentTime; + } else if (!mPreviousCurrentTime.IsNull()) { + mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd)); + } else { + mHoldTime.SetValue(effectEnd); + } + } else if (mPlaybackRate < 0.0 && + !currentTime.IsNull() && + currentTime.Value() <= TimeDuration()) { + if (aSeekFlag == SeekFlag::DidSeek) { + mHoldTime = currentTime; + } else if (!mPreviousCurrentTime.IsNull()) { + mHoldTime.SetValue(std::min(mPreviousCurrentTime.Value(), + TimeDuration(0))); + } else { + mHoldTime.SetValue(0); + } + } else if (mPlaybackRate != 0.0 && + !currentTime.IsNull() && + mTimeline && + !mTimeline->GetCurrentTime().IsNull()) { + if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) { + mStartTime.SetValue(mTimeline->GetCurrentTime().Value() - + (mHoldTime.Value().MultDouble(1 / mPlaybackRate))); + } + mHoldTime.SetNull(); + } + } + + bool currentFinishedState = PlayState() == AnimationPlayState::Finished; + if (currentFinishedState && !mFinishedIsResolved) { + DoFinishNotification(aSyncNotifyFlag); + } else if (!currentFinishedState && mFinishedIsResolved) { + ResetFinishedPromise(); + } + // We must recalculate the current time to take account of any mHoldTime + // changes the code above made. + mPreviousCurrentTime = GetCurrentTime(); +} + +void +Animation::UpdateEffect() +{ + if (mEffect) { + UpdateRelevance(); + + KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect) { + keyframeEffect->NotifyAnimationTimingUpdated(); + } + } +} + +void +Animation::FlushStyle() const +{ + nsIDocument* doc = GetRenderedDocument(); + if (doc) { + doc->FlushPendingNotifications(Flush_Style); + } +} + +void +Animation::PostUpdate() +{ + if (!mEffect) { + return; + } + + KeyframeEffectReadOnly* keyframeEffect = mEffect->AsKeyframeEffect(); + if (!keyframeEffect) { + return; + } + + Maybe target = keyframeEffect->GetTarget(); + if (!target) { + return; + } + + nsPresContext* presContext = keyframeEffect->GetPresContext(); + if (!presContext) { + return; + } + + presContext->EffectCompositor() + ->RequestRestyle(target->mElement, + target->mPseudoType, + EffectCompositor::RestyleType::Layer, + CascadeLevel()); +} + +void +Animation::CancelPendingTasks() +{ + if (mPendingState == PendingState::NotPending) { + return; + } + + nsIDocument* doc = GetRenderedDocument(); + if (doc) { + PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); + if (tracker) { + if (mPendingState == PendingState::PlayPending) { + tracker->RemovePlayPending(*this); + } else { + tracker->RemovePausePending(*this); + } + } + } + + mPendingState = PendingState::NotPending; + mPendingReadyTime.SetNull(); +} + +// https://w3c.github.io/web-animations/#reset-an-animations-pending-tasks +void +Animation::ResetPendingTasks() +{ + if (mPendingState == PendingState::NotPending) { + return; + } + + CancelPendingTasks(); + if (mReady) { + mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } +} + +bool +Animation::IsPossiblyOrphanedPendingAnimation() const +{ + // Check if we are pending but might never start because we are not being + // tracked. + // + // This covers the following cases: + // + // * We started playing but our effect's target element was orphaned + // or bound to a different document. + // (note that for the case of our effect changing we should handle + // that in SetEffect) + // * We started playing but our timeline became inactive. + // In this case the pending animation tracker will drop us from its hashmap + // when we have been painted. + // * When we started playing we couldn't find a PendingAnimationTracker to + // register with (perhaps the effect had no document) so we simply + // set mPendingState in PlayNoUpdate and relied on this method to catch us + // on the next tick. + + // If we're not pending we're ok. + if (mPendingState == PendingState::NotPending) { + return false; + } + + // If we have a pending ready time then we will be started on the next + // tick. + if (!mPendingReadyTime.IsNull()) { + return false; + } + + // If we don't have an active timeline then we shouldn't start until + // we do. + if (!mTimeline || mTimeline->GetCurrentTime().IsNull()) { + return false; + } + + // If we have no rendered document, or we're not in our rendered document's + // PendingAnimationTracker then there's a good chance no one is tracking us. + // + // If we're wrong and another document is tracking us then, at worst, we'll + // simply start/pause the animation one tick too soon. That's better than + // never starting/pausing the animation and is unlikely. + nsIDocument* doc = GetRenderedDocument(); + if (!doc) { + return true; + } + + PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); + return !tracker || + (!tracker->IsWaitingToPlay(*this) && + !tracker->IsWaitingToPause(*this)); +} + +StickyTimeDuration +Animation::EffectEnd() const +{ + if (!mEffect) { + return StickyTimeDuration(0); + } + + return mEffect->SpecifiedTiming().EndTime(); +} + +nsIDocument* +Animation::GetRenderedDocument() const +{ + if (!mEffect || !mEffect->AsKeyframeEffect()) { + return nullptr; + } + + return mEffect->AsKeyframeEffect()->GetRenderedDocument(); +} + +void +Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) +{ + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + + if (aSyncNotifyFlag == SyncNotifyFlag::Sync) { + DoFinishNotificationImmediately(); + } else if (!mFinishNotificationTask.IsPending()) { + RefPtr> runnable = + NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately); + context->DispatchToMicroTask(do_AddRef(runnable)); + mFinishNotificationTask = runnable.forget(); + } +} + +void +Animation::ResetFinishedPromise() +{ + mFinishedIsResolved = false; + mFinished = nullptr; +} + +void +Animation::MaybeResolveFinishedPromise() +{ + if (mFinished) { + mFinished->MaybeResolve(this); + } + mFinishedIsResolved = true; +} + +void +Animation::DoFinishNotificationImmediately() +{ + mFinishNotificationTask.Revoke(); + + if (PlayState() != AnimationPlayState::Finished) { + return; + } + + MaybeResolveFinishedPromise(); + + DispatchPlaybackEvent(NS_LITERAL_STRING("finish")); +} + +void +Animation::DispatchPlaybackEvent(const nsAString& aName) +{ + AnimationPlaybackEventInit init; + + if (aName.EqualsLiteral("finish")) { + init.mCurrentTime = GetCurrentTimeAsDouble(); + } + if (mTimeline) { + init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble(); + } + + RefPtr event = + AnimationPlaybackEvent::Constructor(this, aName, init); + event->SetTrusted(true); + + RefPtr asyncDispatcher = + new AsyncEventDispatcher(this, event); + asyncDispatcher->PostDOMEvent(); +} + +bool +Animation::IsRunningOnCompositor() const +{ + return mEffect && + mEffect->AsKeyframeEffect() && + mEffect->AsKeyframeEffect()->IsRunningOnCompositor(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h new file mode 100644 index 000000000..c59d7d6ce --- /dev/null +++ b/dom/animation/Animation.h @@ -0,0 +1,453 @@ +/* -*- 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_dom_Animation_h +#define mozilla_dom_Animation_h + +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel +#include "mozilla/LinkedList.h" +#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration +#include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState +#include "mozilla/dom/AnimationEffectReadOnly.h" +#include "mozilla/dom/AnimationTimeline.h" +#include "mozilla/dom/Promise.h" +#include "nsCSSPropertyID.h" +#include "nsIGlobalObject.h" + +// X11 has a #define for CurrentTime. +#ifdef CurrentTime +#undef CurrentTime +#endif + +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount(). +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +struct JSContext; +class nsCSSPropertyIDSet; +class nsIDocument; +class nsPresContext; + +namespace mozilla { + +class AnimValuesStyleRule; + +namespace dom { + +class CSSAnimation; +class CSSTransition; + +class Animation + : public DOMEventTargetHelper + , public LinkedListElement +{ +protected: + virtual ~Animation() {} + +public: + explicit Animation(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal) + , mPlaybackRate(1.0) + , mPendingState(PendingState::NotPending) + , mAnimationIndex(sNextAnimationIndex++) + , mFinishedAtLastComposeStyle(false) + , mIsRelevant(false) + , mFinishedIsResolved(false) + { + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation, + DOMEventTargetHelper) + + nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + virtual CSSAnimation* AsCSSAnimation() { return nullptr; } + virtual const CSSAnimation* AsCSSAnimation() const { return nullptr; } + virtual CSSTransition* AsCSSTransition() { return nullptr; } + virtual const CSSTransition* AsCSSTransition() const { return nullptr; } + + /** + * Flag to pass to Play to indicate whether or not it should automatically + * rewind the current time to the start point if the animation is finished. + * For regular calls to play() from script we should do this, but when a CSS + * animation's animation-play-state changes we shouldn't rewind the animation. + */ + enum class LimitBehavior { + AutoRewind, + Continue + }; + + // Animation interface methods + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + AnimationEffectReadOnly* aEffect, + const Optional& aTimeline, + ErrorResult& aRv); + void GetId(nsAString& aResult) const { aResult = mId; } + void SetId(const nsAString& aId); + AnimationEffectReadOnly* GetEffect() const { return mEffect; } + void SetEffect(AnimationEffectReadOnly* aEffect); + AnimationTimeline* GetTimeline() const { return mTimeline; } + void SetTimeline(AnimationTimeline* aTimeline); + Nullable GetStartTime() const { return mStartTime; } + void SetStartTime(const Nullable& aNewStartTime); + Nullable GetCurrentTime() const; + void SetCurrentTime(const TimeDuration& aNewCurrentTime); + double PlaybackRate() const { return mPlaybackRate; } + void SetPlaybackRate(double aPlaybackRate); + AnimationPlayState PlayState() const; + virtual Promise* GetReady(ErrorResult& aRv); + virtual Promise* GetFinished(ErrorResult& aRv); + void Cancel(); + virtual void Finish(ErrorResult& aRv); + virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior); + virtual void Pause(ErrorResult& aRv); + virtual void Reverse(ErrorResult& aRv); + bool IsRunningOnCompositor() const; + IMPL_EVENT_HANDLER(finish); + IMPL_EVENT_HANDLER(cancel); + + // Wrapper functions for Animation DOM methods when called + // from script. + // + // We often use the same methods internally and from script but when called + // from script we (or one of our subclasses) perform extra steps such as + // flushing style or converting the return type. + Nullable GetStartTimeAsDouble() const; + void SetStartTimeAsDouble(const Nullable& aStartTime); + Nullable GetCurrentTimeAsDouble() const; + void SetCurrentTimeAsDouble(const Nullable& aCurrentTime, + ErrorResult& aRv); + virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); } + virtual void PlayFromJS(ErrorResult& aRv) + { + Play(aRv, LimitBehavior::AutoRewind); + } + /** + * PauseFromJS is currently only here for symmetry with PlayFromJS but + * in future we will likely have to flush style in + * CSSAnimation::PauseFromJS so we leave it for now. + */ + void PauseFromJS(ErrorResult& aRv) { Pause(aRv); } + + // Wrapper functions for Animation DOM methods when called from style. + + virtual void CancelFromStyle() { CancelNoUpdate(); } + void SetTimelineNoUpdate(AnimationTimeline* aTimeline); + void SetEffectNoUpdate(AnimationEffectReadOnly* aEffect); + + virtual void Tick(); + bool NeedsTicks() const + { + AnimationPlayState playState = PlayState(); + return playState == AnimationPlayState::Running || + playState == AnimationPlayState::Pending; + } + + /** + * Set the time to use for starting or pausing a pending animation. + * + * Typically, when an animation is played, it does not start immediately but + * is added to a table of pending animations on the document of its effect. + * In the meantime it sets its hold time to the time from which playback + * should begin. + * + * When the document finishes painting, any pending animations in its table + * are marked as being ready to start by calling StartOnNextTick. + * The moment when the paint completed is also recorded, converted to a + * timeline time, and passed to StartOnTick. This is so that when these + * animations do start, they can be timed from the point when painting + * completed. + * + * After calling TriggerOnNextTick, animations remain in the pending state + * until the next refresh driver tick. At that time they transition out of + * the pending state using the time passed to TriggerOnNextTick as the + * effective time at which they resumed. + * + * This approach means that any setup time required for performing the + * initial paint of an animation such as layerization is not deducted from + * the running time of the animation. Without this we can easily drop the + * first few frames of an animation, or, on slower devices, the whole + * animation. + * + * Furthermore: + * + * - Starting the animation immediately when painting finishes is problematic + * because the start time of the animation will be ahead of its timeline + * (since the timeline time is based on the refresh driver time). + * That's a problem because the animation is playing but its timing + * suggests it starts in the future. We could update the timeline to match + * the start time of the animation but then we'd also have to update the + * timing and style of all animations connected to that timeline or else be + * stuck in an inconsistent state until the next refresh driver tick. + * + * - If we simply use the refresh driver time on its next tick, the lag + * between triggering an animation and its effective start is unacceptably + * long. + * + * For pausing, we apply the same asynchronous approach. This is so that we + * synchronize with animations that are running on the compositor. Otherwise + * if the main thread lags behind the compositor there will be a noticeable + * jump backwards when the main thread takes over. Even though main thread + * animations could be paused immediately, we do it asynchronously for + * consistency and so that animations paused together end up in step. + * + * Note that the caller of this method is responsible for removing the + * animation from any PendingAnimationTracker it may have been added to. + */ + void TriggerOnNextTick(const Nullable& aReadyTime); + /** + * Testing only: Start or pause a pending animation using the current + * timeline time. This is used to support existing tests that expect + * animations to begin immediately. Ideally we would rewrite the those tests + * and get rid of this method, but there are a lot of them. + * + * As with TriggerOnNextTick, the caller of this method is responsible for + * removing the animation from any PendingAnimationTracker it may have been + * added to. + */ + void TriggerNow(); + /** + * When StartOnNextTick is called, we store the ready time but we don't apply + * it until the next tick. In the meantime, GetStartTime() will return null. + * + * However, if we build layer animations again before the next tick, we + * should initialize them with the start time that GetStartTime() will return + * on the next tick. + * + * If we were to simply set the start time of layer animations to null, their + * start time would be updated to the current wallclock time when rendering + * finishes, thus making them out of sync with the start time stored here. + * This, in turn, will make the animation jump backwards when we build + * animations on the next tick and apply the start time stored here. + * + * This method returns the start time, if resolved. Otherwise, if we have + * a pending ready time, it returns the corresponding start time. If neither + * of those are available, it returns null. + */ + Nullable GetCurrentOrPendingStartTime() const; + + /** + * Calculates the corresponding start time to use for an animation that is + * currently pending with current time |mHoldTime| but should behave + * as if it began or resumed playback at timeline time |aReadyTime|. + */ + TimeDuration StartTimeFromReadyTime(const TimeDuration& aReadyTime) const; + + /** + * Converts a time in the timescale of this Animation's currentTime, to a + * TimeStamp. Returns a null TimeStamp if the conversion cannot be performed + * because of the current state of this Animation (e.g. it has no timeline, a + * zero playbackRate, an unresolved start time etc.) or the value of the time + * passed-in (e.g. an infinite time). + */ + TimeStamp AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const; + + // Converts an AnimationEvent's elapsedTime value to an equivalent TimeStamp + // that can be used to sort events by when they occurred. + TimeStamp ElapsedTimeToTimeStamp(const StickyTimeDuration& aElapsedTime) const; + + bool IsPausedOrPausing() const + { + return PlayState() == AnimationPlayState::Paused || + mPendingState == PendingState::PausePending; + } + + bool HasCurrentEffect() const + { + return GetEffect() && GetEffect()->IsCurrent(); + } + bool IsInEffect() const + { + return GetEffect() && GetEffect()->IsInEffect(); + } + + /** + * Returns true if this animation's playback state makes it a candidate for + * running on the compositor. + * We send animations to the compositor when their target effect is 'current' + * (a definition that is roughly equivalent to when they are in their before + * or active phase). However, we don't send animations to the compositor when + * they are paused/pausing (including being effectively paused due to + * having a zero playback rate), have a zero-duration active interval, or have + * no target effect at all. + */ + bool IsPlayableOnCompositor() const + { + return HasCurrentEffect() && + mPlaybackRate != 0.0 && + (PlayState() == AnimationPlayState::Running || + mPendingState == PendingState::PlayPending) && + !GetEffect()->IsActiveDurationZero(); + } + bool IsRelevant() const { return mIsRelevant; } + void UpdateRelevance(); + + /** + * Returns true if this Animation has a lower composite order than aOther. + */ + bool HasLowerCompositeOrderThan(const Animation& aOther) const; + + /** + * Returns the level at which the effect(s) associated with this Animation + * are applied to the CSS cascade. + */ + virtual EffectCompositor::CascadeLevel CascadeLevel() const + { + return EffectCompositor::CascadeLevel::Animations; + } + + /** + * Returns true if this animation does not currently need to update + * style on the main thread (e.g. because it is empty, or is + * running on the compositor). + */ + bool CanThrottle() const; + /** + * Updates |aStyleRule| with the animation values of this animation's effect, + * if any. + * Any properties contained in |aPropertiesToSkip| will not be added or + * updated in |aStyleRule|. + */ + void ComposeStyle(RefPtr& aStyleRule, + const nsCSSPropertyIDSet& aPropertiesToSkip); + + void NotifyEffectTimingUpdated(); + +protected: + void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime); + void SilentlySetPlaybackRate(double aPlaybackRate); + void CancelNoUpdate(); + void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior); + void PauseNoUpdate(ErrorResult& aRv); + void ResumeAt(const TimeDuration& aReadyTime); + void PauseAt(const TimeDuration& aReadyTime); + void FinishPendingAt(const TimeDuration& aReadyTime) + { + if (mPendingState == PendingState::PlayPending) { + ResumeAt(aReadyTime); + } else if (mPendingState == PendingState::PausePending) { + PauseAt(aReadyTime); + } else { + NS_NOTREACHED("Can't finish pending if we're not in a pending state"); + } + } + + /** + * Finishing behavior depends on if changes to timing occurred due + * to a seek or regular playback. + */ + enum class SeekFlag { + NoSeek, + DidSeek + }; + + enum class SyncNotifyFlag { + Sync, + Async + }; + + virtual void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag); + void UpdateFinishedState(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag); + void UpdateEffect(); + void FlushStyle() const; + void PostUpdate(); + void ResetFinishedPromise(); + void MaybeResolveFinishedPromise(); + void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag); + void DoFinishNotificationImmediately(); + void DispatchPlaybackEvent(const nsAString& aName); + + /** + * Remove this animation from the pending animation tracker and reset + * mPendingState as necessary. The caller is responsible for resolving or + * aborting the mReady promise as necessary. + */ + void CancelPendingTasks(); + + /** + * Performs the same steps as CancelPendingTasks and also rejects and + * recreates the ready promise if the animation was pending. + */ + void ResetPendingTasks(); + + bool IsPossiblyOrphanedPendingAnimation() const; + StickyTimeDuration EffectEnd() const; + + nsIDocument* GetRenderedDocument() const; + + RefPtr mTimeline; + RefPtr mEffect; + // The beginning of the delay period. + Nullable mStartTime; // Timeline timescale + Nullable mHoldTime; // Animation timescale + Nullable mPendingReadyTime; // Timeline timescale + Nullable mPreviousCurrentTime; // Animation timescale + double mPlaybackRate; + + // A Promise that is replaced on each call to Play() + // and fulfilled when Play() is successfully completed. + // This object is lazily created by GetReady. + // See http://w3c.github.io/web-animations/#current-ready-promise + RefPtr mReady; + + // A Promise that is resolved when we reach the end of the effect, or + // 0 when playing backwards. The Promise is replaced if the animation is + // finished but then a state change makes it not finished. + // This object is lazily created by GetFinished. + // See http://w3c.github.io/web-animations/#current-finished-promise + RefPtr mFinished; + + // Indicates if the animation is in the pending state (and what state it is + // waiting to enter when it finished pending). We use this rather than + // checking if this animation is tracked by a PendingAnimationTracker because + // the animation will continue to be pending even after it has been removed + // from the PendingAnimationTracker while it is waiting for the next tick + // (see TriggerOnNextTick for details). + enum class PendingState { NotPending, PlayPending, PausePending }; + PendingState mPendingState; + + static uint64_t sNextAnimationIndex; + + // The relative position of this animation within the global animation list. + // This is kNoIndex while the animation is in the idle state and is updated + // each time the animation transitions out of the idle state. + // + // Note that subclasses such as CSSTransition and CSSAnimation may repurpose + // this member to implement their own brand of sorting. As a result, it is + // possible for two different objects to have the same index. + uint64_t mAnimationIndex; + + bool mFinishedAtLastComposeStyle; + // Indicates that the animation should be exposed in an element's + // getAnimations() list. + bool mIsRelevant; + + nsRevocableEventPtr> mFinishNotificationTask; + // True if mFinished is resolved or would be resolved if mFinished has + // yet to be created. This is not set when mFinished is rejected since + // in that case mFinished is immediately reset to represent a new current + // finished promise. + bool mFinishedIsResolved; + + nsString mId; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Animation_h diff --git a/dom/animation/AnimationComparator.h b/dom/animation/AnimationComparator.h new file mode 100644 index 000000000..ff665e82a --- /dev/null +++ b/dom/animation/AnimationComparator.h @@ -0,0 +1,32 @@ +/* -*- 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_AnimationComparator_h +#define mozilla_AnimationComparator_h + +namespace mozilla { + +// Although this file is called AnimationComparator, we don't actually +// implement AnimationComparator (to compare const Animation& parameters) +// since it's not actually needed (yet). + +template +class AnimationPtrComparator { +public: + bool Equals(const AnimationPtrType& a, const AnimationPtrType& b) const + { + return a == b; + } + + bool LessThan(const AnimationPtrType& a, const AnimationPtrType& b) const + { + return a->HasLowerCompositeOrderThan(*b); + } +}; + +} // namespace mozilla + +#endif // mozilla_AnimationComparator_h diff --git a/dom/animation/AnimationEffectReadOnly.cpp b/dom/animation/AnimationEffectReadOnly.cpp new file mode 100644 index 000000000..aff28a37b --- /dev/null +++ b/dom/animation/AnimationEffectReadOnly.cpp @@ -0,0 +1,343 @@ +/* -*- 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/. */ + +#include "mozilla/dom/AnimationEffectReadOnly.h" +#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/FloatingPoint.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffectReadOnly) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffectReadOnly) + if (tmp->mTiming) { + tmp->mTiming->Unlink(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mTiming, mAnimation) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffectReadOnly) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mTiming, mAnimation) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffectReadOnly) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffectReadOnly) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffectReadOnly) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffectReadOnly) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +AnimationEffectReadOnly::AnimationEffectReadOnly( + nsIDocument* aDocument, AnimationEffectTimingReadOnly* aTiming) + : mDocument(aDocument) + , mTiming(aTiming) +{ + MOZ_ASSERT(aTiming); +} + +// https://w3c.github.io/web-animations/#current +bool +AnimationEffectReadOnly::IsCurrent() const +{ + if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) { + return false; + } + + ComputedTiming computedTiming = GetComputedTiming(); + return computedTiming.mPhase == ComputedTiming::AnimationPhase::Before || + computedTiming.mPhase == ComputedTiming::AnimationPhase::Active; +} + +// https://w3c.github.io/web-animations/#in-effect +bool +AnimationEffectReadOnly::IsInEffect() const +{ + ComputedTiming computedTiming = GetComputedTiming(); + return !computedTiming.mProgress.IsNull(); +} + +already_AddRefed +AnimationEffectReadOnly::Timing() +{ + RefPtr temp(mTiming); + return temp.forget(); +} + +void +AnimationEffectReadOnly::SetSpecifiedTiming(const TimingParams& aTiming) +{ + if (mTiming->AsTimingParams() == aTiming) { + return; + } + mTiming->SetTimingParams(aTiming); + if (mAnimation) { + mAnimation->NotifyEffectTimingUpdated(); + } + // For keyframe effects, NotifyEffectTimingUpdated above will eventually cause + // KeyframeEffectReadOnly::NotifyAnimationTimingUpdated to be called so it can + // update its registration with the target element as necessary. +} + +ComputedTiming +AnimationEffectReadOnly::GetComputedTimingAt( + const Nullable& aLocalTime, + const TimingParams& aTiming, + double aPlaybackRate) +{ + const StickyTimeDuration zeroDuration; + + // Always return the same object to benefit from return-value optimization. + ComputedTiming result; + + if (aTiming.mDuration) { + MOZ_ASSERT(aTiming.mDuration.ref() >= zeroDuration, + "Iteration duration should be positive"); + result.mDuration = aTiming.mDuration.ref(); + } + + MOZ_ASSERT(aTiming.mIterations >= 0.0 && !IsNaN(aTiming.mIterations), + "mIterations should be nonnegative & finite, as ensured by " + "ValidateIterations or CSSParser"); + result.mIterations = aTiming.mIterations; + + MOZ_ASSERT(aTiming.mIterationStart >= 0.0, + "mIterationStart should be nonnegative, as ensured by " + "ValidateIterationStart"); + result.mIterationStart = aTiming.mIterationStart; + + result.mActiveDuration = aTiming.ActiveDuration(); + result.mEndTime = aTiming.EndTime(); + result.mFill = aTiming.mFill == dom::FillMode::Auto ? + dom::FillMode::None : + aTiming.mFill; + + // The default constructor for ComputedTiming sets all other members to + // values consistent with an animation that has not been sampled. + if (aLocalTime.IsNull()) { + return result; + } + const TimeDuration& localTime = aLocalTime.Value(); + + // Calculate the time within the active interval. + // https://w3c.github.io/web-animations/#active-time + StickyTimeDuration activeTime; + + StickyTimeDuration beforeActiveBoundary = + std::max(std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime), + zeroDuration); + + StickyTimeDuration activeAfterBoundary = + std::max(std::min(StickyTimeDuration(aTiming.mDelay + + result.mActiveDuration), + result.mEndTime), + zeroDuration); + + if (localTime > activeAfterBoundary || + (aPlaybackRate >= 0 && localTime == activeAfterBoundary)) { + result.mPhase = ComputedTiming::AnimationPhase::After; + if (!result.FillsForwards()) { + // The animation isn't active or filling at this time. + return result; + } + activeTime = + std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay), + result.mActiveDuration), + zeroDuration); + } else if (localTime < beforeActiveBoundary || + (aPlaybackRate < 0 && localTime == beforeActiveBoundary)) { + result.mPhase = ComputedTiming::AnimationPhase::Before; + if (!result.FillsBackwards()) { + // The animation isn't active or filling at this time. + return result; + } + activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay), + zeroDuration); + } else { + MOZ_ASSERT(result.mActiveDuration != zeroDuration, + "How can we be in the middle of a zero-duration interval?"); + result.mPhase = ComputedTiming::AnimationPhase::Active; + activeTime = localTime - aTiming.mDelay; + } + + // Convert active time to a multiple of iterations. + // https://w3c.github.io/web-animations/#overall-progress + double overallProgress; + if (result.mDuration == zeroDuration) { + overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before + ? 0.0 + : result.mIterations; + } else { + overallProgress = activeTime / result.mDuration; + } + + // Factor in iteration start offset. + if (IsFinite(overallProgress)) { + overallProgress += result.mIterationStart; + } + + // Determine the 0-based index of the current iteration. + // https://w3c.github.io/web-animations/#current-iteration + result.mCurrentIteration = + IsInfinite(result.mIterations) && + result.mPhase == ComputedTiming::AnimationPhase::After + ? UINT64_MAX // In GetComputedTimingDictionary(), + // we will convert this into Infinity + : static_cast(overallProgress); + + // Convert the overall progress to a fraction of a single iteration--the + // simply iteration progress. + // https://w3c.github.io/web-animations/#simple-iteration-progress + double progress = IsFinite(overallProgress) + ? fmod(overallProgress, 1.0) + : fmod(result.mIterationStart, 1.0); + + // When we finish exactly at the end of an iteration we need to report + // the end of the final iteration and not the start of the next iteration. + // We *don't* want to do this when we have a zero-iteration animation or + // when the animation has been effectively made into a zero-duration animation + // using a negative end-delay, however. + if (result.mPhase == ComputedTiming::AnimationPhase::After && + progress == 0.0 && + result.mIterations != 0.0 && + (activeTime != zeroDuration || result.mDuration == zeroDuration)) { + // The only way we can be in the after phase with a progress of zero and + // a current iteration of zero, is if we have a zero iteration count or + // were clipped using a negative end delay--both of which we should have + // detected above. + MOZ_ASSERT(result.mCurrentIteration != 0, + "Should not have zero current iteration"); + progress = 1.0; + if (result.mCurrentIteration != UINT64_MAX) { + result.mCurrentIteration--; + } + } + + // Factor in the direction. + bool thisIterationReverse = false; + switch (aTiming.mDirection) { + case PlaybackDirection::Normal: + thisIterationReverse = false; + break; + case PlaybackDirection::Reverse: + thisIterationReverse = true; + break; + case PlaybackDirection::Alternate: + thisIterationReverse = (result.mCurrentIteration & 1) == 1; + break; + case PlaybackDirection::Alternate_reverse: + thisIterationReverse = (result.mCurrentIteration & 1) == 0; + break; + default: + MOZ_ASSERT(true, "Unknown PlaybackDirection type"); + } + if (thisIterationReverse) { + progress = 1.0 - progress; + } + + // Calculate the 'before flag' which we use when applying step timing + // functions. + if ((result.mPhase == ComputedTiming::AnimationPhase::After && + thisIterationReverse) || + (result.mPhase == ComputedTiming::AnimationPhase::Before && + !thisIterationReverse)) { + result.mBeforeFlag = ComputedTimingFunction::BeforeFlag::Set; + } + + // Apply the easing. + if (aTiming.mFunction) { + progress = aTiming.mFunction->GetValue(progress, result.mBeforeFlag); + } + + MOZ_ASSERT(IsFinite(progress), "Progress value should be finite"); + result.mProgress.SetValue(progress); + return result; +} + +ComputedTiming +AnimationEffectReadOnly::GetComputedTiming(const TimingParams* aTiming) const +{ + double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1; + return GetComputedTimingAt(GetLocalTime(), + aTiming ? *aTiming : SpecifiedTiming(), + playbackRate); +} + +// Helper functions for generating a ComputedTimingProperties dictionary +static void +GetComputedTimingDictionary(const ComputedTiming& aComputedTiming, + const Nullable& aLocalTime, + const TimingParams& aTiming, + ComputedTimingProperties& aRetVal) +{ + // AnimationEffectTimingProperties + aRetVal.mDelay = aTiming.mDelay.ToMilliseconds(); + aRetVal.mEndDelay = aTiming.mEndDelay.ToMilliseconds(); + aRetVal.mFill = aComputedTiming.mFill; + aRetVal.mIterations = aComputedTiming.mIterations; + aRetVal.mIterationStart = aComputedTiming.mIterationStart; + aRetVal.mDuration.SetAsUnrestrictedDouble() = + aComputedTiming.mDuration.ToMilliseconds(); + aRetVal.mDirection = aTiming.mDirection; + + // ComputedTimingProperties + aRetVal.mActiveDuration = aComputedTiming.mActiveDuration.ToMilliseconds(); + aRetVal.mEndTime = aComputedTiming.mEndTime.ToMilliseconds(); + aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(aLocalTime); + aRetVal.mProgress = aComputedTiming.mProgress; + + if (!aRetVal.mProgress.IsNull()) { + // Convert the returned currentIteration into Infinity if we set + // (uint64_t) aComputedTiming.mCurrentIteration to UINT64_MAX + double iteration = aComputedTiming.mCurrentIteration == UINT64_MAX + ? PositiveInfinity() + : static_cast(aComputedTiming.mCurrentIteration); + aRetVal.mCurrentIteration.SetValue(iteration); + } +} + +void +AnimationEffectReadOnly::GetComputedTimingAsDict( + ComputedTimingProperties& aRetVal) const +{ + double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1; + const Nullable currentTime = GetLocalTime(); + GetComputedTimingDictionary(GetComputedTimingAt(currentTime, + SpecifiedTiming(), + playbackRate), + currentTime, + SpecifiedTiming(), + aRetVal); +} + +AnimationEffectReadOnly::~AnimationEffectReadOnly() +{ + // mTiming is cycle collected, so we have to do null check first even though + // mTiming shouldn't be null during the lifetime of KeyframeEffect. + if (mTiming) { + mTiming->Unlink(); + } +} + +Nullable +AnimationEffectReadOnly::GetLocalTime() const +{ + // Since the *animation* start time is currently always zero, the local + // time is equal to the parent time. + Nullable result; + if (mAnimation) { + result = mAnimation->GetCurrentTime(); + } + return result; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/AnimationEffectReadOnly.h b/dom/animation/AnimationEffectReadOnly.h new file mode 100644 index 000000000..fdea49314 --- /dev/null +++ b/dom/animation/AnimationEffectReadOnly.h @@ -0,0 +1,102 @@ +/* -*- 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_dom_AnimationEffectReadOnly_h +#define mozilla_dom_AnimationEffectReadOnly_h + +#include "mozilla/ComputedTiming.h" +#include "mozilla/dom/AnimationEffectTimingReadOnly.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/Maybe.h" +#include "mozilla/StickyTimeDuration.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/TimingParams.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { + +struct ElementPropertyTransition; + +namespace dom { + +class Animation; +class AnimationEffectTimingReadOnly; +class KeyframeEffectReadOnly; +struct ComputedTimingProperties; + +class AnimationEffectReadOnly : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationEffectReadOnly) + + AnimationEffectReadOnly(nsIDocument* aDocument, + AnimationEffectTimingReadOnly* aTiming); + + virtual KeyframeEffectReadOnly* AsKeyframeEffect() { return nullptr; } + + virtual ElementPropertyTransition* AsTransition() { return nullptr; } + virtual const ElementPropertyTransition* AsTransition() const + { + return nullptr; + } + + nsISupports* GetParentObject() const { return mDocument; } + + bool IsCurrent() const; + bool IsInEffect() const; + bool IsActiveDurationZero() const + { + return SpecifiedTiming().ActiveDuration() == StickyTimeDuration(); + } + + already_AddRefed Timing(); + const TimingParams& SpecifiedTiming() const + { + return mTiming->AsTimingParams(); + } + void SetSpecifiedTiming(const TimingParams& aTiming); + + // This function takes as input the timing parameters of an animation and + // returns the computed timing at the specified local time. + // + // The local time may be null in which case only static parameters such as the + // active duration are calculated. All other members of the returned object + // are given a null/initial value. + // + // This function returns a null mProgress member of the return value + // if the animation should not be run + // (because it is not currently active and is not filling at this time). + static ComputedTiming + GetComputedTimingAt(const Nullable& aLocalTime, + const TimingParams& aTiming, + double aPlaybackRate); + // Shortcut that gets the computed timing using the current local time as + // calculated from the timeline time. + ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const; + void GetComputedTimingAsDict(ComputedTimingProperties& aRetVal) const; + + virtual void SetAnimation(Animation* aAnimation) = 0; + Animation* GetAnimation() const { return mAnimation; }; + +protected: + virtual ~AnimationEffectReadOnly(); + + Nullable GetLocalTime() const; + +protected: + RefPtr mDocument; + RefPtr mTiming; + RefPtr mAnimation; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AnimationEffectReadOnly_h diff --git a/dom/animation/AnimationEffectTiming.cpp b/dom/animation/AnimationEffectTiming.cpp new file mode 100644 index 000000000..8eb4c6edf --- /dev/null +++ b/dom/animation/AnimationEffectTiming.cpp @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/dom/AnimationEffectTiming.h" + +#include "mozilla/dom/AnimatableBinding.h" +#include "mozilla/dom/AnimationEffectTimingBinding.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/TimingParams.h" +#include "nsAString.h" + +namespace mozilla { +namespace dom { + +JSObject* +AnimationEffectTiming::WrapObject(JSContext* aCx, JS::Handle aGivenProto) +{ + return AnimationEffectTimingBinding::Wrap(aCx, this, aGivenProto); +} + +static inline void +PostSpecifiedTimingUpdated(KeyframeEffect* aEffect) +{ + if (aEffect) { + aEffect->NotifySpecifiedTimingUpdated(); + } +} + +void +AnimationEffectTiming::SetDelay(double aDelay) +{ + TimeDuration delay = TimeDuration::FromMilliseconds(aDelay); + if (mTiming.mDelay == delay) { + return; + } + mTiming.mDelay = delay; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetEndDelay(double aEndDelay) +{ + TimeDuration endDelay = TimeDuration::FromMilliseconds(aEndDelay); + if (mTiming.mEndDelay == endDelay) { + return; + } + mTiming.mEndDelay = endDelay; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetFill(const FillMode& aFill) +{ + if (mTiming.mFill == aFill) { + return; + } + mTiming.mFill = aFill; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetIterationStart(double aIterationStart, + ErrorResult& aRv) +{ + if (mTiming.mIterationStart == aIterationStart) { + return; + } + + TimingParams::ValidateIterationStart(aIterationStart, aRv); + if (aRv.Failed()) { + return; + } + + mTiming.mIterationStart = aIterationStart; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetIterations(double aIterations, ErrorResult& aRv) +{ + if (mTiming.mIterations == aIterations) { + return; + } + + TimingParams::ValidateIterations(aIterations, aRv); + if (aRv.Failed()) { + return; + } + + mTiming.mIterations = aIterations; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetDuration(const UnrestrictedDoubleOrString& aDuration, + ErrorResult& aRv) +{ + Maybe newDuration = + TimingParams::ParseDuration(aDuration, aRv); + if (aRv.Failed()) { + return; + } + + if (mTiming.mDuration == newDuration) { + return; + } + + mTiming.mDuration = newDuration; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetDirection(const PlaybackDirection& aDirection) +{ + if (mTiming.mDirection == aDirection) { + return; + } + + mTiming.mDirection = aDirection; + + PostSpecifiedTimingUpdated(mEffect); +} + +void +AnimationEffectTiming::SetEasing(const nsAString& aEasing, ErrorResult& aRv) +{ + Maybe newFunction = + TimingParams::ParseEasing(aEasing, mDocument, aRv); + if (aRv.Failed()) { + return; + } + + if (mTiming.mFunction == newFunction) { + return; + } + + mTiming.mFunction = newFunction; + + PostSpecifiedTimingUpdated(mEffect); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/AnimationEffectTiming.h b/dom/animation/AnimationEffectTiming.h new file mode 100644 index 000000000..06844b320 --- /dev/null +++ b/dom/animation/AnimationEffectTiming.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_dom_AnimationEffectTiming_h +#define mozilla_dom_AnimationEffectTiming_h + +#include "mozilla/dom/AnimationEffectTimingReadOnly.h" +#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF +#include "nsStringFwd.h" + +namespace mozilla { +namespace dom { + +class KeyframeEffect; + +class AnimationEffectTiming : public AnimationEffectTimingReadOnly +{ +public: + AnimationEffectTiming(nsIDocument* aDocument, + const TimingParams& aTiming, + KeyframeEffect* aEffect) + : AnimationEffectTimingReadOnly(aDocument, aTiming) + , mEffect(aEffect) { } + + JSObject* WrapObject(JSContext* aCx, JS::Handle aGivenProto) override; + + void Unlink() override { mEffect = nullptr; } + + void SetDelay(double aDelay); + void SetEndDelay(double aEndDelay); + void SetFill(const FillMode& aFill); + void SetIterationStart(double aIterationStart, ErrorResult& aRv); + void SetIterations(double aIterations, ErrorResult& aRv); + void SetDuration(const UnrestrictedDoubleOrString& aDuration, + ErrorResult& aRv); + void SetDirection(const PlaybackDirection& aDirection); + void SetEasing(const nsAString& aEasing, ErrorResult& aRv); + +private: + KeyframeEffect* MOZ_NON_OWNING_REF mEffect; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AnimationEffectTiming_h diff --git a/dom/animation/AnimationEffectTimingReadOnly.cpp b/dom/animation/AnimationEffectTimingReadOnly.cpp new file mode 100644 index 000000000..76cd53049 --- /dev/null +++ b/dom/animation/AnimationEffectTimingReadOnly.cpp @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/dom/AnimationEffectTimingReadOnly.h" + +#include "mozilla/AnimationUtils.h" +#include "mozilla/dom/AnimatableBinding.h" +#include "mozilla/dom/AnimationEffectTimingReadOnlyBinding.h" +#include "mozilla/dom/CSSPseudoElement.h" +#include "mozilla/dom/KeyframeEffectBinding.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationEffectTimingReadOnly, mDocument) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AnimationEffectTimingReadOnly, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AnimationEffectTimingReadOnly, Release) + +JSObject* +AnimationEffectTimingReadOnly::WrapObject(JSContext* aCx, JS::Handle aGivenProto) +{ + return AnimationEffectTimingReadOnlyBinding::Wrap(aCx, this, aGivenProto); +} + +void +AnimationEffectTimingReadOnly::GetDuration( + OwningUnrestrictedDoubleOrString& aRetVal) const +{ + if (mTiming.mDuration) { + aRetVal.SetAsUnrestrictedDouble() = mTiming.mDuration->ToMilliseconds(); + } else { + aRetVal.SetAsString().AssignLiteral("auto"); + } +} + +void +AnimationEffectTimingReadOnly::GetEasing(nsString& aRetVal) const +{ + if (mTiming.mFunction) { + mTiming.mFunction->AppendToString(aRetVal); + } else { + aRetVal.AssignLiteral("linear"); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/AnimationEffectTimingReadOnly.h b/dom/animation/AnimationEffectTimingReadOnly.h new file mode 100644 index 000000000..1f1d50619 --- /dev/null +++ b/dom/animation/AnimationEffectTimingReadOnly.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_dom_AnimationEffectTimingReadOnly_h +#define mozilla_dom_AnimationEffectTimingReadOnly_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/TimingParams.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/UnionTypes.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace dom { + +class AnimationEffectTimingReadOnly : public nsWrapperCache +{ +public: + AnimationEffectTimingReadOnly() = default; + AnimationEffectTimingReadOnly(nsIDocument* aDocument, + const TimingParams& aTiming) + : mDocument(aDocument) + , mTiming(aTiming) { } + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEffectTimingReadOnly) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AnimationEffectTimingReadOnly) + +protected: + virtual ~AnimationEffectTimingReadOnly() = default; + +public: + nsISupports* GetParentObject() const { return mDocument; } + JSObject* WrapObject(JSContext* aCx, JS::Handle aGivenProto) override; + + double Delay() const { return mTiming.mDelay.ToMilliseconds(); } + double EndDelay() const { return mTiming.mEndDelay.ToMilliseconds(); } + FillMode Fill() const { return mTiming.mFill; } + double IterationStart() const { return mTiming.mIterationStart; } + double Iterations() const { return mTiming.mIterations; } + void GetDuration(OwningUnrestrictedDoubleOrString& aRetVal) const; + PlaybackDirection Direction() const { return mTiming.mDirection; } + void GetEasing(nsString& aRetVal) const; + + const TimingParams& AsTimingParams() const { return mTiming; } + void SetTimingParams(const TimingParams& aTiming) { mTiming = aTiming; } + + virtual void Unlink() { } + +protected: + RefPtr mDocument; + TimingParams mTiming; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AnimationEffectTimingReadOnly_h diff --git a/dom/animation/AnimationPerformanceWarning.cpp b/dom/animation/AnimationPerformanceWarning.cpp new file mode 100644 index 000000000..80ece3198 --- /dev/null +++ b/dom/animation/AnimationPerformanceWarning.cpp @@ -0,0 +1,79 @@ +/* -*- 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/. */ + +#include "AnimationPerformanceWarning.h" + +#include "nsContentUtils.h" + +namespace mozilla { + +template nsresult +AnimationPerformanceWarning::ToLocalizedStringWithIntParams( + const char* aKey, nsXPIDLString& aLocalizedString) const +{ + nsAutoString strings[N]; + const char16_t* charParams[N]; + + for (size_t i = 0, n = mParams->Length(); i < n; i++) { + strings[i].AppendInt((*mParams)[i]); + charParams[i] = strings[i].get(); + } + + return nsContentUtils::FormatLocalizedString( + nsContentUtils::eLAYOUT_PROPERTIES, aKey, charParams, aLocalizedString); +} + +bool +AnimationPerformanceWarning::ToLocalizedString( + nsXPIDLString& aLocalizedString) const +{ + const char* key = nullptr; + + switch (mType) { + case Type::ContentTooSmall: + MOZ_ASSERT(mParams && mParams->Length() == 2, + "Parameter's length should be 2 for ContentTooSmall"); + + return NS_SUCCEEDED( + ToLocalizedStringWithIntParams<2>( + "CompositorAnimationWarningContentTooSmall", aLocalizedString)); + case Type::ContentTooLarge: + MOZ_ASSERT(mParams && mParams->Length() == 7, + "Parameter's length should be 7 for ContentTooLarge"); + + return NS_SUCCEEDED( + ToLocalizedStringWithIntParams<7>( + "CompositorAnimationWarningContentTooLarge", aLocalizedString)); + case Type::TransformBackfaceVisibilityHidden: + key = "CompositorAnimationWarningTransformBackfaceVisibilityHidden"; + break; + case Type::TransformPreserve3D: + key = "CompositorAnimationWarningTransformPreserve3D"; + break; + case Type::TransformSVG: + key = "CompositorAnimationWarningTransformSVG"; + break; + case Type::TransformWithGeometricProperties: + key = "CompositorAnimationWarningTransformWithGeometricProperties"; + break; + case Type::TransformFrameInactive: + key = "CompositorAnimationWarningTransformFrameInactive"; + break; + case Type::OpacityFrameInactive: + key = "CompositorAnimationWarningOpacityFrameInactive"; + break; + case Type::HasRenderingObserver: + key = "CompositorAnimationWarningHasRenderingObserver"; + break; + } + + nsresult rv = + nsContentUtils::GetLocalizedString(nsContentUtils::eLAYOUT_PROPERTIES, + key, aLocalizedString); + return NS_SUCCEEDED(rv); +} + +} // namespace mozilla diff --git a/dom/animation/AnimationPerformanceWarning.h b/dom/animation/AnimationPerformanceWarning.h new file mode 100644 index 000000000..025857e0c --- /dev/null +++ b/dom/animation/AnimationPerformanceWarning.h @@ -0,0 +1,79 @@ +/* -*- 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_dom_AnimationPerformanceWarning_h +#define mozilla_dom_AnimationPerformanceWarning_h + +#include + +class nsXPIDLString; + +namespace mozilla { + +// Represents the reason why we can't run the CSS property on the compositor. +struct AnimationPerformanceWarning +{ + enum class Type : uint8_t { + ContentTooSmall, + ContentTooLarge, + TransformBackfaceVisibilityHidden, + TransformPreserve3D, + TransformSVG, + TransformWithGeometricProperties, + TransformFrameInactive, + OpacityFrameInactive, + HasRenderingObserver, + }; + + explicit AnimationPerformanceWarning(Type aType) + : mType(aType) { } + + AnimationPerformanceWarning(Type aType, + std::initializer_list aParams) + : mType(aType) + { + // FIXME: Once std::initializer_list::size() become a constexpr function, + // we should use static_assert here. + MOZ_ASSERT(aParams.size() <= kMaxParamsForLocalization, + "The length of parameters should be less than " + "kMaxParamsForLocalization"); + mParams.emplace(aParams); + } + + // Maximum number of parameters passed to + // nsContentUtils::FormatLocalizedString to localize warning messages. + // + // NOTE: This constexpr can't be forward declared, so if you want to use + // this variable, please include this header file directly. + // This value is the same as the limit of nsStringBundle::FormatString. + // See the implementation of nsStringBundle::FormatString. + static constexpr uint8_t kMaxParamsForLocalization = 10; + + // Indicates why this property could not be animated on the compositor. + Type mType; + + // Optional parameters that may be used for localization. + Maybe> mParams; + + bool ToLocalizedString(nsXPIDLString& aLocalizedString) const; + template + nsresult ToLocalizedStringWithIntParams( + const char* aKey, nsXPIDLString& aLocalizedString) const; + + bool operator==(const AnimationPerformanceWarning& aOther) const + { + return mType == aOther.mType && + mParams == aOther.mParams; + } + bool operator!=(const AnimationPerformanceWarning& aOther) const + { + return !(*this == aOther); + } +}; + +} // namespace mozilla + +#endif // mozilla_dom_AnimationPerformanceWarning_h diff --git a/dom/animation/AnimationTarget.h b/dom/animation/AnimationTarget.h new file mode 100644 index 000000000..dbfef2e10 --- /dev/null +++ b/dom/animation/AnimationTarget.h @@ -0,0 +1,78 @@ +/* -*- 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_AnimationTarget_h +#define mozilla_AnimationTarget_h + +#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "nsCSSPseudoElements.h" + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +struct OwningAnimationTarget +{ + OwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType) + : mElement(aElement), mPseudoType(aType) { } + + explicit OwningAnimationTarget(dom::Element* aElement) + : mElement(aElement) { } + + bool operator==(const OwningAnimationTarget& aOther) const + { + return mElement == aOther.mElement && + mPseudoType == aOther.mPseudoType; + } + + // mElement represents the parent element of a pseudo-element, not the + // generated content element. + RefPtr mElement; + CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo; +}; + +struct NonOwningAnimationTarget +{ + NonOwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType) + : mElement(aElement), mPseudoType(aType) { } + + explicit NonOwningAnimationTarget(const OwningAnimationTarget& aOther) + : mElement(aOther.mElement), mPseudoType(aOther.mPseudoType) { } + + // mElement represents the parent element of a pseudo-element, not the + // generated content element. + dom::Element* MOZ_NON_OWNING_REF mElement = nullptr; + CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo; +}; + + +// Helper functions for cycle-collecting Maybe +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + Maybe& aTarget, + const char* aName, + uint32_t aFlags = 0) +{ + if (aTarget) { + ImplCycleCollectionTraverse(aCallback, aTarget->mElement, aName, aFlags); + } +} + +inline void +ImplCycleCollectionUnlink(Maybe& aTarget) +{ + if (aTarget) { + ImplCycleCollectionUnlink(aTarget->mElement); + } +} + +} // namespace mozilla + +#endif // mozilla_AnimationTarget_h diff --git a/dom/animation/AnimationTimeline.cpp b/dom/animation/AnimationTimeline.cpp new file mode 100644 index 000000000..643106807 --- /dev/null +++ b/dom/animation/AnimationTimeline.cpp @@ -0,0 +1,63 @@ +/* -*- 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/. */ + +#include "AnimationTimeline.h" +#include "mozilla/AnimationComparator.h" +#include "mozilla/dom/Animation.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationTimeline) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationTimeline) + tmp->mAnimationOrder.clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mAnimations) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationTimeline) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mAnimations) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationTimeline) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationTimeline) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationTimeline) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationTimeline) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +void +AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation) +{ + if (mAnimations.Contains(&aAnimation)) { + return; + } + + if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) { + aAnimation.GetTimeline()->RemoveAnimation(&aAnimation); + } + + mAnimations.PutEntry(&aAnimation); + mAnimationOrder.insertBack(&aAnimation); +} + +void +AnimationTimeline::RemoveAnimation(Animation* aAnimation) +{ + MOZ_ASSERT(!aAnimation->GetTimeline() || aAnimation->GetTimeline() == this); + if (aAnimation->isInList()) { + aAnimation->remove(); + } + mAnimations.RemoveEntry(aAnimation); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/AnimationTimeline.h b/dom/animation/AnimationTimeline.h new file mode 100644 index 000000000..d36cc4027 --- /dev/null +++ b/dom/animation/AnimationTimeline.h @@ -0,0 +1,125 @@ +/* -*- 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_dom_AnimationTimeline_h +#define mozilla_dom_AnimationTimeline_h + +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "js/TypeDecls.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/Attributes.h" +#include "nsHashKeys.h" +#include "nsIGlobalObject.h" +#include "nsTHashtable.h" + +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount(). +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +namespace mozilla { +namespace dom { + +class Animation; + +class AnimationTimeline + : public nsISupports + , public nsWrapperCache +{ +public: + explicit AnimationTimeline(nsIGlobalObject* aWindow) + : mWindow(aWindow) + { + MOZ_ASSERT(mWindow); + } + +protected: + virtual ~AnimationTimeline() + { + mAnimationOrder.clear(); + } + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationTimeline) + + nsIGlobalObject* GetParentObject() const { return mWindow; } + + // AnimationTimeline methods + virtual Nullable GetCurrentTime() const = 0; + + // Wrapper functions for AnimationTimeline DOM methods when called from + // script. + Nullable GetCurrentTimeAsDouble() const { + return AnimationUtils::TimeDurationToDouble(GetCurrentTime()); + } + + /** + * Returns true if the times returned by GetCurrentTime() are convertible + * to and from wallclock-based TimeStamp (e.g. from TimeStamp::Now()) values + * using ToTimelineTime() and ToTimeStamp(). + * + * Typically this is true, but it will be false in the case when this + * timeline has no refresh driver or is tied to a refresh driver under test + * control. + */ + virtual bool TracksWallclockTime() const = 0; + + /** + * Converts a TimeStamp to the equivalent value in timeline time. + * Note that when TracksWallclockTime() is false, there is no correspondence + * between timeline time and wallclock time. In such a case, passing a + * timestamp from TimeStamp::Now() to this method will not return a + * meaningful result. + */ + virtual Nullable ToTimelineTime(const TimeStamp& + aTimeStamp) const = 0; + + virtual TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const = 0; + + /** + * Inform this timeline that |aAnimation| which is or was observing the + * timeline, has been updated. This serves as both the means to associate + * AND disassociate animations with a timeline. The timeline itself will + * determine if it needs to begin, continue or stop tracking this animation. + */ + virtual void NotifyAnimationUpdated(Animation& aAnimation); + + /** + * Returns true if any CSS animations, CSS transitions or Web animations are + * currently associated with this timeline. As soon as an animation is + * applied to an element it is associated with the timeline even if it has a + * delayed start, so this includes animations that may not be active for some + * time. + */ + bool HasAnimations() const { + return !mAnimations.IsEmpty(); + } + + virtual void RemoveAnimation(Animation* aAnimation); + +protected: + nsCOMPtr mWindow; + + // Animations observing this timeline + // + // We store them in (a) a hashset for quick lookup, and (b) an array + // to maintain a fixed sampling order. + // + // The hashset keeps a strong reference to each animation since + // dealing with addref/release with LinkedList is difficult. + typedef nsTHashtable> AnimationSet; + AnimationSet mAnimations; + LinkedList mAnimationOrder; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AnimationTimeline_h diff --git a/dom/animation/AnimationUtils.cpp b/dom/animation/AnimationUtils.cpp new file mode 100644 index 000000000..476652f77 --- /dev/null +++ b/dom/animation/AnimationUtils.cpp @@ -0,0 +1,81 @@ +/* -*- 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/. */ + +#include "AnimationUtils.h" + +#include "nsContentUtils.h" // For nsContentUtils::IsCallerChrome +#include "nsDebug.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsGlobalWindow.h" +#include "nsString.h" +#include "xpcpublic.h" // For xpc::NativeGlobal +#include "mozilla/Preferences.h" + +namespace mozilla { + +/* static */ void +AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage, + const nsIContent* aContent) +{ + if (aContent) { + aMessage.AppendLiteral(" ["); + aMessage.Append(nsAtomCString(aContent->NodeInfo()->NameAtom())); + + nsIAtom* id = aContent->GetID(); + if (id) { + aMessage.AppendLiteral(" with id '"); + aMessage.Append(nsAtomCString(aContent->GetID())); + aMessage.Append('\''); + } + aMessage.Append(']'); + } + aMessage.Append('\n'); + printf_stderr("%s", aMessage.get()); +} + +/* static */ nsIDocument* +AnimationUtils::GetCurrentRealmDocument(JSContext* aCx) +{ + nsGlobalWindow* win = xpc::CurrentWindowOrNull(aCx); + if (!win) { + return nullptr; + } + return win->GetDoc(); +} + +/* static */ bool +AnimationUtils::IsOffscreenThrottlingEnabled() +{ + static bool sOffscreenThrottlingEnabled; + static bool sPrefCached = false; + + if (!sPrefCached) { + sPrefCached = true; + Preferences::AddBoolVarCache(&sOffscreenThrottlingEnabled, + "dom.animations.offscreen-throttling"); + } + + return sOffscreenThrottlingEnabled; +} + +/* static */ bool +AnimationUtils::IsCoreAPIEnabledForCaller() +{ + static bool sCoreAPIEnabled; + static bool sPrefCached = false; + + if (!sPrefCached) { + sPrefCached = true; + Preferences::AddBoolVarCache(&sCoreAPIEnabled, + "dom.animations-api.core.enabled"); + } + + return sCoreAPIEnabled || nsContentUtils::IsCallerChrome(); +} + +} // namespace mozilla diff --git a/dom/animation/AnimationUtils.h b/dom/animation/AnimationUtils.h new file mode 100644 index 000000000..82ae69bc8 --- /dev/null +++ b/dom/animation/AnimationUtils.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_dom_AnimationUtils_h +#define mozilla_dom_AnimationUtils_h + +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/Nullable.h" +#include "nsStringFwd.h" + +class nsIContent; +class nsIDocument; +struct JSContext; + +namespace mozilla { + +class ComputedTimingFunction; + +class AnimationUtils +{ +public: + static dom::Nullable + TimeDurationToDouble(const dom::Nullable& aTime) + { + dom::Nullable result; + + if (!aTime.IsNull()) { + result.SetValue(aTime.Value().ToMilliseconds()); + } + + return result; + } + + static dom::Nullable + DoubleToTimeDuration(const dom::Nullable& aTime) + { + dom::Nullable result; + + if (!aTime.IsNull()) { + result.SetValue(TimeDuration::FromMilliseconds(aTime.Value())); + } + + return result; + } + + static void LogAsyncAnimationFailure(nsCString& aMessage, + const nsIContent* aContent = nullptr); + + /** + * Get the document from the JS context to use when parsing CSS properties. + */ + static nsIDocument* + GetCurrentRealmDocument(JSContext* aCx); + + /** + * Checks if offscreen animation throttling is enabled. + */ + static bool + IsOffscreenThrottlingEnabled(); + + /** + * Returns true if the preference to enable the core Web Animations API is + * true or the caller is chrome. + */ + static bool + IsCoreAPIEnabledForCaller(); +}; + +} // namespace mozilla + +#endif diff --git a/dom/animation/CSSPseudoElement.cpp b/dom/animation/CSSPseudoElement.cpp new file mode 100644 index 000000000..a4dede0b3 --- /dev/null +++ b/dom/animation/CSSPseudoElement.cpp @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/dom/CSSPseudoElement.h" +#include "mozilla/dom/CSSPseudoElementBinding.h" +#include "mozilla/dom/Element.h" +#include "mozilla/AnimationComparator.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CSSPseudoElement, mParentElement) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CSSPseudoElement, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CSSPseudoElement, Release) + +CSSPseudoElement::CSSPseudoElement(Element* aElement, + CSSPseudoElementType aType) + : mParentElement(aElement) + , mPseudoType(aType) +{ + MOZ_ASSERT(aElement); + MOZ_ASSERT(aType == CSSPseudoElementType::after || + aType == CSSPseudoElementType::before, + "Unexpected Pseudo Type"); +} + +CSSPseudoElement::~CSSPseudoElement() +{ + // Element might have been unlinked already, so we have to do null check. + if (mParentElement) { + mParentElement->DeleteProperty( + GetCSSPseudoElementPropertyAtom(mPseudoType)); + } +} + +ParentObject +CSSPseudoElement::GetParentObject() const +{ + return mParentElement->GetParentObject(); +} + +JSObject* +CSSPseudoElement::WrapObject(JSContext* aCx, JS::Handle aGivenProto) +{ + return CSSPseudoElementBinding::Wrap(aCx, this, aGivenProto); +} + +void +CSSPseudoElement::GetAnimations(const AnimationFilter& filter, + nsTArray>& aRetVal) +{ + nsIDocument* doc = mParentElement->GetComposedDoc(); + if (doc) { + doc->FlushPendingNotifications(Flush_Style); + } + + Element::GetAnimationsUnsorted(mParentElement, mPseudoType, aRetVal); + aRetVal.Sort(AnimationPtrComparator>()); +} + +already_AddRefed +CSSPseudoElement::Animate( + JSContext* aContext, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aError) +{ + Nullable target; + target.SetValue().SetAsCSSPseudoElement() = this; + return Element::Animate(target, aContext, aKeyframes, aOptions, aError); +} + +/* static */ already_AddRefed +CSSPseudoElement::GetCSSPseudoElement(Element* aElement, + CSSPseudoElementType aType) +{ + if (!aElement) { + return nullptr; + } + + nsIAtom* propName = CSSPseudoElement::GetCSSPseudoElementPropertyAtom(aType); + RefPtr pseudo = + static_cast(aElement->GetProperty(propName)); + if (pseudo) { + return pseudo.forget(); + } + + // CSSPseudoElement is a purely external interface created on-demand, and + // when all references from script to the pseudo are dropped, we can drop the + // CSSPseudoElement object, so use a non-owning reference from Element to + // CSSPseudoElement. + pseudo = new CSSPseudoElement(aElement, aType); + nsresult rv = aElement->SetProperty(propName, pseudo, nullptr, true); + if (NS_FAILED(rv)) { + NS_WARNING("SetProperty failed"); + return nullptr; + } + return pseudo.forget(); +} + +/* static */ nsIAtom* +CSSPseudoElement::GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType) +{ + switch (aType) { + case CSSPseudoElementType::before: + return nsGkAtoms::cssPseudoElementBeforeProperty; + + case CSSPseudoElementType::after: + return nsGkAtoms::cssPseudoElementAfterProperty; + + default: + NS_NOTREACHED("Should not try to get CSSPseudoElement " + "other than ::before or ::after"); + return nullptr; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/CSSPseudoElement.h b/dom/animation/CSSPseudoElement.h new file mode 100644 index 000000000..00445cc60 --- /dev/null +++ b/dom/animation/CSSPseudoElement.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_dom_CSSPseudoElement_h +#define mozilla_dom_CSSPseudoElement_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Element.h" +#include "mozilla/RefPtr.h" +#include "nsCSSPseudoElements.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace dom { + +class Animation; +class Element; +class UnrestrictedDoubleOrKeyframeAnimationOptions; + +class CSSPseudoElement final : public nsWrapperCache +{ +public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CSSPseudoElement) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CSSPseudoElement) + +protected: + virtual ~CSSPseudoElement(); + +public: + ParentObject GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + CSSPseudoElementType GetType() const { return mPseudoType; } + void GetType(nsString& aRetVal) const + { + MOZ_ASSERT(nsCSSPseudoElements::GetPseudoAtom(mPseudoType), + "All pseudo-types allowed by this class should have a" + " corresponding atom"); + // Our atoms use one colon and we would like to return two colons syntax + // for the returned pseudo type string, so serialize this to the + // non-deprecated two colon syntax. + aRetVal.Assign(char16_t(':')); + aRetVal.Append( + nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(mPseudoType))); + } + already_AddRefed ParentElement() const + { + RefPtr retVal(mParentElement); + return retVal.forget(); + } + + void GetAnimations(const AnimationFilter& filter, + nsTArray>& aRetVal); + already_AddRefed + Animate(JSContext* aContext, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aError); + + // Given an element:pseudoType pair, returns the CSSPseudoElement stored as a + // property on |aElement|. If there is no CSSPseudoElement for the specified + // pseudo-type on element, a new CSSPseudoElement will be created and stored + // on the element. + static already_AddRefed + GetCSSPseudoElement(Element* aElement, CSSPseudoElementType aType); + +private: + // Only ::before and ::after are supported. + CSSPseudoElement(Element* aElement, CSSPseudoElementType aType); + + static nsIAtom* GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType); + + // mParentElement needs to be an owning reference since if script is holding + // on to the pseudo-element, it needs to continue to be able to refer to + // the parent element. + RefPtr mParentElement; + CSSPseudoElementType mPseudoType; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_CSSPseudoElement_h diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h new file mode 100644 index 000000000..4a98e3933 --- /dev/null +++ b/dom/animation/ComputedTiming.h @@ -0,0 +1,78 @@ +/* -*- 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" +#include "mozilla/ComputedTimingFunction.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; + + ComputedTimingFunction::BeforeFlag mBeforeFlag = + ComputedTimingFunction::BeforeFlag::Unset; +}; + +} // namespace mozilla + +#endif // mozilla_ComputedTiming_h diff --git a/dom/animation/ComputedTimingFunction.cpp b/dom/animation/ComputedTimingFunction.cpp new file mode 100644 index 000000000..95b7fa785 --- /dev/null +++ b/dom/animation/ComputedTimingFunction.cpp @@ -0,0 +1,194 @@ +/* -*- 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/. */ + +#include "ComputedTimingFunction.h" +#include "nsAlgorithm.h" // For clamped() +#include "nsStyleUtil.h" + +namespace mozilla { + +void +ComputedTimingFunction::Init(const nsTimingFunction &aFunction) +{ + mType = aFunction.mType; + if (nsTimingFunction::IsSplineType(mType)) { + mTimingFunction.Init(aFunction.mFunc.mX1, aFunction.mFunc.mY1, + aFunction.mFunc.mX2, aFunction.mFunc.mY2); + } else { + mSteps = aFunction.mSteps; + } +} + +static inline double +StepTiming(uint32_t aSteps, + double aPortion, + ComputedTimingFunction::BeforeFlag aBeforeFlag, + nsTimingFunction::Type aType) +{ + MOZ_ASSERT(0.0 <= aPortion && aPortion <= 1.0, "out of range"); + MOZ_ASSERT(aType == nsTimingFunction::Type::StepStart || + aType == nsTimingFunction::Type::StepEnd, "invalid type"); + + if (aPortion == 1.0) { + return 1.0; + } + + // Calculate current step using step-end behavior + uint32_t step = uint32_t(aPortion * aSteps); // floor + + // step-start is one step ahead + if (aType == nsTimingFunction::Type::StepStart) { + step++; + } + + // If the "before flag" is set and we are at a transition point, + // drop back a step (but only if we are not already at the zero point-- + // we do this clamping here since |step| is an unsigned integer) + if (step != 0 && + aBeforeFlag == ComputedTimingFunction::BeforeFlag::Set && + fmod(aPortion * aSteps, 1) == 0) { + step--; + } + + // Convert to a progress value + return double(step) / double(aSteps); +} + +double +ComputedTimingFunction::GetValue( + double aPortion, + ComputedTimingFunction::BeforeFlag aBeforeFlag) const +{ + if (HasSpline()) { + // Check for a linear curve. + // (GetSplineValue(), below, also checks this but doesn't work when + // aPortion is outside the range [0.0, 1.0]). + if (mTimingFunction.X1() == mTimingFunction.Y1() && + mTimingFunction.X2() == mTimingFunction.Y2()) { + return aPortion; + } + + // Ensure that we return 0 or 1 on both edges. + if (aPortion == 0.0) { + return 0.0; + } + if (aPortion == 1.0) { + return 1.0; + } + + // For negative values, try to extrapolate with tangent (p1 - p0) or, + // if p1 is coincident with p0, with (p2 - p0). + if (aPortion < 0.0) { + if (mTimingFunction.X1() > 0.0) { + return aPortion * mTimingFunction.Y1() / mTimingFunction.X1(); + } else if (mTimingFunction.Y1() == 0 && mTimingFunction.X2() > 0.0) { + return aPortion * mTimingFunction.Y2() / mTimingFunction.X2(); + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 0.0; + } + + // For values greater than 1, try to extrapolate with tangent (p2 - p3) or, + // if p2 is coincident with p3, with (p1 - p3). + if (aPortion > 1.0) { + if (mTimingFunction.X2() < 1.0) { + return 1.0 + (aPortion - 1.0) * + (mTimingFunction.Y2() - 1) / (mTimingFunction.X2() - 1); + } else if (mTimingFunction.Y2() == 1 && mTimingFunction.X1() < 1.0) { + return 1.0 + (aPortion - 1.0) * + (mTimingFunction.Y1() - 1) / (mTimingFunction.X1() - 1); + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 1.0; + } + + return mTimingFunction.GetSplineValue(aPortion); + } + + // Since we use endpoint-exclusive timing, the output of a steps(start) timing + // function when aPortion = 0.0 is the top of the first step. When aPortion is + // negative, however, we should use the bottom of the first step. We handle + // negative values of aPortion specially here since once we clamp aPortion + // to [0,1] below we will no longer be able to distinguish to the two cases. + if (aPortion < 0.0) { + return 0.0; + } + + // Clamp in case of steps(end) and steps(start) for values greater than 1. + aPortion = clamped(aPortion, 0.0, 1.0); + + return StepTiming(mSteps, aPortion, aBeforeFlag, mType); +} + +int32_t +ComputedTimingFunction::Compare(const ComputedTimingFunction& aRhs) const +{ + if (mType != aRhs.mType) { + return int32_t(mType) - int32_t(aRhs.mType); + } + + if (mType == nsTimingFunction::Type::CubicBezier) { + int32_t order = mTimingFunction.Compare(aRhs.mTimingFunction); + if (order != 0) { + return order; + } + } else if (mType == nsTimingFunction::Type::StepStart || + mType == nsTimingFunction::Type::StepEnd) { + if (mSteps != aRhs.mSteps) { + return int32_t(mSteps) - int32_t(aRhs.mSteps); + } + } + + return 0; +} + +void +ComputedTimingFunction::AppendToString(nsAString& aResult) const +{ + switch (mType) { + case nsTimingFunction::Type::CubicBezier: + nsStyleUtil::AppendCubicBezierTimingFunction(mTimingFunction.X1(), + mTimingFunction.Y1(), + mTimingFunction.X2(), + mTimingFunction.Y2(), + aResult); + break; + case nsTimingFunction::Type::StepStart: + case nsTimingFunction::Type::StepEnd: + nsStyleUtil::AppendStepsTimingFunction(mType, mSteps, aResult); + break; + default: + nsStyleUtil::AppendCubicBezierKeywordTimingFunction(mType, aResult); + break; + } +} + +/* static */ int32_t +ComputedTimingFunction::Compare(const Maybe& aLhs, + const Maybe& aRhs) +{ + // We can't use |operator<| for const Maybe<>& here because + // 'ease' is prior to 'linear' which is represented by Nothing(). + // So we have to convert Nothing() as 'linear' and check it first. + nsTimingFunction::Type lhsType = aLhs.isNothing() ? + nsTimingFunction::Type::Linear : aLhs->GetType(); + nsTimingFunction::Type rhsType = aRhs.isNothing() ? + nsTimingFunction::Type::Linear : aRhs->GetType(); + + if (lhsType != rhsType) { + return int32_t(lhsType) - int32_t(rhsType); + } + + // Both of them are Nothing(). + if (lhsType == nsTimingFunction::Type::Linear) { + return 0; + } + + // Other types. + return aLhs->Compare(aRhs.value()); +} + +} // namespace mozilla diff --git a/dom/animation/ComputedTimingFunction.h b/dom/animation/ComputedTimingFunction.h new file mode 100644 index 000000000..998097265 --- /dev/null +++ b/dom/animation/ComputedTimingFunction.h @@ -0,0 +1,65 @@ +/* -*- 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_ComputedTimingFunction_h +#define mozilla_ComputedTimingFunction_h + +#include "nsSMILKeySpline.h" // nsSMILKeySpline +#include "nsStyleStruct.h" // nsTimingFunction + +namespace mozilla { + +class ComputedTimingFunction +{ +public: + // BeforeFlag is used in step timing function. + // https://w3c.github.io/web-animations/#before-flag + enum class BeforeFlag { + Unset, + Set + }; + void Init(const nsTimingFunction &aFunction); + double GetValue(double aPortion, BeforeFlag aBeforeFlag) const; + const nsSMILKeySpline* GetFunction() const + { + NS_ASSERTION(HasSpline(), "Type mismatch"); + return &mTimingFunction; + } + nsTimingFunction::Type GetType() const { return mType; } + bool HasSpline() const { return nsTimingFunction::IsSplineType(mType); } + uint32_t GetSteps() const { return mSteps; } + bool operator==(const ComputedTimingFunction& aOther) const + { + return mType == aOther.mType && + (HasSpline() ? + mTimingFunction == aOther.mTimingFunction : + mSteps == aOther.mSteps); + } + bool operator!=(const ComputedTimingFunction& aOther) const + { + return !(*this == aOther); + } + int32_t Compare(const ComputedTimingFunction& aRhs) const; + void AppendToString(nsAString& aResult) const; + + static double GetPortion(const Maybe& aFunction, + double aPortion, + BeforeFlag aBeforeFlag) + { + return aFunction ? aFunction->GetValue(aPortion, aBeforeFlag) : aPortion; + } + static int32_t Compare(const Maybe& aLhs, + const Maybe& aRhs); + +private: + nsTimingFunction::Type mType; + nsSMILKeySpline mTimingFunction; + uint32_t mSteps; +}; + +} // namespace mozilla + +#endif // mozilla_ComputedTimingFunction_h diff --git a/dom/animation/DocumentTimeline.cpp b/dom/animation/DocumentTimeline.cpp new file mode 100644 index 000000000..78a4877d2 --- /dev/null +++ b/dom/animation/DocumentTimeline.cpp @@ -0,0 +1,283 @@ +/* -*- 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/. */ + +#include "DocumentTimeline.h" +#include "mozilla/dom/DocumentTimelineBinding.h" +#include "AnimationUtils.h" +#include "nsContentUtils.h" +#include "nsDOMMutationObserver.h" +#include "nsDOMNavigationTiming.h" +#include "nsIPresShell.h" +#include "nsPresContext.h" +#include "nsRefreshDriver.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline, + AnimationTimeline) + tmp->UnregisterFromRefreshDriver(); + if (tmp->isInList()) { + tmp->remove(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline, + AnimationTimeline) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline, + AnimationTimeline) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocumentTimeline) +NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline) + +NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline) +NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline) + +JSObject* +DocumentTimeline::WrapObject(JSContext* aCx, JS::Handle aGivenProto) +{ + return DocumentTimelineBinding::Wrap(aCx, this, aGivenProto); +} + +/* static */ already_AddRefed +DocumentTimeline::Constructor(const GlobalObject& aGlobal, + const DocumentTimelineOptions& aOptions, + ErrorResult& aRv) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + TimeDuration originTime = + TimeDuration::FromMilliseconds(aOptions.mOriginTime); + + if (originTime == TimeDuration::Forever() || + originTime == -TimeDuration::Forever()) { + aRv.ThrowTypeError( + NS_LITERAL_STRING("Origin time")); + return nullptr; + } + RefPtr timeline = new DocumentTimeline(doc, originTime); + + return timeline.forget(); +} + +Nullable +DocumentTimeline::GetCurrentTime() const +{ + return ToTimelineTime(GetCurrentTimeStamp()); +} + +TimeStamp +DocumentTimeline::GetCurrentTimeStamp() const +{ + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + TimeStamp refreshTime = refreshDriver + ? refreshDriver->MostRecentRefresh() + : TimeStamp(); + + // Always return the same object to benefit from return-value optimization. + TimeStamp result = !refreshTime.IsNull() + ? refreshTime + : mLastRefreshDriverTime; + + // If we don't have a refresh driver and we've never had one use the + // timeline's zero time. + if (result.IsNull()) { + RefPtr timing = mDocument->GetNavigationTiming(); + if (timing) { + result = timing->GetNavigationStartTimeStamp(); + // Also, let this time represent the current refresh time. This way + // we'll save it as the last refresh time and skip looking up + // navigation timing each time. + refreshTime = result; + } + } + + if (!refreshTime.IsNull()) { + mLastRefreshDriverTime = refreshTime; + } + + return result; +} + +Nullable +DocumentTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const +{ + Nullable result; // Initializes to null + if (aTimeStamp.IsNull()) { + return result; + } + + RefPtr timing = mDocument->GetNavigationTiming(); + if (MOZ_UNLIKELY(!timing)) { + return result; + } + + result.SetValue(aTimeStamp + - timing->GetNavigationStartTimeStamp() + - mOriginTime); + return result; +} + +void +DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation) +{ + AnimationTimeline::NotifyAnimationUpdated(aAnimation); + + if (!mIsObservingRefreshDriver) { + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + MOZ_ASSERT(isInList(), + "We should not register with the refresh driver if we are not" + " in the document's list of timelines"); + refreshDriver->AddRefreshObserver(this, Flush_Style); + mIsObservingRefreshDriver = true; + } + } +} + +void +DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime) +{ + MOZ_ASSERT(mIsObservingRefreshDriver); + MOZ_ASSERT(GetRefreshDriver(), + "Should be able to reach refresh driver from within WillRefresh"); + + bool needsTicks = false; + nsTArray animationsToRemove(mAnimations.Count()); + + nsAutoAnimationMutationBatch mb(mDocument); + + for (Animation* animation = mAnimationOrder.getFirst(); animation; + animation = animation->getNext()) { + // Skip any animations that are longer need associated with this timeline. + if (animation->GetTimeline() != this) { + // If animation has some other timeline, it better not be also in the + // animation list of this timeline object! + MOZ_ASSERT(!animation->GetTimeline()); + animationsToRemove.AppendElement(animation); + continue; + } + + needsTicks |= animation->NeedsTicks(); + // Even if |animation| doesn't need future ticks, we should still + // Tick it this time around since it might just need a one-off tick in + // order to dispatch events. + animation->Tick(); + + if (!animation->IsRelevant() && !animation->NeedsTicks()) { + animationsToRemove.AppendElement(animation); + } + } + + for (Animation* animation : animationsToRemove) { + RemoveAnimation(animation); + } + + if (!needsTicks) { + // We already assert that GetRefreshDriver() is non-null at the beginning + // of this function but we check it again here to be sure that ticking + // animations does not have any side effects that cause us to lose the + // connection with the refresh driver, such as triggering the destruction + // of mDocument's PresShell. + MOZ_ASSERT(GetRefreshDriver(), + "Refresh driver should still be valid at end of WillRefresh"); + UnregisterFromRefreshDriver(); + } +} + +void +DocumentTimeline::NotifyRefreshDriverCreated(nsRefreshDriver* aDriver) +{ + MOZ_ASSERT(!mIsObservingRefreshDriver, + "Timeline should not be observing the refresh driver before" + " it is created"); + + if (!mAnimationOrder.isEmpty()) { + MOZ_ASSERT(isInList(), + "We should not register with the refresh driver if we are not" + " in the document's list of timelines"); + aDriver->AddRefreshObserver(this, Flush_Style); + mIsObservingRefreshDriver = true; + } +} + +void +DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver) +{ + if (!mIsObservingRefreshDriver) { + return; + } + + aDriver->RemoveRefreshObserver(this, Flush_Style); + mIsObservingRefreshDriver = false; +} + +void +DocumentTimeline::RemoveAnimation(Animation* aAnimation) +{ + AnimationTimeline::RemoveAnimation(aAnimation); + + if (mIsObservingRefreshDriver && mAnimations.IsEmpty()) { + UnregisterFromRefreshDriver(); + } +} + +TimeStamp +DocumentTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const +{ + TimeStamp result; + RefPtr timing = mDocument->GetNavigationTiming(); + if (MOZ_UNLIKELY(!timing)) { + return result; + } + + result = + timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime); + return result; +} + +nsRefreshDriver* +DocumentTimeline::GetRefreshDriver() const +{ + nsIPresShell* presShell = mDocument->GetShell(); + if (MOZ_UNLIKELY(!presShell)) { + return nullptr; + } + + nsPresContext* presContext = presShell->GetPresContext(); + if (MOZ_UNLIKELY(!presContext)) { + return nullptr; + } + + return presContext->RefreshDriver(); +} + +void +DocumentTimeline::UnregisterFromRefreshDriver() +{ + if (!mIsObservingRefreshDriver) { + return; + } + + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (!refreshDriver) { + return; + } + + refreshDriver->RemoveRefreshObserver(this, Flush_Style); + mIsObservingRefreshDriver = false; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/DocumentTimeline.h b/dom/animation/DocumentTimeline.h new file mode 100644 index 000000000..888a1d33d --- /dev/null +++ b/dom/animation/DocumentTimeline.h @@ -0,0 +1,111 @@ +/* -*- 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_dom_DocumentTimeline_h +#define mozilla_dom_DocumentTimeline_h + +#include "mozilla/dom/DocumentTimelineBinding.h" +#include "mozilla/LinkedList.h" +#include "mozilla/TimeStamp.h" +#include "AnimationTimeline.h" +#include "nsIDocument.h" +#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp +#include "nsRefreshDriver.h" + +struct JSContext; + +// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to +// GetTickCount(). +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +namespace mozilla { +namespace dom { + +class DocumentTimeline final + : public AnimationTimeline + , public nsARefreshObserver + , public LinkedListElement +{ +public: + DocumentTimeline(nsIDocument* aDocument, const TimeDuration& aOriginTime) + : AnimationTimeline(aDocument->GetParentObject()) + , mDocument(aDocument) + , mIsObservingRefreshDriver(false) + , mOriginTime(aOriginTime) + { + if (mDocument) { + mDocument->Timelines().insertBack(this); + } + } + +protected: + virtual ~DocumentTimeline() + { + MOZ_ASSERT(!mIsObservingRefreshDriver, "Timeline should have disassociated" + " from the refresh driver before being destroyed"); + if (isInList()) { + remove(); + } + } + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DocumentTimeline, + AnimationTimeline) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const DocumentTimelineOptions& aOptions, + ErrorResult& aRv); + + // AnimationTimeline methods + virtual Nullable GetCurrentTime() const override; + + bool TracksWallclockTime() const override + { + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + return !refreshDriver || + !refreshDriver->IsTestControllingRefreshesEnabled(); + } + Nullable ToTimelineTime(const TimeStamp& aTimeStamp) const + override; + TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override; + + void NotifyAnimationUpdated(Animation& aAnimation) override; + + void RemoveAnimation(Animation* aAnimation) override; + + // nsARefreshObserver methods + void WillRefresh(TimeStamp aTime) override; + + void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver); + +protected: + TimeStamp GetCurrentTimeStamp() const; + nsRefreshDriver* GetRefreshDriver() const; + void UnregisterFromRefreshDriver(); + + nsCOMPtr mDocument; + + // The most recently used refresh driver time. This is used in cases where + // we don't have a refresh driver (e.g. because we are in a display:none + // iframe). + mutable TimeStamp mLastRefreshDriverTime; + bool mIsObservingRefreshDriver; + + TimeDuration mOriginTime; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_DocumentTimeline_h diff --git a/dom/animation/EffectCompositor.cpp b/dom/animation/EffectCompositor.cpp new file mode 100644 index 000000000..c88cabe90 --- /dev/null +++ b/dom/animation/EffectCompositor.cpp @@ -0,0 +1,920 @@ +/* -*- 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/. */ + +#include "EffectCompositor.h" + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffectReadOnly.h" +#include "mozilla/AnimationComparator.h" +#include "mozilla/AnimationPerformanceWarning.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/EffectSet.h" +#include "mozilla/LayerAnimationInfo.h" +#include "mozilla/RestyleManagerHandle.h" +#include "mozilla/RestyleManagerHandleInlines.h" +#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetPresShellForContent +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" +#include "nsIPresShell.h" +#include "nsLayoutUtils.h" +#include "nsRuleNode.h" // For nsRuleNode::ComputePropertiesOverridingAnimation +#include "nsRuleProcessorData.h" // For ElementRuleProcessorData etc. +#include "nsTArray.h" +#include +#include + +using mozilla::dom::Animation; +using mozilla::dom::Element; +using mozilla::dom::KeyframeEffectReadOnly; + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor) + for (auto& elementSet : tmp->mElementsToRestyle) { + elementSet.Clear(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EffectCompositor) + for (auto& elementSet : tmp->mElementsToRestyle) { + for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) { + CycleCollectionNoteChild(cb, iter.Key().mElement, + "EffectCompositor::mElementsToRestyle[]", + cb.Flags()); + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(EffectCompositor, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(EffectCompositor, Release) + +// Helper function to factor out the common logic from +// GetAnimationsForCompositor and HasAnimationsForCompositor. +// +// Takes an optional array to fill with eligible animations. +// +// Returns true if there are eligible animations, false otherwise. +bool +FindAnimationsForCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty, + nsTArray>* aMatches /*out*/) +{ + MOZ_ASSERT(!aMatches || aMatches->IsEmpty(), + "Matches array, if provided, should be empty"); + + EffectSet* effects = EffectSet::GetEffectSet(aFrame); + if (!effects || effects->IsEmpty()) { + return false; + } + + // If the property will be added to the animations level of the cascade but + // there is an !important rule for that property in the cascade then the + // animation will not be applied since the !important rule overrides it. + if (effects->PropertiesWithImportantRules().HasProperty(aProperty) && + effects->PropertiesForAnimationsLevel().HasProperty(aProperty)) { + return false; + } + + if (aFrame->RefusedAsyncAnimation()) { + return false; + } + + // The animation cascade will almost always be up-to-date by this point + // but there are some cases such as when we are restoring the refresh driver + // from test control after seeking where it might not be the case. + // + // Those cases are probably not important but just to be safe, let's make + // sure the cascade is up to date since if it *is* up to date, this is + // basically a no-op. + Maybe pseudoElement = + EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame); + if (pseudoElement) { + EffectCompositor::MaybeUpdateCascadeResults(pseudoElement->mElement, + pseudoElement->mPseudoType, + aFrame->StyleContext()); + } + + if (!nsLayoutUtils::AreAsyncAnimationsEnabled()) { + if (nsLayoutUtils::IsAnimationLoggingEnabled()) { + nsCString message; + message.AppendLiteral("Performance warning: Async animations are " + "disabled"); + AnimationUtils::LogAsyncAnimationFailure(message); + } + return false; + } + + // Disable async animations if we have a rendering observer that + // depends on our content (svg masking, -moz-element etc) so that + // it gets updated correctly. + nsIContent* content = aFrame->GetContent(); + while (content) { + if (content->HasRenderingObservers()) { + EffectCompositor::SetPerformanceWarning( + aFrame, aProperty, + AnimationPerformanceWarning( + AnimationPerformanceWarning::Type::HasRenderingObserver)); + return false; + } + content = content->GetParent(); + } + + bool foundSome = false; + for (KeyframeEffectReadOnly* effect : *effects) { + MOZ_ASSERT(effect && effect->GetAnimation()); + Animation* animation = effect->GetAnimation(); + + if (!animation->IsPlayableOnCompositor()) { + continue; + } + + AnimationPerformanceWarning::Type warningType; + if (aProperty == eCSSProperty_transform && + effect->ShouldBlockAsyncTransformAnimations(aFrame, + warningType)) { + if (aMatches) { + aMatches->Clear(); + } + EffectCompositor::SetPerformanceWarning( + aFrame, aProperty, + AnimationPerformanceWarning(warningType)); + return false; + } + + if (!effect->HasEffectiveAnimationOfProperty(aProperty)) { + continue; + } + + if (aMatches) { + aMatches->AppendElement(animation); + } + foundSome = true; + } + + MOZ_ASSERT(!foundSome || !aMatches || !aMatches->IsEmpty(), + "If return value is true, matches array should be non-empty"); + + if (aMatches && foundSome) { + aMatches->Sort(AnimationPtrComparator>()); + } + return foundSome; +} + +void +EffectCompositor::RequestRestyle(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + RestyleType aRestyleType, + CascadeLevel aCascadeLevel) +{ + if (!mPresContext) { + // Pres context will be null after the effect compositor is disconnected. + return; + } + + // Ignore animations on orphaned elements. + if (!aElement->IsInComposedDoc()) { + return; + } + + auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel]; + PseudoElementHashEntry::KeyType key = { aElement, aPseudoType }; + + if (aRestyleType == RestyleType::Throttled) { + if (!elementsToRestyle.Contains(key)) { + elementsToRestyle.Put(key, false); + } + mPresContext->Document()->SetNeedStyleFlush(); + } else { + // Get() returns 0 if the element is not found. It will also return + // false if the element is found but does not have a pending restyle. + bool hasPendingRestyle = elementsToRestyle.Get(key); + if (!hasPendingRestyle) { + PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel); + } + elementsToRestyle.Put(key, true); + } + + if (aRestyleType == RestyleType::Layer) { + // Prompt layers to re-sync their animations. + MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(), + "stylo: Servo-backed style system should not be using " + "EffectCompositor"); + mPresContext->RestyleManager()->AsGecko()->IncrementAnimationGeneration(); + EffectSet* effectSet = + EffectSet::GetEffectSet(aElement, aPseudoType); + if (effectSet) { + effectSet->UpdateAnimationGeneration(mPresContext); + } + } +} + +void +EffectCompositor::PostRestyleForAnimation(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel) +{ + if (!mPresContext) { + return; + } + + dom::Element* element = GetElementToRestyle(aElement, aPseudoType); + if (!element) { + return; + } + + nsRestyleHint hint = aCascadeLevel == CascadeLevel::Transitions ? + eRestyle_CSSTransitions : + eRestyle_CSSAnimations; + mPresContext->PresShell()->RestyleForAnimation(element, hint); +} + +void +EffectCompositor::PostRestyleForThrottledAnimations() +{ + for (size_t i = 0; i < kCascadeLevelCount; i++) { + CascadeLevel cascadeLevel = CascadeLevel(i); + auto& elementSet = mElementsToRestyle[cascadeLevel]; + + for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) { + bool& postedRestyle = iter.Data(); + if (postedRestyle) { + continue; + } + + PostRestyleForAnimation(iter.Key().mElement, + iter.Key().mPseudoType, + cascadeLevel); + postedRestyle = true; + } + } +} + +void +EffectCompositor::UpdateEffectProperties(nsStyleContext* aStyleContext, + dom::Element* aElement, + CSSPseudoElementType aPseudoType) +{ + EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType); + if (!effectSet) { + return; + } + + // Style context change might cause CSS cascade level, + // e.g removing !important, so we should update the cascading result. + effectSet->MarkCascadeNeedsUpdate(); + + for (KeyframeEffectReadOnly* effect : *effectSet) { + effect->UpdateProperties(aStyleContext); + } +} + +void +EffectCompositor::MaybeUpdateAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + nsStyleContext* aStyleContext) +{ + // First update cascade results since that may cause some elements to + // be marked as needing a restyle. + MaybeUpdateCascadeResults(aElement, aPseudoType, aStyleContext); + + auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel]; + PseudoElementHashEntry::KeyType key = { aElement, aPseudoType }; + + if (!mPresContext || !elementsToRestyle.Contains(key)) { + return; + } + + ComposeAnimationRule(aElement, aPseudoType, aCascadeLevel, + mPresContext->RefreshDriver()->MostRecentRefresh()); + + elementsToRestyle.Remove(key); +} + +nsIStyleRule* +EffectCompositor::GetAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + nsStyleContext* aStyleContext) +{ + // NOTE: We need to be careful about early returns in this method where + // we *don't* update mElementsToRestyle. When we get a call to + // RequestRestyle that results in a call to PostRestyleForAnimation, we + // will set a bool flag in mElementsToRestyle indicating that we've + // called PostRestyleForAnimation so we don't need to call it again + // until that restyle happens. During that restyle, if we arrive here + // and *don't* update mElementsToRestyle we'll continue to skip calling + // PostRestyleForAnimation from RequestRestyle. + + if (!mPresContext || !mPresContext->IsDynamic()) { + // For print or print preview, ignore animations. + return nullptr; + } + + MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(), + "stylo: Servo-backed style system should not be using " + "EffectCompositor"); + if (mPresContext->RestyleManager()->AsGecko()->SkipAnimationRules()) { + // We don't need to worry about updating mElementsToRestyle in this case + // since this is not the animation restyle we requested when we called + // PostRestyleForAnimation (see comment at start of this method). + return nullptr; + } + + MaybeUpdateAnimationRule(aElement, aPseudoType, aCascadeLevel, aStyleContext); + +#ifdef DEBUG + { + auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel]; + PseudoElementHashEntry::KeyType key = { aElement, aPseudoType }; + MOZ_ASSERT(!elementsToRestyle.Contains(key), + "Element should no longer require a restyle after its " + "animation rule has been updated"); + } +#endif + + EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType); + if (!effectSet) { + return nullptr; + } + + return effectSet->AnimationRule(aCascadeLevel); +} + +/* static */ dom::Element* +EffectCompositor::GetElementToRestyle(dom::Element* aElement, + CSSPseudoElementType aPseudoType) +{ + if (aPseudoType == CSSPseudoElementType::NotPseudo) { + return aElement; + } + + nsIFrame* primaryFrame = aElement->GetPrimaryFrame(); + if (!primaryFrame) { + return nullptr; + } + nsIFrame* pseudoFrame; + if (aPseudoType == CSSPseudoElementType::before) { + pseudoFrame = nsLayoutUtils::GetBeforeFrame(primaryFrame); + } else if (aPseudoType == CSSPseudoElementType::after) { + pseudoFrame = nsLayoutUtils::GetAfterFrame(primaryFrame); + } else { + NS_NOTREACHED("Should not try to get the element to restyle for a pseudo " + "other that :before or :after"); + return nullptr; + } + if (!pseudoFrame) { + return nullptr; + } + return pseudoFrame->GetContent()->AsElement(); +} + +bool +EffectCompositor::HasPendingStyleUpdates() const +{ + for (auto& elementSet : mElementsToRestyle) { + if (elementSet.Count()) { + return true; + } + } + + return false; +} + +bool +EffectCompositor::HasThrottledStyleUpdates() const +{ + for (auto& elementSet : mElementsToRestyle) { + for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) { + if (!iter.Data()) { + return true; + } + } + } + + return false; +} + +void +EffectCompositor::AddStyleUpdatesTo(RestyleTracker& aTracker) +{ + if (!mPresContext) { + return; + } + + for (size_t i = 0; i < kCascadeLevelCount; i++) { + CascadeLevel cascadeLevel = CascadeLevel(i); + auto& elementSet = mElementsToRestyle[cascadeLevel]; + + // Copy the list of elements to restyle to a separate array that we can + // iterate over. This is because we need to call MaybeUpdateCascadeResults + // on each element, but doing that can mutate elementSet. In this case + // it will only mutate the bool value associated with each element in the + // set but even doing that will cause assertions in PLDHashTable to fail + // if we are iterating over the hashtable at the same time. + nsTArray elementsToRestyle( + elementSet.Count()); + for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) { + // Skip animations on elements that have been orphaned since they + // requested a restyle. + if (iter.Key().mElement->IsInComposedDoc()) { + elementsToRestyle.AppendElement(iter.Key()); + } + } + + for (auto& pseudoElem : elementsToRestyle) { + MaybeUpdateCascadeResults(pseudoElem.mElement, + pseudoElem.mPseudoType, + nullptr); + + ComposeAnimationRule(pseudoElem.mElement, + pseudoElem.mPseudoType, + cascadeLevel, + mPresContext->RefreshDriver()->MostRecentRefresh()); + + dom::Element* elementToRestyle = + GetElementToRestyle(pseudoElem.mElement, pseudoElem.mPseudoType); + if (elementToRestyle) { + nsRestyleHint rshint = cascadeLevel == CascadeLevel::Transitions ? + eRestyle_CSSTransitions : + eRestyle_CSSAnimations; + aTracker.AddPendingRestyle(elementToRestyle, rshint, nsChangeHint(0)); + } + } + + elementSet.Clear(); + // Note: mElement pointers in elementsToRestyle might now dangle + } +} + +/* static */ bool +EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty) +{ + return FindAnimationsForCompositor(aFrame, aProperty, nullptr); +} + +/* static */ nsTArray> +EffectCompositor::GetAnimationsForCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty) +{ + nsTArray> result; + +#ifdef DEBUG + bool foundSome = +#endif + FindAnimationsForCompositor(aFrame, aProperty, &result); + MOZ_ASSERT(!foundSome || !result.IsEmpty(), + "If return value is true, matches array should be non-empty"); + + return result; +} + +/* static */ void +EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame *aFrame, + nsCSSPropertyID aProperty) +{ + EffectSet* effects = EffectSet::GetEffectSet(aFrame); + if (!effects) { + return; + } + + for (KeyframeEffectReadOnly* effect : *effects) { + effect->SetIsRunningOnCompositor(aProperty, false); + } +} + +/* static */ void +EffectCompositor::MaybeUpdateCascadeResults(Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext) +{ + EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType); + if (!effects || !effects->CascadeNeedsUpdate()) { + return; + } + + nsStyleContext* styleContext = aStyleContext; + if (!styleContext) { + dom::Element* elementToRestyle = GetElementToRestyle(aElement, aPseudoType); + if (elementToRestyle) { + nsIFrame* frame = elementToRestyle->GetPrimaryFrame(); + if (frame) { + styleContext = frame->StyleContext(); + } + } + } + UpdateCascadeResults(*effects, aElement, aPseudoType, styleContext); + + MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state"); +} + +namespace { + class EffectCompositeOrderComparator { + public: + bool Equals(const KeyframeEffectReadOnly* a, + const KeyframeEffectReadOnly* b) const + { + return a == b; + } + + bool LessThan(const KeyframeEffectReadOnly* a, + const KeyframeEffectReadOnly* b) const + { + MOZ_ASSERT(a->GetAnimation() && b->GetAnimation()); + MOZ_ASSERT( + Equals(a, b) || + a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) != + b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation())); + return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()); + } + }; +} + +/* static */ void +EffectCompositor::UpdateCascadeResults(Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext) +{ + EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType); + if (!effects) { + return; + } + + UpdateCascadeResults(*effects, aElement, aPseudoType, aStyleContext); +} + +/* static */ Maybe +EffectCompositor::GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame) +{ + // Always return the same object to benefit from return-value optimization. + Maybe result; + + CSSPseudoElementType pseudoType = + aFrame->StyleContext()->GetPseudoType(); + + if (pseudoType != CSSPseudoElementType::NotPseudo && + pseudoType != CSSPseudoElementType::before && + pseudoType != CSSPseudoElementType::after) { + return result; + } + + nsIContent* content = aFrame->GetContent(); + if (!content) { + return result; + } + + if (pseudoType == CSSPseudoElementType::before || + pseudoType == CSSPseudoElementType::after) { + content = content->GetParent(); + if (!content) { + return result; + } + } + + if (!content->IsElement()) { + return result; + } + + result.emplace(content->AsElement(), pseudoType); + + return result; +} + +/* static */ void +EffectCompositor::ComposeAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + TimeStamp aRefreshTime) +{ + EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType); + if (!effects) { + return; + } + + // The caller is responsible for calling MaybeUpdateCascadeResults first. + MOZ_ASSERT(!effects->CascadeNeedsUpdate(), + "Animation cascade out of date when composing animation rule"); + + // Get a list of effects sorted by composite order. + nsTArray sortedEffectList(effects->Count()); + for (KeyframeEffectReadOnly* effect : *effects) { + sortedEffectList.AppendElement(effect); + } + sortedEffectList.Sort(EffectCompositeOrderComparator()); + + RefPtr& animationRule = + effects->AnimationRule(aCascadeLevel); + animationRule = nullptr; + + // If multiple animations affect the same property, animations with higher + // composite order (priority) override or add or animations with lower + // priority except properties in propertiesToSkip. + const nsCSSPropertyIDSet& propertiesToSkip = + aCascadeLevel == CascadeLevel::Animations + ? effects->PropertiesForAnimationsLevel().Invert() + : effects->PropertiesForAnimationsLevel(); + for (KeyframeEffectReadOnly* effect : sortedEffectList) { + effect->GetAnimation()->ComposeStyle(animationRule, propertiesToSkip); + } + + MOZ_ASSERT(effects == EffectSet::GetEffectSet(aElement, aPseudoType), + "EffectSet should not change while composing style"); + + effects->UpdateAnimationRuleRefreshTime(aCascadeLevel, aRefreshTime); +} + +/* static */ void +EffectCompositor::GetOverriddenProperties(nsStyleContext* aStyleContext, + EffectSet& aEffectSet, + nsCSSPropertyIDSet& + aPropertiesOverridden) +{ + AutoTArray propertiesToTrack; + { + nsCSSPropertyIDSet propertiesToTrackAsSet; + for (KeyframeEffectReadOnly* effect : aEffectSet) { + for (const AnimationProperty& property : effect->Properties()) { + if (nsCSSProps::PropHasFlags(property.mProperty, + CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) && + !propertiesToTrackAsSet.HasProperty(property.mProperty)) { + propertiesToTrackAsSet.AddProperty(property.mProperty); + propertiesToTrack.AppendElement(property.mProperty); + } + } + // Skip iterating over the rest of the effects if we've already + // found all the compositor-animatable properties. + if (propertiesToTrack.Length() == LayerAnimationInfo::kRecords) { + break; + } + } + } + + if (propertiesToTrack.IsEmpty()) { + return; + } + + nsRuleNode::ComputePropertiesOverridingAnimation(propertiesToTrack, + aStyleContext, + aPropertiesOverridden); +} + +/* static */ void +EffectCompositor::UpdateCascadeResults(EffectSet& aEffectSet, + Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(EffectSet::GetEffectSet(aElement, aPseudoType) == &aEffectSet, + "Effect set should correspond to the specified (pseudo-)element"); + if (aEffectSet.IsEmpty()) { + aEffectSet.MarkCascadeUpdated(); + return; + } + + // Get a list of effects sorted by composite order. + nsTArray sortedEffectList(aEffectSet.Count()); + for (KeyframeEffectReadOnly* effect : aEffectSet) { + sortedEffectList.AppendElement(effect); + } + sortedEffectList.Sort(EffectCompositeOrderComparator()); + + // Get properties that override the *animations* level of the cascade. + // + // We only do this for properties that we can animate on the compositor + // since we will apply other properties on the main thread where the usual + // cascade applies. + nsCSSPropertyIDSet overriddenProperties; + if (aStyleContext) { + GetOverriddenProperties(aStyleContext, aEffectSet, overriddenProperties); + } + + // Returns a bitset the represents which properties from + // LayerAnimationInfo::sRecords are present in |aPropertySet|. + auto compositorPropertiesInSet = + [](nsCSSPropertyIDSet& aPropertySet) -> + std::bitset { + std::bitset result; + for (size_t i = 0; i < LayerAnimationInfo::kRecords; i++) { + if (aPropertySet.HasProperty( + LayerAnimationInfo::sRecords[i].mProperty)) { + result.set(i); + } + } + return result; + }; + + nsCSSPropertyIDSet& propertiesWithImportantRules = + aEffectSet.PropertiesWithImportantRules(); + nsCSSPropertyIDSet& propertiesForAnimationsLevel = + aEffectSet.PropertiesForAnimationsLevel(); + + // Record which compositor-animatable properties were originally set so we can + // compare for changes later. + std::bitset + prevCompositorPropertiesWithImportantRules = + compositorPropertiesInSet(propertiesWithImportantRules); + std::bitset + prevCompositorPropertiesForAnimationsLevel = + compositorPropertiesInSet(propertiesForAnimationsLevel); + + propertiesWithImportantRules.Empty(); + propertiesForAnimationsLevel.Empty(); + + bool hasCompositorPropertiesForTransition = false; + + for (const KeyframeEffectReadOnly* effect : sortedEffectList) { + MOZ_ASSERT(effect->GetAnimation(), + "Effects on a target element should have an Animation"); + CascadeLevel cascadeLevel = effect->GetAnimation()->CascadeLevel(); + + for (const AnimationProperty& prop : effect->Properties()) { + if (overriddenProperties.HasProperty(prop.mProperty)) { + propertiesWithImportantRules.AddProperty(prop.mProperty); + } + if (cascadeLevel == EffectCompositor::CascadeLevel::Animations) { + propertiesForAnimationsLevel.AddProperty(prop.mProperty); + } + + if (nsCSSProps::PropHasFlags(prop.mProperty, + CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) && + cascadeLevel == EffectCompositor::CascadeLevel::Transitions) { + hasCompositorPropertiesForTransition = true; + } + } + } + + aEffectSet.MarkCascadeUpdated(); + + nsPresContext* presContext = GetPresContext(aElement); + if (!presContext) { + return; + } + + // If properties for compositor are newly overridden by !important rules, or + // released from being overridden by !important rules, we need to update + // layers for animations level because it's a trigger to send animations to + // the compositor or pull animations back from the compositor. + if (prevCompositorPropertiesWithImportantRules != + compositorPropertiesInSet(propertiesWithImportantRules)) { + presContext->EffectCompositor()-> + RequestRestyle(aElement, aPseudoType, + EffectCompositor::RestyleType::Layer, + EffectCompositor::CascadeLevel::Animations); + } + // If we have transition properties for compositor and if the same propery + // for animations level is newly added or removed, we need to update layers + // for transitions level because composite order has been changed now. + if (hasCompositorPropertiesForTransition && + prevCompositorPropertiesForAnimationsLevel != + compositorPropertiesInSet(propertiesForAnimationsLevel)) { + presContext->EffectCompositor()-> + RequestRestyle(aElement, aPseudoType, + EffectCompositor::RestyleType::Layer, + EffectCompositor::CascadeLevel::Transitions); + } +} + +/* static */ nsPresContext* +EffectCompositor::GetPresContext(Element* aElement) +{ + MOZ_ASSERT(aElement); + nsIPresShell* shell = nsComputedDOMStyle::GetPresShellForContent(aElement); + if (!shell) { + return nullptr; + } + return shell->GetPresContext(); +} + +/* static */ void +EffectCompositor::SetPerformanceWarning( + const nsIFrame *aFrame, + nsCSSPropertyID aProperty, + const AnimationPerformanceWarning& aWarning) +{ + EffectSet* effects = EffectSet::GetEffectSet(aFrame); + if (!effects) { + return; + } + + for (KeyframeEffectReadOnly* effect : *effects) { + effect->SetPerformanceWarning(aProperty, aWarning); + } +} + +// --------------------------------------------------------- +// +// Nested class: AnimationStyleRuleProcessor +// +// --------------------------------------------------------- + +NS_IMPL_ISUPPORTS(EffectCompositor::AnimationStyleRuleProcessor, + nsIStyleRuleProcessor) + +nsRestyleHint +EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle( + StateRuleProcessorData* aData) +{ + return nsRestyleHint(0); +} + +nsRestyleHint +EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle( + PseudoElementStateRuleProcessorData* aData) +{ + return nsRestyleHint(0); +} + +bool +EffectCompositor::AnimationStyleRuleProcessor::HasDocumentStateDependentStyle( + StateRuleProcessorData* aData) +{ + return false; +} + +nsRestyleHint +EffectCompositor::AnimationStyleRuleProcessor::HasAttributeDependentStyle( + AttributeRuleProcessorData* aData, + RestyleHintData& aRestyleHintDataResult) +{ + return nsRestyleHint(0); +} + +bool +EffectCompositor::AnimationStyleRuleProcessor::MediumFeaturesChanged( + nsPresContext* aPresContext) +{ + return false; +} + +void +EffectCompositor::AnimationStyleRuleProcessor::RulesMatching( + ElementRuleProcessorData* aData) +{ + nsIStyleRule *rule = + mCompositor->GetAnimationRule(aData->mElement, + CSSPseudoElementType::NotPseudo, + mCascadeLevel, + nullptr); + if (rule) { + aData->mRuleWalker->Forward(rule); + aData->mRuleWalker->CurrentNode()->SetIsAnimationRule(); + } +} + +void +EffectCompositor::AnimationStyleRuleProcessor::RulesMatching( + PseudoElementRuleProcessorData* aData) +{ + if (aData->mPseudoType != CSSPseudoElementType::before && + aData->mPseudoType != CSSPseudoElementType::after) { + return; + } + + nsIStyleRule *rule = + mCompositor->GetAnimationRule(aData->mElement, + aData->mPseudoType, + mCascadeLevel, + nullptr); + if (rule) { + aData->mRuleWalker->Forward(rule); + aData->mRuleWalker->CurrentNode()->SetIsAnimationRule(); + } +} + +void +EffectCompositor::AnimationStyleRuleProcessor::RulesMatching( + AnonBoxRuleProcessorData* aData) +{ +} + +#ifdef MOZ_XUL +void +EffectCompositor::AnimationStyleRuleProcessor::RulesMatching( + XULTreeRuleProcessorData* aData) +{ +} +#endif + +size_t +EffectCompositor::AnimationStyleRuleProcessor::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const +{ + return 0; +} + +size_t +EffectCompositor::AnimationStyleRuleProcessor::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const +{ + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +} // namespace mozilla diff --git a/dom/animation/EffectCompositor.h b/dom/animation/EffectCompositor.h new file mode 100644 index 000000000..732fbb333 --- /dev/null +++ b/dom/animation/EffectCompositor.h @@ -0,0 +1,307 @@ +/* -*- 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_EffectCompositor_h +#define mozilla_EffectCompositor_h + +#include "mozilla/EnumeratedArray.h" +#include "mozilla/Maybe.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/PseudoElementHashEntry.h" +#include "mozilla/RefPtr.h" +#include "nsCSSPropertyID.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDataHashtable.h" +#include "nsIStyleRuleProcessor.h" +#include "nsTArray.h" + +class nsCSSPropertyIDSet; +class nsIFrame; +class nsIStyleRule; +class nsPresContext; +class nsStyleContext; + +namespace mozilla { + +class EffectSet; +class RestyleTracker; +struct AnimationPerformanceWarning; +struct NonOwningAnimationTarget; + +namespace dom { +class Animation; +class Element; +} + +class EffectCompositor +{ +public: + explicit EffectCompositor(nsPresContext* aPresContext) + : mPresContext(aPresContext) + { + for (size_t i = 0; i < kCascadeLevelCount; i++) { + CascadeLevel cascadeLevel = CascadeLevel(i); + mRuleProcessors[cascadeLevel] = + new AnimationStyleRuleProcessor(this, cascadeLevel); + } + } + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EffectCompositor) + + void Disconnect() { + mPresContext = nullptr; + } + + // Animations can be applied at two different levels in the CSS cascade: + enum class CascadeLevel { + // The animations sheet (CSS animations, script-generated animations, + // and CSS transitions that are no longer tied to CSS markup) + Animations, + // The transitions sheet (CSS transitions that are tied to CSS markup) + Transitions + }; + // We don't define this as part of CascadeLevel as then we'd have to add + // explicit checks for the Count enum value everywhere CascadeLevel is used. + static const size_t kCascadeLevelCount = + static_cast(CascadeLevel::Transitions) + 1; + + // NOTE: This can return null after Disconnect(). + nsPresContext* PresContext() const { return mPresContext; } + + enum class RestyleType { + // Animation style has changed but the compositor is applying the same + // change so we might be able to defer updating the main thread until it + // becomes necessary. + Throttled, + // Animation style has changed and needs to be updated on the main thread. + Standard, + // Animation style has changed and needs to be updated on the main thread + // as well as forcing animations on layers to be updated. + // This is needed in cases such as when an animation becomes paused or has + // its playback rate changed. In such cases, although the computed style + // and refresh driver time might not change, we still need to ensure the + // corresponding animations on layers are updated to reflect the new + // configuration of the animation. + Layer + }; + + // Notifies the compositor that the animation rule for the specified + // (pseudo-)element at the specified cascade level needs to be updated. + // The specified steps taken to update the animation rule depend on + // |aRestyleType| whose values are described above. + void RequestRestyle(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + RestyleType aRestyleType, + CascadeLevel aCascadeLevel); + + // Schedule an animation restyle. This is called automatically by + // RequestRestyle when necessary. However, it is exposed here since we also + // need to perform this step when triggering transitions *without* also + // invalidating the animation style rule (which RequestRestyle would do). + void PostRestyleForAnimation(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel); + + // Posts an animation restyle for any elements whose animation style rule + // is out of date but for which an animation restyle has not yet been + // posted because updates on the main thread are throttled. + void PostRestyleForThrottledAnimations(); + + // Called when the style context on the specified (pseudo-) element might + // have changed so that any context-sensitive values stored within + // animation effects (e.g. em-based endpoints used in keyframe effects) + // can be re-resolved to computed values. + void UpdateEffectProperties(nsStyleContext* aStyleContext, + dom::Element* aElement, + CSSPseudoElementType aPseudoType); + + // Updates the animation rule stored on the EffectSet for the + // specified (pseudo-)element for cascade level |aLevel|. + // If the animation rule is not marked as needing an update, + // no work is done. + // |aStyleContext| is used for UpdateCascadingResults. + // |aStyleContext| can be nullptr if style context, which is associated with + // the primary frame of the specified (pseudo-)element, is the current style + // context. + // If we are resolving a new style context, we shoud pass the newly created + // style context, otherwise we may use an old style context, it will result + // unexpected cascading results. + void MaybeUpdateAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + nsStyleContext *aStyleContext); + + // We need to pass the newly resolved style context as |aStyleContext| when + // we call this function during resolving style context because this function + // calls UpdateCascadingResults with a style context if necessary, at the + // time, we end up using the previous style context if we don't pass the new + // style context. + // When we are not resolving style context, |aStyleContext| can be nullptr, we + // will use a style context associated with the primary frame of the specified + // (pseudo-)element. + nsIStyleRule* GetAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + nsStyleContext* aStyleContext); + + bool HasPendingStyleUpdates() const; + bool HasThrottledStyleUpdates() const; + + // Tell the restyle tracker about all the animated styles that have + // pending updates so that it can update the animation rule for these + // elements. + void AddStyleUpdatesTo(RestyleTracker& aTracker); + + nsIStyleRuleProcessor* RuleProcessor(CascadeLevel aCascadeLevel) const + { + return mRuleProcessors[aCascadeLevel]; + } + + static bool HasAnimationsForCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty); + + static nsTArray> + GetAnimationsForCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty); + + static void ClearIsRunningOnCompositor(const nsIFrame* aFrame, + nsCSSPropertyID aProperty); + + // Update animation cascade results for the specified (pseudo-)element + // but only if we have marked the cascade as needing an update due a + // the change in the set of effects or a change in one of the effects' + // "in effect" state. + // |aStyleContext| may be nullptr in which case we will use the + // nsStyleContext of the primary frame of the specified (pseudo-)element. + // + // This method does NOT detect if other styles that apply above the + // animation level of the cascade have changed. + static void + MaybeUpdateCascadeResults(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext); + + // Update the mPropertiesWithImportantRules and + // mPropertiesForAnimationsLevel members of the corresponding EffectSet. + // + // This can be expensive so we should only call it if styles that apply + // above the animation level of the cascade might have changed. For all + // other cases we should call MaybeUpdateCascadeResults. + static void + UpdateCascadeResults(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext); + + // Helper to fetch the corresponding element and pseudo-type from a frame. + // + // For frames corresponding to pseudo-elements, the returned element is the + // element on which we store the animations (i.e. the EffectSet and/or + // AnimationCollection), *not* the generated content. + // + // Returns an empty result when a suitable element cannot be found including + // when the frame represents a pseudo-element on which we do not support + // animations. + static Maybe + GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame); + + // Associates a performance warning with effects on |aFrame| that animates + // |aProperty|. + static void SetPerformanceWarning( + const nsIFrame* aFrame, + nsCSSPropertyID aProperty, + const AnimationPerformanceWarning& aWarning); + +private: + ~EffectCompositor() = default; + + // Rebuilds the animation rule corresponding to |aCascadeLevel| on the + // EffectSet associated with the specified (pseudo-)element. + static void ComposeAnimationRule(dom::Element* aElement, + CSSPseudoElementType aPseudoType, + CascadeLevel aCascadeLevel, + TimeStamp aRefreshTime); + + static dom::Element* GetElementToRestyle(dom::Element* aElement, + CSSPseudoElementType + aPseudoType); + + // Get the properties in |aEffectSet| that we are able to animate on the + // compositor but which are also specified at a higher level in the cascade + // than the animations level in |aStyleContext|. + static void + GetOverriddenProperties(nsStyleContext* aStyleContext, + EffectSet& aEffectSet, + nsCSSPropertyIDSet& aPropertiesOverridden); + + static void + UpdateCascadeResults(EffectSet& aEffectSet, + dom::Element* aElement, + CSSPseudoElementType aPseudoType, + nsStyleContext* aStyleContext); + + static nsPresContext* GetPresContext(dom::Element* aElement); + + nsPresContext* mPresContext; + + // Elements with a pending animation restyle. The associated bool value is + // true if a pending animation restyle has also been dispatched. For + // animations that can be throttled, we will add an entry to the hashtable to + // indicate that the style rule on the element is out of date but without + // posting a restyle to update it. + EnumeratedArray> + mElementsToRestyle; + + class AnimationStyleRuleProcessor final : public nsIStyleRuleProcessor + { + public: + AnimationStyleRuleProcessor(EffectCompositor* aCompositor, + CascadeLevel aCascadeLevel) + : mCompositor(aCompositor) + , mCascadeLevel(aCascadeLevel) + { + MOZ_ASSERT(aCompositor); + } + + NS_DECL_ISUPPORTS + + // nsIStyleRuleProcessor (parts) + nsRestyleHint HasStateDependentStyle( + StateRuleProcessorData* aData) override; + nsRestyleHint HasStateDependentStyle( + PseudoElementStateRuleProcessorData* aData) override; + bool HasDocumentStateDependentStyle(StateRuleProcessorData* aData) override; + nsRestyleHint HasAttributeDependentStyle( + AttributeRuleProcessorData* aData, + RestyleHintData& aRestyleHintDataResult) override; + bool MediumFeaturesChanged(nsPresContext* aPresContext) override; + void RulesMatching(ElementRuleProcessorData* aData) override; + void RulesMatching(PseudoElementRuleProcessorData* aData) override; + void RulesMatching(AnonBoxRuleProcessorData* aData) override; +#ifdef MOZ_XUL + void RulesMatching(XULTreeRuleProcessorData* aData) override; +#endif + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) + const MOZ_MUST_OVERRIDE override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) + const MOZ_MUST_OVERRIDE override; + + private: + ~AnimationStyleRuleProcessor() = default; + + EffectCompositor* mCompositor; + CascadeLevel mCascadeLevel; + }; + + EnumeratedArray> + mRuleProcessors; +}; + +} // namespace mozilla + +#endif // mozilla_EffectCompositor_h diff --git a/dom/animation/EffectSet.cpp b/dom/animation/EffectSet.cpp new file mode 100644 index 000000000..ffd3bb523 --- /dev/null +++ b/dom/animation/EffectSet.cpp @@ -0,0 +1,177 @@ +/* -*- 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/. */ + +#include "EffectSet.h" +#include "mozilla/dom/Element.h" // For Element +#include "mozilla/RestyleManagerHandle.h" +#include "mozilla/RestyleManagerHandleInlines.h" +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType +#include "nsCycleCollectionNoteChild.h" // For CycleCollectionNoteChild +#include "nsPresContext.h" +#include "nsLayoutUtils.h" + +namespace mozilla { + +/* static */ void +EffectSet::PropertyDtor(void* aObject, nsIAtom* aPropertyName, + void* aPropertyValue, void* aData) +{ + EffectSet* effectSet = static_cast(aPropertyValue); + +#ifdef DEBUG + MOZ_ASSERT(!effectSet->mCalledPropertyDtor, "Should not call dtor twice"); + effectSet->mCalledPropertyDtor = true; +#endif + + delete effectSet; +} + +void +EffectSet::Traverse(nsCycleCollectionTraversalCallback& aCallback) +{ + for (auto iter = mEffects.Iter(); !iter.Done(); iter.Next()) { + CycleCollectionNoteChild(aCallback, iter.Get()->GetKey(), + "EffectSet::mEffects[]", aCallback.Flags()); + } +} + +/* static */ EffectSet* +EffectSet::GetEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType) +{ + nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType); + return static_cast(aElement->GetProperty(propName)); +} + +/* static */ EffectSet* +EffectSet::GetEffectSet(const nsIFrame* aFrame) +{ + Maybe target = + EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame); + + if (!target) { + return nullptr; + } + + if (!target->mElement->MayHaveAnimations()) { + return nullptr; + } + + return GetEffectSet(target->mElement, target->mPseudoType); +} + +/* static */ EffectSet* +EffectSet::GetOrCreateEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType) +{ + EffectSet* effectSet = GetEffectSet(aElement, aPseudoType); + if (effectSet) { + return effectSet; + } + + nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType); + effectSet = new EffectSet(); + + nsresult rv = aElement->SetProperty(propName, effectSet, + &EffectSet::PropertyDtor, true); + if (NS_FAILED(rv)) { + NS_WARNING("SetProperty failed"); + // The set must be destroyed via PropertyDtor, otherwise + // mCalledPropertyDtor assertion is triggered in destructor. + EffectSet::PropertyDtor(aElement, propName, effectSet, nullptr); + return nullptr; + } + + aElement->SetMayHaveAnimations(); + + return effectSet; +} + +/* static */ void +EffectSet::DestroyEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType) +{ + nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType); + EffectSet* effectSet = + static_cast(aElement->GetProperty(propName)); + if (!effectSet) { + return; + } + + MOZ_ASSERT(!effectSet->IsBeingEnumerated(), + "Should not destroy an effect set while it is being enumerated"); + effectSet = nullptr; + + aElement->DeleteProperty(propName); +} + +void +EffectSet::UpdateAnimationGeneration(nsPresContext* aPresContext) +{ + MOZ_ASSERT(aPresContext->RestyleManager()->IsGecko(), + "stylo: Servo-backed style system should not be using " + "EffectSet"); + mAnimationGeneration = + aPresContext->RestyleManager()->AsGecko()->GetAnimationGeneration(); +} + +/* static */ nsIAtom** +EffectSet::GetEffectSetPropertyAtoms() +{ + static nsIAtom* effectSetPropertyAtoms[] = + { + nsGkAtoms::animationEffectsProperty, + nsGkAtoms::animationEffectsForBeforeProperty, + nsGkAtoms::animationEffectsForAfterProperty, + nullptr + }; + + return effectSetPropertyAtoms; +} + +/* static */ nsIAtom* +EffectSet::GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType) +{ + switch (aPseudoType) { + case CSSPseudoElementType::NotPseudo: + return nsGkAtoms::animationEffectsProperty; + + case CSSPseudoElementType::before: + return nsGkAtoms::animationEffectsForBeforeProperty; + + case CSSPseudoElementType::after: + return nsGkAtoms::animationEffectsForAfterProperty; + + default: + NS_NOTREACHED("Should not try to get animation effects for a pseudo " + "other that :before or :after"); + return nullptr; + } +} + +void +EffectSet::AddEffect(dom::KeyframeEffectReadOnly& aEffect) +{ + if (mEffects.Contains(&aEffect)) { + return; + } + + mEffects.PutEntry(&aEffect); + MarkCascadeNeedsUpdate(); +} + +void +EffectSet::RemoveEffect(dom::KeyframeEffectReadOnly& aEffect) +{ + if (!mEffects.Contains(&aEffect)) { + return; + } + + mEffects.RemoveEntry(&aEffect); + MarkCascadeNeedsUpdate(); +} + +} // namespace mozilla diff --git a/dom/animation/EffectSet.h b/dom/animation/EffectSet.h new file mode 100644 index 000000000..9ba31ef91 --- /dev/null +++ b/dom/animation/EffectSet.h @@ -0,0 +1,261 @@ +/* -*- 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_EffectSet_h +#define mozilla_EffectSet_h + +#include "mozilla/AnimValuesStyleRule.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EffectCompositor.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/KeyframeEffectReadOnly.h" +#include "nsHashKeys.h" // For nsPtrHashKey +#include "nsTHashtable.h" // For nsTHashtable + +class nsPresContext; + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +enum class CSSPseudoElementType : uint8_t; + +// A wrapper around a hashset of AnimationEffect objects to handle +// storing the set as a property of an element. +class EffectSet +{ +public: + EffectSet() + : mCascadeNeedsUpdate(false) + , mAnimationGeneration(0) +#ifdef DEBUG + , mActiveIterators(0) + , mCalledPropertyDtor(false) +#endif + { + MOZ_COUNT_CTOR(EffectSet); + } + + ~EffectSet() + { + MOZ_ASSERT(mCalledPropertyDtor, + "must call destructor through element property dtor"); + MOZ_ASSERT(mActiveIterators == 0, + "Effect set should not be destroyed while it is being " + "enumerated"); + MOZ_COUNT_DTOR(EffectSet); + } + static void PropertyDtor(void* aObject, nsIAtom* aPropertyName, + void* aPropertyValue, void* aData); + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback& aCallback); + + static EffectSet* GetEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType); + static EffectSet* GetEffectSet(const nsIFrame* aFrame); + static EffectSet* GetOrCreateEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType); + static void DestroyEffectSet(dom::Element* aElement, + CSSPseudoElementType aPseudoType); + + void AddEffect(dom::KeyframeEffectReadOnly& aEffect); + void RemoveEffect(dom::KeyframeEffectReadOnly& aEffect); + +private: + typedef nsTHashtable> + OwningEffectSet; + +public: + // A simple iterator to support iterating over the effects in this object in + // range-based for loops. + // + // This allows us to avoid exposing mEffects directly and saves the + // caller from having to dereference hashtable iterators using + // the rather complicated: iter.Get()->GetKey(). + class Iterator + { + public: + explicit Iterator(EffectSet& aEffectSet) + : mEffectSet(aEffectSet) + , mHashIterator(mozilla::Move(aEffectSet.mEffects.Iter())) + , mIsEndIterator(false) + { +#ifdef DEBUG + mEffectSet.mActiveIterators++; +#endif + } + + Iterator(Iterator&& aOther) + : mEffectSet(aOther.mEffectSet) + , mHashIterator(mozilla::Move(aOther.mHashIterator)) + , mIsEndIterator(aOther.mIsEndIterator) + { +#ifdef DEBUG + mEffectSet.mActiveIterators++; +#endif + } + + static Iterator EndIterator(EffectSet& aEffectSet) + { + Iterator result(aEffectSet); + result.mIsEndIterator = true; + return result; + } + + ~Iterator() + { +#ifdef DEBUG + MOZ_ASSERT(mEffectSet.mActiveIterators > 0); + mEffectSet.mActiveIterators--; +#endif + } + + bool operator!=(const Iterator& aOther) const { + if (Done() || aOther.Done()) { + return Done() != aOther.Done(); + } + return mHashIterator.Get() != aOther.mHashIterator.Get(); + } + + Iterator& operator++() { + MOZ_ASSERT(!Done()); + mHashIterator.Next(); + return *this; + } + + dom::KeyframeEffectReadOnly* operator* () + { + MOZ_ASSERT(!Done()); + return mHashIterator.Get()->GetKey(); + } + + private: + Iterator() = delete; + Iterator(const Iterator&) = delete; + Iterator& operator=(const Iterator&) = delete; + Iterator& operator=(const Iterator&&) = delete; + + bool Done() const { + return mIsEndIterator || mHashIterator.Done(); + } + + EffectSet& mEffectSet; + OwningEffectSet::Iterator mHashIterator; + bool mIsEndIterator; + }; + + friend class Iterator; + + Iterator begin() { return Iterator(*this); } + Iterator end() { return Iterator::EndIterator(*this); } +#ifdef DEBUG + bool IsBeingEnumerated() const { return mActiveIterators != 0; } +#endif + + bool IsEmpty() const { return mEffects.IsEmpty(); } + + size_t Count() const { return mEffects.Count(); } + + RefPtr& AnimationRule(EffectCompositor::CascadeLevel + aCascadeLevel) + { + return mAnimationRule[aCascadeLevel]; + } + + const TimeStamp& AnimationRuleRefreshTime(EffectCompositor::CascadeLevel + aCascadeLevel) const + { + return mAnimationRuleRefreshTime[aCascadeLevel]; + } + void UpdateAnimationRuleRefreshTime(EffectCompositor::CascadeLevel + aCascadeLevel, + const TimeStamp& aRefreshTime) + { + mAnimationRuleRefreshTime[aCascadeLevel] = aRefreshTime; + } + + bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; } + void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; } + void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; } + + void UpdateAnimationGeneration(nsPresContext* aPresContext); + uint64_t GetAnimationGeneration() const { return mAnimationGeneration; } + + static nsIAtom** GetEffectSetPropertyAtoms(); + + nsCSSPropertyIDSet& PropertiesWithImportantRules() + { + return mPropertiesWithImportantRules; + } + nsCSSPropertyIDSet& PropertiesForAnimationsLevel() + { + return mPropertiesForAnimationsLevel; + } + +private: + static nsIAtom* GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType); + + OwningEffectSet mEffects; + + // These style rules contain the style data for currently animating + // values. They only match when styling with animation. When we + // style without animation, we need to not use them so that we can + // detect any new changes; if necessary we restyle immediately + // afterwards with animation. + EnumeratedArray> mAnimationRule; + + // A parallel array to mAnimationRule that records the refresh driver + // timestamp when the rule was last updated. This is used for certain + // animations which are updated only periodically (e.g. transform animations + // running on the compositor that affect the scrollable overflow region). + EnumeratedArray mAnimationRuleRefreshTime; + + // Dirty flag to represent when the mPropertiesWithImportantRules and + // mPropertiesForAnimationsLevel on effects in this set might need to be + // updated. + // + // Set to true any time the set of effects is changed or when + // one the effects goes in or out of the "in effect" state. + bool mCascadeNeedsUpdate; + + // RestyleManager keeps track of the number of animation restyles. + // 'mini-flushes' (see nsTransitionManager::UpdateAllThrottledStyles()). + // mAnimationGeneration is the sequence number of the last flush where a + // transition/animation changed. We keep a similar count on the + // corresponding layer so we can check that the layer is up to date with + // the animation manager. + uint64_t mAnimationGeneration; + + // Specifies the compositor-animatable properties that are overridden by + // !important rules. + nsCSSPropertyIDSet mPropertiesWithImportantRules; + // Specifies the properties for which the result will be added to the + // animations level of the cascade and hence should be skipped when we are + // composing the animation style for the transitions level of the cascede. + nsCSSPropertyIDSet mPropertiesForAnimationsLevel; + +#ifdef DEBUG + // Track how many iterators are referencing this effect set when we are + // destroyed, we can assert that nothing is still pointing to us. + uint64_t mActiveIterators; + + bool mCalledPropertyDtor; +#endif +}; + +} // namespace mozilla + +#endif // mozilla_EffectSet_h diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp new file mode 100644 index 000000000..decbf6305 --- /dev/null +++ b/dom/animation/KeyframeEffect.cpp @@ -0,0 +1,211 @@ +/* -*- 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/. */ + +#include "mozilla/dom/KeyframeEffect.h" + +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" + // For UnrestrictedDoubleOrKeyframeAnimationOptions +#include "mozilla/dom/AnimationEffectTiming.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/KeyframeUtils.h" +#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch +#include "nsIScriptError.h" + +namespace mozilla { +namespace dom { + +KeyframeEffect::KeyframeEffect(nsIDocument* aDocument, + const Maybe& aTarget, + const TimingParams& aTiming, + const KeyframeEffectParams& aOptions) + : KeyframeEffectReadOnly(aDocument, aTarget, + new AnimationEffectTiming(aDocument, aTiming, this), + aOptions) +{ +} + +JSObject* +KeyframeEffect::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) +{ + return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto); +} + +/* static */ already_AddRefed +KeyframeEffect::Constructor( + const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, + aOptions, aRv); +} + +/* static */ already_AddRefed +KeyframeEffect::Constructor(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect(aGlobal, aSource, aRv); +} + +/* static */ already_AddRefed +KeyframeEffect::Constructor( + const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, + aOptions, aRv); +} + +void +KeyframeEffect::NotifySpecifiedTimingUpdated() +{ + // Use the same document for a pseudo element and its parent element. + // Use nullptr if we don't have mTarget, so disable the mutation batch. + nsAutoAnimationMutationBatch mb(mTarget ? mTarget->mElement->OwnerDoc() + : nullptr); + + if (mAnimation) { + mAnimation->NotifyEffectTimingUpdated(); + + if (mAnimation->IsRelevant()) { + nsNodeUtils::AnimationChanged(mAnimation); + } + + RequestRestyle(EffectCompositor::RestyleType::Layer); + } +} + +void +KeyframeEffect::SetTarget(const Nullable& aTarget) +{ + Maybe newTarget = ConvertTarget(aTarget); + if (mTarget == newTarget) { + // Assign the same target, skip it. + return; + } + + if (mTarget) { + UnregisterTarget(); + ResetIsRunningOnCompositor(); + + RequestRestyle(EffectCompositor::RestyleType::Layer); + + nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc()); + if (mAnimation) { + nsNodeUtils::AnimationRemoved(mAnimation); + } + } + + mTarget = newTarget; + + if (mTarget) { + UpdateTargetRegistration(); + RefPtr styleContext = GetTargetStyleContext(); + if (styleContext) { + UpdateProperties(styleContext); + } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) { + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); + } + + MaybeUpdateFrameForCompositor(); + + RequestRestyle(EffectCompositor::RestyleType::Layer); + + nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc()); + if (mAnimation) { + nsNodeUtils::AnimationAdded(mAnimation); + } + } else if (mEffectOptions.mSpacingMode == SpacingMode::paced) { + // New target is null, so fall back to distribute spacing. + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); + } +} + +void +KeyframeEffect::SetIterationComposite( + const IterationCompositeOperation& aIterationComposite) +{ + // Ignore iterationComposite if the Web Animations API is not enabled, + // then the default value 'Replace' will be used. + if (!AnimationUtils::IsCoreAPIEnabledForCaller()) { + return; + } + + if (mEffectOptions.mIterationComposite == aIterationComposite) { + return; + } + + if (mAnimation && mAnimation->IsRelevant()) { + nsNodeUtils::AnimationChanged(mAnimation); + } + + mEffectOptions.mIterationComposite = aIterationComposite; + RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +void +KeyframeEffect::SetSpacing(JSContext* aCx, + const nsAString& aSpacing, + ErrorResult& aRv) +{ + SpacingMode spacingMode = SpacingMode::distribute; + nsCSSPropertyID pacedProperty = eCSSProperty_UNKNOWN; + nsAutoString invalidPacedProperty; + KeyframeEffectParams::ParseSpacing(aSpacing, + spacingMode, + pacedProperty, + invalidPacedProperty, + aRv); + if (aRv.Failed()) { + return; + } + + if (!invalidPacedProperty.IsEmpty()) { + const char16_t* params[] = { invalidPacedProperty.get() }; + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Animation"), + doc, + nsContentUtils::eDOM_PROPERTIES, + "UnanimatablePacedProperty", + params, ArrayLength(params)); + } + + if (mEffectOptions.mSpacingMode == spacingMode && + mEffectOptions.mPacedProperty == pacedProperty) { + return; + } + + mEffectOptions.mSpacingMode = spacingMode; + mEffectOptions.mPacedProperty = pacedProperty; + + // Apply spacing. We apply distribute here. If the new spacing is paced, + // UpdateProperties() will apply it. + if (mEffectOptions.mSpacingMode == SpacingMode::distribute) { + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); + } + + if (mAnimation && mAnimation->IsRelevant()) { + nsNodeUtils::AnimationChanged(mAnimation); + } + + if (mTarget) { + RefPtr styleContext = GetTargetStyleContext(); + if (styleContext) { + UpdateProperties(styleContext); + } + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/KeyframeEffect.h b/dom/animation/KeyframeEffect.h new file mode 100644 index 000000000..3c704a820 --- /dev/null +++ b/dom/animation/KeyframeEffect.h @@ -0,0 +1,82 @@ +/* -*- 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_dom_KeyframeEffect_h +#define mozilla_dom_KeyframeEffect_h + +#include "nsWrapperCache.h" +#include "mozilla/dom/KeyframeEffectReadOnly.h" +#include "mozilla/AnimationTarget.h" // For (Non)OwningAnimationTarget +#include "mozilla/Maybe.h" + +struct JSContext; +class JSObject; +class nsIDocument; + +namespace mozilla { + +class ErrorResult; +struct KeyframeEffectParams; +struct TimingParams; + +namespace dom { + +class ElementOrCSSPseudoElement; +class GlobalObject; +class UnrestrictedDoubleOrKeyframeAnimationOptions; +class UnrestrictedDoubleOrKeyframeEffectOptions; + +class KeyframeEffect : public KeyframeEffectReadOnly +{ +public: + KeyframeEffect(nsIDocument* aDocument, + const Maybe& aTarget, + const TimingParams& aTiming, + const KeyframeEffectParams& aOptions); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv); + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv); + + // Variant of Constructor that accepts a KeyframeAnimationOptions object + // for use with for Animatable.animate. + // Not exposed to content. + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv); + + void NotifySpecifiedTimingUpdated(); + + // This method calls GetTargetStyleContext which is not safe to use when + // we are in the middle of updating style. If we need to use this when + // updating style, we should pass the nsStyleContext into this method and use + // that to update the properties rather than calling + // GetStyleContextForElement. + void SetTarget(const Nullable& aTarget); + + void SetSpacing(JSContext* aCx, const nsAString& aSpacing, ErrorResult& aRv); + void SetIterationComposite( + const IterationCompositeOperation& aIterationComposite); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_KeyframeEffect_h diff --git a/dom/animation/KeyframeEffectParams.cpp b/dom/animation/KeyframeEffectParams.cpp new file mode 100644 index 000000000..257640691 --- /dev/null +++ b/dom/animation/KeyframeEffectParams.cpp @@ -0,0 +1,169 @@ +/* -*- 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/. */ + +#include "mozilla/KeyframeEffectParams.h" + +#include "mozilla/AnimationUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/KeyframeUtils.h" +#include "mozilla/RangedPtr.h" +#include "nsReadableUtils.h" + +namespace mozilla { + +static inline bool +IsLetter(char16_t aCh) +{ + return (0x41 <= aCh && aCh <= 0x5A) || (0x61 <= aCh && aCh <= 0x7A); +} + +static inline bool +IsDigit(char16_t aCh) +{ + return 0x30 <= aCh && aCh <= 0x39; +} + +static inline bool +IsNameStartCode(char16_t aCh) +{ + return IsLetter(aCh) || aCh >= 0x80 || aCh == '_'; +} + +static inline bool +IsNameCode(char16_t aCh) +{ + return IsNameStartCode(aCh) || IsDigit(aCh) || aCh == '-'; +} + +static inline bool +IsNewLine(char16_t aCh) +{ + // 0x0A (LF), 0x0C (FF), 0x0D (CR), or pairs of CR followed by LF are + // replaced by LF. + return aCh == 0x0A || aCh == 0x0C || aCh == 0x0D; +} + +static inline bool +IsValidEscape(char16_t aFirst, char16_t aSecond) +{ + return aFirst == '\\' && !IsNewLine(aSecond); +} + +static bool +IsIdentStart(RangedPtr aIter, + const char16_t* const aEnd) +{ + if (aIter == aEnd) { + return false; + } + + if (*aIter == '-') { + if (aIter + 1 == aEnd) { + return false; + } + char16_t second = *(aIter + 1); + return IsNameStartCode(second) || + second == '-' || + (aIter + 2 != aEnd && IsValidEscape(second, *(aIter + 2))); + } + return IsNameStartCode(*aIter) || + (aIter + 1 != aEnd && IsValidEscape(*aIter, *(aIter + 1))); +} + +static void +ConsumeIdentToken(RangedPtr& aIter, + const char16_t* const aEnd, + nsAString& aResult) +{ + aResult.Truncate(); + + // Check if it starts with an identifier. + if (!IsIdentStart(aIter, aEnd)) { + return; + } + + // Start to consume. + while (aIter != aEnd) { + if (IsNameCode(*aIter)) { + aResult.Append(*aIter); + } else if (*aIter == '\\') { + const RangedPtr secondChar = aIter + 1; + if (secondChar == aEnd || !IsValidEscape(*aIter, *secondChar)) { + break; + } + // Consume '\\' and append the character following this '\\'. + ++aIter; + aResult.Append(*aIter); + } else { + break; + } + ++aIter; + } +} + +/* static */ void +KeyframeEffectParams::ParseSpacing(const nsAString& aSpacing, + SpacingMode& aSpacingMode, + nsCSSPropertyID& aPacedProperty, + nsAString& aInvalidPacedProperty, + ErrorResult& aRv) +{ + aInvalidPacedProperty.Truncate(); + + // Ignore spacing if the core API is not enabled since it is not yet ready to + // ship. + if (!AnimationUtils::IsCoreAPIEnabledForCaller()) { + aSpacingMode = SpacingMode::distribute; + return; + } + + // Parse spacing. + // distribute | paced({ident}) + // https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing + // 1. distribute spacing. + if (aSpacing.EqualsLiteral("distribute")) { + aSpacingMode = SpacingMode::distribute; + return; + } + + // 2. paced spacing. + static const nsLiteralString kPacedPrefix = NS_LITERAL_STRING("paced("); + if (!StringBeginsWith(aSpacing, kPacedPrefix)) { + aRv.ThrowTypeError(aSpacing); + return; + } + + RangedPtr iter(aSpacing.Data() + kPacedPrefix.Length(), + aSpacing.Data(), aSpacing.Length()); + const char16_t* const end = aSpacing.EndReading(); + + nsAutoString identToken; + ConsumeIdentToken(iter, end, identToken); + if (identToken.IsEmpty()) { + aRv.ThrowTypeError(aSpacing); + return; + } + + aPacedProperty = + nsCSSProps::LookupProperty(identToken, CSSEnabledState::eForAllContent); + if (aPacedProperty == eCSSProperty_UNKNOWN || + aPacedProperty == eCSSPropertyExtra_variable || + !KeyframeUtils::IsAnimatableProperty(aPacedProperty)) { + aPacedProperty = eCSSProperty_UNKNOWN; + aInvalidPacedProperty = identToken; + } + + if (end - iter.get() != 1 || *iter != ')') { + aRv.ThrowTypeError(aSpacing); + return; + } + + aSpacingMode = aPacedProperty == eCSSProperty_UNKNOWN + ? SpacingMode::distribute + : SpacingMode::paced; +} + +} // namespace mozilla diff --git a/dom/animation/KeyframeEffectParams.h b/dom/animation/KeyframeEffectParams.h new file mode 100644 index 000000000..92a284eae --- /dev/null +++ b/dom/animation/KeyframeEffectParams.h @@ -0,0 +1,68 @@ +/* -*- 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_KeyframeEffectParams_h +#define mozilla_KeyframeEffectParams_h + +#include "nsCSSProps.h" +#include "nsString.h" +// X11 has a #define for None +#ifdef None +#undef None +#endif +#include "mozilla/dom/KeyframeEffectBinding.h" // IterationCompositeOperation + +namespace mozilla { + +class ErrorResult; + +enum class SpacingMode +{ + distribute, + paced +}; + +struct KeyframeEffectParams +{ + void GetSpacingAsString(nsAString& aSpacing) const + { + if (mSpacingMode == SpacingMode::distribute) { + aSpacing.AssignLiteral("distribute"); + } else { + aSpacing.AssignLiteral("paced("); + aSpacing.AppendASCII(nsCSSProps::GetStringValue(mPacedProperty).get()); + aSpacing.AppendLiteral(")"); + } + } + + /** + * Parse spacing string. + * + * @param aSpacing The input spacing string. + * @param [out] aSpacingMode The parsed spacing mode. + * @param [out] aPacedProperty The parsed CSS property if using paced spacing. + * @param [out] aInvalidPacedProperty A string that, if we parsed a string of + * the form 'paced()' where + * is not a recognized animatable property, + * will be set to . + * @param [out] aRv The error result. + */ + static void ParseSpacing(const nsAString& aSpacing, + SpacingMode& aSpacingMode, + nsCSSPropertyID& aPacedProperty, + nsAString& aInvalidPacedProperty, + ErrorResult& aRv); + + dom::IterationCompositeOperation mIterationComposite = + dom::IterationCompositeOperation::Replace; + // FIXME: Bug 1216844: Add CompositeOperation + SpacingMode mSpacingMode = SpacingMode::distribute; + nsCSSPropertyID mPacedProperty = eCSSProperty_UNKNOWN; +}; + +} // namespace mozilla + +#endif // mozilla_KeyframeEffectParams_h diff --git a/dom/animation/KeyframeEffectReadOnly.cpp b/dom/animation/KeyframeEffectReadOnly.cpp new file mode 100644 index 000000000..639e0b2b0 --- /dev/null +++ b/dom/animation/KeyframeEffectReadOnly.cpp @@ -0,0 +1,1430 @@ +/* -*- 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/. */ + +#include "mozilla/dom/KeyframeEffectReadOnly.h" + +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" + // For UnrestrictedDoubleOrKeyframeAnimationOptions; +#include "mozilla/dom/CSSPseudoElement.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/EffectSet.h" +#include "mozilla/FloatingPoint.h" // For IsFinite +#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt +#include "mozilla/KeyframeUtils.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "Layers.h" // For Layer +#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement +#include "nsContentUtils.h" // nsContentUtils::ReportToConsole +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType +#include "nsIPresShell.h" +#include "nsIScriptError.h" + +namespace mozilla { + +bool +PropertyValuePair::operator==(const PropertyValuePair& aOther) const +{ + if (mProperty != aOther.mProperty || mValue != aOther.mValue) { + return false; + } + if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) { + return true; + } + if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) { + return false; + } + return Servo_DeclarationBlock_Equals(mServoDeclarationBlock, + aOther.mServoDeclarationBlock); +} + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly, + AnimationEffectReadOnly, + mTarget) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly, + AnimationEffectReadOnly) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly) +NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly) + +NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly) +NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly) + +KeyframeEffectReadOnly::KeyframeEffectReadOnly( + nsIDocument* aDocument, + const Maybe& aTarget, + const TimingParams& aTiming, + const KeyframeEffectParams& aOptions) + : KeyframeEffectReadOnly(aDocument, aTarget, + new AnimationEffectTimingReadOnly(aDocument, + aTiming), + aOptions) +{ +} + +KeyframeEffectReadOnly::KeyframeEffectReadOnly( + nsIDocument* aDocument, + const Maybe& aTarget, + AnimationEffectTimingReadOnly* aTiming, + const KeyframeEffectParams& aOptions) + : AnimationEffectReadOnly(aDocument, aTiming) + , mTarget(aTarget) + , mEffectOptions(aOptions) + , mInEffectOnLastAnimationTimingUpdate(false) + , mCumulativeChangeHint(nsChangeHint(0)) +{ +} + +JSObject* +KeyframeEffectReadOnly::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) +{ + return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto); +} + +IterationCompositeOperation +KeyframeEffectReadOnly::IterationComposite() const +{ + return mEffectOptions.mIterationComposite; +} + +CompositeOperation +KeyframeEffectReadOnly::Composite() const +{ + return CompositeOperation::Replace; +} + +void +KeyframeEffectReadOnly::NotifyAnimationTimingUpdated() +{ + UpdateTargetRegistration(); + + // If the effect is not relevant it will be removed from the target + // element's effect set. However, effects not in the effect set + // will not be included in the set of candidate effects for running on + // the compositor and hence they won't have their compositor status + // updated. As a result, we need to make sure we clear their compositor + // status here. + bool isRelevant = mAnimation && mAnimation->IsRelevant(); + if (!isRelevant) { + ResetIsRunningOnCompositor(); + } + + // Detect changes to "in effect" status since we need to recalculate the + // animation cascade for this element whenever that changes. + bool inEffect = IsInEffect(); + if (inEffect != mInEffectOnLastAnimationTimingUpdate) { + MarkCascadeNeedsUpdate(); + mInEffectOnLastAnimationTimingUpdate = inEffect; + } + + // Request restyle if necessary. + if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) { + EffectCompositor::RestyleType restyleType = + CanThrottle() ? + EffectCompositor::RestyleType::Throttled : + EffectCompositor::RestyleType::Standard; + RequestRestyle(restyleType); + } + + // If we're no longer "in effect", our ComposeStyle method will never be + // called and we will never have a chance to update mProgressOnLastCompose + // and mCurrentIterationOnLastCompose. + // We clear them here to ensure that if we later become "in effect" we will + // request a restyle (above). + if (!inEffect) { + mProgressOnLastCompose.SetNull(); + mCurrentIterationOnLastCompose = 0; + } +} + +static bool +KeyframesEqualIgnoringComputedOffsets(const nsTArray& aLhs, + const nsTArray& aRhs) +{ + if (aLhs.Length() != aRhs.Length()) { + return false; + } + + for (size_t i = 0, len = aLhs.Length(); i < len; ++i) { + const Keyframe& a = aLhs[i]; + const Keyframe& b = aRhs[i]; + if (a.mOffset != b.mOffset || + a.mTimingFunction != b.mTimingFunction || + a.mPropertyValues != b.mPropertyValues) { + return false; + } + } + return true; +} + +// https://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes +void +KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext, + JS::Handle aKeyframes, + ErrorResult& aRv) +{ + nsTArray keyframes = + KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv); + if (aRv.Failed()) { + return; + } + + RefPtr styleContext = GetTargetStyleContext(); + SetKeyframes(Move(keyframes), styleContext); +} + +void +KeyframeEffectReadOnly::SetKeyframes(nsTArray&& aKeyframes, + nsStyleContext* aStyleContext) +{ + if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) { + return; + } + + mKeyframes = Move(aKeyframes); + // Apply distribute spacing irrespective of the spacing mode. We will apply + // the specified spacing mode when we generate computed animation property + // values from the keyframes since both operations require a style context + // and need to be performed whenever the style context changes. + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); + + if (mAnimation && mAnimation->IsRelevant()) { + nsNodeUtils::AnimationChanged(mAnimation); + } + + if (aStyleContext) { + UpdateProperties(aStyleContext); + MaybeUpdateFrameForCompositor(); + } +} + +const AnimationProperty* +KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty( + nsCSSPropertyID aProperty) const +{ + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + for (size_t propIdx = 0, propEnd = mProperties.Length(); + propIdx != propEnd; ++propIdx) { + if (aProperty == mProperties[propIdx].mProperty) { + const AnimationProperty* result = &mProperties[propIdx]; + // Skip if there is a property of animation level that is overridden + // by !important rules. + if (effectSet && + effectSet->PropertiesWithImportantRules() + .HasProperty(result->mProperty) && + effectSet->PropertiesForAnimationsLevel() + .HasProperty(result->mProperty)) { + result = nullptr; + } + return result; + } + } + return nullptr; +} + +bool +KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const +{ + for (const AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty) { + return true; + } + } + return false; +} + +#ifdef DEBUG +bool +SpecifiedKeyframeArraysAreEqual(const nsTArray& aA, + const nsTArray& aB) +{ + if (aA.Length() != aB.Length()) { + return false; + } + + for (size_t i = 0; i < aA.Length(); i++) { + const Keyframe& a = aA[i]; + const Keyframe& b = aB[i]; + if (a.mOffset != b.mOffset || + a.mTimingFunction != b.mTimingFunction || + a.mPropertyValues != b.mPropertyValues) { + return false; + } + } + + return true; +} +#endif + +void +KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aStyleContext); + + nsTArray properties = BuildProperties(aStyleContext); + + if (mProperties == properties) { + return; + } + + // Preserve the state of the mIsRunningOnCompositor flag. + nsCSSPropertyIDSet runningOnCompositorProperties; + + for (const AnimationProperty& property : mProperties) { + if (property.mIsRunningOnCompositor) { + runningOnCompositorProperties.AddProperty(property.mProperty); + } + } + + mProperties = Move(properties); + + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = + runningOnCompositorProperties.HasProperty(property.mProperty); + } + + // FIXME (bug 1303235): Do this for Servo too + if (aStyleContext->PresContext()->StyleSet()->IsGecko()) { + CalculateCumulativeChangeHint(aStyleContext); + } + + MarkCascadeNeedsUpdate(); + + RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +void +KeyframeEffectReadOnly::ComposeStyle( + RefPtr& aStyleRule, + const nsCSSPropertyIDSet& aPropertiesToSkip) +{ + ComputedTiming computedTiming = GetComputedTiming(); + mProgressOnLastCompose = computedTiming.mProgress; + mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration; + + // If the progress is null, we don't have fill data for the current + // time so we shouldn't animate. + if (computedTiming.mProgress.IsNull()) { + return; + } + + for (size_t propIdx = 0, propEnd = mProperties.Length(); + propIdx != propEnd; ++propIdx) + { + const AnimationProperty& prop = mProperties[propIdx]; + + MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key"); + MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0, + "incorrect last to key"); + + if (aPropertiesToSkip.HasProperty(prop.mProperty)) { + continue; + } + + MOZ_ASSERT(prop.mSegments.Length() > 0, + "property should not be in animations if it has no segments"); + + // FIXME: Maybe cache the current segment? + const AnimationPropertySegment *segment = prop.mSegments.Elements(), + *segmentEnd = segment + prop.mSegments.Length(); + while (segment->mToKey <= computedTiming.mProgress.Value()) { + MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys"); + if ((segment+1) == segmentEnd) { + break; + } + ++segment; + MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys"); + } + MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys"); + MOZ_ASSERT(segment >= prop.mSegments.Elements() && + size_t(segment - prop.mSegments.Elements()) < + prop.mSegments.Length(), + "out of array bounds"); + + if (!aStyleRule) { + // Allocate the style rule now that we know we have animation data. + aStyleRule = new AnimValuesStyleRule(); + } + + StyleAnimationValue fromValue = segment->mFromValue; + StyleAnimationValue toValue = segment->mToValue; + // Iteration composition for accumulate + if (mEffectOptions.mIterationComposite == + IterationCompositeOperation::Accumulate && + computedTiming.mCurrentIteration > 0) { + const AnimationPropertySegment& lastSegment = + prop.mSegments.LastElement(); + // FIXME: Bug 1293492: Add a utility function to calculate both of + // below StyleAnimationValues. + DebugOnly accumulateResult = + StyleAnimationValue::Accumulate(prop.mProperty, + fromValue, + lastSegment.mToValue, + computedTiming.mCurrentIteration); + // We can't check the accumulation result in case of filter property. + // That's because some filter property can't accumulate, + // e.g. 'contrast(2) brightness(2)' onto 'brightness(1) contrast(1)' + // because of mismatch of the order. + MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter, + "could not accumulate value"); + accumulateResult = + StyleAnimationValue::Accumulate(prop.mProperty, + toValue, + lastSegment.mToValue, + computedTiming.mCurrentIteration); + MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter, + "could not accumulate value"); + } + + // Special handling for zero-length segments + if (segment->mToKey == segment->mFromKey) { + if (computedTiming.mProgress.Value() < 0) { + aStyleRule->AddValue(prop.mProperty, Move(fromValue)); + } else { + aStyleRule->AddValue(prop.mProperty, Move(toValue)); + } + continue; + } + + double positionInSegment = + (computedTiming.mProgress.Value() - segment->mFromKey) / + (segment->mToKey - segment->mFromKey); + double valuePosition = + ComputedTimingFunction::GetPortion(segment->mTimingFunction, + positionInSegment, + computedTiming.mBeforeFlag); + + MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite"); + StyleAnimationValue val; + if (StyleAnimationValue::Interpolate(prop.mProperty, + fromValue, + toValue, + valuePosition, val)) { + aStyleRule->AddValue(prop.mProperty, Move(val)); + } else if (valuePosition < 0.5) { + aStyleRule->AddValue(prop.mProperty, Move(fromValue)); + } else { + aStyleRule->AddValue(prop.mProperty, Move(toValue)); + } + } +} + +bool +KeyframeEffectReadOnly::IsRunningOnCompositor() const +{ + // We consider animation is running on compositor if there is at least + // one property running on compositor. + // Animation.IsRunningOnCompotitor will return more fine grained + // information in bug 1196114. + for (const AnimationProperty& property : mProperties) { + if (property.mIsRunningOnCompositor) { + return true; + } + } + return false; +} + +void +KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID aProperty, + bool aIsRunning) +{ + MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty, + CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR), + "Property being animated on compositor is a recognized " + "compositor-animatable property"); + for (AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty) { + property.mIsRunningOnCompositor = aIsRunning; + // We currently only set a performance warning message when animations + // cannot be run on the compositor, so if this animation is running + // on the compositor we don't need a message. + if (aIsRunning) { + property.mPerformanceWarning.reset(); + } + return; + } + } +} + +void +KeyframeEffectReadOnly::ResetIsRunningOnCompositor() +{ + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = false; + } +} + +static const KeyframeEffectOptions& +KeyframeEffectOptionsFromUnion( + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeEffectOptions()); + return aOptions.GetAsKeyframeEffectOptions(); +} + +static const KeyframeEffectOptions& +KeyframeEffectOptionsFromUnion( + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions()); + return aOptions.GetAsKeyframeAnimationOptions(); +} + +template +static KeyframeEffectParams +KeyframeEffectParamsFromUnion(const OptionsType& aOptions, + nsAString& aInvalidPacedProperty, + ErrorResult& aRv) +{ + KeyframeEffectParams result; + if (!aOptions.IsUnrestrictedDouble()) { + const KeyframeEffectOptions& options = + KeyframeEffectOptionsFromUnion(aOptions); + KeyframeEffectParams::ParseSpacing(options.mSpacing, + result.mSpacingMode, + result.mPacedProperty, + aInvalidPacedProperty, + aRv); + // Ignore iterationComposite if the Web Animations API is not enabled, + // then the default value 'Replace' will be used. + if (AnimationUtils::IsCoreAPIEnabledForCaller()) { + result.mIterationComposite = options.mIterationComposite; + } + } + return result; +} + +/* static */ Maybe +KeyframeEffectReadOnly::ConvertTarget( + const Nullable& aTarget) +{ + // Return value optimization. + Maybe result; + + if (aTarget.IsNull()) { + return result; + } + + const ElementOrCSSPseudoElement& target = aTarget.Value(); + MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(), + "Uninitialized target"); + + if (target.IsElement()) { + result.emplace(&target.GetAsElement()); + } else { + RefPtr elem = target.GetAsCSSPseudoElement().ParentElement(); + result.emplace(elem, target.GetAsCSSPseudoElement().GetType()); + } + return result; +} + +template +/* static */ already_AddRefed +KeyframeEffectReadOnly::ConstructKeyframeEffect( + const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const OptionsType& aOptions, + ErrorResult& aRv) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + TimingParams timingParams = + TimingParams::FromOptionsUnion(aOptions, doc, aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsAutoString invalidPacedProperty; + KeyframeEffectParams effectOptions = + KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!invalidPacedProperty.IsEmpty()) { + const char16_t* params[] = { invalidPacedProperty.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Animation"), + doc, + nsContentUtils::eDOM_PROPERTIES, + "UnanimatablePacedProperty", + params, ArrayLength(params)); + } + + Maybe target = ConvertTarget(aTarget); + RefPtr effect = + new KeyframeEffectType(doc, target, timingParams, effectOptions); + + effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return effect.forget(); +} + +template +/* static */ already_AddRefed +KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv) +{ + nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Create a new KeyframeEffectReadOnly object with aSource's target, + // iteration composite operation, composite operation, and spacing mode. + // The constructor creates a new AnimationEffect(ReadOnly) object by + // aSource's TimingParams. + // Note: we don't need to re-throw exceptions since the value specified on + // aSource's timing object can be assumed valid. + RefPtr effect = + new KeyframeEffectType(doc, + aSource.mTarget, + aSource.SpecifiedTiming(), + aSource.mEffectOptions); + // Copy cumulative change hint. mCumulativeChangeHint should be the same as + // the source one because both of targets are the same. + effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint; + + // Copy aSource's keyframes and animation properties. + // Note: We don't call SetKeyframes directly, which might revise the + // computed offsets and rebuild the animation properties. + // FIXME: Bug 1314537: We have to make sure SharedKeyframeList is handled + // properly. + effect->mKeyframes = aSource.mKeyframes; + effect->mProperties = aSource.mProperties; + return effect.forget(); +} + +nsTArray +KeyframeEffectReadOnly::BuildProperties(nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aStyleContext); + + nsTArray result; + // If mTarget is null, return an empty property array. + if (!mTarget) { + return result; + } + + // When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes + // calculate computed values from |mKeyframes|, they could possibly + // trigger a subsequent restyle in which we rebuild animations. If that + // happens we could find that |mKeyframes| is overwritten while it is + // being iterated over. Normally that shouldn't happen but just in case we + // make a copy of |mKeyframes| first and iterate over that instead. + auto keyframesCopy(mKeyframes); + + nsTArray computedValues = + KeyframeUtils::GetComputedKeyframeValues(keyframesCopy, + mTarget->mElement, + aStyleContext); + + if (mEffectOptions.mSpacingMode == SpacingMode::paced) { + KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced, + mEffectOptions.mPacedProperty, + computedValues, aStyleContext); + } + + result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy, + computedValues, + aStyleContext); + +#ifdef DEBUG + MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy), + "Apart from the computed offset members, the keyframes array" + " should not be modified"); +#endif + + mKeyframes.SwapElements(keyframesCopy); + return result; +} + +void +KeyframeEffectReadOnly::UpdateTargetRegistration() +{ + if (!mTarget) { + return; + } + + bool isRelevant = mAnimation && mAnimation->IsRelevant(); + + // Animation::IsRelevant() returns a cached value. It only updates when + // something calls Animation::UpdateRelevance. Whenever our timing changes, + // we should be notifying our Animation before calling this, so + // Animation::IsRelevant() should be up-to-date by the time we get here. + MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(), + "Out of date Animation::IsRelevant value"); + + if (isRelevant) { + EffectSet* effectSet = + EffectSet::GetOrCreateEffectSet(mTarget->mElement, mTarget->mPseudoType); + effectSet->AddEffect(*this); + } else { + UnregisterTarget(); + } +} + +void +KeyframeEffectReadOnly::UnregisterTarget() +{ + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + if (effectSet) { + effectSet->RemoveEffect(*this); + if (effectSet->IsEmpty()) { + EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType); + } + } +} + +void +KeyframeEffectReadOnly::RequestRestyle( + EffectCompositor::RestyleType aRestyleType) +{ + nsPresContext* presContext = GetPresContext(); + if (presContext && mTarget && mAnimation) { + presContext->EffectCompositor()-> + RequestRestyle(mTarget->mElement, mTarget->mPseudoType, + aRestyleType, mAnimation->CascadeLevel()); + } +} + +already_AddRefed +KeyframeEffectReadOnly::GetTargetStyleContext() +{ + nsIPresShell* shell = GetPresShell(); + if (!shell) { + return nullptr; + } + + MOZ_ASSERT(mTarget, + "Should only have a presshell when we have a target element"); + + nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count + ? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType) + : nullptr; + return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement, + pseudo, shell); +} + +#ifdef DEBUG +void +DumpAnimationProperties(nsTArray& aAnimationProperties) +{ + for (auto& p : aAnimationProperties) { + printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get()); + for (auto& s : p.mSegments) { + nsString fromValue, toValue; + Unused << StyleAnimationValue::UncomputeValue(p.mProperty, + s.mFromValue, + fromValue); + Unused << StyleAnimationValue::UncomputeValue(p.mProperty, + s.mToValue, + toValue); + printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey, + NS_ConvertUTF16toUTF8(fromValue).get(), + NS_ConvertUTF16toUTF8(toValue).get()); + } + } +} +#endif + +/* static */ already_AddRefed +KeyframeEffectReadOnly::Constructor( + const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect(aGlobal, aTarget, + aKeyframes, aOptions, + aRv); +} + +/* static */ already_AddRefed +KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv) +{ + return ConstructKeyframeEffect(aGlobal, aSource, aRv); +} + +void +KeyframeEffectReadOnly::GetTarget( + Nullable& aRv) const +{ + if (!mTarget) { + aRv.SetNull(); + return; + } + + switch (mTarget->mPseudoType) { + case CSSPseudoElementType::before: + case CSSPseudoElementType::after: + aRv.SetValue().SetAsCSSPseudoElement() = + CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement, + mTarget->mPseudoType); + break; + + case CSSPseudoElementType::NotPseudo: + aRv.SetValue().SetAsElement() = mTarget->mElement; + break; + + default: + NS_NOTREACHED("Animation of unsupported pseudo-type"); + aRv.SetNull(); + } +} + +static void +CreatePropertyValue(nsCSSPropertyID aProperty, + float aOffset, + const Maybe& aTimingFunction, + const StyleAnimationValue& aValue, + AnimationPropertyValueDetails& aResult) +{ + aResult.mOffset = aOffset; + + nsString stringValue; + DebugOnly uncomputeResult = + StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue); + MOZ_ASSERT(uncomputeResult, "failed to uncompute value"); + aResult.mValue = stringValue; + + if (aTimingFunction) { + aResult.mEasing.Construct(); + aTimingFunction->AppendToString(aResult.mEasing.Value()); + } else { + aResult.mEasing.Construct(NS_LITERAL_STRING("linear")); + } + + aResult.mComposite = CompositeOperation::Replace; +} + +void +KeyframeEffectReadOnly::GetProperties( + nsTArray& aProperties, + ErrorResult& aRv) const +{ + for (const AnimationProperty& property : mProperties) { + AnimationPropertyDetails propertyDetails; + propertyDetails.mProperty = + NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty)); + propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor; + + nsXPIDLString localizedString; + if (property.mPerformanceWarning && + property.mPerformanceWarning->ToLocalizedString(localizedString)) { + propertyDetails.mWarning.Construct(localizedString); + } + + if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(), + mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length(); + segmentIdx < segmentLen; + segmentIdx++) + { + const AnimationPropertySegment& segment = property.mSegments[segmentIdx]; + + binding_detail::FastAnimationPropertyValueDetails fromValue; + CreatePropertyValue(property.mProperty, segment.mFromKey, + segment.mTimingFunction, segment.mFromValue, + fromValue); + // We don't apply timing functions for zero-length segments, so + // don't return one here. + if (segment.mFromKey == segment.mToKey) { + fromValue.mEasing.Reset(); + } + // The following won't fail since we have already allocated the capacity + // above. + propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible); + + // Normally we can ignore the to-value for this segment since it is + // identical to the from-value from the next segment. However, we need + // to add it if either: + // a) this is the last segment, or + // b) the next segment's from-value differs. + if (segmentIdx == segmentLen - 1 || + property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) { + binding_detail::FastAnimationPropertyValueDetails toValue; + CreatePropertyValue(property.mProperty, segment.mToKey, + Nothing(), segment.mToValue, toValue); + // It doesn't really make sense to have a timing function on the + // last property value or before a sudden jump so we just drop the + // easing property altogether. + toValue.mEasing.Reset(); + propertyDetails.mValues.AppendElement(toValue, mozilla::fallible); + } + } + + aProperties.AppendElement(propertyDetails); + } +} + +void +KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx, + nsTArray& aResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aResult.IsEmpty()); + MOZ_ASSERT(!aRv.Failed()); + + if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (const Keyframe& keyframe : mKeyframes) { + // Set up a dictionary object for the explicit members + BaseComputedKeyframe keyframeDict; + if (keyframe.mOffset) { + keyframeDict.mOffset.SetValue(keyframe.mOffset.value()); + } + MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet, + "Invalid computed offset"); + keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset); + if (keyframe.mTimingFunction) { + keyframeDict.mEasing.Truncate(); + keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing); + } // else if null, leave easing as its default "linear". + + JS::Rooted keyframeJSValue(aCx); + if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + JS::Rooted keyframeObject(aCx, &keyframeJSValue.toObject()); + for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) { + + const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty); + + // nsCSSValue::AppendToString does not accept shorthands properties but + // works with token stream values if we pass eCSSProperty_UNKNOWN as + // the property. + nsCSSPropertyID propertyForSerializing = + nsCSSProps::IsShorthand(propertyValue.mProperty) + ? eCSSProperty_UNKNOWN + : propertyValue.mProperty; + + nsAutoString stringValue; + if (propertyValue.mServoDeclarationBlock) { + Servo_DeclarationBlock_SerializeOneValue( + propertyValue.mServoDeclarationBlock, &stringValue); + } else { + propertyValue.mValue.AppendToString( + propertyForSerializing, stringValue, nsCSSValue::eNormalized); + } + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, stringValue, &value) || + !JS_DefineProperty(aCx, keyframeObject, name, value, + JSPROP_ENUMERATE)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + + aResult.AppendElement(keyframeObject); + } +} + +/* static */ const TimeDuration +KeyframeEffectReadOnly::OverflowRegionRefreshInterval() +{ + // The amount of time we can wait between updating throttled animations + // on the main thread that influence the overflow region. + static const TimeDuration kOverflowRegionRefreshInterval = + TimeDuration::FromMilliseconds(200); + + return kOverflowRegionRefreshInterval; +} + +bool +KeyframeEffectReadOnly::CanThrottle() const +{ + // Unthrottle if we are not in effect or current. This will be the case when + // our owning animation has finished, is idle, or when we are in the delay + // phase (but without a backwards fill). In each case the computed progress + // value produced on each tick will be the same so we will skip requesting + // unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get + // here will be because of a change in state (e.g. we are newly finished or + // newly no longer in effect) in which case we shouldn't throttle the sample. + if (!IsInEffect() || !IsCurrent()) { + return false; + } + + nsIFrame* frame = GetAnimationFrame(); + if (!frame) { + // There are two possible cases here. + // a) No target element + // b) The target element has no frame, e.g. because it is in a display:none + // subtree. + // In either case we can throttle the animation because there is no + // need to update on the main thread. + return true; + } + + // We can throttle the animation if the animation is paint only and + // the target frame is out of view or the document is in background tabs. + if (CanIgnoreIfNotVisible()) { + nsIPresShell* presShell = GetPresShell(); + if ((presShell && !presShell->IsActive()) || + frame->IsScrolledOutOfView()) { + return true; + } + } + + // First we need to check layer generation and transform overflow + // prior to the property.mIsRunningOnCompositor check because we should + // occasionally unthrottle these animations even if the animations are + // already running on compositor. + for (const LayerAnimationInfo::Record& record : + LayerAnimationInfo::sRecords) { + // Skip properties that are overridden by !important rules. + // (GetEffectiveAnimationOfProperty, as called by + // HasEffectiveAnimationOfProperty, only returns a property which is + // neither overridden by !important rules nor overridden by other + // animation.) + if (!HasEffectiveAnimationOfProperty(record.mProperty)) { + continue; + } + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect " + "associated with a target element"); + layers::Layer* layer = + FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType); + // Unthrottle if the layer needs to be brought up to date + if (!layer || + effectSet->GetAnimationGeneration() != + layer->GetAnimationGeneration()) { + return false; + } + + // If this is a transform animation that affects the overflow region, + // we should unthrottle the animation periodically. + if (record.mProperty == eCSSProperty_transform && + !CanThrottleTransformChanges(*frame)) { + return false; + } + } + + for (const AnimationProperty& property : mProperties) { + if (!property.mIsRunningOnCompositor) { + return false; + } + } + + return true; +} + +bool +KeyframeEffectReadOnly::CanThrottleTransformChanges(nsIFrame& aFrame) const +{ + // If we know that the animation cannot cause overflow, + // we can just disable flushes for this animation. + + // If we don't show scrollbars, we don't care about overflow. + if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0) { + return true; + } + + nsPresContext* presContext = GetPresContext(); + // CanThrottleTransformChanges is only called as part of a refresh driver tick + // in which case we expect to has a pres context. + MOZ_ASSERT(presContext); + + TimeStamp now = + presContext->RefreshDriver()->MostRecentRefresh(); + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called" + " on an effect in an effect set"); + MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called" + " on an effect with a parent animation"); + TimeStamp animationRuleRefreshTime = + effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel()); + // If this animation can cause overflow, we can throttle some of the ticks. + if (!animationRuleRefreshTime.IsNull() && + (now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) { + return true; + } + + // If the nearest scrollable ancestor has overflow:hidden, + // we don't care about overflow. + nsIScrollableFrame* scrollable = + nsLayoutUtils::GetNearestScrollableFrame(&aFrame); + if (!scrollable) { + return true; + } + + ScrollbarStyles ss = scrollable->GetScrollbarStyles(); + if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN && + ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN && + scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) { + return true; + } + + return false; +} + +nsIFrame* +KeyframeEffectReadOnly::GetAnimationFrame() const +{ + if (!mTarget) { + return nullptr; + } + + nsIFrame* frame = mTarget->mElement->GetPrimaryFrame(); + if (!frame) { + return nullptr; + } + + if (mTarget->mPseudoType == CSSPseudoElementType::before) { + frame = nsLayoutUtils::GetBeforeFrame(frame); + } else if (mTarget->mPseudoType == CSSPseudoElementType::after) { + frame = nsLayoutUtils::GetAfterFrame(frame); + } else { + MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo, + "unknown mTarget->mPseudoType"); + } + if (!frame) { + return nullptr; + } + + return nsLayoutUtils::GetStyleFrame(frame); +} + +nsIDocument* +KeyframeEffectReadOnly::GetRenderedDocument() const +{ + if (!mTarget) { + return nullptr; + } + return mTarget->mElement->GetComposedDoc(); +} + +nsIPresShell* +KeyframeEffectReadOnly::GetPresShell() const +{ + nsIDocument* doc = GetRenderedDocument(); + if (!doc) { + return nullptr; + } + return doc->GetShell(); +} + +nsPresContext* +KeyframeEffectReadOnly::GetPresContext() const +{ + nsIPresShell* shell = GetPresShell(); + if (!shell) { + return nullptr; + } + return shell->GetPresContext(); +} + +/* static */ bool +KeyframeEffectReadOnly::IsGeometricProperty( + const nsCSSPropertyID aProperty) +{ + switch (aProperty) { + case eCSSProperty_bottom: + case eCSSProperty_height: + case eCSSProperty_left: + case eCSSProperty_right: + case eCSSProperty_top: + case eCSSProperty_width: + return true; + default: + return false; + } +} + +/* static */ bool +KeyframeEffectReadOnly::CanAnimateTransformOnCompositor( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) +{ + // Disallow OMTA for preserve-3d transform. Note that we check the style property + // rather than Extend3DContext() since that can recurse back into this function + // via HasOpacity(). See bug 779598. + if (aFrame->Combines3DTransformWithAncestors() || + aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) { + aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D; + return false; + } + // Note that testing BackfaceIsHidden() is not a sufficient test for + // what we need for animating backface-visibility correctly if we + // remove the above test for Extend3DContext(); that would require + // looking at backface-visibility on descendants as well. See bug 1186204. + if (aFrame->BackfaceIsHidden()) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden; + return false; + } + // Async 'transform' animations of aFrames with SVG transforms is not + // supported. See bug 779599. + if (aFrame->IsSVGTransformed()) { + aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG; + return false; + } + + return true; +} + +bool +KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) const +{ + // We currently only expect this method to be called for effects whose + // animations are eligible for the compositor since, Animations that are + // paused, zero-duration, finished etc. should not block other animations from + // running on the compositor. + MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor()); + + EffectSet* effectSet = + EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType); + for (const AnimationProperty& property : mProperties) { + // If there is a property for animations level that is overridden by + // !important rules, it should not block other animations from running + // on the compositor. + // NOTE: We don't currently check for !important rules for properties that + // don't run on the compositor. As result such properties (e.g. margin-left) + // can still block async animations even if they are overridden by + // !important rules. + if (effectSet && + effectSet->PropertiesWithImportantRules() + .HasProperty(property.mProperty) && + effectSet->PropertiesForAnimationsLevel() + .HasProperty(property.mProperty)) { + continue; + } + // Check for geometric properties + if (IsGeometricProperty(property.mProperty)) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformWithGeometricProperties; + return true; + } + + // Check for unsupported transform animations + if (property.mProperty == eCSSProperty_transform) { + if (!CanAnimateTransformOnCompositor(aFrame, + aPerformanceWarning)) { + return true; + } + } + } + + return false; +} + +void +KeyframeEffectReadOnly::SetPerformanceWarning( + nsCSSPropertyID aProperty, + const AnimationPerformanceWarning& aWarning) +{ + for (AnimationProperty& property : mProperties) { + if (property.mProperty == aProperty && + (!property.mPerformanceWarning || + *property.mPerformanceWarning != aWarning)) { + property.mPerformanceWarning = Some(aWarning); + + nsXPIDLString localizedString; + if (nsLayoutUtils::IsAnimationLoggingEnabled() && + property.mPerformanceWarning->ToLocalizedString(localizedString)) { + nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString); + AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement); + } + return; + } + } +} + +static already_AddRefed +CreateStyleContextForAnimationValue(nsCSSPropertyID aProperty, + const StyleAnimationValue& aValue, + nsStyleContext* aBaseStyleContext) +{ + MOZ_ASSERT(aBaseStyleContext, + "CreateStyleContextForAnimationValue needs to be called " + "with a valid nsStyleContext"); + + RefPtr styleRule = new AnimValuesStyleRule(); + styleRule->AddValue(aProperty, aValue); + + nsCOMArray rules; + rules.AppendObject(styleRule); + + MOZ_ASSERT(aBaseStyleContext->PresContext()->StyleSet()->IsGecko(), + "ServoStyleSet should not use StyleAnimationValue for animations"); + nsStyleSet* styleSet = + aBaseStyleContext->PresContext()->StyleSet()->AsGecko(); + + RefPtr styleContext = + styleSet->ResolveStyleByAddingRules(aBaseStyleContext, rules); + + // We need to call StyleData to generate cached data for the style context. + // Otherwise CalcStyleDifference returns no meaningful result. + styleContext->StyleData(nsCSSProps::kSIDTable[aProperty]); + + return styleContext.forget(); +} + +void +KeyframeEffectReadOnly::CalculateCumulativeChangeHint( + nsStyleContext *aStyleContext) +{ + mCumulativeChangeHint = nsChangeHint(0); + + for (const AnimationProperty& property : mProperties) { + for (const AnimationPropertySegment& segment : property.mSegments) { + RefPtr fromContext = + CreateStyleContextForAnimationValue(property.mProperty, + segment.mFromValue, aStyleContext); + + RefPtr toContext = + CreateStyleContextForAnimationValue(property.mProperty, + segment.mToValue, aStyleContext); + + uint32_t equalStructs = 0; + uint32_t samePointerStructs = 0; + nsChangeHint changeHint = + fromContext->CalcStyleDifference(toContext, + nsChangeHint(0), + &equalStructs, + &samePointerStructs); + + mCumulativeChangeHint |= changeHint; + } + } +} + +void +KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation) +{ + if (mAnimation == aAnimation) { + return; + } + + // Restyle for the old animation. + RequestRestyle(EffectCompositor::RestyleType::Layer); + + mAnimation = aAnimation; + + // The order of these function calls is important: + // NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check + // if it should create the effectSet or not, and MarkCascadeNeedsUpdate() + // needs a valid effectSet, so we should call them in this order. + if (mAnimation) { + mAnimation->UpdateRelevance(); + } + NotifyAnimationTimingUpdated(); + if (mAnimation) { + MarkCascadeNeedsUpdate(); + } +} + +bool +KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const +{ + if (!AnimationUtils::IsOffscreenThrottlingEnabled()) { + return false; + } + + // FIXME (bug 1303235): We don't calculate mCumulativeChangeHint for + // the Servo backend yet + nsPresContext* presContext = GetPresContext(); + if (!presContext || presContext->StyleSet()->IsServo()) { + return false; + } + + // FIXME: For further sophisticated optimization we need to check + // change hint on the segment corresponding to computedTiming.progress. + return NS_IsHintSubset( + mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible); +} + +void +KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor() +{ + nsIFrame* frame = GetAnimationFrame(); + if (!frame) { + return; + } + + // FIXME: Bug 1272495: If this effect does not win in the cascade, the + // NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation + // will be removed from effect set or the transform keyframes are removed + // by setKeyframes. The latter case will be hard to solve though. + for (const AnimationProperty& property : mProperties) { + if (property.mProperty == eCSSProperty_transform) { + frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED); + return; + } + } +} + +void +KeyframeEffectReadOnly::MarkCascadeNeedsUpdate() +{ + if (!mTarget) { + return; + } + + EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement, + mTarget->mPseudoType); + if (!effectSet) { + return; + } + effectSet->MarkCascadeNeedsUpdate(); +} + +bool +KeyframeEffectReadOnly::HasComputedTimingChanged() const +{ + // Typically we don't need to request a restyle if the progress hasn't + // changed since the last call to ComposeStyle. The one exception is if the + // iteration composite mode is 'accumulate' and the current iteration has + // changed, since that will often produce a different result. + ComputedTiming computedTiming = GetComputedTiming(); + return computedTiming.mProgress != mProgressOnLastCompose || + (mEffectOptions.mIterationComposite == + IterationCompositeOperation::Accumulate && + computedTiming.mCurrentIteration != + mCurrentIterationOnLastCompose); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/KeyframeEffectReadOnly.h b/dom/animation/KeyframeEffectReadOnly.h new file mode 100644 index 000000000..889159b38 --- /dev/null +++ b/dom/animation/KeyframeEffectReadOnly.h @@ -0,0 +1,439 @@ +/* -*- 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_dom_KeyframeEffectReadOnly_h +#define mozilla_dom_KeyframeEffectReadOnly_h + +#include "nsChangeHint.h" +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" +#include "mozilla/AnimationPerformanceWarning.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/Attributes.h" +#include "mozilla/ComputedTimingFunction.h" +#include "mozilla/EffectCompositor.h" +#include "mozilla/KeyframeEffectParams.h" +#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords +#include "mozilla/ServoBindingTypes.h" // RawServoDeclarationBlock and + // associated RefPtrTraits +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/AnimationEffectReadOnly.h" +#include "mozilla/dom/Element.h" + +struct JSContext; +class JSObject; +class nsCSSPropertyIDSet; +class nsIContent; +class nsIDocument; +class nsIFrame; +class nsIPresShell; +class nsPresContext; + +namespace mozilla { + +class AnimValuesStyleRule; +enum class CSSPseudoElementType : uint8_t; +class ErrorResult; +struct TimingParams; + +namespace dom { +class ElementOrCSSPseudoElement; +class GlobalObject; +class OwningElementOrCSSPseudoElement; +class UnrestrictedDoubleOrKeyframeAnimationOptions; +class UnrestrictedDoubleOrKeyframeEffectOptions; +enum class IterationCompositeOperation : uint32_t; +enum class CompositeOperation : uint32_t; +struct AnimationPropertyDetails; +} + +/** + * A property-value pair specified on a keyframe. + */ +struct PropertyValuePair +{ + nsCSSPropertyID 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; + + // The specified value when using the Servo backend. However, even when + // using the Servo backend, we still fill in |mValue| in the case where we + // fail to parse the value since we use it to store the original string. + RefPtr mServoDeclarationBlock; + + bool operator==(const PropertyValuePair&) const; +}; + +/** + * 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 getKeyframes() + * 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 +{ + Keyframe() = default; + Keyframe(const Keyframe& aOther) = default; + Keyframe(Keyframe&& aOther) + { + *this = Move(aOther); + } + + Keyframe& operator=(const Keyframe& aOther) = default; + Keyframe& operator=(Keyframe&& aOther) + { + mOffset = aOther.mOffset; + mComputedOffset = aOther.mComputedOffset; + mTimingFunction = Move(aOther.mTimingFunction); + mPropertyValues = Move(aOther.mPropertyValues); + return *this; + } + + Maybe mOffset; + static constexpr double kComputedOffsetNotSet = -1.0; + double mComputedOffset = kComputedOffsetNotSet; + Maybe mTimingFunction; // Nothing() here means + // "linear" + nsTArray mPropertyValues; +}; + +struct AnimationPropertySegment +{ + float mFromKey, mToKey; + StyleAnimationValue mFromValue, mToValue; + Maybe mTimingFunction; + + bool operator==(const AnimationPropertySegment& aOther) const + { + return mFromKey == aOther.mFromKey && + mToKey == aOther.mToKey && + mFromValue == aOther.mFromValue && + mToValue == aOther.mToValue && + mTimingFunction == aOther.mTimingFunction; + } + bool operator!=(const AnimationPropertySegment& aOther) const + { + return !(*this == aOther); + } +}; + +struct AnimationProperty +{ + nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN; + + // If true, the propery is currently being animated on the compositor. + // + // Note that when the owning Animation requests a non-throttled restyle, in + // between calling RequestRestyle on its EffectCompositor and when the + // restyle is performed, this member may temporarily become false even if + // the animation remains on the layer after the restyle. + // + // **NOTE**: This member is not included when comparing AnimationProperty + // objects for equality. + bool mIsRunningOnCompositor = false; + + Maybe mPerformanceWarning; + + InfallibleTArray mSegments; + + // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and + // mPerformanceWarning. + AnimationProperty() = default; + AnimationProperty(const AnimationProperty& aOther) + : mProperty(aOther.mProperty), mSegments(aOther.mSegments) { } + AnimationProperty& operator=(const AnimationProperty& aOther) + { + mProperty = aOther.mProperty; + mSegments = aOther.mSegments; + return *this; + } + + // NOTE: This operator does *not* compare the mIsRunningOnCompositor member. + // This is because AnimationProperty objects are compared when recreating + // CSS animations to determine if mutation observer change records need to + // be created or not. However, at the point when these objects are compared + // the mIsRunningOnCompositor will not have been set on the new objects so + // we ignore this member to avoid generating spurious change records. + bool operator==(const AnimationProperty& aOther) const + { + return mProperty == aOther.mProperty && + mSegments == aOther.mSegments; + } + bool operator!=(const AnimationProperty& aOther) const + { + return !(*this == aOther); + } +}; + +struct ElementPropertyTransition; + +namespace dom { + +class Animation; + +class KeyframeEffectReadOnly : public AnimationEffectReadOnly +{ +public: + KeyframeEffectReadOnly(nsIDocument* aDocument, + const Maybe& aTarget, + const TimingParams& aTiming, + const KeyframeEffectParams& aOptions); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffectReadOnly, + AnimationEffectReadOnly) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + KeyframeEffectReadOnly* AsKeyframeEffect() override { return this; } + + // KeyframeEffectReadOnly interface + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv); + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv); + + void GetTarget(Nullable& aRv) const; + Maybe GetTarget() const + { + Maybe result; + if (mTarget) { + result.emplace(*mTarget); + } + return result; + } + void GetKeyframes(JSContext*& aCx, + nsTArray& aResult, + ErrorResult& aRv); + void GetProperties(nsTArray& aProperties, + ErrorResult& aRv) const; + + IterationCompositeOperation IterationComposite() const; + CompositeOperation Composite() const; + void GetSpacing(nsString& aRetVal) const + { + mEffectOptions.GetSpacingAsString(aRetVal); + } + + void NotifyAnimationTimingUpdated(); + + void SetAnimation(Animation* aAnimation) override; + + void SetKeyframes(JSContext* aContext, JS::Handle aKeyframes, + ErrorResult& aRv); + void SetKeyframes(nsTArray&& aKeyframes, + nsStyleContext* aStyleContext); + + // Returns true if the effect includes |aProperty| regardless of whether the + // property is overridden by !important rule. + bool HasAnimationOfProperty(nsCSSPropertyID aProperty) const; + + // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding + // to a given CSS property if the effect includes the property and the + // property is not overridden by !important rules. + // Also EffectiveAnimationOfProperty returns true under the same condition. + // + // NOTE: We don't currently check for !important rules for properties that + // can't run on the compositor. + bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const + { + return GetEffectiveAnimationOfProperty(aProperty) != nullptr; + } + const AnimationProperty* GetEffectiveAnimationOfProperty( + nsCSSPropertyID aProperty) const; + + const InfallibleTArray& Properties() const + { + return mProperties; + } + + // Update |mProperties| by recalculating from |mKeyframes| using + // |aStyleContext| to resolve specified values. + void UpdateProperties(nsStyleContext* aStyleContext); + + // Updates |aStyleRule| with the animation values produced by this + // AnimationEffect for the current time except any properties contained + // in |aPropertiesToSkip|. + void ComposeStyle(RefPtr& aStyleRule, + const nsCSSPropertyIDSet& aPropertiesToSkip); + // Returns true if at least one property is being animated on compositor. + bool IsRunningOnCompositor() const; + void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning); + void ResetIsRunningOnCompositor(); + + // Returns true if this effect, applied to |aFrame|, contains properties + // that mean we shouldn't run transform compositor animations on this element. + // + // For example, if we have an animation of geometric properties like 'left' + // and 'top' on an element, we force all 'transform' animations running at + // the same time on the same element to run on the main thread. + // + // When returning true, |aPerformanceWarning| stores the reason why + // we shouldn't run the transform animations. + bool ShouldBlockAsyncTransformAnimations( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) const; + + nsIDocument* GetRenderedDocument() const; + nsPresContext* GetPresContext() const; + nsIPresShell* GetPresShell() const; + + // Associates a warning with the animated property on the specified frame + // indicating why, for example, the property could not be animated on the + // compositor. |aParams| and |aParamsLength| are optional parameters which + // will be used to generate a localized message for devtools. + void SetPerformanceWarning( + nsCSSPropertyID aProperty, + const AnimationPerformanceWarning& aWarning); + + // Cumulative change hint on each segment for each property. + // This is used for deciding the animation is paint-only. + void CalculateCumulativeChangeHint(nsStyleContext* aStyleContext); + + // Returns true if all of animation properties' change hints + // can ignore painting if the animation is not visible. + // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h + // in detail which change hint can be ignored. + bool CanIgnoreIfNotVisible() const; + +protected: + KeyframeEffectReadOnly(nsIDocument* aDocument, + const Maybe& aTarget, + AnimationEffectTimingReadOnly* aTiming, + const KeyframeEffectParams& aOptions); + + ~KeyframeEffectReadOnly() override = default; + + static Maybe + ConvertTarget(const Nullable& aTarget); + + template + static already_AddRefed + ConstructKeyframeEffect(const GlobalObject& aGlobal, + const Nullable& aTarget, + JS::Handle aKeyframes, + const OptionsType& aOptions, + ErrorResult& aRv); + + template + static already_AddRefed + ConstructKeyframeEffect(const GlobalObject& aGlobal, + KeyframeEffectReadOnly& aSource, + ErrorResult& aRv); + + // Build properties by recalculating from |mKeyframes| using |aStyleContext| + // to resolve specified values. This function also applies paced spacing if + // needed. + nsTArray BuildProperties(nsStyleContext* aStyleContext); + + // This effect is registered with its target element so long as: + // + // (a) It has a target element, and + // (b) It is "relevant" (i.e. yet to finish but not idle, or finished but + // filling forwards) + // + // As a result, we need to make sure this gets called whenever anything + // changes with regards to this effects's timing including changes to the + // owning Animation's timing. + void UpdateTargetRegistration(); + + // Remove the current effect target from its EffectSet. + void UnregisterTarget(); + + void RequestRestyle(EffectCompositor::RestyleType aRestyleType); + + // Update the associated frame state bits so that, if necessary, a stacking + // context will be created and the effect sent to the compositor. We + // typically need to do this when the properties referenced by the keyframe + // have changed, or when the target frame might have changed. + void MaybeUpdateFrameForCompositor(); + + // Looks up the style context associated with the target element, if any. + // We need to be careful to *not* call this when we are updating the style + // context. That's because calling GetStyleContextForElement when we are in + // the process of building a style context may trigger various forms of + // infinite recursion. + already_AddRefed + GetTargetStyleContext(); + + // A wrapper for marking cascade update according to the current + // target and its effectSet. + void MarkCascadeNeedsUpdate(); + + Maybe mTarget; + + KeyframeEffectParams mEffectOptions; + + // The specified keyframes. + nsTArray mKeyframes; + + // A set of per-property value arrays, derived from |mKeyframes|. + nsTArray mProperties; + + // The computed progress last time we composed the style rule. This is + // used to detect when the progress is not changing (e.g. due to a step + // timing function) so we can avoid unnecessary style updates. + Nullable mProgressOnLastCompose; + + // The purpose of this value is the same as mProgressOnLastCompose but + // this is used to detect when the current iteration is not changing + // in the case when iterationComposite is accumulate. + uint64_t mCurrentIterationOnLastCompose = 0; + + // We need to track when we go to or from being "in effect" since + // we need to re-evaluate the cascade of animations when that changes. + bool mInEffectOnLastAnimationTimingUpdate; + +private: + nsChangeHint mCumulativeChangeHint; + + nsIFrame* GetAnimationFrame() const; + + bool CanThrottle() const; + bool CanThrottleTransformChanges(nsIFrame& aFrame) const; + + // Returns true if the computedTiming has changed since the last + // composition. + bool HasComputedTimingChanged() const; + + // Returns true unless Gecko limitations prevent performing transform + // animations for |aFrame|. When returning true, the reason for the + // limitation is stored in |aOutPerformanceWarning|. + static bool CanAnimateTransformOnCompositor( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning); + static bool IsGeometricProperty(const nsCSSPropertyID aProperty); + + static const TimeDuration OverflowRegionRefreshInterval(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_KeyframeEffectReadOnly_h diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp new file mode 100644 index 000000000..8e396f84c --- /dev/null +++ b/dom/animation/KeyframeUtils.cpp @@ -0,0 +1,1667 @@ +/* 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/RangedArray.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/TimingParams.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc. +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc. +#include "jsapi.h" // For ForOfIterator etc. +#include "nsClassHashtable.h" +#include "nsCSSParser.h" +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" +#include "nsCSSPseudoElements.h" // For CSSPseudoElementType +#include "nsTArray.h" +#include // For std::stable_sort + +namespace mozilla { + +// ------------------------------------------------------------------ +// +// Internal data types +// +// ------------------------------------------------------------------ + +// This is used while calculating paced spacing. If the keyframe is not pacable, +// we set its cumulative distance to kNotPaceable, so we can use this to check. +const double kNotPaceable = -1.0; + +// For the aAllowList parameter of AppendStringOrStringSequence and +// GetPropertyValuesPairs. +enum class ListAllowance { eDisallow, eAllow }; + +/** + * A comparator to sort nsCSSPropertyID values such that longhands are sorted + * before shorthands, and shorthands with fewer 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(nsCSSPropertyID aLhs, nsCSSPropertyID aRhs) const + { + return aLhs == aRhs; + } + + bool LessThan(nsCSSPropertyID aLhs, + nsCSSPropertyID 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(nsCSSPropertyID aProperty) const + { + if (!mSubpropertyCountInitialized) { + PodZero(&mSubpropertyCount); + mSubpropertyCountInitialized = true; + } + if (mSubpropertyCount[aProperty] == 0) { + uint32_t count = 0; + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( + p, aProperty, CSSEnabledState::eForAllContent) { + ++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 + { + nsCSSPropertyID 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 +{ + nsCSSPropertyID mProperty; + nsTArray mValues; + + typedef TPropertyPriorityComparator Comparator; +}; + +/** + * An additional property (for a property-values pair) found on a + * BaseKeyframe or BasePropertyIndexedKeyframe object. + */ +struct AdditionalProperty +{ + nsCSSPropertyID 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); + } + }; +}; + +/** + * Data for a segment in a keyframe animation of a given property + * whose value is a StyleAnimationValue. + * + * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes + * to gather data for each individual segment. + */ +struct KeyframeValueEntry +{ + nsCSSPropertyID mProperty; + StyleAnimationValue mValue; + 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; + } +}; + +// ------------------------------------------------------------------ +// +// Inlined helper methods +// +// ------------------------------------------------------------------ + +inline bool +IsInvalidValuePair(const PropertyValuePair& aPair, StyleBackendType aBackend) +{ + if (aBackend == StyleBackendType::Servo) { + return !aPair.mServoDeclarationBlock; + } + + // There are three types of values we store as token streams: + // + // * Shorthand values (where we manually extract the token stream's string + // value) and pass that along to various parsing methods + // * Longhand values with variable references + // * Invalid values + // + // We can distinguish between the last two cases because for invalid values + // we leave the token stream's mPropertyID as eCSSProperty_UNKNOWN. + return !nsCSSProps::IsShorthand(aPair.mProperty) && + aPair.mValue.GetUnit() == eCSSUnit_TokenStream && + aPair.mValue.GetTokenStreamValue()->mPropertyID + == eCSSProperty_UNKNOWN; +} + + +// ------------------------------------------------------------------ +// +// Internal helper method declarations +// +// ------------------------------------------------------------------ + +static void +GetKeyframeListFromKeyframeSequence(JSContext* aCx, + nsIDocument* aDocument, + JS::ForOfIterator& aIterator, + nsTArray& aResult, + ErrorResult& aRv); + +static bool +ConvertKeyframeSequence(JSContext* aCx, + nsIDocument* aDocument, + 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(nsCSSPropertyID aProperty, const nsAString& aStringValue, + nsCSSParser& aParser, nsIDocument* aDocument); + +static bool +HasValidOffsets(const nsTArray& aKeyframes); + +static void +MarkAsComputeValuesFailureKey(PropertyValuePair& aPair); + +static bool +IsComputeValuesFailureKey(const PropertyValuePair& aPair); + +static void +BuildSegmentsFromValueEntries(nsTArray& aEntries, + nsTArray& aResult); + +static void +GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx, + nsIDocument* aDocument, + JS::Handle aValue, + nsTArray& aResult, + ErrorResult& aRv); + +static bool +RequiresAdditiveAnimation(const nsTArray& aKeyframes, + nsIDocument* aDocument); + +static void +DistributeRange(const Range& aSpacingRange, + const Range& aRangeToAdjust); + +static void +DistributeRange(const Range& aSpacingRange); + +static void +PaceRange(const Range& aKeyframes, + const Range& aCumulativeDistances); + +static nsTArray +GetCumulativeDistances(const nsTArray& aValues, + nsCSSPropertyID aProperty, + nsStyleContext* aStyleContext); + +// ------------------------------------------------------------------ +// +// Public API +// +// ------------------------------------------------------------------ + +/* static */ nsTArray +KeyframeUtils::GetKeyframesFromObject(JSContext* aCx, + nsIDocument* aDocument, + 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, aDocument, iter, keyframes, aRv); + } else { + GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, 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. + + if (RequiresAdditiveAnimation(keyframes, aDocument)) { + aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR); + keyframes.Clear(); + } + + return keyframes; +} + +/* static */ void +KeyframeUtils::ApplySpacing(nsTArray& aKeyframes, + SpacingMode aSpacingMode, + nsCSSPropertyID aProperty, + nsTArray& aComputedValues, + nsStyleContext* aStyleContext) +{ + if (aKeyframes.IsEmpty()) { + return; + } + + nsTArray cumulativeDistances; + if (aSpacingMode == SpacingMode::paced) { + MOZ_ASSERT(IsAnimatableProperty(aProperty), + "Paced property should be animatable"); + + cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty, + aStyleContext); + // Reset the computed offsets if using paced spacing. + for (Keyframe& keyframe : aKeyframes) { + keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet; + } + } + + // If the first keyframe has an unspecified offset, fill it in with 0%. + // If there is only a single keyframe, then it gets 100%. + if (aKeyframes.Length() > 1) { + Keyframe& firstElement = aKeyframes[0]; + firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0); + // We will fill in the last keyframe's offset below + } else { + Keyframe& lastElement = aKeyframes.LastElement(); + lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0); + } + + // Fill in remaining missing offsets. + const Keyframe* const last = aKeyframes.cend() - 1; + const RangedPtr begin(aKeyframes.begin(), aKeyframes.Length()); + RangedPtr keyframeA = begin; + while (keyframeA != last) { + // Find keyframe A and keyframe B *between* which we will apply spacing. + RangedPtr keyframeB = keyframeA + 1; + while (keyframeB->mOffset.isNothing() && keyframeB != last) { + ++keyframeB; + } + keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0); + + // Fill computed offsets in (keyframe A, keyframe B). + if (aSpacingMode == SpacingMode::distribute) { + DistributeRange(Range(keyframeA, keyframeB + 1)); + } else { + // a) Find Paced A (first paceable keyframe) and + // Paced B (last paceable keyframe) in [keyframe A, keyframe B]. + RangedPtr pacedA = keyframeA; + while (pacedA < keyframeB && + cumulativeDistances[pacedA - begin] == kNotPaceable) { + ++pacedA; + } + RangedPtr pacedB = keyframeB; + while (pacedB > keyframeA && + cumulativeDistances[pacedB - begin] == kNotPaceable) { + --pacedB; + } + // As spec says, if there is no paceable keyframe + // in [keyframe A, keyframe B], we let Paced A and Paced B refer to + // keyframe B. + if (pacedA > pacedB) { + pacedA = pacedB = keyframeB; + } + // b) Apply distributing offsets in (keyframe A, Paced A] and + // [Paced B, keyframe B). + DistributeRange(Range(keyframeA, keyframeB + 1), + Range(keyframeA + 1, pacedA + 1)); + DistributeRange(Range(keyframeA, keyframeB + 1), + Range(pacedB, keyframeB)); + // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B). + // We pass the range [Paced A, Paced B] since PaceRange needs the end + // points of the range in order to calculate the correct offset. + PaceRange(Range(pacedA, pacedB + 1), + Range(&cumulativeDistances[pacedA - begin], + pacedB - pacedA + 1)); + // d) Fill in any computed offsets in (Paced A, Paced B) that are still + // not set (e.g. because the keyframe was not paceable, or because the + // cumulative distance between paceable properties was zero). + for (RangedPtr frame = pacedA + 1; frame < pacedB; ++frame) { + if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) { + continue; + } + + RangedPtr start = frame - 1; + RangedPtr end = frame + 1; + while (end < pacedB && + end->mComputedOffset == Keyframe::kComputedOffsetNotSet) { + ++end; + } + DistributeRange(Range(start, end + 1)); + frame = end; + } + } + keyframeA = keyframeB; + } +} + +/* static */ void +KeyframeUtils::ApplyDistributeSpacing(nsTArray& aKeyframes) +{ + nsTArray emptyArray; + ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN, + emptyArray, nullptr); +} + +/* static */ nsTArray +KeyframeUtils::GetComputedKeyframeValues(const nsTArray& aKeyframes, + dom::Element* aElement, + nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aStyleContext); + MOZ_ASSERT(aElement); + + StyleBackendType styleBackend = aElement->OwnerDoc()->GetStyleBackendType(); + + const size_t len = aKeyframes.Length(); + nsTArray result(len); + + for (const Keyframe& frame : aKeyframes) { + nsCSSPropertyIDSet propertiesOnThisKeyframe; + ComputedKeyframeValues* computedValues = result.AppendElement(); + for (const PropertyValuePair& pair : + PropertyPriorityIterator(frame.mPropertyValues)) { + MOZ_ASSERT(!pair.mServoDeclarationBlock || + styleBackend == StyleBackendType::Servo, + "Animation values were parsed using Servo backend but target" + " element is not using Servo backend?"); + + if (IsInvalidValuePair(pair, styleBackend)) { + continue; + } + + // Expand each value into the set of longhands and produce + // a KeyframeValueEntry for each value. + nsTArray values; + + if (styleBackend == StyleBackendType::Servo) { + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + CSSEnabledState::eForAllContent, aStyleContext, + *pair.mServoDeclarationBlock, values)) { + continue; + } + } else { + // 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, + CSSEnabledState::eForAllContent, aElement, aStyleContext, + tokenStream->mTokenStream, /* aUseSVGMode */ false, values) || + IsComputeValuesFailureKey(pair)) { + continue; + } + } else { + if (!StyleAnimationValue::ComputeValues(pair.mProperty, + CSSEnabledState::eForAllContent, aElement, aStyleContext, + pair.mValue, /* aUseSVGMode */ false, values)) { + continue; + } + MOZ_ASSERT(values.Length() == 1, + "Longhand properties should produce a single" + " StyleAnimationValue"); + } + } + + 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; + } + computedValues->AppendElement(value); + propertiesOnThisKeyframe.AddProperty(value.mProperty); + } + } + } + + MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch"); + return result; +} + +/* static */ nsTArray +KeyframeUtils::GetAnimationPropertiesFromKeyframes( + const nsTArray& aKeyframes, + const nsTArray& aComputedValues, + nsStyleContext* aStyleContext) +{ + MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(), + "Array length mismatch"); + + nsTArray entries(aKeyframes.Length()); + + const size_t len = aKeyframes.Length(); + for (size_t i = 0; i < len; ++i) { + const Keyframe& frame = aKeyframes[i]; + for (auto& value : aComputedValues[i]) { + MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet, + "Invalid computed offset"); + KeyframeValueEntry* entry = entries.AppendElement(); + entry->mOffset = frame.mComputedOffset; + entry->mProperty = value.mProperty; + entry->mValue = value.mValue; + entry->mTimingFunction = frame.mTimingFunction; + } + } + + nsTArray result; + BuildSegmentsFromValueEntries(entries, result); + return result; +} + +/* static */ bool +KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty) +{ + if (aProperty == eCSSProperty_UNKNOWN) { + return false; + } + + if (!nsCSSProps::IsShorthand(aProperty)) { + return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None; + } + + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty, + CSSEnabledState::eForAllContent) { + if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) { + return true; + } + } + + return false; +} + +// ------------------------------------------------------------------ +// +// Internal helpers +// +// ------------------------------------------------------------------ + +/** + * Converts a JS object to an IDL sequence. + * + * @param aCx The JSContext corresponding to |aIterator|. + * @param aDocument The document to use when parsing CSS properties. + * @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, + nsIDocument* aDocument, + 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, aDocument, 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 Keyframe objects in + * aResult. + */ +static bool +ConvertKeyframeSequence(JSContext* aCx, + nsIDocument* aDocument, + JS::ForOfIterator& aIterator, + nsTArray& aResult) +{ + JS::Rooted value(aCx); + nsCSSParser parser(aDocument->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, aDocument, 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, + aDocument)); + + // When we go to convert keyframes into arrays of property values we + // call StyleAnimation::ComputeValues. This should normally return true + // but in order to test the case where it does not, BaseKeyframeDict + // includes a chrome-only member that can be set to indicate that + // ComputeValues should fail for shorthand property values on that + // keyframe. + if (nsCSSProps::IsShorthand(pair.mProperty) && + keyframeDict.mSimulateComputeValuesFailure) { + MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement()); + } + } + } + + 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; + } + nsCSSPropertyID property = + nsCSSProps::LookupPropertyByIDLName(propName, + CSSEnabledState::eForAllContent); + if (KeyframeUtils::IsAnimatableProperty(property)) { + 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(nsCSSPropertyID aProperty, const nsAString& aStringValue, + nsCSSParser& aParser, nsIDocument* aDocument) +{ + MOZ_ASSERT(aDocument); + PropertyValuePair result; + + result.mProperty = aProperty; + + if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) { + nsCString name = nsCSSProps::GetStringValue(aProperty); + + NS_ConvertUTF16toUTF8 value(aStringValue); + RefPtr base = + new ThreadSafeURIHolder(aDocument->GetDocumentURI()); + RefPtr referrer = + new ThreadSafeURIHolder(aDocument->GetDocumentURI()); + RefPtr principal = + new ThreadSafePrincipalHolder(aDocument->NodePrincipal()); + + nsCString baseString; + aDocument->GetDocumentURI()->GetSpec(baseString); + + RefPtr servoDeclarationBlock = + Servo_ParseProperty(&name, &value, &baseString, + base, referrer, principal).Consume(); + + if (servoDeclarationBlock) { + result.mServoDeclarationBlock = servoDeclarationBlock.forget(); + return result; + } + } + + 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; + + // We are about to convert a null value to a token stream value but + // by leaving the mPropertyID as unknown, we will be able to + // distinguish between invalid values and valid token stream values + // (e.g. values with variable references). + MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN, + "The property of a token stream should be initialized" + " to unknown"); + + // 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); + } else { + // If we succeeded in parsing with Gecko, but not Servo the animation is + // not going to work since, for the purposes of animation, we're going to + // ignore |mValue| when the backend is Servo. + NS_WARNING_ASSERTION(aDocument->GetStyleBackendType() != + StyleBackendType::Servo, + "Gecko succeeded in parsing where Servo failed"); + } + + result.mValue = value; + + return result; +} + +/** + * 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; +} + +/** + * Takes a property-value pair for a shorthand property and modifies the + * value to indicate that when we call StyleAnimationValue::ComputeValues on + * that value we should behave as if that function had failed. + * + * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be + * a shorthand property. + */ +static void +MarkAsComputeValuesFailureKey(PropertyValuePair& aPair) +{ + MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty), + "Only shorthand property values can be marked as failure values"); + + // We store shorthand values as nsCSSValueTokenStream objects whose mProperty + // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream + // member contains the shorthand property's value as a string. + // + // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that + // nsCSSValue::AppendToString returns the mTokenStream value, but we can + // update mPropertyID to a special value to indicate that this is + // a special failure sentinel. + nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue(); + MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN, + "Shorthand value should initially have an unknown property ID"); + tokenStream->mPropertyID = eCSSPropertyExtra_no_properties; +} + +/** + * Returns true if |aPair| is a property-value pair on which we have + * previously called MarkAsComputeValuesFailureKey (and hence we should + * simulate failure when calling StyleAnimationValue::ComputeValues using its + * value). + * + * @param aPair The property-value pair to test. + * @return True if |aPair| represents a failure value. + */ +static bool +IsComputeValuesFailureKey(const PropertyValuePair& aPair) +{ + return nsCSSProps::IsShorthand(aPair.mProperty) && + aPair.mValue.GetTokenStreamValue()->mPropertyID == + eCSSPropertyExtra_no_properties; +} + +/** + * 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); + + // 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. + // + // Typically, for each property in |aEntries|, we expect there to be at least + // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0. + // However, since it is possible that when building |aEntries|, the call to + // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed. + // Furthermore, since we don't yet implement additive animation and hence + // don't have sensible fallback behavior when these values are missing, the + // following loop takes care to identify properties that lack a value at + // offset 0.0/1.0 and drops those properties from |aResult|. + + nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN; + AnimationProperty* animationProperty = nullptr; + + size_t i = 0, n = aEntries.Length(); + + while (i < n) { + // Check that the last property ends with an entry at offset 1. + if (i + 1 == n) { + if (aEntries[i].mOffset != 1.0f && animationProperty) { + aResult.RemoveElementAt(aResult.Length() - 1); + animationProperty = nullptr; + } + break; + } + + MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN && + aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN, + "Each entry should specify a valid property"); + + // Skip properties that don't have an entry with offset 0. + if (aEntries[i].mProperty != lastProperty && + aEntries[i].mOffset != 0.0f) { + // Since the entries are sorted by offset for a given property, and + // since we don't update |lastProperty|, we will keep hitting this + // condition until we change property. + ++i; + continue; + } + + // Drop properties that don't end with an entry with offset 1. + if (aEntries[i].mProperty != aEntries[i + 1].mProperty && + aEntries[i].mOffset != 1.0f) { + if (animationProperty) { + aResult.RemoveElementAt(aResult.Length() - 1); + animationProperty = nullptr; + } + ++i; + continue; + } + + // 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 && + aEntries[j + 1].mProperty == aEntries[j].mProperty) { + ++j; + } + } else if (aEntries[i].mOffset == 1.0f) { + if (aEntries[i + 1].mOffset == 1.0f && + aEntries[i + 1].mProperty == aEntries[i].mProperty) { + // We need to generate a final zero-length segment. + j = i + 1; + while (j + 1 < n && + aEntries[j + 1].mOffset == 1.0f && + aEntries[j + 1].mProperty == aEntries[j].mProperty) { + ++j; + } + } else { + // New property. + MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty); + animationProperty = nullptr; + ++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); + MOZ_ASSERT(!animationProperty); + 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 representing a property-indexed keyframe into + * an array of Keyframe objects. + * + * @param aCx The JSContext for |aValue|. + * @param aDocument The document to use when parsing CSS properties. + * @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, + nsIDocument* aDocument, + 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; + } + + Maybe easing = + TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, 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(aDocument->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, aDocument)); + } + } + + 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. + + nsCSSPropertyIDSet properties; // All properties encountered. + nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value. + nsCSSPropertyIDSet propertiesWithToValue; // Those with a defined 100% value. + + auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) { + properties.AddProperty(aProperty); + if (aOffset == 0.0) { + propertiesWithFromValue.AddProperty(aProperty); + } else if (aOffset == 1.0) { + propertiesWithToValue.AddProperty(aProperty); + } + }; + + StyleBackendType styleBackend = aDocument->GetStyleBackendType(); + + for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) { + const Keyframe& frame = aKeyframes[i]; + + // We won't have called ApplySpacing 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 (IsInvalidValuePair(pair, styleBackend)) { + continue; + } + + if (nsCSSProps::IsShorthand(pair.mProperty)) { + if (styleBackend == StyleBackendType::Gecko) { + nsCSSValueTokenStream* tokenStream = + pair.mValue.GetTokenStreamValue(); + nsCSSParser parser(aDocument->CSSLoader()); + if (!parser.IsValueValidForProperty(pair.mProperty, + tokenStream->mTokenStream)) { + continue; + } + } + // For the Servo backend, invalid shorthand values are represented by + // a null mServoDeclarationBlock member which we skip above in + // IsInvalidValuePair. + MOZ_ASSERT(styleBackend != StyleBackendType::Servo || + pair.mServoDeclarationBlock); + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( + prop, pair.mProperty, CSSEnabledState::eForAllContent) { + addToPropertySets(*prop, offsetToUse); + } + } else { + addToPropertySets(pair.mProperty, offsetToUse); + } + } + } + + return !propertiesWithFromValue.Equals(properties) || + !propertiesWithToValue.Equals(properties); +} + +/** + * Evenly distribute the computed offsets in (A, B). + * We pass the range keyframes in [A, B] and use A, B to calculate distributing + * computed offsets in (A, B). The second range, aRangeToAdjust, is passed, so + * we can know which keyframe we want to apply to. aRangeToAdjust should be in + * the range of aSpacingRange. + * + * @param aSpacingRange The sequence of keyframes between whose endpoints we + * should apply distribute spacing. + * @param aRangeToAdjust The range of keyframes we want to apply to. + */ +static void +DistributeRange(const Range& aSpacingRange, + const Range& aRangeToAdjust) +{ + MOZ_ASSERT(aRangeToAdjust.begin() >= aSpacingRange.begin() && + aRangeToAdjust.end() <= aSpacingRange.end(), + "Out of range"); + const size_t n = aSpacingRange.length() - 1; + const double startOffset = aSpacingRange[0].mComputedOffset; + const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset; + for (auto iter = aRangeToAdjust.begin(); + iter != aRangeToAdjust.end(); + ++iter) { + size_t index = iter - aSpacingRange.begin(); + iter->mComputedOffset = startOffset + double(index) / n * diffOffset; + } +} + +/** + * Overload of DistributeRange to apply distribute spacing to all keyframes in + * between the endpoints of the given range. + * + * @param aSpacingRange The sequence of keyframes between whose endpoints we + * should apply distribute spacing. + */ +static void +DistributeRange(const Range& aSpacingRange) +{ + // We don't need to apply distribute spacing to keyframe A and keyframe B. + DistributeRange(aSpacingRange, + Range(aSpacingRange.begin() + 1, + aSpacingRange.end() - 1)); +} + +/** + * Apply paced spacing to all paceable keyframes in between the endpoints of the + * given range. + * + * @param aKeyframes The range of keyframes between whose endpoints we should + * apply paced spacing. Both endpoints should be paceable, i.e. the + * corresponding elements in |aCumulativeDist| should not be kNotPaceable. + * Within this function, we refer to the start and end points of this range + * as Paced A and Paced B respectively in keeping with the notation used in + * the spec. + * @param aCumulativeDistances The sequence of cumulative distances of the paced + * property as returned by GetCumulativeDistances(). This acts as a + * parallel range to |aKeyframes|. + */ +static void +PaceRange(const Range& aKeyframes, + const Range& aCumulativeDistances) +{ + MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(), + "Range length mismatch"); + + const size_t len = aKeyframes.length(); + // If there is nothing between the end points, there is nothing to space. + if (len < 3) { + return; + } + + const double distA = *(aCumulativeDistances.begin()); + const double distB = *(aCumulativeDistances.end() - 1); + MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable, + "Both Paced A and Paced B should be paceable"); + + // If the total distance is zero, we should fall back to distribute spacing. + // The caller will fill-in any keyframes without a computed offset using + // distribute spacing so we can just return here. + if (distA == distB) { + return; + } + + const RangedPtr pacedA = aKeyframes.begin(); + const RangedPtr pacedB = aKeyframes.end() - 1; + MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet && + pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet, + "Both Paced A and Paced B should have valid computed offsets"); + + // Apply computed offset. + const double offsetA = pacedA->mComputedOffset; + const double diffOffset = pacedB->mComputedOffset - offsetA; + const double initialDist = distA; + const double totalDist = distB - initialDist; + for (auto iter = pacedA + 1; iter != pacedB; ++iter) { + size_t k = iter - aKeyframes.begin(); + if (aCumulativeDistances[k] == kNotPaceable) { + continue; + } + + double dist = aCumulativeDistances[k] - initialDist; + iter->mComputedOffset = offsetA + diffOffset * dist / totalDist; + } +} + +/** + * Get cumulative distances for the paced property. + * + * @param aValues The computed values returned by GetComputedKeyframeValues. + * @param aPacedProperty The paced property. + * @param aStyleContext The style context for computing distance on transform. + * @return The cumulative distances for the paced property. The length will be + * the same as aValues. + */ +static nsTArray +GetCumulativeDistances(const nsTArray& aValues, + nsCSSPropertyID aPacedProperty, + nsStyleContext* aStyleContext) +{ + // a) If aPacedProperty is a shorthand property, get its components. + // Otherwise, just add the longhand property into the set. + size_t pacedPropertyCount = 0; + nsCSSPropertyIDSet pacedPropertySet; + bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty); + if (isShorthand) { + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty, + CSSEnabledState::eForAllContent) { + pacedPropertySet.AddProperty(*p); + ++pacedPropertyCount; + } + } else { + pacedPropertySet.AddProperty(aPacedProperty); + pacedPropertyCount = 1; + } + + // b) Search each component (shorthand) or the longhand property, and + // calculate the cumulative distances of paceable keyframe pairs. + const size_t len = aValues.Length(); + nsTArray cumulativeDistances(len); + // cumulativeDistances is a parallel array to |aValues|, so set its length to + // the length of |aValues|. + cumulativeDistances.SetLength(len); + ComputedKeyframeValues prevPacedValues; + size_t preIdx = 0; + for (size_t i = 0; i < len; ++i) { + // Find computed values of the paced property. + ComputedKeyframeValues pacedValues; + for (const PropertyStyleAnimationValuePair& pair : aValues[i]) { + if (pacedPropertySet.HasProperty(pair.mProperty)) { + pacedValues.AppendElement(pair); + } + } + + // Check we have values for all the paceable longhand components. + if (pacedValues.Length() != pacedPropertyCount) { + // This keyframe is not paceable, assign kNotPaceable and skip it. + cumulativeDistances[i] = kNotPaceable; + continue; + } + + // Sort the pacedValues first, so the order of subproperties of + // pacedValues is always the same as that of prevPacedValues. + if (isShorthand) { + pacedValues.Sort( + TPropertyPriorityComparator()); + } + + if (prevPacedValues.IsEmpty()) { + // This is the first paceable keyframe so its cumulative distance is 0.0. + cumulativeDistances[i] = 0.0; + } else { + double dist = 0.0; + if (isShorthand) { + // Apply the distance by the square root of the sum of squares of + // longhand component distances. + for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) { + nsCSSPropertyID prop = prevPacedValues[propIdx].mProperty; + MOZ_ASSERT(pacedValues[propIdx].mProperty == prop, + "Property mismatch"); + + double componentDistance = 0.0; + if (StyleAnimationValue::ComputeDistance( + prop, + prevPacedValues[propIdx].mValue, + pacedValues[propIdx].mValue, + aStyleContext, + componentDistance)) { + dist += componentDistance * componentDistance; + } + } + dist = sqrt(dist); + } else { + // If the property is longhand, we just use the 1st value. + // If ComputeDistance() fails, |dist| will remain zero so there will be + // no distance between the previous paced value and this value. + Unused << + StyleAnimationValue::ComputeDistance(aPacedProperty, + prevPacedValues[0].mValue, + pacedValues[0].mValue, + aStyleContext, + dist); + } + cumulativeDistances[i] = cumulativeDistances[preIdx] + dist; + } + prevPacedValues.SwapElements(pacedValues); + preIdx = i; + } + return cumulativeDistances; +} + +} // namespace mozilla diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h new file mode 100644 index 000000000..dbaed97f5 --- /dev/null +++ b/dom/animation/KeyframeUtils.h @@ -0,0 +1,151 @@ +/* -*- 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 +#include "mozilla/KeyframeEffectParams.h" // SpacingMode + +struct JSContext; +class JSObject; +class nsIDocument; +class nsStyleContext; + +namespace mozilla { +struct AnimationProperty; +enum class CSSPseudoElementType : uint8_t; +class ErrorResult; +struct Keyframe; +struct PropertyStyleAnimationValuePair; + +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + + +namespace mozilla { + +// Represents the set of property-value pairs on a Keyframe converted to +// computed values. +using ComputedKeyframeValues = nsTArray; + +/** + * Utility methods for processing keyframes. + */ +class KeyframeUtils +{ +public: + /** + * 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 aDocument The document to use when parsing CSS properties. + * @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, + nsIDocument* aDocument, + JS::Handle aFrames, + ErrorResult& aRv); + + /** + * Calculate the StyleAnimationValues of properties of each keyframe. + * This involves expanding shorthand properties into longhand properties, + * removing the duplicated properties for each keyframe, and creating an + * array of |property:computed value| pairs for each keyframe. + * + * These computed values are used *both* when computing the final set of + * per-property animation values (see GetAnimationPropertiesFromKeyframes) as + * well when applying paced spacing. By returning these values here, we allow + * the result to be re-used in both operations. + * + * @param aKeyframes The input keyframes. + * @param aElement The context element. + * @param aStyleContext The style context to use when computing values. + * @return The set of ComputedKeyframeValues. The length will be the same as + * aFrames. + */ + static nsTArray + GetComputedKeyframeValues(const nsTArray& aKeyframes, + dom::Element* aElement, + nsStyleContext* aStyleContext); + + /** + * Fills in the mComputedOffset member of each keyframe in the given array + * using the specified spacing mode. + * + * https://w3c.github.io/web-animations/#spacing-keyframes + * + * @param aKeyframes The set of keyframes to adjust. + * @param aSpacingMode The spacing mode to apply. + * @param aProperty The paced property. Only used when |aSpacingMode| is + * SpacingMode::paced. In all other cases it is ignored and hence may be + * any value, e.g. eCSSProperty_UNKNOWN. + * @param aComputedValues The set of computed keyframe values as returned by + * GetComputedKeyframeValues. Only used when |aSpacingMode| is + * SpacingMode::paced. In all other cases this parameter is unused and may + * be any value including an empty array. + * @param aStyleContext The style context used for calculating paced spacing + * on transform. + */ + static void ApplySpacing(nsTArray& aKeyframes, + SpacingMode aSpacingMode, + nsCSSPropertyID aProperty, + nsTArray& aComputedValues, + nsStyleContext* aStyleContext); + + /** + * Wrapper for ApplySpacing to simplify using distribute spacing. + * + * @param aKeyframes 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 creating an array of computed values for each + * longhand property and determining the offset and timing function to use + * for each value. + * + * @param aKeyframes The input keyframes. + * @param aComputedValues The computed keyframe values (as returned by + * GetComputedKeyframeValues) used to fill in the individual + * AnimationPropertySegment objects. Although these values could be + * calculated from |aKeyframes|, passing them in as a separate parameter + * allows the result of GetComputedKeyframeValues to be re-used both + * here and in ApplySpacing. + * @param aStyleContext The style context to calculate the style difference. + * @return The set of animation properties. If an error occurs, the returned + * array will be empty. + */ + static nsTArray GetAnimationPropertiesFromKeyframes( + const nsTArray& aKeyframes, + const nsTArray& aComputedValues, + nsStyleContext* aStyleContext); + + /** + * Check if the property or, for shorthands, one or more of + * its subproperties, is animatable. + * + * @param aProperty The property to check. + * @return true if |aProperty| is animatable. + */ + static bool IsAnimatableProperty(nsCSSPropertyID aProperty); +}; + +} // namespace mozilla + +#endif // mozilla_KeyframeUtils_h diff --git a/dom/animation/PendingAnimationTracker.cpp b/dom/animation/PendingAnimationTracker.cpp new file mode 100644 index 000000000..a97814a7c --- /dev/null +++ b/dom/animation/PendingAnimationTracker.cpp @@ -0,0 +1,124 @@ +/* -*- 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/. */ + +#include "PendingAnimationTracker.h" + +#include "mozilla/dom/AnimationTimeline.h" +#include "nsIFrame.h" +#include "nsIPresShell.h" + +using namespace mozilla; + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker, + mPlayPendingSet, + mPausePendingSet, + mDocument) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PendingAnimationTracker, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PendingAnimationTracker, Release) + +void +PendingAnimationTracker::AddPending(dom::Animation& aAnimation, + AnimationSet& aSet) +{ + aSet.PutEntry(&aAnimation); + + // Schedule a paint. Otherwise animations that don't trigger a paint by + // themselves (e.g. CSS animations with an empty keyframes rule) won't + // start until something else paints. + EnsurePaintIsScheduled(); +} + +void +PendingAnimationTracker::RemovePending(dom::Animation& aAnimation, + AnimationSet& aSet) +{ + aSet.RemoveEntry(&aAnimation); +} + +bool +PendingAnimationTracker::IsWaiting(const dom::Animation& aAnimation, + const AnimationSet& aSet) const +{ + return aSet.Contains(const_cast(&aAnimation)); +} + +void +PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(const TimeStamp& + aReadyTime) +{ + auto triggerAnimationsAtReadyTime = [aReadyTime](AnimationSet& aAnimationSet) + { + for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) { + dom::Animation* animation = iter.Get()->GetKey(); + dom::AnimationTimeline* timeline = animation->GetTimeline(); + + // If the animation does not have a timeline, just drop it from the map. + // The animation will detect that it is not being tracked and will trigger + // itself on the next tick where it has a timeline. + if (!timeline) { + iter.Remove(); + continue; + } + + // When the timeline's refresh driver is under test control, its values + // have no correspondance to wallclock times so we shouldn't try to + // convert aReadyTime (which is a wallclock time) to a timeline value. + // Instead, the animation will be started/paused when the refresh driver + // is next advanced since this will trigger a call to + // TriggerPendingAnimationsNow. + if (!timeline->TracksWallclockTime()) { + continue; + } + + Nullable readyTime = timeline->ToTimelineTime(aReadyTime); + animation->TriggerOnNextTick(readyTime); + + iter.Remove(); + } + }; + + triggerAnimationsAtReadyTime(mPlayPendingSet); + triggerAnimationsAtReadyTime(mPausePendingSet); +} + +void +PendingAnimationTracker::TriggerPendingAnimationsNow() +{ + auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) { + for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetKey()->TriggerNow(); + } + aAnimationSet.Clear(); + }; + + triggerAndClearAnimations(mPlayPendingSet); + triggerAndClearAnimations(mPausePendingSet); +} + +void +PendingAnimationTracker::EnsurePaintIsScheduled() +{ + if (!mDocument) { + return; + } + + nsIPresShell* presShell = mDocument->GetShell(); + if (!presShell) { + return; + } + + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) { + return; + } + + rootFrame->SchedulePaint(); +} + +} // namespace mozilla diff --git a/dom/animation/PendingAnimationTracker.h b/dom/animation/PendingAnimationTracker.h new file mode 100644 index 000000000..8d638c73f --- /dev/null +++ b/dom/animation/PendingAnimationTracker.h @@ -0,0 +1,84 @@ +/* -*- 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_dom_PendingAnimationTracker_h +#define mozilla_dom_PendingAnimationTracker_h + +#include "mozilla/dom/Animation.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDocument.h" +#include "nsTHashtable.h" + +class nsIFrame; + +namespace mozilla { + +class PendingAnimationTracker final +{ +public: + explicit PendingAnimationTracker(nsIDocument* aDocument) + : mDocument(aDocument) + { } + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker) + + void AddPlayPending(dom::Animation& aAnimation) + { + MOZ_ASSERT(!IsWaitingToPause(aAnimation), + "Animation is already waiting to pause"); + AddPending(aAnimation, mPlayPendingSet); + } + void RemovePlayPending(dom::Animation& aAnimation) + { + RemovePending(aAnimation, mPlayPendingSet); + } + bool IsWaitingToPlay(const dom::Animation& aAnimation) const + { + return IsWaiting(aAnimation, mPlayPendingSet); + } + + void AddPausePending(dom::Animation& aAnimation) + { + MOZ_ASSERT(!IsWaitingToPlay(aAnimation), + "Animation is already waiting to play"); + AddPending(aAnimation, mPausePendingSet); + } + void RemovePausePending(dom::Animation& aAnimation) + { + RemovePending(aAnimation, mPausePendingSet); + } + bool IsWaitingToPause(const dom::Animation& aAnimation) const + { + return IsWaiting(aAnimation, mPausePendingSet); + } + + void TriggerPendingAnimationsOnNextTick(const TimeStamp& aReadyTime); + void TriggerPendingAnimationsNow(); + bool HasPendingAnimations() const { + return mPlayPendingSet.Count() > 0 || mPausePendingSet.Count() > 0; + } + +private: + ~PendingAnimationTracker() { } + + void EnsurePaintIsScheduled(); + + typedef nsTHashtable> AnimationSet; + + void AddPending(dom::Animation& aAnimation, AnimationSet& aSet); + void RemovePending(dom::Animation& aAnimation, AnimationSet& aSet); + bool IsWaiting(const dom::Animation& aAnimation, + const AnimationSet& aSet) const; + + AnimationSet mPlayPendingSet; + AnimationSet mPausePendingSet; + nsCOMPtr mDocument; +}; + +} // namespace mozilla + +#endif // mozilla_dom_PendingAnimationTracker_h diff --git a/dom/animation/PseudoElementHashEntry.h b/dom/animation/PseudoElementHashEntry.h new file mode 100644 index 000000000..63f9c60c8 --- /dev/null +++ b/dom/animation/PseudoElementHashEntry.h @@ -0,0 +1,58 @@ +/* -*- 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_PseudoElementHashEntry_h +#define mozilla_PseudoElementHashEntry_h + +#include "mozilla/dom/Element.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/HashFunctions.h" +#include "PLDHashTable.h" + +namespace mozilla { + +// A hash entry that uses a RefPtr, CSSPseudoElementType pair +class PseudoElementHashEntry : public PLDHashEntryHdr +{ +public: + typedef NonOwningAnimationTarget KeyType; + typedef const NonOwningAnimationTarget* KeyTypePointer; + + explicit PseudoElementHashEntry(KeyTypePointer aKey) + : mElement(aKey->mElement) + , mPseudoType(aKey->mPseudoType) { } + explicit PseudoElementHashEntry(const PseudoElementHashEntry& aCopy)=default; + + ~PseudoElementHashEntry() = default; + + KeyType GetKey() const { return { mElement, mPseudoType }; } + bool KeyEquals(KeyTypePointer aKey) const + { + return mElement == aKey->mElement && + mPseudoType == aKey->mPseudoType; + } + + static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) + { + if (!aKey) + return 0; + + // Convert the scoped enum into an integer while adding it to hash. + // Note: CSSPseudoElementTypeBase is uint8_t, so we convert it into + // uint8_t directly to avoid including the header. + return mozilla::HashGeneric(aKey->mElement, + static_cast(aKey->mPseudoType)); + } + enum { ALLOW_MEMMOVE = true }; + + RefPtr mElement; + CSSPseudoElementType mPseudoType; +}; + +} // namespace mozilla + +#endif // mozilla_PseudoElementHashEntry_h diff --git a/dom/animation/TimingParams.cpp b/dom/animation/TimingParams.cpp new file mode 100644 index 000000000..db61c8447 --- /dev/null +++ b/dom/animation/TimingParams.cpp @@ -0,0 +1,182 @@ +/* -*- 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/. */ + +#include "mozilla/TimingParams.h" + +#include "mozilla/AnimationUtils.h" +#include "mozilla/dom/AnimatableBinding.h" +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "nsCSSParser.h" // For nsCSSParser +#include "nsIDocument.h" +#include "nsRuleNode.h" + +namespace mozilla { + +template +static const dom::AnimationEffectTimingProperties& +GetTimingProperties(const OptionsType& aOptions); + +template <> +/* static */ const dom::AnimationEffectTimingProperties& +GetTimingProperties( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeEffectOptions()); + return aOptions.GetAsKeyframeEffectOptions(); +} + +template <> +/* static */ const dom::AnimationEffectTimingProperties& +GetTimingProperties( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) +{ + MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions()); + return aOptions.GetAsKeyframeAnimationOptions(); +} + +template +static TimingParams +TimingParamsFromOptionsUnion(const OptionsType& aOptions, + nsIDocument* aDocument, + ErrorResult& aRv) +{ + TimingParams result; + if (aOptions.IsUnrestrictedDouble()) { + double durationInMs = aOptions.GetAsUnrestrictedDouble(); + if (durationInMs >= 0) { + result.mDuration.emplace( + StickyTimeDuration::FromMilliseconds(durationInMs)); + } else { + aRv.Throw(NS_ERROR_DOM_TYPE_ERR); + } + } else { + const dom::AnimationEffectTimingProperties& timing = + GetTimingProperties(aOptions); + + Maybe duration = + TimingParams::ParseDuration(timing.mDuration, aRv); + if (aRv.Failed()) { + return result; + } + TimingParams::ValidateIterationStart(timing.mIterationStart, aRv); + 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); + result.mEndDelay = TimeDuration::FromMilliseconds(timing.mEndDelay); + result.mIterations = timing.mIterations; + result.mIterationStart = timing.mIterationStart; + result.mDirection = timing.mDirection; + result.mFill = timing.mFill; + result.mFunction = easing; + } + return result; +} + +/* static */ TimingParams +TimingParams::FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + nsIDocument* aDocument, + ErrorResult& aRv) +{ + return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv); +} + +/* static */ TimingParams +TimingParams::FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + nsIDocument* aDocument, + ErrorResult& aRv) +{ + 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(aEasing); + return Nothing(); +} + +bool +TimingParams::operator==(const TimingParams& aOther) const +{ + return mDuration == aOther.mDuration && + mDelay == aOther.mDelay && + mIterations == aOther.mIterations && + mIterationStart == aOther.mIterationStart && + mDirection == aOther.mDirection && + mFill == aOther.mFill && + mFunction == aOther.mFunction; +} + +} // namespace mozilla diff --git a/dom/animation/TimingParams.h b/dom/animation/TimingParams.h new file mode 100644 index 000000000..bfecee90c --- /dev/null +++ b/dom/animation/TimingParams.h @@ -0,0 +1,130 @@ +/* -*- 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_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 +#ifdef None +#undef None +#endif +#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for FillMode + // and PlaybackDirection + +class nsIDocument; + +namespace mozilla { + +namespace dom { +class UnrestrictedDoubleOrKeyframeEffectOptions; +class UnrestrictedDoubleOrKeyframeAnimationOptions; +} + +struct TimingParams +{ + TimingParams() = default; + + static TimingParams FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + nsIDocument* aDocument, ErrorResult& aRv); + static TimingParams FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + nsIDocument* aDocument, ErrorResult& aRv); + + // Range-checks and validates an UnrestrictedDoubleOrString or + // OwningUnrestrictedDoubleOrString object and converts to a + // StickyTimeDuration value or Nothing() if aDuration is "auto". + // Caller must check aRv.Failed(). + template + static Maybe ParseDuration(DoubleOrString& aDuration, + ErrorResult& aRv) + { + Maybe result; + if (aDuration.IsUnrestrictedDouble()) { + double durationInMs = aDuration.GetAsUnrestrictedDouble(); + if (durationInMs >= 0) { + result.emplace(StickyTimeDuration::FromMilliseconds(durationInMs)); + } else { + aRv.ThrowTypeError( + NS_LITERAL_STRING("duration")); + } + } else if (!aDuration.GetAsString().EqualsLiteral("auto")) { + aRv.ThrowTypeError( + aDuration.GetAsString()); + } + return result; + } + + static void ValidateIterationStart(double aIterationStart, + ErrorResult& aRv) + { + if (aIterationStart < 0) { + 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 + TimeDuration mEndDelay; + double mIterations = 1.0; // Can be NaN, negative, +/-Infinity + double mIterationStart = 0.0; + dom::PlaybackDirection mDirection = dom::PlaybackDirection::Normal; + dom::FillMode mFill = dom::FillMode::Auto; + Maybe mFunction; + + // Return the duration of the active interval calculated by duration and + // iteration count. + StickyTimeDuration ActiveDuration() const + { + // If either the iteration duration or iteration count is zero, + // Web Animations says that the active duration is zero. This is to + // ensure that the result is defined when the other argument is Infinity. + static const StickyTimeDuration zeroDuration; + if (!mDuration || *mDuration == zeroDuration || mIterations == 0.0) { + return zeroDuration; + } + + return mDuration->MultDouble(mIterations); + } + + StickyTimeDuration EndTime() const + { + return std::max(mDelay + ActiveDuration() + mEndDelay, + StickyTimeDuration()); + } + + bool operator==(const TimingParams& aOther) const; + bool operator!=(const TimingParams& aOther) const + { + return !(*this == aOther); + } +}; + +} // namespace mozilla + +#endif // mozilla_TimingParams_h diff --git a/dom/animation/moz.build b/dom/animation/moz.build new file mode 100644 index 000000000..bd8c93707 --- /dev/null +++ b/dom/animation/moz.build @@ -0,0 +1,67 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] +MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] + +EXPORTS.mozilla.dom += [ + 'Animation.h', + 'AnimationEffectReadOnly.h', + 'AnimationEffectTiming.h', + 'AnimationEffectTimingReadOnly.h', + 'AnimationTimeline.h', + 'CSSPseudoElement.h', + 'DocumentTimeline.h', + 'KeyframeEffect.h', + 'KeyframeEffectReadOnly.h', +] + +EXPORTS.mozilla += [ + 'AnimationComparator.h', + 'AnimationPerformanceWarning.h', + 'AnimationTarget.h', + 'AnimationUtils.h', + 'AnimValuesStyleRule.h', + 'ComputedTiming.h', + 'ComputedTimingFunction.h', + 'EffectCompositor.h', + 'EffectSet.h', + 'KeyframeEffectParams.h', + 'KeyframeUtils.h', + 'PendingAnimationTracker.h', + 'PseudoElementHashEntry.h', + 'TimingParams.h', +] + +UNIFIED_SOURCES += [ + 'Animation.cpp', + 'AnimationEffectReadOnly.cpp', + 'AnimationEffectTiming.cpp', + 'AnimationEffectTimingReadOnly.cpp', + 'AnimationPerformanceWarning.cpp', + 'AnimationTimeline.cpp', + 'AnimationUtils.cpp', + 'AnimValuesStyleRule.cpp', + 'ComputedTimingFunction.cpp', + 'CSSPseudoElement.cpp', + 'DocumentTimeline.cpp', + 'EffectCompositor.cpp', + 'EffectSet.cpp', + 'KeyframeEffect.cpp', + 'KeyframeEffectParams.cpp', + 'KeyframeEffectReadOnly.cpp', + 'KeyframeUtils.cpp', + 'PendingAnimationTracker.cpp', + 'TimingParams.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', + '/layout/base', + '/layout/style', +] + +FINAL_LIBRARY = 'xul' diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini new file mode 100644 index 000000000..9026bcbd2 --- /dev/null +++ b/dom/animation/test/chrome.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + testcommon.js + ../../imptests/testharness.js + ../../imptests/testharnessreport.js + !/dom/animation/test/chrome/file_animate_xrays.html + +[chrome/test_animate_xrays.html] +# file_animate_xrays.html needs to go in mochitest.ini since it is served +# over HTTP +[chrome/test_animation_observers.html] +[chrome/test_animation_performance_warning.html] +[chrome/test_animation_properties.html] +[chrome/test_generated_content_getAnimations.html] +[chrome/test_observers_for_sync_api.html] +[chrome/test_restyles.html] +[chrome/test_running_on_compositor.html] diff --git a/dom/animation/test/chrome/file_animate_xrays.html b/dom/animation/test/chrome/file_animate_xrays.html new file mode 100644 index 000000000..8a68fc548 --- /dev/null +++ b/dom/animation/test/chrome/file_animate_xrays.html @@ -0,0 +1,19 @@ + + + + + + +
+ + diff --git a/dom/animation/test/chrome/test_animate_xrays.html b/dom/animation/test/chrome/test_animate_xrays.html new file mode 100644 index 000000000..56b981bf1 --- /dev/null +++ b/dom/animation/test/chrome/test_animate_xrays.html @@ -0,0 +1,31 @@ + + + + + + + + +Mozilla Bug 1045994 +
+ + + diff --git a/dom/animation/test/chrome/test_animation_observers.html b/dom/animation/test/chrome/test_animation_observers.html new file mode 100644 index 000000000..237128e04 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers.html @@ -0,0 +1,1177 @@ + + +Test chrome-only MutationObserver animation notifications + + + + + +
+ diff --git a/dom/animation/test/chrome/test_animation_performance_warning.html b/dom/animation/test/chrome/test_animation_performance_warning.html new file mode 100644 index 000000000..a3bd63efc --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,957 @@ + + + +Bug 1196114 - Test metadata related to which animation properties + are running on the compositor + + + + + + +Mozilla Bug 1196114 +
+ + + diff --git a/dom/animation/test/chrome/test_animation_properties.html b/dom/animation/test/chrome/test_animation_properties.html new file mode 100644 index 000000000..534901306 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties.html @@ -0,0 +1,993 @@ + + + +Bug 1254419 - Test the values returned by + KeyframeEffectReadOnly.getProperties() + + + + + +Mozilla Bug 1254419 +
+ + + diff --git a/dom/animation/test/chrome/test_generated_content_getAnimations.html b/dom/animation/test/chrome/test_generated_content_getAnimations.html new file mode 100644 index 000000000..04e0f9bd0 --- /dev/null +++ b/dom/animation/test/chrome/test_generated_content_getAnimations.html @@ -0,0 +1,83 @@ + + + +Test getAnimations() for generated-content elements + + + + + + +
+
+
+
+ + diff --git a/dom/animation/test/chrome/test_observers_for_sync_api.html b/dom/animation/test/chrome/test_observers_for_sync_api.html new file mode 100644 index 000000000..20c3f3670 --- /dev/null +++ b/dom/animation/test/chrome/test_observers_for_sync_api.html @@ -0,0 +1,854 @@ + + + +Test chrome-only MutationObserver animation notifications for sync APIs + + + + +
+ + diff --git a/dom/animation/test/chrome/test_restyles.html b/dom/animation/test/chrome/test_restyles.html new file mode 100644 index 000000000..e59967c19 --- /dev/null +++ b/dom/animation/test/chrome/test_restyles.html @@ -0,0 +1,815 @@ + + + +Tests restyles caused by animations + + + + + + + + + + + diff --git a/dom/animation/test/chrome/test_running_on_compositor.html b/dom/animation/test/chrome/test_running_on_compositor.html new file mode 100644 index 000000000..cd6c679b8 --- /dev/null +++ b/dom/animation/test/chrome/test_running_on_compositor.html @@ -0,0 +1,966 @@ + + + +Bug 1045994 - Add a chrome-only property to inspect if an animation is + running on the compositor or not + + + + + + +Mozilla Bug 1045994 +
+ + diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html new file mode 100644 index 000000000..8df6808ae --- /dev/null +++ b/dom/animation/test/crashtests/1216842-1.html @@ -0,0 +1,35 @@ + + + + Bug 1216842: effect-level easing function produces negative values (compositor thread) + + + +
+ + + diff --git a/dom/animation/test/crashtests/1216842-2.html b/dom/animation/test/crashtests/1216842-2.html new file mode 100644 index 000000000..ec2a2e167 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-2.html @@ -0,0 +1,35 @@ + + + + Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread) + + + +
+ + + diff --git a/dom/animation/test/crashtests/1216842-3.html b/dom/animation/test/crashtests/1216842-3.html new file mode 100644 index 000000000..2e5a762aa --- /dev/null +++ b/dom/animation/test/crashtests/1216842-3.html @@ -0,0 +1,27 @@ + + + + Bug 1216842: effect-level easing function produces values greater than 1 (main-thread) + + +
+ + + diff --git a/dom/animation/test/crashtests/1216842-4.html b/dom/animation/test/crashtests/1216842-4.html new file mode 100644 index 000000000..2951adc95 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-4.html @@ -0,0 +1,27 @@ + + + + Bug 1216842: effect-level easing function produces negative values (main-thread) + + +
+ + + diff --git a/dom/animation/test/crashtests/1216842-5.html b/dom/animation/test/crashtests/1216842-5.html new file mode 100644 index 000000000..65c64fa48 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-5.html @@ -0,0 +1,38 @@ + + + + + Bug 1216842: effect-level easing function produces negative values passed + to step-end function (compositor thread) + + + + +
+ + + diff --git a/dom/animation/test/crashtests/1216842-6.html b/dom/animation/test/crashtests/1216842-6.html new file mode 100644 index 000000000..a588c68f1 --- /dev/null +++ b/dom/animation/test/crashtests/1216842-6.html @@ -0,0 +1,38 @@ + + + + + Bug 1216842: effect-level easing function produces values greater than 1 + which are passed to step-end function (compositor thread) + + + + +
+ + + diff --git a/dom/animation/test/crashtests/1239889-1.html b/dom/animation/test/crashtests/1239889-1.html new file mode 100644 index 000000000..476f3322b --- /dev/null +++ b/dom/animation/test/crashtests/1239889-1.html @@ -0,0 +1,12 @@ + + + + Bug 1239889 + + + + + diff --git a/dom/animation/test/crashtests/1244595-1.html b/dom/animation/test/crashtests/1244595-1.html new file mode 100644 index 000000000..13b2e2d7e --- /dev/null +++ b/dom/animation/test/crashtests/1244595-1.html @@ -0,0 +1,3 @@ +
diff --git a/dom/animation/test/crashtests/1272475-1.html b/dom/animation/test/crashtests/1272475-1.html new file mode 100644 index 000000000..e0b049538 --- /dev/null +++ b/dom/animation/test/crashtests/1272475-1.html @@ -0,0 +1,20 @@ + + + + Bug 1272475 - scale function with an extreme large value + + + + + diff --git a/dom/animation/test/crashtests/1272475-2.html b/dom/animation/test/crashtests/1272475-2.html new file mode 100644 index 000000000..da0e8605b --- /dev/null +++ b/dom/animation/test/crashtests/1272475-2.html @@ -0,0 +1,20 @@ + + + + Bug 1272475 - rotate function with an extreme large value + + + + + diff --git a/dom/animation/test/crashtests/1277272-1-inner.html b/dom/animation/test/crashtests/1277272-1-inner.html new file mode 100644 index 000000000..2ba52174d --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1-inner.html @@ -0,0 +1,19 @@ + + + + + + + diff --git a/dom/animation/test/crashtests/1277272-1.html b/dom/animation/test/crashtests/1277272-1.html new file mode 100644 index 000000000..f398bcf6d --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1.html @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html new file mode 100644 index 000000000..812d88ef2 --- /dev/null +++ b/dom/animation/test/document-timeline/test_document-timeline.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/document-timeline/test_request_animation_frame.html b/dom/animation/test/document-timeline/test_request_animation_frame.html new file mode 100644 index 000000000..302a385b7 --- /dev/null +++ b/dom/animation/test/document-timeline/test_request_animation_frame.html @@ -0,0 +1,27 @@ + + +Test RequestAnimationFrame Timestamps are monotonically increasing + + + diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini new file mode 100644 index 000000000..feb424518 --- /dev/null +++ b/dom/animation/test/mochitest.ini @@ -0,0 +1,111 @@ +[DEFAULT] +# Support files for chrome tests that we want to load over HTTP need +# to go in here, not chrome.ini. +support-files = + chrome/file_animate_xrays.html + css-animations/file_animation-cancel.html + css-animations/file_animation-computed-timing.html + css-animations/file_animation-currenttime.html + css-animations/file_animation-finish.html + css-animations/file_animation-finished.html + css-animations/file_animation-id.html + css-animations/file_animation-pausing.html + css-animations/file_animation-playstate.html + css-animations/file_animation-ready.html + css-animations/file_animation-reverse.html + css-animations/file_animation-starttime.html + css-animations/file_animations-dynamic-changes.html + css-animations/file_cssanimation-animationname.html + css-animations/file_document-get-animations.html + css-animations/file_effect-target.html + css-animations/file_element-get-animations.html + css-animations/file_keyframeeffect-getkeyframes.html + css-animations/file_pseudoElement-get-animations.html + css-transitions/file_animation-cancel.html + css-transitions/file_animation-computed-timing.html + css-transitions/file_animation-currenttime.html + css-transitions/file_animation-finished.html + css-transitions/file_animation-pausing.html + css-transitions/file_animation-ready.html + css-transitions/file_animation-starttime.html + css-transitions/file_csstransition-transitionproperty.html + css-transitions/file_document-get-animations.html + css-transitions/file_effect-target.html + css-transitions/file_element-get-animations.html + css-transitions/file_keyframeeffect-getkeyframes.html + css-transitions/file_pseudoElement-get-animations.html + css-transitions/file_setting-effect.html + document-timeline/file_document-timeline.html + mozilla/file_cubic_bezier_limits.html + mozilla/file_deferred_start.html + mozilla/file_disabled_properties.html + mozilla/file_disable_animations_api_core.html + mozilla/file_discrete-animations.html + mozilla/file_document-timeline-origin-time-range.html + mozilla/file_hide_and_show.html + mozilla/file_partial_keyframes.html + mozilla/file_spacing_property_order.html + mozilla/file_spacing_transform.html + mozilla/file_transform_limits.html + mozilla/file_transition_finish_on_compositor.html + mozilla/file_underlying-discrete-value.html + mozilla/file_set-easing.html + style/file_animation-seeking-with-current-time.html + style/file_animation-seeking-with-start-time.html + style/file_animation-setting-effect.html + style/file_animation-setting-spacing.html + testcommon.js + +[css-animations/test_animations-dynamic-changes.html] +[css-animations/test_animation-cancel.html] +[css-animations/test_animation-computed-timing.html] +[css-animations/test_animation-currenttime.html] +[css-animations/test_animation-finish.html] +[css-animations/test_animation-finished.html] +[css-animations/test_animation-id.html] +[css-animations/test_animation-pausing.html] +[css-animations/test_animation-playstate.html] +[css-animations/test_animation-ready.html] +[css-animations/test_animation-reverse.html] +[css-animations/test_animation-starttime.html] +[css-animations/test_cssanimation-animationname.html] +[css-animations/test_document-get-animations.html] +[css-animations/test_effect-target.html] +[css-animations/test_element-get-animations.html] +[css-animations/test_keyframeeffect-getkeyframes.html] +[css-animations/test_pseudoElement-get-animations.html] +[css-transitions/test_animation-cancel.html] +[css-transitions/test_animation-computed-timing.html] +[css-transitions/test_animation-currenttime.html] +[css-transitions/test_animation-finished.html] +[css-transitions/test_animation-pausing.html] +[css-transitions/test_animation-ready.html] +[css-transitions/test_animation-starttime.html] +[css-transitions/test_csstransition-transitionproperty.html] +[css-transitions/test_document-get-animations.html] +[css-transitions/test_effect-target.html] +[css-transitions/test_element-get-animations.html] +[css-transitions/test_keyframeeffect-getkeyframes.html] +[css-transitions/test_pseudoElement-get-animations.html] +[css-transitions/test_setting-effect.html] +[document-timeline/test_document-timeline.html] +[document-timeline/test_request_animation_frame.html] +[mozilla/test_cubic_bezier_limits.html] +[mozilla/test_deferred_start.html] +[mozilla/test_disable_animations_api_core.html] +[mozilla/test_disabled_properties.html] +[mozilla/test_discrete-animations.html] +[mozilla/test_document-timeline-origin-time-range.html] +[mozilla/test_hide_and_show.html] +[mozilla/test_partial_keyframes.html] +[mozilla/test_set-easing.html] +[mozilla/test_spacing_property_order.html] +[mozilla/test_spacing_transform.html] +[mozilla/test_transform_limits.html] +[mozilla/test_transition_finish_on_compositor.html] +skip-if = toolkit == 'android' +[mozilla/test_underlying-discrete-value.html] +[style/test_animation-seeking-with-current-time.html] +[style/test_animation-seeking-with-start-time.html] +[style/test_animation-setting-effect.html] +[style/test_animation-setting-spacing.html] diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/file_cubic_bezier_limits.html new file mode 100644 index 000000000..a0378f395 --- /dev/null +++ b/dom/animation/test/mozilla/file_cubic_bezier_limits.html @@ -0,0 +1,167 @@ + + + + + + + diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html new file mode 100644 index 000000000..3be3f56aa --- /dev/null +++ b/dom/animation/test/mozilla/file_deferred_start.html @@ -0,0 +1,121 @@ + + + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_core.html b/dom/animation/test/mozilla/file_disable_animations_api_core.html new file mode 100644 index 000000000..ef77988d9 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html @@ -0,0 +1,30 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/file_disabled_properties.html new file mode 100644 index 000000000..f1b72973f --- /dev/null +++ b/dom/animation/test/mozilla/file_disabled_properties.html @@ -0,0 +1,73 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete-animations.html new file mode 100644 index 000000000..35e818a90 --- /dev/null +++ b/dom/animation/test/mozilla/file_discrete-animations.html @@ -0,0 +1,170 @@ + + + +Test Mozilla-specific discrete animatable properties + + + + + diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html new file mode 100644 index 000000000..083bf0903 --- /dev/null +++ b/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html @@ -0,0 +1,30 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/file_hide_and_show.html new file mode 100644 index 000000000..0771fcce1 --- /dev/null +++ b/dom/animation/test/mozilla/file_hide_and_show.html @@ -0,0 +1,162 @@ + + + + + + + diff --git a/dom/animation/test/mozilla/file_partial_keyframes.html b/dom/animation/test/mozilla/file_partial_keyframes.html new file mode 100644 index 000000000..68832be7a --- /dev/null +++ b/dom/animation/test/mozilla/file_partial_keyframes.html @@ -0,0 +1,41 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/file_set-easing.html new file mode 100644 index 000000000..072b125cb --- /dev/null +++ b/dom/animation/test/mozilla/file_set-easing.html @@ -0,0 +1,34 @@ + + + +Test setting easing in sandbox + + + + + diff --git a/dom/animation/test/mozilla/file_spacing_property_order.html b/dom/animation/test/mozilla/file_spacing_property_order.html new file mode 100644 index 000000000..1338d6081 --- /dev/null +++ b/dom/animation/test/mozilla/file_spacing_property_order.html @@ -0,0 +1,33 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_spacing_transform.html b/dom/animation/test/mozilla/file_spacing_transform.html new file mode 100644 index 000000000..0de773786 --- /dev/null +++ b/dom/animation/test/mozilla/file_spacing_transform.html @@ -0,0 +1,240 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/file_transform_limits.html new file mode 100644 index 000000000..d4c813c67 --- /dev/null +++ b/dom/animation/test/mozilla/file_transform_limits.html @@ -0,0 +1,55 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_transition_finish_on_compositor.html b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html new file mode 100644 index 000000000..4912d05dd --- /dev/null +++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html @@ -0,0 +1,67 @@ + + + + + + + + diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/file_underlying-discrete-value.html new file mode 100644 index 000000000..3be01b904 --- /dev/null +++ b/dom/animation/test/mozilla/file_underlying-discrete-value.html @@ -0,0 +1,192 @@ + + + + + + diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html new file mode 100644 index 000000000..e67e5dbbb --- /dev/null +++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_deferred_start.html b/dom/animation/test/mozilla/test_deferred_start.html new file mode 100644 index 000000000..4db4bf676 --- /dev/null +++ b/dom/animation/test/mozilla/test_deferred_start.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_core.html b/dom/animation/test/mozilla/test_disable_animations_api_core.html new file mode 100644 index 000000000..cfb64e537 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_core.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html new file mode 100644 index 000000000..86d02e6b6 --- /dev/null +++ b/dom/animation/test/mozilla/test_disabled_properties.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html new file mode 100644 index 000000000..2a36bd50e --- /dev/null +++ b/dom/animation/test/mozilla/test_discrete-animations.html @@ -0,0 +1,18 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html new file mode 100644 index 000000000..f73c233d3 --- /dev/null +++ b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html new file mode 100644 index 000000000..929a31bd4 --- /dev/null +++ b/dom/animation/test/mozilla/test_hide_and_show.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_partial_keyframes.html b/dom/animation/test/mozilla/test_partial_keyframes.html new file mode 100644 index 000000000..28eb4c588 --- /dev/null +++ b/dom/animation/test/mozilla/test_partial_keyframes.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html new file mode 100644 index 000000000..e0069ff1c --- /dev/null +++ b/dom/animation/test/mozilla/test_set-easing.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_spacing_property_order.html b/dom/animation/test/mozilla/test_spacing_property_order.html new file mode 100644 index 000000000..afcc12bed --- /dev/null +++ b/dom/animation/test/mozilla/test_spacing_property_order.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_spacing_transform.html b/dom/animation/test/mozilla/test_spacing_transform.html new file mode 100644 index 000000000..38dce7e99 --- /dev/null +++ b/dom/animation/test/mozilla/test_spacing_transform.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html new file mode 100644 index 000000000..6c9b5e4fa --- /dev/null +++ b/dom/animation/test/mozilla/test_transform_limits.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html new file mode 100644 index 000000000..357e5297e --- /dev/null +++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html new file mode 100644 index 000000000..7feee53a1 --- /dev/null +++ b/dom/animation/test/mozilla/test_underlying-discrete-value.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/style/file_animation-seeking-with-current-time.html b/dom/animation/test/style/file_animation-seeking-with-current-time.html new file mode 100644 index 000000000..c3a590394 --- /dev/null +++ b/dom/animation/test/style/file_animation-seeking-with-current-time.html @@ -0,0 +1,121 @@ + + + + + Tests for seeking using Animation.currentTime + + + + + + + diff --git a/dom/animation/test/style/file_animation-seeking-with-start-time.html b/dom/animation/test/style/file_animation-seeking-with-start-time.html new file mode 100644 index 000000000..ba09827c6 --- /dev/null +++ b/dom/animation/test/style/file_animation-seeking-with-start-time.html @@ -0,0 +1,121 @@ + + + + + Tests for seeking using Animation.startTime + + + + + + + diff --git a/dom/animation/test/style/file_animation-setting-effect.html b/dom/animation/test/style/file_animation-setting-effect.html new file mode 100644 index 000000000..cf50cf2ce --- /dev/null +++ b/dom/animation/test/style/file_animation-setting-effect.html @@ -0,0 +1,125 @@ + + + + + Tests for setting effects by using Animation.effect + + + + + + diff --git a/dom/animation/test/style/file_animation-setting-spacing.html b/dom/animation/test/style/file_animation-setting-spacing.html new file mode 100644 index 000000000..6098b7433 --- /dev/null +++ b/dom/animation/test/style/file_animation-setting-spacing.html @@ -0,0 +1,111 @@ + + + +Tests for setting spacing by using KeyframeEffect.spacing + + + + + diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html new file mode 100644 index 000000000..386e57788 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html new file mode 100644 index 000000000..0a1691a08 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html new file mode 100644 index 000000000..1199b3e75 --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-effect.html @@ -0,0 +1,15 @@ + + + + +
+ + diff --git a/dom/animation/test/style/test_animation-setting-spacing.html b/dom/animation/test/style/test_animation-setting-spacing.html new file mode 100644 index 000000000..1c703e2a3 --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-spacing.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js new file mode 100644 index 000000000..b9001e4f4 --- /dev/null +++ b/dom/animation/test/testcommon.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Use this variable if you specify duration or some other properties + * for script animation. + * E.g., div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC); + * + * NOTE: Creating animations with short duration may cause intermittent + * failures in asynchronous test. For example, the short duration animation + * might be finished when animation.ready has been fulfilled because of slow + * platforms or busyness of the main thread. + * Setting short duration to cancel its animation does not matter but + * if you don't want to cancel the animation, consider using longer duration. + */ +const MS_PER_SEC = 1000; + +/* The recommended minimum precision to use for time values[1]. + * + * [1] https://w3c.github.io/web-animations/#precision-of-time-values + */ +var TIME_PRECISION = 0.0005; // ms + +/* + * Allow implementations to substitute an alternative method for comparing + * times based on their precision requirements. + */ +function assert_times_equal(actual, expected, description) { + assert_approx_equals(actual, expected, TIME_PRECISION, description); +} + +/** + * Appends a div to the document body and creates an animation on the div. + * NOTE: This function asserts when trying to create animations with durations + * shorter than 100s because the shorter duration may cause intermittent + * failures. If you are not sure how long it is suitable, use 100s; it's + * long enough but shorter than our test framework timeout (330s). + * If you really need to use shorter durations, use animate() function directly. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * @param attrs A dictionary object with attribute names and values to set on + * the div. + * @param frames The keyframes passed to Element.animate(). + * @param options The options passed to Element.animate(). + */ +function addDivAndAnimate(t, attrs, frames, options) { + let animDur = (typeof options === 'object') ? + options.duration : options; + assert_greater_than_equal(animDur, 100 * MS_PER_SEC, + 'Clients of this addDivAndAnimate API must request a duration ' + + 'of at least 100s, to avoid intermittent failures from e.g.' + + 'the main thread being busy for an extended period'); + + return addDiv(t, attrs).animate(frames, options); +} + +/** + * Appends a div to the document body. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the div when the test + * finishes. + * + * @param attrs A dictionary object with attribute names and values to set on + * the div. + */ +function addDiv(t, attrs) { + var div = document.createElement('div'); + if (attrs) { + for (var attrName in attrs) { + div.setAttribute(attrName, attrs[attrName]); + } + } + document.body.appendChild(div); + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(function() { + if (div.parentNode) { + div.parentNode.removeChild(div); + } + }); + } + return div; +} + +/** + * Appends a style div to the document head. + * + * @param t The testharness.js Test object. If provided, this will be used + * to register a cleanup callback to remove the style element + * when the test finishes. + * + * @param rules A dictionary object with selector names and rules to set on + * the style sheet. + */ +function addStyle(t, rules) { + var extraStyle = document.createElement('style'); + document.head.appendChild(extraStyle); + if (rules) { + var sheet = extraStyle.sheet; + for (var selector in rules) { + sheet.insertRule(selector + '{' + rules[selector] + '}', + sheet.cssRules.length); + } + } + + if (t && typeof t.add_cleanup === 'function') { + t.add_cleanup(function() { + extraStyle.remove(); + }); + } +} + +/** + * Promise wrapper for requestAnimationFrame. + */ +function waitForFrame() { + return new Promise(function(resolve, reject) { + window.requestAnimationFrame(resolve); + }); +} + +/** + * Returns a Promise that is resolved after the given number of consecutive + * animation frames have occured (using requestAnimationFrame callbacks). + * + * @param frameCount The number of animation frames. + * @param onFrame An optional function to be processed in each animation frame. + */ +function waitForAnimationFrames(frameCount, onFrame) { + return new Promise(function(resolve, reject) { + function handleFrame() { + if (onFrame && typeof onFrame === 'function') { + onFrame(); + } + if (--frameCount <= 0) { + resolve(); + } else { + window.requestAnimationFrame(handleFrame); // wait another frame + } + } + window.requestAnimationFrame(handleFrame); + }); +} + +/** + * Wrapper that takes a sequence of N animations and returns: + * + * Promise.all([animations[0].ready, animations[1].ready, ... animations[N-1].ready]); + */ +function waitForAllAnimations(animations) { + return Promise.all(animations.map(function(animation) { + return animation.ready; + })); +} + +/** + * Flush the computed style for the given element. This is useful, for example, + * when we are testing a transition and need the initial value of a property + * to be computed so that when we synchronouslyet set it to a different value + * we actually get a transition instead of that being the initial value. + */ +function flushComputedStyle(elem) { + var cs = window.getComputedStyle(elem); + cs.marginLeft; +} + +if (opener) { + for (var funcName of ["async_test", "assert_not_equals", "assert_equals", + "assert_approx_equals", "assert_less_than", + "assert_less_than_equal", "assert_greater_than", + "assert_between_inclusive", + "assert_true", "assert_false", + "assert_class_string", "assert_throws", + "assert_unreached", "promise_test", "test"]) { + window[funcName] = opener[funcName].bind(opener); + } + + window.EventWatcher = opener.EventWatcher; + + function done() { + opener.add_completion_callback(function() { + self.close(); + }); + opener.done(); + } +} + +/** + * Return a new MutaionObserver which started observing |target| element + * with { animations: true, subtree: |subtree| } option. + * NOTE: This observer should be used only with takeRecords(). If any of + * MutationRecords are observed in the callback of the MutationObserver, + * it will raise an assertion. + */ +function setupSynchronousObserver(t, target, subtree) { + var observer = new MutationObserver(records => { + assert_unreached("Any MutationRecords should not be observed in this " + + "callback"); + }); + t.add_cleanup(() => { + observer.disconnect(); + }); + observer.observe(target, { animations: true, subtree: subtree }); + return observer; +} + +/** + * Returns true if off-main-thread animations. + */ +function isOMTAEnabled() { + const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations'; + return SpecialPowers.DOMWindowUtils.layerManagerRemote && + SpecialPowers.getBoolPref(OMTAPrefKey); +} -- cgit v1.2.3