From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/animation/Animation.cpp | 2210 +++++++++++++++++++ dom/animation/Animation.h | 709 ++++++ dom/animation/AnimationComparator.h | 32 + dom/animation/AnimationEffect.cpp | 370 ++++ dom/animation/AnimationEffect.h | 117 + dom/animation/AnimationEventDispatcher.cpp | 62 + dom/animation/AnimationEventDispatcher.h | 407 ++++ dom/animation/AnimationPerformanceWarning.cpp | 81 + dom/animation/AnimationPerformanceWarning.h | 81 + dom/animation/AnimationPropertySegment.h | 55 + dom/animation/AnimationTarget.h | 108 + dom/animation/AnimationTimeline.cpp | 116 + dom/animation/AnimationTimeline.h | 143 ++ dom/animation/AnimationUtils.cpp | 145 ++ dom/animation/AnimationUtils.h | 134 ++ dom/animation/CSSAnimation.cpp | 376 ++++ dom/animation/CSSAnimation.h | 238 ++ dom/animation/CSSPseudoElement.cpp | 90 + dom/animation/CSSPseudoElement.h | 73 + dom/animation/CSSTransition.cpp | 333 +++ dom/animation/CSSTransition.h | 230 ++ dom/animation/ComputedTiming.h | 71 + dom/animation/DocumentTimeline.cpp | 311 +++ dom/animation/DocumentTimeline.h | 97 + dom/animation/EffectCompositor.cpp | 969 +++++++++ dom/animation/EffectCompositor.h | 258 +++ dom/animation/EffectSet.cpp | 132 ++ dom/animation/EffectSet.h | 246 +++ dom/animation/ElementAnimationData.cpp | 126 ++ dom/animation/ElementAnimationData.h | 262 +++ dom/animation/Keyframe.h | 83 + dom/animation/KeyframeEffect.cpp | 2117 ++++++++++++++++++ dom/animation/KeyframeEffect.h | 529 +++++ dom/animation/KeyframeEffectParams.h | 34 + dom/animation/KeyframeUtils.cpp | 1255 +++++++++++ dom/animation/KeyframeUtils.h | 110 + dom/animation/PendingAnimationTracker.cpp | 193 ++ dom/animation/PendingAnimationTracker.h | 113 + dom/animation/PostRestyleMode.h | 16 + dom/animation/PseudoElementHashEntry.h | 51 + dom/animation/ScrollTimeline.cpp | 295 +++ dom/animation/ScrollTimeline.h | 281 +++ dom/animation/ScrollTimelineAnimationTracker.cpp | 48 + dom/animation/ScrollTimelineAnimationTracker.h | 58 + dom/animation/TimingParams.cpp | 291 +++ dom/animation/TimingParams.h | 265 +++ dom/animation/ViewTimeline.cpp | 166 ++ dom/animation/ViewTimeline.h | 86 + dom/animation/moz.build | 79 + dom/animation/test/chrome.ini | 29 + dom/animation/test/chrome/file_animate_xrays.html | 18 + dom/animation/test/chrome/test_animate_xrays.html | 40 + .../chrome/test_animation_observers_async.html | 665 ++++++ .../test/chrome/test_animation_observers_sync.html | 1587 ++++++++++++++ .../chrome/test_animation_performance_warning.html | 1692 +++++++++++++++ .../test/chrome/test_animation_properties.html | 838 +++++++ .../chrome/test_animation_properties_display.html | 43 + .../test_cssanimation_missing_keyframes.html | 73 + .../test_generated_content_getAnimations.html | 83 + .../test/chrome/test_keyframe_effect_xrays.html | 45 + ...bserver_for_element_removal_in_shadow_tree.html | 45 + .../test/chrome/test_running_on_compositor.html | 1516 +++++++++++++ .../test_simulate_compute_values_failure.html | 371 ++++ dom/animation/test/crashtests/1134538.html | 8 + 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 | 16 + 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 | 25 + dom/animation/test/crashtests/1278485-1.html | 26 + dom/animation/test/crashtests/1282691-1.html | 23 + dom/animation/test/crashtests/1291413-1.html | 20 + dom/animation/test/crashtests/1291413-2.html | 21 + dom/animation/test/crashtests/1304886-1.html | 14 + dom/animation/test/crashtests/1309198-1.html | 40 + dom/animation/test/crashtests/1322291-1.html | 24 + dom/animation/test/crashtests/1322291-2.html | 31 + dom/animation/test/crashtests/1322382-1.html | 16 + dom/animation/test/crashtests/1323114-1.html | 12 + dom/animation/test/crashtests/1323114-2.html | 18 + dom/animation/test/crashtests/1323119-1.html | 13 + dom/animation/test/crashtests/1324554-1.html | 19 + dom/animation/test/crashtests/1325193-1.html | 18 + dom/animation/test/crashtests/1330190-1.html | 11 + dom/animation/test/crashtests/1330190-2.html | 36 + dom/animation/test/crashtests/1330513-1.html | 8 + dom/animation/test/crashtests/1332588-1.html | 25 + dom/animation/test/crashtests/1333539-1.html | 30 + dom/animation/test/crashtests/1333539-2.html | 38 + dom/animation/test/crashtests/1334582-1.html | 11 + dom/animation/test/crashtests/1334582-2.html | 11 + dom/animation/test/crashtests/1334583-1.html | 9 + dom/animation/test/crashtests/1335998-1.html | 28 + dom/animation/test/crashtests/1343589-1.html | 18 + dom/animation/test/crashtests/1359658-1.html | 33 + dom/animation/test/crashtests/1373712-1.html | 11 + dom/animation/test/crashtests/1379606-1.html | 21 + dom/animation/test/crashtests/1393605-1.html | 15 + dom/animation/test/crashtests/1400022-1.html | 10 + dom/animation/test/crashtests/1401809.html | 14 + dom/animation/test/crashtests/1411318-1.html | 15 + dom/animation/test/crashtests/1467277-1.html | 6 + dom/animation/test/crashtests/1468294-1.html | 7 + dom/animation/test/crashtests/1524480-1.html | 37 + dom/animation/test/crashtests/1575926.html | 24 + dom/animation/test/crashtests/1585770.html | 22 + dom/animation/test/crashtests/1604500-1.html | 24 + dom/animation/test/crashtests/1611847.html | 23 + dom/animation/test/crashtests/1612891-1.html | 15 + dom/animation/test/crashtests/1612891-2.html | 15 + dom/animation/test/crashtests/1612891-3.html | 10 + dom/animation/test/crashtests/1633442.html | 15 + dom/animation/test/crashtests/1633486.html | 20 + dom/animation/test/crashtests/1656419.html | 23 + dom/animation/test/crashtests/1699890.html | 13 + dom/animation/test/crashtests/1706157.html | 19 + dom/animation/test/crashtests/1714421.html | 8 + dom/animation/test/crashtests/1807966.html | 13 + dom/animation/test/crashtests/crashtests.list | 61 + .../document-timeline/test_document-timeline.html | 147 ++ .../test_request_animation_frame.html | 27 + dom/animation/test/mochitest.ini | 81 + dom/animation/test/mozilla/empty.html | 2 + .../test/mozilla/file_deferred_start.html | 179 ++ .../file_disable_animations_api_autoremove.html | 69 + .../file_disable_animations_api_compositing.html | 137 ++ ...file_disable_animations_api_get_animations.html | 20 + ..._disable_animations_api_implicit_keyframes.html | 48 + .../file_disable_animations_api_timelines.html | 30 + .../test/mozilla/file_discrete_animations.html | 122 ++ dom/animation/test/mozilla/file_restyles.html | 2275 ++++++++++++++++++++ .../file_transition_finish_on_compositor.html | 67 + dom/animation/test/mozilla/test_cascade.html | 37 + .../test/mozilla/test_cubic_bezier_limits.html | 168 ++ .../test/mozilla/test_deferred_start.html | 21 + .../test_disable_animations_api_autoremove.html | 15 + .../test_disable_animations_api_compositing.html | 14 + ...test_disable_animations_api_get_animations.html | 14 + ..._disable_animations_api_implicit_keyframes.html | 14 + .../test_disable_animations_api_timelines.html | 16 + .../test/mozilla/test_disabled_properties.html | 73 + .../test/mozilla/test_discrete_animations.html | 16 + .../test/mozilla/test_distance_of_basic_shape.html | 91 + .../test/mozilla/test_distance_of_filter.html | 248 +++ .../mozilla/test_distance_of_path_function.html | 140 ++ .../test/mozilla/test_distance_of_transform.html | 404 ++++ .../test_document_timeline_origin_time_range.html | 32 + .../test/mozilla/test_event_listener_leaks.html | 43 + .../test_get_animations_on_scroll_animations.html | 74 + dom/animation/test/mozilla/test_hide_and_show.html | 198 ++ .../test_mainthread_synchronization_pref.html | 42 + .../test/mozilla/test_moz_prefixed_properties.html | 93 + .../mozilla/test_pending_animation_tracker.html | 134 ++ dom/animation/test/mozilla/test_restyles.html | 22 + .../test/mozilla/test_restyling_xhr_doc.html | 106 + dom/animation/test/mozilla/test_set_easing.html | 36 + .../test_style_after_finished_on_compositor.html | 138 ++ .../test/mozilla/test_transform_limits.html | 56 + .../test_transition_finish_on_compositor.html | 22 + .../mozilla/test_underlying_discrete_value.html | 188 ++ dom/animation/test/mozilla/test_unstyled.html | 54 + dom/animation/test/mozilla/xhr_doc.html | 2 + .../test_animation-seeking-with-current-time.html | 123 ++ .../test_animation-seeking-with-start-time.html | 123 ++ .../test/style/test_animation-setting-effect.html | 127 ++ dom/animation/test/style/test_composite.html | 142 ++ ...terpolation-from-interpolatematrix-to-none.html | 43 + .../style/test_missing-keyframe-on-compositor.html | 577 +++++ .../test/style/test_missing-keyframe.html | 110 + .../test_transform-non-normalizable-rotate3d.html | 28 + dom/animation/test/testcommon.js | 527 +++++ 178 files changed, 30517 insertions(+) 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/AnimationEffect.cpp create mode 100644 dom/animation/AnimationEffect.h create mode 100644 dom/animation/AnimationEventDispatcher.cpp create mode 100644 dom/animation/AnimationEventDispatcher.h create mode 100644 dom/animation/AnimationPerformanceWarning.cpp create mode 100644 dom/animation/AnimationPerformanceWarning.h create mode 100644 dom/animation/AnimationPropertySegment.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/CSSAnimation.cpp create mode 100644 dom/animation/CSSAnimation.h create mode 100644 dom/animation/CSSPseudoElement.cpp create mode 100644 dom/animation/CSSPseudoElement.h create mode 100644 dom/animation/CSSTransition.cpp create mode 100644 dom/animation/CSSTransition.h create mode 100644 dom/animation/ComputedTiming.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/ElementAnimationData.cpp create mode 100644 dom/animation/ElementAnimationData.h create mode 100644 dom/animation/Keyframe.h create mode 100644 dom/animation/KeyframeEffect.cpp create mode 100644 dom/animation/KeyframeEffect.h create mode 100644 dom/animation/KeyframeEffectParams.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/PostRestyleMode.h create mode 100644 dom/animation/PseudoElementHashEntry.h create mode 100644 dom/animation/ScrollTimeline.cpp create mode 100644 dom/animation/ScrollTimeline.h create mode 100644 dom/animation/ScrollTimelineAnimationTracker.cpp create mode 100644 dom/animation/ScrollTimelineAnimationTracker.h create mode 100644 dom/animation/TimingParams.cpp create mode 100644 dom/animation/TimingParams.h create mode 100644 dom/animation/ViewTimeline.cpp create mode 100644 dom/animation/ViewTimeline.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_async.html create mode 100644 dom/animation/test/chrome/test_animation_observers_sync.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_animation_properties_display.html create mode 100644 dom/animation/test/chrome/test_cssanimation_missing_keyframes.html create mode 100644 dom/animation/test/chrome/test_generated_content_getAnimations.html create mode 100644 dom/animation/test/chrome/test_keyframe_effect_xrays.html create mode 100644 dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html create mode 100644 dom/animation/test/chrome/test_running_on_compositor.html create mode 100644 dom/animation/test/chrome/test_simulate_compute_values_failure.html create mode 100644 dom/animation/test/crashtests/1134538.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/1282691-1.html create mode 100644 dom/animation/test/crashtests/1291413-1.html create mode 100644 dom/animation/test/crashtests/1291413-2.html create mode 100644 dom/animation/test/crashtests/1304886-1.html create mode 100644 dom/animation/test/crashtests/1309198-1.html create mode 100644 dom/animation/test/crashtests/1322291-1.html create mode 100644 dom/animation/test/crashtests/1322291-2.html create mode 100644 dom/animation/test/crashtests/1322382-1.html create mode 100644 dom/animation/test/crashtests/1323114-1.html create mode 100644 dom/animation/test/crashtests/1323114-2.html create mode 100644 dom/animation/test/crashtests/1323119-1.html create mode 100644 dom/animation/test/crashtests/1324554-1.html create mode 100644 dom/animation/test/crashtests/1325193-1.html create mode 100644 dom/animation/test/crashtests/1330190-1.html create mode 100644 dom/animation/test/crashtests/1330190-2.html create mode 100644 dom/animation/test/crashtests/1330513-1.html create mode 100644 dom/animation/test/crashtests/1332588-1.html create mode 100644 dom/animation/test/crashtests/1333539-1.html create mode 100644 dom/animation/test/crashtests/1333539-2.html create mode 100644 dom/animation/test/crashtests/1334582-1.html create mode 100644 dom/animation/test/crashtests/1334582-2.html create mode 100644 dom/animation/test/crashtests/1334583-1.html create mode 100644 dom/animation/test/crashtests/1335998-1.html create mode 100644 dom/animation/test/crashtests/1343589-1.html create mode 100644 dom/animation/test/crashtests/1359658-1.html create mode 100644 dom/animation/test/crashtests/1373712-1.html create mode 100644 dom/animation/test/crashtests/1379606-1.html create mode 100644 dom/animation/test/crashtests/1393605-1.html create mode 100644 dom/animation/test/crashtests/1400022-1.html create mode 100644 dom/animation/test/crashtests/1401809.html create mode 100644 dom/animation/test/crashtests/1411318-1.html create mode 100644 dom/animation/test/crashtests/1467277-1.html create mode 100644 dom/animation/test/crashtests/1468294-1.html create mode 100644 dom/animation/test/crashtests/1524480-1.html create mode 100644 dom/animation/test/crashtests/1575926.html create mode 100644 dom/animation/test/crashtests/1585770.html create mode 100644 dom/animation/test/crashtests/1604500-1.html create mode 100644 dom/animation/test/crashtests/1611847.html create mode 100644 dom/animation/test/crashtests/1612891-1.html create mode 100644 dom/animation/test/crashtests/1612891-2.html create mode 100644 dom/animation/test/crashtests/1612891-3.html create mode 100644 dom/animation/test/crashtests/1633442.html create mode 100644 dom/animation/test/crashtests/1633486.html create mode 100644 dom/animation/test/crashtests/1656419.html create mode 100644 dom/animation/test/crashtests/1699890.html create mode 100644 dom/animation/test/crashtests/1706157.html create mode 100644 dom/animation/test/crashtests/1714421.html create mode 100644 dom/animation/test/crashtests/1807966.html create mode 100644 dom/animation/test/crashtests/crashtests.list 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/empty.html create mode 100644 dom/animation/test/mozilla/file_deferred_start.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_autoremove.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_compositing.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_get_animations.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html create mode 100644 dom/animation/test/mozilla/file_disable_animations_api_timelines.html create mode 100644 dom/animation/test/mozilla/file_discrete_animations.html create mode 100644 dom/animation/test/mozilla/file_restyles.html create mode 100644 dom/animation/test/mozilla/file_transition_finish_on_compositor.html create mode 100644 dom/animation/test/mozilla/test_cascade.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_autoremove.html create mode 100644 dom/animation/test/mozilla/test_disable_animations_api_compositing.html create mode 100644 dom/animation/test/mozilla/test_disable_animations_api_get_animations.html create mode 100644 dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html create mode 100644 dom/animation/test/mozilla/test_disable_animations_api_timelines.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_distance_of_basic_shape.html create mode 100644 dom/animation/test/mozilla/test_distance_of_filter.html create mode 100644 dom/animation/test/mozilla/test_distance_of_path_function.html create mode 100644 dom/animation/test/mozilla/test_distance_of_transform.html create mode 100644 dom/animation/test/mozilla/test_document_timeline_origin_time_range.html create mode 100644 dom/animation/test/mozilla/test_event_listener_leaks.html create mode 100644 dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html create mode 100644 dom/animation/test/mozilla/test_hide_and_show.html create mode 100644 dom/animation/test/mozilla/test_mainthread_synchronization_pref.html create mode 100644 dom/animation/test/mozilla/test_moz_prefixed_properties.html create mode 100644 dom/animation/test/mozilla/test_pending_animation_tracker.html create mode 100644 dom/animation/test/mozilla/test_restyles.html create mode 100644 dom/animation/test/mozilla/test_restyling_xhr_doc.html create mode 100644 dom/animation/test/mozilla/test_set_easing.html create mode 100644 dom/animation/test/mozilla/test_style_after_finished_on_compositor.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/mozilla/test_unstyled.html create mode 100644 dom/animation/test/mozilla/xhr_doc.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_composite.html create mode 100644 dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html create mode 100644 dom/animation/test/style/test_missing-keyframe-on-compositor.html create mode 100644 dom/animation/test/style/test_missing-keyframe.html create mode 100644 dom/animation/test/style/test_transform-non-normalizable-rotate3d.html create mode 100644 dom/animation/test/testcommon.js (limited to 'dom/animation') diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp new file mode 100644 index 0000000000..7e20dfd9e6 --- /dev/null +++ b/dom/animation/Animation.cpp @@ -0,0 +1,2210 @@ +/* -*- 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 "mozAutoDocUpdate.h" +#include "mozilla/dom/AnimationBinding.h" +#include "mozilla/dom/AnimationPlaybackEvent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/DocumentTimeline.h" +#include "mozilla/dom/MutationObservers.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/AnimationEventDispatcher.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/Maybe.h" // For Maybe +#include "mozilla/StaticPrefs_dom.h" +#include "nsAnimationManager.h" // For CSSAnimation +#include "nsComputedDOMStyle.h" +#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch +#include "nsDOMCSSAttrDeclaration.h" // For nsDOMCSSAttributeDeclaration +#include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr +#include "nsTransitionManager.h" // For CSSTransition +#include "PendingAnimationTracker.h" // For PendingAnimationTracker +#include "ScrollTimelineAnimationTracker.h" + +namespace mozilla::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(Animation) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +JSObject* Animation::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::Animation_Binding::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) { + NonOwningAnimationTarget target = aAnimation.GetTargetForAnimation(); + if (!target) { + return; + } + + // For mutation observers, we use the OwnerDoc. + mAutoBatch.emplace(target.mElement->OwnerDoc()); + } + + private: + Maybe mAutoBatch; +}; +} // namespace + +// --------------------------------------------------------------------------- +// +// Animation interface: +// +// --------------------------------------------------------------------------- + +Animation::Animation(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal), + mAnimationIndex(sNextAnimationIndex++), + mRTPCallerType(aGlobal->GetRTPCallerType()) {} + +Animation::~Animation() = default; + +/* static */ +already_AddRefed Animation::ClonePausedAnimation( + nsIGlobalObject* aGlobal, const Animation& aOther, AnimationEffect& aEffect, + AnimationTimeline& aTimeline) { + // FIXME: Bug 1805950: Support printing for scroll-timeline once we resolve + // the spec issue. + if (aOther.UsingScrollTimeline()) { + return nullptr; + } + + RefPtr animation = new Animation(aGlobal); + + // Setup the timeline. We always use document-timeline of the new document, + // even if the timeline of |aOther| is null. + animation->mTimeline = &aTimeline; + + // Setup the playback rate. + animation->mPlaybackRate = aOther.mPlaybackRate; + + // Setup the timing. + const Nullable currentTime = aOther.GetCurrentTimeAsDuration(); + if (!aOther.GetTimeline()) { + // This simulates what we do in SetTimelineNoUpdate(). It's possible to + // preserve the progress if the previous timeline is a scroll-timeline. + // So for null timeline, it may have a progress and the non-null current + // time. + if (!currentTime.IsNull()) { + animation->SilentlySetCurrentTime(currentTime.Value()); + } + animation->mPreviousCurrentTime = animation->GetCurrentTimeAsDuration(); + } else { + animation->mHoldTime = currentTime; + if (!currentTime.IsNull()) { + // FIXME: Should we use |timelineTime| as previous current time here? It + // seems we should use animation->GetCurrentTimeAsDuration(), per + // UpdateFinishedState(). + const Nullable timelineTime = + aTimeline.GetCurrentTimeAsDuration(); + MOZ_ASSERT(!timelineTime.IsNull(), "Timeline not yet set"); + animation->mPreviousCurrentTime = timelineTime; + } + } + + // Setup the effect's link to this. + animation->mEffect = &aEffect; + animation->mEffect->SetAnimation(animation); + + animation->mPendingState = PendingState::PausePending; + + Document* doc = animation->GetRenderedDocument(); + MOZ_ASSERT(doc, + "Cloning animation should already have the rendered document"); + PendingAnimationTracker* tracker = doc->GetOrCreatePendingAnimationTracker(); + tracker->AddPausePending(*animation); + + // We expect our relevance to be the same as the orginal. + animation->mIsRelevant = aOther.mIsRelevant; + + animation->PostUpdate(); + animation->mTimeline->NotifyAnimationUpdated(*animation); + return animation.forget(); +} + +NonOwningAnimationTarget Animation::GetTargetForAnimation() const { + AnimationEffect* effect = GetEffect(); + NonOwningAnimationTarget target; + if (!effect || !effect->AsKeyframeEffect()) { + return target; + } + return effect->AsKeyframeEffect()->GetAnimationTarget(); +} + +/* static */ +already_AddRefed Animation::Constructor( + const GlobalObject& aGlobal, AnimationEffect* aEffect, + const Optional& aTimeline, ErrorResult& aRv) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + + AnimationTimeline* timeline; + Document* document = + AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + + if (aTimeline.WasPassed()) { + timeline = aTimeline.Value(); + } else { + if (!document) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + timeline = document->Timeline(); + } + + RefPtr animation = new Animation(global); + animation->SetTimelineNoUpdate(timeline); + animation->SetEffectNoUpdate(aEffect); + + return animation.forget(); +} + +void Animation::SetId(const nsAString& aId) { + if (mId == aId) { + return; + } + mId = aId; + MutationObservers::NotifyAnimationChanged(this); +} + +void Animation::SetEffect(AnimationEffect* aEffect) { + SetEffectNoUpdate(aEffect); + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#setting-the-target-effect +void Animation::SetEffectNoUpdate(AnimationEffect* aEffect) { + RefPtr kungFuDeathGrip(this); + + if (mEffect == aEffect) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + bool wasRelevant = mIsRelevant; + + if (mEffect) { + // 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) { + MutationObservers::NotifyAnimationRemoved(this); + } + + // Break links with the old effect and then drop it. + RefPtr oldEffect = mEffect; + mEffect = nullptr; + if (IsPartialPrerendered()) { + if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) { + oldKeyframeEffect->ResetPartialPrerendered(); + } + } + 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) { + MutationObservers::NotifyAnimationChanged(this); + } + + ReschedulePendingTasks(); + } + + MaybeScheduleReplacementCheck(); + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); +} + +void Animation::SetTimeline(AnimationTimeline* aTimeline) { + SetTimelineNoUpdate(aTimeline); + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#setting-the-timeline +void Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) { + if (mTimeline == aTimeline) { + return; + } + + StickyTimeDuration activeTime = + mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration(); + + const AnimationPlayState previousPlayState = PlayState(); + const Nullable previousCurrentTime = GetCurrentTimeAsDuration(); + // FIXME: The definition of end time in web-animation-1 is different from that + // in web-animation-2, which includes the start time. We are still using the + // definition in web-animation-1 here for now. + const TimeDuration endTime = TimeDuration(EffectEnd()); + double previousProgress = 0.0; + if (!previousCurrentTime.IsNull() && !endTime.IsZero()) { + previousProgress = + previousCurrentTime.Value().ToSeconds() / endTime.ToSeconds(); + } + + RefPtr oldTimeline = mTimeline; + if (oldTimeline) { + oldTimeline->RemoveAnimation(this); + } + + mTimeline = aTimeline; + + mResetCurrentTimeOnResume = false; + + if (mEffect) { + mEffect->UpdateNormalizedTiming(); + } + + if (mTimeline && !mTimeline->IsMonotonicallyIncreasing()) { + // If "to finite timeline" is true. + + ApplyPendingPlaybackRate(); + Nullable seekTime; + if (mPlaybackRate >= 0.0) { + seekTime.SetValue(TimeDuration()); + } else { + seekTime.SetValue(TimeDuration(EffectEnd())); + } + + switch (previousPlayState) { + case AnimationPlayState::Running: + case AnimationPlayState::Finished: + mStartTime = seekTime; + break; + case AnimationPlayState::Paused: + if (!previousCurrentTime.IsNull()) { + mResetCurrentTimeOnResume = true; + mStartTime.SetNull(); + mHoldTime.SetValue( + TimeDuration(EffectEnd().MultDouble(previousProgress))); + } else { + mStartTime = seekTime; + } + break; + case AnimationPlayState::Idle: + default: + break; + } + } else if (oldTimeline && !oldTimeline->IsMonotonicallyIncreasing() && + !previousCurrentTime.IsNull()) { + // If "from finite timeline" and previous progress is resolved. + SetCurrentTimeNoUpdate( + TimeDuration(EffectEnd().MultDouble(previousProgress))); + } + + if (!mStartTime.IsNull()) { + mHoldTime.SetNull(); + } + + if (!aTimeline) { + MaybeQueueCancelEvent(activeTime); + } + + UpdatePendingAnimationTracker(oldTimeline, aTimeline); + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + + // FIXME: Bug 1799071: Check if we need to add + // MutationObservers::NotifyAnimationChanged(this) here. +} + +// https://drafts.csswg.org/web-animations/#set-the-animation-start-time +void Animation::SetStartTime(const Nullable& aNewStartTime) { + // Return early if the start time will not change. However, if we + // are pending, then setting the start time to any value + // including the current value has the effect of aborting + // pending tasks so we should not return early in that case. + if (!Pending() && 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->GetCurrentTimeAsDuration(); + } + if (timelineTime.IsNull() && !aNewStartTime.IsNull()) { + mHoldTime.SetNull(); + } + + Nullable previousCurrentTime = GetCurrentTimeAsDuration(); + + ApplyPendingPlaybackRate(); + mStartTime = aNewStartTime; + + mResetCurrentTimeOnResume = false; + + 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()) { + MutationObservers::NotifyAnimationChanged(this); + } + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#current-time +Nullable Animation::GetCurrentTimeForHoldTime( + const Nullable& aHoldTime) const { + Nullable result; + if (!aHoldTime.IsNull()) { + result = aHoldTime; + return result; + } + + if (mTimeline && !mStartTime.IsNull()) { + Nullable timelineTime = mTimeline->GetCurrentTimeAsDuration(); + if (!timelineTime.IsNull()) { + result = CurrentTimeFromTimelineTime(timelineTime.Value(), + mStartTime.Value(), mPlaybackRate); + } + } + return result; +} + +// https://drafts.csswg.org/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) == GetCurrentTimeAsDuration()) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + SetCurrentTimeNoUpdate(aSeekTime); + + if (IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } + PostUpdate(); +} + +void Animation::SetCurrentTimeNoUpdate(const TimeDuration& aSeekTime) { + SilentlySetCurrentTime(aSeekTime); + + if (mPendingState == PendingState::PausePending) { + // Finish the pause operation + mHoldTime.SetValue(aSeekTime); + + ApplyPendingPlaybackRate(); + mStartTime.SetNull(); + + if (mReady) { + mReady->MaybeResolve(this); + } + CancelPendingTasks(); + } + + UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async); +} + +// https://drafts.csswg.org/web-animations/#set-the-playback-rate +void Animation::SetPlaybackRate(double aPlaybackRate) { + mPendingPlaybackRate.reset(); + + if (aPlaybackRate == mPlaybackRate) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + Nullable previousTime = GetCurrentTimeAsDuration(); + mPlaybackRate = aPlaybackRate; + if (!previousTime.IsNull()) { + SetCurrentTime(previousTime.Value()); + } + + // In the case where GetCurrentTimeAsDuration() 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()) { + MutationObservers::NotifyAnimationChanged(this); + } + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#seamlessly-update-the-playback-rate +void Animation::UpdatePlaybackRate(double aPlaybackRate) { + if (mPendingPlaybackRate && mPendingPlaybackRate.value() == aPlaybackRate) { + return; + } + + // Calculate the play state using the existing playback rate since below we + // want to know if the animation is _currently_ finished or not, not whether + // it _will_ be finished. + AnimationPlayState playState = PlayState(); + + mPendingPlaybackRate = Some(aPlaybackRate); + + if (Pending()) { + // If we already have a pending task, there is nothing more to do since the + // playback rate will be applied then. + // + // However, as with the idle/paused case below, we still need to update the + // relevance (and effect set to make sure it only contains relevant + // animations) since the relevance is based on the Animation play state + // which incorporates the _pending_ playback rate. + UpdateEffect(PostRestyleMode::Never); + return; + } + + AutoMutationBatchForAnimation mb(*this); + + if (playState == AnimationPlayState::Idle || + playState == AnimationPlayState::Paused || + GetCurrentTimeAsDuration().IsNull()) { + // If |previous play state| is idle or paused, or the current time is + // unresolved, we apply any pending playback rate on animation immediately. + ApplyPendingPlaybackRate(); + + // We don't need to update timing or post an update here because: + // + // * the current time hasn't changed -- it's either unresolved or fixed + // with a hold time -- so the output won't have changed + // * the finished state won't have changed even if the sign of the + // playback rate changed since we're not finished (we're paused or idle) + // * the playback rate on layers doesn't need to be updated since we're not + // moving. Once we get a start time etc. we'll update the playback rate + // then. + // + // However we still need to update the relevance and effect set as well as + // notifying observers. + UpdateEffect(PostRestyleMode::Never); + if (IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } + } else if (playState == AnimationPlayState::Finished) { + MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(), + "If we have no active timeline, we should be idle or paused"); + if (aPlaybackRate != 0) { + // The unconstrained current time can only be unresolved if either we + // don't have an active timeline (and we already asserted that is not + // true) or we have an unresolved start time (in which case we should be + // paused). + MOZ_ASSERT(!GetUnconstrainedCurrentTime().IsNull(), + "Unconstrained current time should be resolved"); + TimeDuration unconstrainedCurrentTime = + GetUnconstrainedCurrentTime().Value(); + TimeDuration timelineTime = mTimeline->GetCurrentTimeAsDuration().Value(); + mStartTime = StartTimeFromTimelineTime( + timelineTime, unconstrainedCurrentTime, aPlaybackRate); + } else { + mStartTime = mTimeline->GetCurrentTimeAsDuration(); + } + + ApplyPendingPlaybackRate(); + + // Even though we preserve the current time, we might now leave the finished + // state (e.g. if the playback rate changes sign) so we need to update + // timing. + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } + PostUpdate(); + } else { + ErrorResult rv; + Play(rv, LimitBehavior::Continue); + MOZ_ASSERT(!rv.Failed(), + "We should only fail to play when using auto-rewind behavior"); + } +} + +// https://drafts.csswg.org/web-animations/#play-state +AnimationPlayState Animation::PlayState() const { + Nullable currentTime = GetCurrentTimeAsDuration(); + if (currentTime.IsNull() && mStartTime.IsNull() && !Pending()) { + return AnimationPlayState::Idle; + } + + if (mPendingState == PendingState::PausePending || + (mStartTime.IsNull() && !Pending())) { + return AnimationPlayState::Paused; + } + + double playbackRate = CurrentOrPendingPlaybackRate(); + if (!currentTime.IsNull() && + ((playbackRate > 0.0 && currentTime.Value() >= EffectEnd()) || + (playbackRate < 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); + return nullptr; + } + if (!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); + return nullptr; + } + if (mFinishedIsResolved) { + MaybeResolveFinishedPromise(); + } + return mFinished; +} + +// https://drafts.csswg.org/web-animations/#cancel-an-animation +void Animation::Cancel(PostRestyleMode aPostRestyle) { + bool newlyIdle = false; + + if (PlayState() != AnimationPlayState::Idle) { + newlyIdle = true; + + ResetPendingTasks(); + + if (mFinished) { + mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + // mFinished can already be resolved. + MOZ_ALWAYS_TRUE(mFinished->SetAnyPromiseIsHandled()); + } + ResetFinishedPromise(); + + QueuePlaybackEvent(u"cancel"_ns, GetTimelineCurrentTimeAsTimeStamp()); + } + + StickyTimeDuration activeTime = + mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration(); + + mHoldTime.SetNull(); + mStartTime.SetNull(); + + // Allow our effect to remove itself from the its target element's EffectSet. + UpdateEffect(aPostRestyle); + + if (mTimeline) { + mTimeline->RemoveAnimation(this); + } + MaybeQueueCancelEvent(activeTime); + + if (newlyIdle && aPostRestyle == PostRestyleMode::IfNeeded) { + PostUpdate(); + } +} + +// https://drafts.csswg.org/web-animations/#finish-an-animation +void Animation::Finish(ErrorResult& aRv) { + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + + if (effectivePlaybackRate == 0) { + return aRv.ThrowInvalidStateError( + "Can't finish animation with zero playback rate"); + } + if (effectivePlaybackRate > 0 && EffectEnd() == TimeDuration::Forever()) { + return aRv.ThrowInvalidStateError("Can't finish infinite animation"); + } + + AutoMutationBatchForAnimation mb(*this); + + ApplyPendingPlaybackRate(); + + // Seek to the end + TimeDuration limit = + mPlaybackRate > 0 ? TimeDuration(EffectEnd()) : TimeDuration(0); + bool didChange = GetCurrentTimeAsDuration() != 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->GetCurrentTimeAsDuration().IsNull()) { + mStartTime = StartTimeFromTimelineTime( + mTimeline->GetCurrentTimeAsDuration().Value(), limit, 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()) { + MutationObservers::NotifyAnimationChanged(this); + } + PostUpdate(); +} + +void Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior) { + PlayNoUpdate(aRv, aLimitBehavior); + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#reverse-an-animation +void Animation::Reverse(ErrorResult& aRv) { + if (!mTimeline) { + return aRv.ThrowInvalidStateError( + "Can't reverse an animation with no associated timeline"); + } + if (mTimeline->GetCurrentTimeAsDuration().IsNull()) { + return aRv.ThrowInvalidStateError( + "Can't reverse an animation associated with an inactive timeline"); + } + + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + + if (effectivePlaybackRate == 0.0) { + return; + } + + Maybe originalPendingPlaybackRate = mPendingPlaybackRate; + + mPendingPlaybackRate = Some(-effectivePlaybackRate); + + Play(aRv, LimitBehavior::AutoRewind); + + // If Play() threw, restore state and don't report anything to mutation + // observers. + if (aRv.Failed()) { + mPendingPlaybackRate = originalPendingPlaybackRate; + } + + // Play(), above, unconditionally calls PostUpdate so we don't need to do + // it here. +} + +void Animation::Persist() { + if (mReplaceState == AnimationReplaceState::Persisted) { + return; + } + + bool wasRemoved = mReplaceState == AnimationReplaceState::Removed; + + mReplaceState = AnimationReplaceState::Persisted; + + // If the animation is not (yet) removed, there should be no side effects of + // persisting it. + if (wasRemoved) { + UpdateEffect(PostRestyleMode::IfNeeded); + PostUpdate(); + } +} + +DocGroup* Animation::GetDocGroup() { + Document* doc = GetRenderedDocument(); + return doc ? doc->GetDocGroup() : nullptr; +} + +// https://drafts.csswg.org/web-animations/#dom-animation-commitstyles +void Animation::CommitStyles(ErrorResult& aRv) { + if (!mEffect) { + return; + } + + // Take an owning reference to the keyframe effect. This will ensure that + // this Animation and the target element remain alive after flushing style. + RefPtr keyframeEffect = mEffect->AsKeyframeEffect(); + if (!keyframeEffect) { + return; + } + + NonOwningAnimationTarget target = keyframeEffect->GetAnimationTarget(); + if (!target) { + return; + } + + if (target.mPseudoType != PseudoStyleType::NotPseudo) { + return aRv.ThrowNoModificationAllowedError( + "Can't commit styles of a pseudo-element"); + } + + // Check it is an element with a style attribute + RefPtr styledElement = + nsStyledElement::FromNodeOrNull(target.mElement); + if (!styledElement) { + return aRv.ThrowNoModificationAllowedError( + "Target is not capable of having a style attribute"); + } + + // Hold onto a strong reference to the doc in case the flush destroys it. + RefPtr doc = target.mElement->GetComposedDoc(); + + // Flush frames before checking if the target element is rendered since the + // result could depend on pending style changes, and IsRendered() looks at the + // primary frame. + if (doc) { + doc->FlushPendingNotifications(FlushType::Frames); + } + if (!target.mElement->IsRendered()) { + return aRv.ThrowInvalidStateError("Target is not rendered"); + } + nsPresContext* presContext = + nsContentUtils::GetContextForContent(target.mElement); + if (!presContext) { + return aRv.ThrowInvalidStateError("Target is not rendered"); + } + + // Get the computed animation values + UniquePtr animationValues( + Servo_AnimationValueMap_Create()); + if (!presContext->EffectCompositor()->ComposeServoAnimationRuleForEffect( + *keyframeEffect, CascadeLevel(), animationValues.get())) { + NS_WARNING("Failed to compose animation style to commit"); + return; + } + + // Calling SetCSSDeclaration will trigger attribute setting code. + // Start the update now so that the old rule doesn't get used + // between when we mutate the declaration and when we set the new + // rule. + mozAutoDocUpdate autoUpdate(target.mElement->OwnerDoc(), true); + + // Get the inline style to append to + RefPtr declarationBlock; + if (auto* existing = target.mElement->GetInlineStyleDeclaration()) { + declarationBlock = existing->EnsureMutable(); + } else { + declarationBlock = new DeclarationBlock(); + declarationBlock->SetDirty(); + } + + // Prepare the callback + MutationClosureData closureData; + closureData.mShouldBeCalled = true; + closureData.mElement = target.mElement; + DeclarationBlockMutationClosure beforeChangeClosure = { + nsDOMCSSAttributeDeclaration::MutationClosureFunction, + &closureData, + }; + + // Set the animated styles + bool changed = false; + nsCSSPropertyIDSet properties = keyframeEffect->GetPropertySet(); + for (nsCSSPropertyID property : properties) { + RefPtr computedValue = + Servo_AnimationValueMap_GetValue(animationValues.get(), property) + .Consume(); + if (computedValue) { + changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue( + declarationBlock->Raw(), computedValue, beforeChangeClosure); + } + } + + if (!changed) { + MOZ_ASSERT(!closureData.mWasCalled); + return; + } + + MOZ_ASSERT(closureData.mWasCalled); + // Update inline style declaration + target.mElement->SetInlineStyleDeclaration(*declarationBlock, closureData); +} + +// --------------------------------------------------------------------------- +// +// JS wrappers for Animation interface: +// +// --------------------------------------------------------------------------- + +Nullable Animation::GetStartTimeAsDouble() const { + return AnimationUtils::TimeDurationToDouble(mStartTime, mRTPCallerType); +} + +void Animation::SetStartTimeAsDouble(const Nullable& aStartTime) { + return SetStartTime(AnimationUtils::DoubleToTimeDuration(aStartTime)); +} + +Nullable Animation::GetCurrentTimeAsDouble() const { + return AnimationUtils::TimeDurationToDouble(GetCurrentTimeAsDuration(), + mRTPCallerType); +} + +void Animation::SetCurrentTimeAsDouble(const Nullable& aCurrentTime, + ErrorResult& aRv) { + if (aCurrentTime.IsNull()) { + if (!GetCurrentTimeAsDuration().IsNull()) { + aRv.ThrowTypeError( + "Current time is resolved but trying to set it to an unresolved " + "time"); + } + 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->GetCurrentTimeAsDuration().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. + TimeDuration currentTime = mTimeline->GetCurrentTimeAsDuration().Value(); + if (currentTime < mPendingReadyTime.Value()) { + mPendingReadyTime.SetValue(currentTime); + } + FinishPendingAt(mPendingReadyTime.Value()); + mPendingReadyTime.SetNull(); + } + + if (IsPossiblyOrphanedPendingAnimation()) { + MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(), + "Orphaned pending animations should have an active timeline"); + FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value()); + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync); + + // Check for changes to whether or not this animation is replaceable. + bool isReplaceable = IsReplaceable(); + if (isReplaceable && !mWasReplaceableAtLastTick) { + ScheduleReplacementCheck(); + } + mWasReplaceableAtLastTick = isReplaceable; + + if (!mEffect) { + return; + } + + // Update layers if we are newly finished. + KeyframeEffect* 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 (!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 (!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->GetCurrentTimeAsDuration().IsNull()) { + NS_WARNING("Failed to trigger an animation with an active timeline"); + return; + } + + FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value()); +} + +bool Animation::TryTriggerNowForFiniteTimeline() { + // 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 (!Pending()) { + return true; + } + + MOZ_ASSERT(mTimeline && !mTimeline->IsMonotonicallyIncreasing()); + + // It's possible that the primary frame or the scrollable frame is not ready + // when setting up this animation. So we don't finish pending right now. In + // this case, the timeline is inactive so it is still pending. The caller + // should handle this case by trying this later once the scrollable frame is + // ready. + const auto currentTime = mTimeline->GetCurrentTimeAsDuration(); + if (currentTime.IsNull()) { + return false; + } + + FinishPendingAt(currentTime.Value()); + return true; +} + +Nullable Animation::GetCurrentOrPendingStartTime() const { + Nullable result; + + // If we have a pending playback rate, work out what start time we will use + // when we come to updating that playback rate. + // + // This logic roughly shadows that in ResumeAt but is just different enough + // that it is difficult to extract out the common functionality (and + // extracting that functionality out would make it harder to match ResumeAt up + // against the spec). + if (mPendingPlaybackRate && !mPendingReadyTime.IsNull() && + !mStartTime.IsNull()) { + // If we have a hold time, use it as the current time to match. + TimeDuration currentTimeToMatch = + !mHoldTime.IsNull() + ? mHoldTime.Value() + : CurrentTimeFromTimelineTime(mPendingReadyTime.Value(), + mStartTime.Value(), mPlaybackRate); + + result = StartTimeFromTimelineTime( + mPendingReadyTime.Value(), currentTimeToMatch, *mPendingPlaybackRate); + return 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 = StartTimeFromTimelineTime(mPendingReadyTime.Value(), + mHoldTime.Value(), mPlaybackRate); + + return result; +} + +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: + // current 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 { + TimeDuration delay = + mEffect ? mEffect->NormalizedTiming().Delay() : TimeDuration(); + return AnimationTimeToTimeStamp(aElapsedTime + delay); +} + +// https://drafts.csswg.org/web-animations/#silently-set-the-current-time +void Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) { + // TODO: Bug 1762238: Introduce "valid seek time" after introducing + // CSSNumberish time values. + // https://drafts.csswg.org/web-animations-2/#silently-set-the-current-time + + if (!mHoldTime.IsNull() || mStartTime.IsNull() || !mTimeline || + mTimeline->GetCurrentTimeAsDuration().IsNull() || mPlaybackRate == 0.0) { + mHoldTime.SetValue(aSeekTime); + } else { + mStartTime = + StartTimeFromTimelineTime(mTimeline->GetCurrentTimeAsDuration().Value(), + aSeekTime, mPlaybackRate); + } + + if (!mTimeline || mTimeline->GetCurrentTimeAsDuration().IsNull()) { + mStartTime.SetNull(); + } + + mPreviousCurrentTime.SetNull(); + mResetCurrentTimeOnResume = false; +} + +bool Animation::ShouldBeSynchronizedWithMainThread( + const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning) const { + // Only synchronize playing animations + if (!IsPlaying()) { + return false; + } + + // Currently only transform animations need to be synchronized + if (!aPropertySet.Intersects(nsCSSPropertyIDSet::TransformLikeProperties())) { + return false; + } + + KeyframeEffect* keyframeEffect = + mEffect ? mEffect->AsKeyframeEffect() : nullptr; + if (!keyframeEffect) { + return false; + } + + // Are we starting at the same time as other geometric animations? + // We check this before calling ShouldBlockAsyncTransformAnimations, partly + // because it's cheaper, but also because it's often the most useful thing + // to know when you're debugging performance. + // Note: |mSyncWithGeometricAnimations| wouldn't be set if the geometric + // animations use scroll-timeline. + if (StaticPrefs:: + dom_animations_mainthread_synchronization_with_geometric_animations() && + mSyncWithGeometricAnimations && + keyframeEffect->HasAnimationOfPropertySet( + nsCSSPropertyIDSet::TransformLikeProperties())) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformWithSyncGeometricAnimations; + return true; + } + + return keyframeEffect->ShouldBlockAsyncTransformAnimations( + aFrame, aPropertySet, aPerformanceWarning); +} + +void Animation::UpdateRelevance() { + bool wasRelevant = mIsRelevant; + mIsRelevant = mReplaceState != AnimationReplaceState::Removed && + (HasCurrentEffect() || IsInEffect()); + + // Notify animation observers. + if (wasRelevant && !mIsRelevant) { + MutationObservers::NotifyAnimationRemoved(this); + } else if (!wasRelevant && mIsRelevant) { + MutationObservers::NotifyAnimationAdded(this); + } +} + +template +bool IsMarkupAnimation(T* aAnimation) { + return aAnimation && aAnimation->IsTiedToMarkup(); +} + +// https://drafts.csswg.org/web-animations/#replaceable-animation +bool Animation::IsReplaceable() const { + // We never replace CSS animations or CSS transitions since they are managed + // by CSS. + if (IsMarkupAnimation(AsCSSAnimation()) || + IsMarkupAnimation(AsCSSTransition())) { + return false; + } + + // Only finished animations can be replaced. + if (PlayState() != AnimationPlayState::Finished) { + return false; + } + + // Already removed animations cannot be replaced. + if (ReplaceState() == AnimationReplaceState::Removed) { + return false; + } + + // We can only replace an animation if we know that, uninterfered, it would + // never start playing again. That excludes any animations on timelines that + // aren't monotonically increasing. + // + // If we don't have any timeline at all, then we can't be in the finished + // state (since we need both a resolved start time and current time for that) + // and will have already returned false above. + // + // (However, if it ever does become possible to be finished without a timeline + // then we will want to return false here since it probably suggests an + // animation being driven directly by script, in which case we can't assume + // anything about how they will behave.) + if (!GetTimeline() || !GetTimeline()->TracksWallclockTime()) { + return false; + } + + // If the animation doesn't have an effect then we can't determine if it is + // filling or not so just leave it alone. + if (!GetEffect()) { + return false; + } + + // At the time of writing we only know about KeyframeEffects. If we introduce + // other types of effects we will need to decide if they are replaceable or + // not. + MOZ_ASSERT(GetEffect()->AsKeyframeEffect(), + "Effect should be a keyframe effect"); + + // We only replace animations that are filling. + if (GetEffect()->GetComputedTiming().mProgress.IsNull()) { + return false; + } + + // We should only replace animations with a target element (since otherwise + // what other effects would we consider when determining if they are covered + // or not?). + if (!GetEffect()->AsKeyframeEffect()->GetAnimationTarget()) { + return false; + } + + return true; +} + +bool Animation::IsRemovable() const { + return ReplaceState() == AnimationReplaceState::Active && IsReplaceable(); +} + +void Animation::ScheduleReplacementCheck() { + MOZ_ASSERT( + IsReplaceable(), + "Should only schedule a replacement check for a replaceable animation"); + + // If IsReplaceable() is true, the following should also hold + MOZ_ASSERT(GetEffect()); + MOZ_ASSERT(GetEffect()->AsKeyframeEffect()); + + NonOwningAnimationTarget target = + GetEffect()->AsKeyframeEffect()->GetAnimationTarget(); + + MOZ_ASSERT(target); + + nsPresContext* presContext = + nsContentUtils::GetContextForContent(target.mElement); + if (presContext) { + presContext->EffectCompositor()->NoteElementForReducing(target); + } +} + +void Animation::MaybeScheduleReplacementCheck() { + if (!IsReplaceable()) { + return; + } + + ScheduleReplacementCheck(); +} + +void Animation::Remove() { + MOZ_ASSERT(IsRemovable(), + "Should not be trying to remove an effect that is not removable"); + + mReplaceState = AnimationReplaceState::Removed; + + UpdateEffect(PostRestyleMode::IfNeeded); + PostUpdate(); + + QueuePlaybackEvent(u"remove"_ns, GetTimelineCurrentTimeAsTimeStamp()); +} + +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) { + // Cancelled transitions no longer have an owning element. To be strictly + // correct we should store a strong reference to the owning element + // so that if we arrive here while sorting cancel events, we can sort + // them in the correct order. + // + // However, given that cancel events are almost always queued + // synchronously in some deterministic manner, we can be fairly sure + // that cancel events will be dispatched in a deterministic order + // (which is our only hard requirement until specs say otherwise). + // Furthermore, we only reach here when we have events with equal + // timestamps so this is an edge case we can probably ignore for now. + 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::WillComposeStyle() { + mFinishedAtLastComposeStyle = (PlayState() == AnimationPlayState::Finished); + + MOZ_ASSERT(mEffect); + + KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect) { + keyframeEffect->WillComposeStyle(); + } +} + +void Animation::ComposeStyle(StyleAnimationValueMap& aComposeResult, + const nsCSSPropertyIDSet& aPropertiesToSkip) { + if (!mEffect) { + 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 + // GetCurrentTimeAsDuration. 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. + bool pending = Pending(); + { + AutoRestore> restoreHoldTime(mHoldTime); + + if (pending && mHoldTime.IsNull() && !mStartTime.IsNull()) { + Nullable timeToUse = mPendingReadyTime; + if (timeToUse.IsNull() && mTimeline && mTimeline->TracksWallclockTime()) { + timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now()); + } + if (!timeToUse.IsNull()) { + mHoldTime = CurrentTimeFromTimelineTime( + timeToUse.Value(), mStartTime.Value(), mPlaybackRate); + } + } + + KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect) { + keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip); + } + } + + MOZ_ASSERT( + pending == Pending(), + "Pending state should not change during the course of compositing"); +} + +void Animation::NotifyEffectTimingUpdated() { + MOZ_ASSERT(mEffect, + "We should only update effect timing when we have a target " + "effect"); + UpdateTiming(Animation::SeekFlag::NoSeek, Animation::SyncNotifyFlag::Async); +} + +void Animation::NotifyEffectPropertiesUpdated() { + MOZ_ASSERT(mEffect, + "We should only update effect properties when we have a target " + "effect"); + + MaybeScheduleReplacementCheck(); +} + +void Animation::NotifyEffectTargetUpdated() { + MOZ_ASSERT(mEffect, + "We should only update the effect target when we have a target " + "effect"); + + MaybeScheduleReplacementCheck(); +} + +void Animation::NotifyGeometricAnimationsStartingThisFrame() { + if (!IsNewlyStarted() || !mEffect) { + return; + } + + mSyncWithGeometricAnimations = true; +} + +// https://drafts.csswg.org/web-animations/#play-an-animation +void Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) { + AutoMutationBatchForAnimation mb(*this); + + const bool isAutoRewind = aLimitBehavior == LimitBehavior::AutoRewind; + const bool abortedPause = mPendingState == PendingState::PausePending; + double effectivePlaybackRate = CurrentOrPendingPlaybackRate(); + + Nullable currentTime = GetCurrentTimeAsDuration(); + if (mResetCurrentTimeOnResume) { + currentTime.SetNull(); + mResetCurrentTimeOnResume = false; + } + + Nullable seekTime; + if (isAutoRewind) { + if (effectivePlaybackRate >= 0.0 && + (currentTime.IsNull() || currentTime.Value() < TimeDuration() || + currentTime.Value() >= EffectEnd())) { + seekTime.SetValue(TimeDuration()); + } else if (effectivePlaybackRate < 0.0 && + (currentTime.IsNull() || currentTime.Value() <= TimeDuration() || + currentTime.Value() > EffectEnd())) { + if (EffectEnd() == TimeDuration::Forever()) { + return aRv.ThrowInvalidStateError( + "Can't rewind animation with infinite effect end"); + } + seekTime.SetValue(TimeDuration(EffectEnd())); + } + } + + if (seekTime.IsNull() && mStartTime.IsNull() && currentTime.IsNull()) { + seekTime.SetValue(TimeDuration()); + } + + if (!seekTime.IsNull()) { + if (HasFiniteTimeline()) { + mStartTime = seekTime; + mHoldTime.SetNull(); + ApplyPendingPlaybackRate(); + } else { + mHoldTime = seekTime; + } + } + + bool reuseReadyPromise = false; + if (mPendingState != PendingState::NotPending) { + CancelPendingTasks(); + reuseReadyPromise = true; + } + + // If the hold time is null then we're already playing normally and, + // typically, we can bail out here. + // + // However, there are two cases where we can't do that: + // + // (a) If we just aborted a pause. In this case, for consistency, we need to + // go through the motions of doing an asynchronous start. + // + // (b) If we have timing changes (specifically a change to the playbackRate) + // that should be applied asynchronously. + // + if (mHoldTime.IsNull() && seekTime.IsNull() && !abortedPause && + !mPendingPlaybackRate) { + 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; + + // Clear flag that causes us to sync transform animations with the main + // thread for now. We'll set this when we go to set up compositor + // animations if it applies. + mSyncWithGeometricAnimations = false; + + if (HasFiniteTimeline()) { + // Always schedule a task even if we would like to let this animation + // immedidately ready, per spec. + // https://drafts.csswg.org/web-animations/#playing-an-animation-section + if (Document* doc = GetRenderedDocument()) { + doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this); + } // else: we fail to track this animation, so let the scroll frame to + // trigger it when ticking. + } else { + if (Document* doc = GetRenderedDocument()) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + tracker->AddPlayPending(*this); + } else { + TriggerOnNextTick(Nullable()); + } + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } +} + +// https://drafts.csswg.org/web-animations/#pause-an-animation +void Animation::Pause(ErrorResult& aRv) { + if (IsPausedOrPausing()) { + return; + } + + AutoMutationBatchForAnimation mb(*this); + + Nullable seekTime; + // If we are transitioning from idle, fill in the current time + if (GetCurrentTimeAsDuration().IsNull()) { + if (mPlaybackRate >= 0.0) { + seekTime.SetValue(TimeDuration(0)); + } else { + if (EffectEnd() == TimeDuration::Forever()) { + return aRv.ThrowInvalidStateError("Can't seek to infinite effect end"); + } + seekTime.SetValue(TimeDuration(EffectEnd())); + } + } + + if (!seekTime.IsNull()) { + if (HasFiniteTimeline()) { + mStartTime = seekTime; + } else { + mHoldTime = seekTime; + } + } + + 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; + + if (HasFiniteTimeline()) { + // Always schedule a task even if we would like to let this animation + // immedidately ready, per spec. + // https://drafts.csswg.org/web-animations/#playing-an-animation-section + if (Document* doc = GetRenderedDocument()) { + doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this); + } // else: we fail to track this animation, so let the scroll frame to + // trigger it when ticking. + } else { + if (Document* doc = GetRenderedDocument()) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + tracker->AddPausePending(*this); + } else { + TriggerOnNextTick(Nullable()); + } + } + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); + if (IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } + + PostUpdate(); +} + +// https://drafts.csswg.org/web-animations/#play-an-animation +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"); + + AutoMutationBatchForAnimation mb(*this); + bool hadPendingPlaybackRate = mPendingPlaybackRate.isSome(); + + if (!mHoldTime.IsNull()) { + // The hold time is set, so we don't need any special handling to preserve + // the current time. + ApplyPendingPlaybackRate(); + mStartTime = + StartTimeFromTimelineTime(aReadyTime, mHoldTime.Value(), mPlaybackRate); + if (mPlaybackRate != 0) { + mHoldTime.SetNull(); + } + } else if (!mStartTime.IsNull() && mPendingPlaybackRate) { + // Apply any pending playback rate, preserving the current time. + TimeDuration currentTimeToMatch = CurrentTimeFromTimelineTime( + aReadyTime, mStartTime.Value(), mPlaybackRate); + ApplyPendingPlaybackRate(); + mStartTime = StartTimeFromTimelineTime(aReadyTime, currentTimeToMatch, + mPlaybackRate); + if (mPlaybackRate == 0) { + mHoldTime.SetValue(currentTimeToMatch); + } + } + + mPendingState = PendingState::NotPending; + + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync); + + // If we had a pending playback rate, we will have now applied it so we need + // to notify observers. + if (hadPendingPlaybackRate && IsRelevant()) { + MutationObservers::NotifyAnimationChanged(this); + } + + 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 = CurrentTimeFromTimelineTime(aReadyTime, mStartTime.Value(), + mPlaybackRate); + } + ApplyPendingPlaybackRate(); + 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(PostRestyleMode::IfNeeded); + + if (mTimeline) { + mTimeline->NotifyAnimationUpdated(*this); + } +} + +// https://drafts.csswg.org/web-animations/#update-an-animations-finished-state +void Animation::UpdateFinishedState(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) { + Nullable unconstrainedCurrentTime = + aSeekFlag == SeekFlag::NoSeek ? GetUnconstrainedCurrentTime() + : GetCurrentTimeAsDuration(); + TimeDuration effectEnd = TimeDuration(EffectEnd()); + + if (!unconstrainedCurrentTime.IsNull() && !mStartTime.IsNull() && + mPendingState == PendingState::NotPending) { + if (mPlaybackRate > 0.0 && unconstrainedCurrentTime.Value() >= effectEnd) { + if (aSeekFlag == SeekFlag::DidSeek) { + mHoldTime = unconstrainedCurrentTime; + } else if (!mPreviousCurrentTime.IsNull()) { + mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd)); + } else { + mHoldTime.SetValue(effectEnd); + } + } else if (mPlaybackRate < 0.0 && + unconstrainedCurrentTime.Value() <= TimeDuration()) { + if (aSeekFlag == SeekFlag::DidSeek) { + mHoldTime = unconstrainedCurrentTime; + } else if (!mPreviousCurrentTime.IsNull()) { + mHoldTime.SetValue( + std::min(mPreviousCurrentTime.Value(), TimeDuration(0))); + } else { + mHoldTime.SetValue(0); + } + } else if (mPlaybackRate != 0.0 && mTimeline && + !mTimeline->GetCurrentTimeAsDuration().IsNull()) { + if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) { + mStartTime = StartTimeFromTimelineTime( + mTimeline->GetCurrentTimeAsDuration().Value(), mHoldTime.Value(), + mPlaybackRate); + } + mHoldTime.SetNull(); + } + } + + // We must recalculate the current time to take account of any mHoldTime + // changes the code above made. + mPreviousCurrentTime = GetCurrentTimeAsDuration(); + + bool currentFinishedState = PlayState() == AnimationPlayState::Finished; + if (currentFinishedState && !mFinishedIsResolved) { + DoFinishNotification(aSyncNotifyFlag); + } else if (!currentFinishedState && mFinishedIsResolved) { + ResetFinishedPromise(); + } +} + +void Animation::UpdateEffect(PostRestyleMode aPostRestyle) { + if (mEffect) { + UpdateRelevance(); + + KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); + if (keyframeEffect) { + keyframeEffect->NotifyAnimationTimingUpdated(aPostRestyle); + } + } +} + +void Animation::FlushUnanimatedStyle() const { + if (Document* doc = GetRenderedDocument()) { + doc->FlushPendingNotifications( + ChangesToFlush(FlushType::Style, false /* flush animations */)); + } +} + +void Animation::PostUpdate() { + if (!mEffect) { + return; + } + + KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect(); + if (!keyframeEffect) { + return; + } + keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +void Animation::CancelPendingTasks() { + if (mPendingState == PendingState::NotPending) { + return; + } + + if (Document* doc = GetRenderedDocument()) { + PendingAnimationTracker* tracker = doc->GetPendingAnimationTracker(); + if (tracker) { + if (mPendingState == PendingState::PlayPending) { + tracker->RemovePlayPending(*this); + } else { + tracker->RemovePausePending(*this); + } + } + } + + mPendingState = PendingState::NotPending; + mPendingReadyTime.SetNull(); +} + +// https://drafts.csswg.org/web-animations/#reset-an-animations-pending-tasks +void Animation::ResetPendingTasks() { + if (mPendingState == PendingState::NotPending) { + return; + } + + CancelPendingTasks(); + ApplyPendingPlaybackRate(); + + if (mReady) { + mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + MOZ_ALWAYS_TRUE(mReady->SetAnyPromiseIsHandled()); + mReady = nullptr; + } +} + +void Animation::ReschedulePendingTasks() { + if (mPendingState == PendingState::NotPending) { + return; + } + + mPendingReadyTime.SetNull(); + + if (Document* doc = GetRenderedDocument()) { + PendingAnimationTracker* tracker = + doc->GetOrCreatePendingAnimationTracker(); + if (mPendingState == PendingState::PlayPending && + !tracker->IsWaitingToPlay(*this)) { + tracker->AddPlayPending(*this); + } else if (mPendingState == PendingState::PausePending && + !tracker->IsWaitingToPause(*this)) { + tracker->AddPausePending(*this); + } + } +} + +// https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary +/* static*/ Animation::ProgressTimelinePosition +Animation::AtProgressTimelineBoundary( + const Nullable& aTimelineDuration, + const Nullable& aCurrentTime, + const TimeDuration& aEffectStartTime, const double aPlaybackRate) { + // Based on changed defined in: https://github.com/w3c/csswg-drafts/pull/6702 + // 1. If any of the following conditions are true: + // * the associated animation's timeline is not a progress-based timeline, + // or + // * the associated animation's timeline duration is unresolved or zero, + // or + // * the animation's playback rate is zero + // return false + // Note: We can detect a progress-based timeline by relying on the fact that + // monotonic timelines (i.e. non-progress-based timelines) have an unresolved + // timeline duration. + if (aTimelineDuration.IsNull() || aTimelineDuration.Value().IsZero() || + aPlaybackRate == 0.0) { + return ProgressTimelinePosition::NotBoundary; + } + + // 2. Let effective start time be the animation's start time if resolved, or + // zero otherwise. + const TimeDuration& effectiveStartTime = aEffectStartTime; + + // 3. Let effective timeline time be (animation's current time / animation's + // playback rate) + effective start time. + // Note: we use zero if the current time is unresolved. See the spec issue: + // https://github.com/w3c/csswg-drafts/issues/7458 + const TimeDuration effectiveTimelineTime = + (aCurrentTime.IsNull() + ? TimeDuration() + : aCurrentTime.Value().MultDouble(1.0 / aPlaybackRate)) + + effectiveStartTime; + + // 4. Let effective timeline progress be (effective timeline time / timeline + // duration) + // 5. If effective timeline progress is 0 or 1, return true, + // We avoid the division here but it is effectively the same as 4 & 5 above. + return effectiveTimelineTime.IsZero() || + (AnimationUtils::IsWithinAnimationTimeTolerance( + effectiveTimelineTime, aTimelineDuration.Value())) + ? ProgressTimelinePosition::Boundary + : ProgressTimelinePosition::NotBoundary; +} + +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/ScrollTimelineAnimationTracker to register with + // (perhaps the effect had no document) so we may + // 1. simply set mPendingState in PlayNoUpdate and relied on this method to + // catch us on the next tick, or + // 2. rely on the scroll frame to tick this animation and catch us in this + // method. + + // 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->GetCurrentTimeAsDuration().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. + Document* 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->NormalizedTiming().EndTime(); +} + +Document* Animation::GetRenderedDocument() const { + if (!mEffect || !mEffect->AsKeyframeEffect()) { + return nullptr; + } + + return mEffect->AsKeyframeEffect()->GetRenderedDocument(); +} + +Document* Animation::GetTimelineDocument() const { + return mTimeline ? mTimeline->GetDocument() : nullptr; +} + +void Animation::UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline, + AnimationTimeline* aNewTimeline) { + // If we are still in pending, we may have to move this animation into the + // correct animation tracker. + Document* doc = GetRenderedDocument(); + if (!doc || !Pending()) { + return; + } + + const bool fromFiniteTimeline = + aOldTimeline && !aOldTimeline->IsMonotonicallyIncreasing(); + const bool toFiniteTimeline = + aNewTimeline && !aNewTimeline->IsMonotonicallyIncreasing(); + if (fromFiniteTimeline == toFiniteTimeline) { + return; + } + + const bool isPlayPending = mPendingState == PendingState::PlayPending; + if (toFiniteTimeline) { + // From null/document-timeline to scroll-timeline + if (auto* tracker = doc->GetPendingAnimationTracker()) { + if (isPlayPending) { + tracker->RemovePlayPending(*this); + } else { + tracker->RemovePausePending(*this); + } + } + + doc->GetOrCreateScrollTimelineAnimationTracker()->AddPending(*this); + } else { + // From scroll-timeline to null/document-timeline + if (auto* tracker = doc->GetScrollTimelineAnimationTracker()) { + tracker->RemovePending(*this); + } + + auto* tracker = doc->GetOrCreatePendingAnimationTracker(); + if (isPlayPending) { + tracker->AddPlayPending(*this); + } else { + tracker->AddPausePending(*this); + } + } +} + +class AsyncFinishNotification : public MicroTaskRunnable { + public: + explicit AsyncFinishNotification(Animation* aAnimation) + : MicroTaskRunnable(), mAnimation(aAnimation) {} + + virtual void Run(AutoSlowOperation& aAso) override { + mAnimation->DoFinishNotificationImmediately(this); + mAnimation = nullptr; + } + + virtual bool Suppressed() override { + nsIGlobalObject* global = mAnimation->GetOwnerGlobal(); + return global && global->IsInSyncOperation(); + } + + private: + RefPtr mAnimation; +}; + +void Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) { + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + + if (aSyncNotifyFlag == SyncNotifyFlag::Sync) { + DoFinishNotificationImmediately(); + } else if (!mFinishNotificationTask) { + RefPtr runnable = new AsyncFinishNotification(this); + context->DispatchToMicroTask(do_AddRef(runnable)); + mFinishNotificationTask = std::move(runnable); + } +} + +void Animation::ResetFinishedPromise() { + mFinishedIsResolved = false; + mFinished = nullptr; +} + +void Animation::MaybeResolveFinishedPromise() { + if (mFinished) { + mFinished->MaybeResolve(this); + } + mFinishedIsResolved = true; +} + +void Animation::DoFinishNotificationImmediately(MicroTaskRunnable* aAsync) { + if (aAsync && aAsync != mFinishNotificationTask) { + return; + } + + mFinishNotificationTask = nullptr; + + if (PlayState() != AnimationPlayState::Finished) { + return; + } + + MaybeResolveFinishedPromise(); + + QueuePlaybackEvent(u"finish"_ns, AnimationTimeToTimeStamp(EffectEnd())); +} + +void Animation::QueuePlaybackEvent(const nsAString& aName, + TimeStamp&& aScheduledEventTime) { + // Use document for timing. + // https://drafts.csswg.org/web-animations-1/#document-for-timing + Document* doc = GetTimelineDocument(); + if (!doc) { + return; + } + + nsPresContext* presContext = doc->GetPresContext(); + if (!presContext) { + return; + } + + AnimationPlaybackEventInit init; + + if (aName.EqualsLiteral("finish") || aName.EqualsLiteral("remove")) { + init.mCurrentTime = GetCurrentTimeAsDouble(); + } + if (mTimeline) { + init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble(); + } + + RefPtr event = + AnimationPlaybackEvent::Constructor(this, aName, init); + event->SetTrusted(true); + + presContext->AnimationEventDispatcher()->QueueEvent(AnimationEventInfo( + aName, std::move(event), std::move(aScheduledEventTime), this)); +} + +bool Animation::IsRunningOnCompositor() const { + return mEffect && mEffect->AsKeyframeEffect() && + mEffect->AsKeyframeEffect()->IsRunningOnCompositor(); +} + +bool Animation::HasCurrentEffect() const { + return GetEffect() && GetEffect()->IsCurrent(); +} + +bool Animation::IsInEffect() const { + return GetEffect() && GetEffect()->IsInEffect(); +} + +void Animation::SetHiddenByContentVisibility(bool hidden) { + if (mHiddenByContentVisibility == hidden) { + return; + } + + mHiddenByContentVisibility = hidden; + + if (!GetTimeline()) { + return; + } + + GetTimeline()->NotifyAnimationContentVisibilityChanged(this, !hidden); +} + +StickyTimeDuration Animation::IntervalStartTime( + const StickyTimeDuration& aActiveDuration) const { + MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(), + "Should be called for CSS animations or transitions"); + static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); + return std::max( + std::min(StickyTimeDuration(-mEffect->NormalizedTiming().Delay()), + aActiveDuration), + zeroDuration); +} + +// Later side of the elapsed time range reported in CSS Animations and CSS +// Transitions events. +// +// https://drafts.csswg.org/css-animations-2/#interval-end +// https://drafts.csswg.org/css-transitions-2/#interval-end +StickyTimeDuration Animation::IntervalEndTime( + const StickyTimeDuration& aActiveDuration) const { + MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(), + "Should be called for CSS animations or transitions"); + + static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); + const StickyTimeDuration& effectEnd = EffectEnd(); + + // If both "associated effect end" and "start delay" are Infinity, we skip it + // because we will get NaN when computing "Infinity - Infinity", and + // using NaN in std::min or std::max is undefined. + if (MOZ_UNLIKELY(effectEnd == TimeDuration::Forever() && + effectEnd == mEffect->NormalizedTiming().Delay())) { + // Note: If we use TimeDuration::Forever(), within our animation event + // handling, we'd end up turning that into a null TimeStamp which can causes + // errors if we try to do any arithmetic with it. Given that we should never + // end up _using_ the interval end time. So returning zeroDuration here is + // probably fine. + return zeroDuration; + } + + return std::max(std::min(effectEnd - mEffect->NormalizedTiming().Delay(), + aActiveDuration), + zeroDuration); +} + +} // namespace mozilla::dom diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h new file mode 100644 index 0000000000..bbe2ab7a3e --- /dev/null +++ b/dom/animation/Animation.h @@ -0,0 +1,709 @@ +/* -*- 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 "X11UndefineNone.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/AnimationPerformanceWarning.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel +#include "mozilla/LinkedList.h" +#include "mozilla/Maybe.h" +#include "mozilla/PostRestyleMode.h" +#include "mozilla/StickyTimeDuration.h" +#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration +#include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState +#include "mozilla/dom/AnimationTimeline.h" + +struct JSContext; +class nsCSSPropertyIDSet; +class nsIFrame; +class nsIGlobalObject; + +namespace mozilla { + +struct AnimationRule; +class MicroTaskRunnable; + +namespace dom { + +class AnimationEffect; +class AsyncFinishNotification; +class CSSAnimation; +class CSSTransition; +class Document; +class Promise; + +class Animation : public DOMEventTargetHelper, + public LinkedListElement { + protected: + virtual ~Animation(); + + public: + explicit Animation(nsIGlobalObject* aGlobal); + + // Constructs a copy of |aOther| with a new effect and timeline. + // This is only intended to be used while making a static clone of a document + // during printing, and does not assume that |aOther| is in the same document + // as any of the other arguments. + static already_AddRefed ClonePausedAnimation( + nsIGlobalObject* aGlobal, const Animation& aOther, + AnimationEffect& aEffect, AnimationTimeline& aTimeline); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation, DOMEventTargetHelper) + + nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); } + + /** + * Utility function to get the target (pseudo-)element associated with an + * animation. + */ + NonOwningAnimationTarget GetTargetForAnimation() const; + + 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, AnimationEffect* aEffect, + const Optional& aTimeline, ErrorResult& aRv); + + void GetId(nsAString& aResult) const { aResult = mId; } + void SetId(const nsAString& aId); + + AnimationEffect* GetEffect() const { return mEffect; } + virtual void SetEffect(AnimationEffect* aEffect); + void SetEffectNoUpdate(AnimationEffect* aEffect); + + // FIXME: Bug 1676794. This is a tentative solution before we implement + // ScrollTimeline interface. If the timeline is scroll/view timeline, we + // return null. Once we implement ScrollTimeline interface, we can drop this. + already_AddRefed GetTimelineFromJS() const { + return mTimeline && mTimeline->IsScrollTimeline() ? nullptr + : do_AddRef(mTimeline); + } + void SetTimelineFromJS(AnimationTimeline* aTimeline) { + SetTimeline(aTimeline); + } + + AnimationTimeline* GetTimeline() const { return mTimeline; } + void SetTimeline(AnimationTimeline* aTimeline); + void SetTimelineNoUpdate(AnimationTimeline* aTimeline); + + Nullable GetStartTime() const { return mStartTime; } + Nullable GetStartTimeAsDouble() const; + void SetStartTime(const Nullable& aNewStartTime); + virtual void SetStartTimeAsDouble(const Nullable& aStartTime); + + // This is deliberately _not_ called GetCurrentTime since that would clash + // with a macro defined in winbase.h + Nullable GetCurrentTimeAsDuration() const { + return GetCurrentTimeForHoldTime(mHoldTime); + } + Nullable GetCurrentTimeAsDouble() const; + void SetCurrentTime(const TimeDuration& aSeekTime); + void SetCurrentTimeNoUpdate(const TimeDuration& aSeekTime); + void SetCurrentTimeAsDouble(const Nullable& aCurrentTime, + ErrorResult& aRv); + + double PlaybackRate() const { return mPlaybackRate; } + void SetPlaybackRate(double aPlaybackRate); + + AnimationPlayState PlayState() const; + virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); } + + bool Pending() const { return mPendingState != PendingState::NotPending; } + virtual bool PendingFromJS() const { return Pending(); } + AnimationReplaceState ReplaceState() const { return mReplaceState; } + + virtual Promise* GetReady(ErrorResult& aRv); + Promise* GetFinished(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(finish); + IMPL_EVENT_HANDLER(cancel); + IMPL_EVENT_HANDLER(remove); + + void Cancel(PostRestyleMode aPostRestyle = PostRestyleMode::IfNeeded); + + void Finish(ErrorResult& aRv); + + void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior); + virtual void PlayFromJS(ErrorResult& aRv) { + Play(aRv, LimitBehavior::AutoRewind); + } + + void Pause(ErrorResult& aRv); + virtual void PauseFromJS(ErrorResult& aRv) { Pause(aRv); } + + void UpdatePlaybackRate(double aPlaybackRate); + virtual void Reverse(ErrorResult& aRv); + + void Persist(); + MOZ_CAN_RUN_SCRIPT void CommitStyles(ErrorResult& aRv); + + bool IsRunningOnCompositor() const; + + virtual void Tick(); + bool NeedsTicks() const { + return Pending() || + (PlayState() == AnimationPlayState::Running && + // An animation with a zero playback rate doesn't need ticks even if + // it is running since it effectively behaves as if it is paused. + // + // It's important we return false in this case since a zero playback + // rate animation in the before or after phase that doesn't fill + // won't be relevant and hence won't be returned by GetAnimations(). + // We don't want its timeline to keep it alive (which would happen + // if we return true) since otherwise it will effectively be leaked. + PlaybackRate() != 0.0) || + // Always return true for not idle animations attached to not + // monotonically increasing timelines even if the animation is + // finished. This is required to accommodate cases where timeline + // ticks back in time. + (mTimeline && !mTimeline->IsMonotonicallyIncreasing() && + PlayState() != AnimationPlayState::Idle); + } + + /** + * 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 TriggerOnNextTick. + * 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); + /** + * For the monotonically increasing timeline, we use this only for testing: + * 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(); + /** + * For the non-monotonically increasing timeline (e.g. ScrollTimeline), we try + * to trigger it in ScrollTimelineAnimationTracker by this method. This uses + * the current scroll position as the ready time. Return true if we don't need + * to trigger it or we trigger it successfully. + */ + bool TryTriggerNowForFiniteTimeline(); + /** + * When TriggerOnNextTick 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; + + /** + * As with the start time, we should use the pending playback rate when + * producing layer animations. + */ + double CurrentOrPendingPlaybackRate() const { + return mPendingPlaybackRate.valueOr(mPlaybackRate); + } + bool HasPendingPlaybackRate() const { return mPendingPlaybackRate.isSome(); } + + /** + * The following relationship from the definition of the 'current time' is + * re-used in many algorithms so we extract it here into a static method that + * can be re-used: + * + * current time = (timeline time - start time) * playback rate + * + * As per https://drafts.csswg.org/web-animations-1/#current-time + */ + static TimeDuration CurrentTimeFromTimelineTime( + const TimeDuration& aTimelineTime, const TimeDuration& aStartTime, + float aPlaybackRate) { + return (aTimelineTime - aStartTime).MultDouble(aPlaybackRate); + } + + /** + * As with calculating the current time, we often need to calculate a start + * time from a current time. The following method simply inverts the current + * time relationship. + * + * In each case where this is used, the desired behavior for playbackRate == + * 0 is to return the specified timeline time (often referred to as the ready + * time). + */ + static TimeDuration StartTimeFromTimelineTime( + const TimeDuration& aTimelineTime, const TimeDuration& aCurrentTime, + float aPlaybackRate) { + TimeDuration result = aTimelineTime; + if (aPlaybackRate == 0) { + return result; + } + + result -= aCurrentTime.MultDouble(1.0 / aPlaybackRate); + return result; + } + + /** + * 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; + } + + bool HasCurrentEffect() const; + bool IsInEffect() const; + + bool IsPlaying() const { + return mPlaybackRate != 0.0 && mTimeline && + !mTimeline->GetCurrentTimeAsDuration().IsNull() && + PlayState() == AnimationPlayState::Running; + } + + bool ShouldBeSynchronizedWithMainThread( + const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const; + + bool IsRelevant() const { return mIsRelevant; } + void UpdateRelevance(); + + // https://drafts.csswg.org/web-animations-1/#replaceable-animation + bool IsReplaceable() const; + + /** + * Returns true if this Animation satisfies the requirements for being + * removed when it is replaced. + * + * Returning true does not imply this animation _should_ be removed. + * Determining that depends on the other effects in the same EffectSet to + * which this animation's effect, if any, contributes. + */ + bool IsRemovable() const; + + /** + * Make this animation's target effect no-longer part of the effect stack + * while preserving its timing information. + */ + void Remove(); + + /** + * 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 various bits of state that we need to update as the result of + * running ComposeStyle(). + * See the comment of KeyframeEffect::WillComposeStyle for more detail. + */ + void WillComposeStyle(); + + /** + * Updates |aComposeResult| with the animation values of this animation's + * effect, if any. + * Any properties contained in |aPropertiesToSkip| will not be added or + * updated in |aComposeResult|. + */ + void ComposeStyle(StyleAnimationValueMap& aComposeResult, + const nsCSSPropertyIDSet& aPropertiesToSkip); + + void NotifyEffectTimingUpdated(); + void NotifyEffectPropertiesUpdated(); + void NotifyEffectTargetUpdated(); + void NotifyGeometricAnimationsStartingThisFrame(); + + /** + * Reschedule pending pause or pending play tasks when updating the target + * effect. + * + * If we are pending, we will either be registered in the pending animation + * tracker and have a null pending ready time, or, after our effect has been + * painted, we will be removed from the tracker and assigned a pending ready + * time. + * + * When the target effect is updated, we'll typically need to repaint so for + * the latter case where we already have a pending ready time, clear it and + * put ourselves back in the pending animation tracker. + */ + void ReschedulePendingTasks(); + + /** + * Used by subclasses to synchronously queue a cancel event in situations + * where the Animation may have been cancelled. + * + * We need to do this synchronously because after a CSS animation/transition + * is canceled, it will be released by its owning element and may not still + * exist when we would normally go to queue events on the next tick. + */ + virtual void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime){}; + + Maybe& CachedChildIndexRef() { return mCachedChildIndex; } + + void SetPartialPrerendered(uint64_t aIdOnCompositor) { + mIdOnCompositor = aIdOnCompositor; + mIsPartialPrerendered = true; + } + bool IsPartialPrerendered() const { return mIsPartialPrerendered; } + uint64_t IdOnCompositor() const { return mIdOnCompositor; } + /** + * Needs to be called when the pre-rendered animation is going to no longer + * run on the compositor. + */ + void ResetPartialPrerendered() { + MOZ_ASSERT(mIsPartialPrerendered); + mIsPartialPrerendered = false; + mIdOnCompositor = 0; + } + /** + * Called via NotifyJankedAnimations IPC call from the compositor to update + * pre-rendered area on the main-thread. + */ + void UpdatePartialPrerendered() { + ResetPartialPrerendered(); + PostUpdate(); + } + + bool UsingScrollTimeline() const { + return mTimeline && mTimeline->IsScrollTimeline(); + } + + /** + * Returns true if this is at the progress timeline boundary. + * https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary + */ + enum class ProgressTimelinePosition : uint8_t { Boundary, NotBoundary }; + static ProgressTimelinePosition AtProgressTimelineBoundary( + const Nullable& aTimelineDuration, + const Nullable& aCurrentTime, + const TimeDuration& aEffectStartTime, const double aPlaybackRate); + ProgressTimelinePosition AtProgressTimelineBoundary() const { + Nullable currentTime = GetUnconstrainedCurrentTime(); + return AtProgressTimelineBoundary( + mTimeline ? mTimeline->TimelineDuration() : nullptr, + // Set unlimited current time based on the first matching condition: + // 1. start time is resolved: + // (timeline time - start time) × playback rate + // 2. Otherwise: + // animation’s current time + !currentTime.IsNull() ? currentTime : GetCurrentTimeAsDuration(), + mStartTime.IsNull() ? TimeDuration() : mStartTime.Value(), + mPlaybackRate); + } + + void SetHiddenByContentVisibility(bool hidden); + bool IsHiddenByContentVisibility() const { + return mHiddenByContentVisibility; + } + + DocGroup* GetDocGroup(); + + protected: + void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime); + void CancelNoUpdate(); + void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior); + 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 { + MOZ_ASSERT_UNREACHABLE( + "Can't finish pending if we're not in a pending state"); + } + } + void ApplyPendingPlaybackRate() { + if (mPendingPlaybackRate) { + mPlaybackRate = *mPendingPlaybackRate; + mPendingPlaybackRate.reset(); + } + } + + /** + * 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(PostRestyleMode aPostRestyle); + /** + * Flush all pending styles other than throttled animation styles (e.g. + * animations running on the compositor). + */ + void FlushUnanimatedStyle() const; + void PostUpdate(); + void ResetFinishedPromise(); + void MaybeResolveFinishedPromise(); + void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag); + friend class AsyncFinishNotification; + void DoFinishNotificationImmediately(MicroTaskRunnable* aAsync = nullptr); + void QueuePlaybackEvent(const nsAString& aName, + TimeStamp&& aScheduledEventTime); + + /** + * 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(); + + /** + * Returns true if this animation is not only play-pending, but has + * yet to be given a pending ready time. This roughly corresponds to + * animations that are waiting to be painted (since we set the pending + * ready time at the end of painting). Identifying such animations is + * useful because in some cases animations that are painted together + * may need to be synchronized. + * + * We don't, however, want to include animations with a fixed start time such + * as animations that are simply having their playbackRate updated or which + * are resuming from an aborted pause. + */ + bool IsNewlyStarted() const { + return mPendingState == PendingState::PlayPending && + mPendingReadyTime.IsNull() && mStartTime.IsNull(); + } + bool IsPossiblyOrphanedPendingAnimation() const; + StickyTimeDuration EffectEnd() const; + + Nullable GetCurrentTimeForHoldTime( + const Nullable& aHoldTime) const; + Nullable GetUnconstrainedCurrentTime() const { + return GetCurrentTimeForHoldTime(Nullable()); + } + + void ScheduleReplacementCheck(); + void MaybeScheduleReplacementCheck(); + + // Earlier side of the elapsed time range reported in CSS Animations and CSS + // Transitions events. + // + // https://drafts.csswg.org/css-animations-2/#interval-start + // https://drafts.csswg.org/css-transitions-2/#interval-start + StickyTimeDuration IntervalStartTime( + const StickyTimeDuration& aActiveDuration) const; + + // Later side of the elapsed time range reported in CSS Animations and CSS + // Transitions events. + // + // https://drafts.csswg.org/css-animations-2/#interval-end + // https://drafts.csswg.org/css-transitions-2/#interval-end + StickyTimeDuration IntervalEndTime( + const StickyTimeDuration& aActiveDuration) const; + + TimeStamp GetTimelineCurrentTimeAsTimeStamp() const { + return mTimeline ? mTimeline->GetCurrentTimeAsTimeStamp() : TimeStamp(); + } + + Document* GetRenderedDocument() const; + Document* GetTimelineDocument() const; + + bool HasFiniteTimeline() const { + return mTimeline && !mTimeline->IsMonotonicallyIncreasing(); + } + + void UpdatePendingAnimationTracker(AnimationTimeline* aOldTimeline, + AnimationTimeline* aNewTimeline); + + 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 = 1.0; + Maybe mPendingPlaybackRate; + + // 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://drafts.csswg.org/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://drafts.csswg.org/web-animations/#current-finished-promise + RefPtr mFinished; + + static uint64_t sNextAnimationIndex; + + // The relative position of this animation within the global animation list. + // + // 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; + + // While ordering Animation objects for event dispatch, the index of the + // target node in its parent may be cached in mCachedChildIndex. + Maybe mCachedChildIndex; + + // 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 : uint8_t { NotPending, PlayPending, PausePending }; + PendingState mPendingState = PendingState::NotPending; + + // Handling of this animation's target effect when filling while finished. + AnimationReplaceState mReplaceState = AnimationReplaceState::Active; + + bool mFinishedAtLastComposeStyle = false; + bool mWasReplaceableAtLastTick = false; + + bool mHiddenByContentVisibility = false; + + // Indicates that the animation should be exposed in an element's + // getAnimations() list. + bool mIsRelevant = false; + + // 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 = false; + + // True if this animation was triggered at the same time as one or more + // geometric animations and hence we should run any transform animations on + // the main thread. + bool mSyncWithGeometricAnimations = false; + + RefPtr mFinishNotificationTask; + + nsString mId; + + bool mResetCurrentTimeOnResume = false; + + // Whether the Animation is System, ResistFingerprinting, or neither + RTPCallerType mRTPCallerType; + + private: + // The id for this animaiton on the compositor. + uint64_t mIdOnCompositor = 0; + bool mIsPartialPrerendered = false; +}; + +} // 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 0000000000..080eb42475 --- /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 + +#include "mozilla/dom/Animation.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/AnimationEffect.cpp b/dom/animation/AnimationEffect.cpp new file mode 100644 index 0000000000..6ef8c30d49 --- /dev/null +++ b/dom/animation/AnimationEffect.cpp @@ -0,0 +1,370 @@ +/* -*- 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/AnimationEffect.h" +#include "mozilla/dom/AnimationEffectBinding.h" + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/dom/MutationObservers.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/FloatingPoint.h" +#include "nsDOMMutationObserver.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationEffect) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffect) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mAnimation) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffect) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mAnimation) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffect) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffect) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffect) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +AnimationEffect::AnimationEffect(Document* aDocument, TimingParams&& aTiming) + : mDocument(aDocument), mTiming(std::move(aTiming)) { + mRTPCallerType = mDocument->GetScopeObject()->GetRTPCallerType(); +} + +AnimationEffect::~AnimationEffect() = default; + +nsISupports* AnimationEffect::GetParentObject() const { + return ToSupports(mDocument); +} + +// https://drafts.csswg.org/web-animations/#current +bool AnimationEffect::IsCurrent() const { + if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) { + return false; + } + + ComputedTiming computedTiming = GetComputedTiming(); + if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) { + return true; + } + + return (mAnimation->PlaybackRate() > 0 && + computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) || + (mAnimation->PlaybackRate() < 0 && + computedTiming.mPhase == ComputedTiming::AnimationPhase::After); +} + +// https://drafts.csswg.org/web-animations/#in-effect +bool AnimationEffect::IsInEffect() const { + ComputedTiming computedTiming = GetComputedTiming(); + return !computedTiming.mProgress.IsNull(); +} + +void AnimationEffect::SetSpecifiedTiming(TimingParams&& aTiming) { + if (mTiming == aTiming) { + return; + } + + mTiming = aTiming; + + UpdateNormalizedTiming(); + + if (mAnimation) { + Maybe mb; + if (AsKeyframeEffect() && AsKeyframeEffect()->GetAnimationTarget()) { + mb.emplace(AsKeyframeEffect()->GetAnimationTarget().mElement->OwnerDoc()); + } + + mAnimation->NotifyEffectTimingUpdated(); + + if (mAnimation->IsRelevant()) { + MutationObservers::NotifyAnimationChanged(mAnimation); + } + + if (AsKeyframeEffect()) { + AsKeyframeEffect()->RequestRestyle(EffectCompositor::RestyleType::Layer); + } + } + + // For keyframe effects, NotifyEffectTimingUpdated above will eventually + // cause KeyframeEffect::NotifyAnimationTimingUpdated to be called so it can + // update its registration with the target element as necessary. +} + +ComputedTiming AnimationEffect::GetComputedTimingAt( + const Nullable& aLocalTime, const TimingParams& aTiming, + double aPlaybackRate, + Animation::ProgressTimelinePosition aProgressTimelinePosition) { + static const StickyTimeDuration zeroDuration; + + // Always return the same object to benefit from return-value optimization. + ComputedTiming result; + + if (aTiming.Duration()) { + MOZ_ASSERT(aTiming.Duration().ref() >= zeroDuration, + "Iteration duration should be positive"); + result.mDuration = aTiming.Duration().ref(); + } + + MOZ_ASSERT(aTiming.Iterations() >= 0.0 && !std::isnan(aTiming.Iterations()), + "mIterations should be nonnegative & finite, as ensured by " + "ValidateIterations or CSSParser"); + result.mIterations = aTiming.Iterations(); + + MOZ_ASSERT(aTiming.IterationStart() >= 0.0, + "mIterationStart should be nonnegative, as ensured by " + "ValidateIterationStart"); + result.mIterationStart = aTiming.IterationStart(); + + result.mActiveDuration = aTiming.ActiveDuration(); + result.mEndTime = aTiming.EndTime(); + result.mFill = aTiming.Fill() == dom::FillMode::Auto ? dom::FillMode::None + : aTiming.Fill(); + + // 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(); + const bool atProgressTimelineBoundary = + aProgressTimelinePosition == + Animation::ProgressTimelinePosition::Boundary; + + StickyTimeDuration beforeActiveBoundary = aTiming.CalcBeforeActiveBoundary(); + StickyTimeDuration activeAfterBoundary = aTiming.CalcActiveAfterBoundary(); + + if (localTime > activeAfterBoundary || + (aPlaybackRate >= 0 && localTime == activeAfterBoundary && + !atProgressTimelineBoundary)) { + result.mPhase = ComputedTiming::AnimationPhase::After; + if (!result.FillsForwards()) { + // The animation isn't active or filling at this time. + return result; + } + result.mActiveTime = + std::max(std::min(StickyTimeDuration(localTime - aTiming.Delay()), + result.mActiveDuration), + zeroDuration); + } else if (localTime < beforeActiveBoundary || + (aPlaybackRate < 0 && localTime == beforeActiveBoundary && + !atProgressTimelineBoundary)) { + result.mPhase = ComputedTiming::AnimationPhase::Before; + if (!result.FillsBackwards()) { + // The animation isn't active or filling at this time. + return result; + } + result.mActiveTime = + std::max(StickyTimeDuration(localTime - aTiming.Delay()), zeroDuration); + } else { + // Note: For progress-based timeline, it's possible to have a zero active + // duration with active phase. + result.mPhase = ComputedTiming::AnimationPhase::Active; + result.mActiveTime = localTime - aTiming.Delay(); + } + + // Convert active time to a multiple of iterations. + // https://drafts.csswg.org/web-animations/#overall-progress + double overallProgress; + if (!result.mDuration) { + overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before + ? 0.0 + : result.mIterations; + } else { + overallProgress = result.mActiveTime / result.mDuration; + } + + // Factor in iteration start offset. + if (std::isfinite(overallProgress)) { + overallProgress += result.mIterationStart; + } + + // Determine the 0-based index of the current iteration. + // https://drafts.csswg.org/web-animations/#current-iteration + result.mCurrentIteration = + (result.mIterations >= double(UINT64_MAX) && + result.mPhase == ComputedTiming::AnimationPhase::After) || + overallProgress >= double(UINT64_MAX) + ? UINT64_MAX // In GetComputedTimingDictionary(), + // we will convert this into Infinity + : static_cast(std::max(overallProgress, 0.0)); + + // Convert the overall progress to a fraction of a single iteration--the + // simply iteration progress. + // https://drafts.csswg.org/web-animations/#simple-iteration-progress + double progress = std::isfinite(overallProgress) + ? fmod(overallProgress, 1.0) + : fmod(result.mIterationStart, 1.0); + + // When we are at the end of the active interval and 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, however, when we have + // a zero-iteration animation. + if (progress == 0.0 && + (result.mPhase == ComputedTiming::AnimationPhase::After || + result.mPhase == ComputedTiming::AnimationPhase::Active) && + result.mActiveTime == result.mActiveDuration && + result.mIterations != 0.0) { + // The only way we can reach the end of the active interval and have + // a progress of zero and a current iteration of zero, is if we have a + // zero iteration count -- something 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.Direction()) { + 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_UNREACHABLE("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 = true; + } + + // Apply the easing. + if (const auto& fn = aTiming.TimingFunction()) { + progress = fn->At(progress, result.mBeforeFlag); + } + + MOZ_ASSERT(std::isfinite(progress), "Progress value should be finite"); + result.mProgress.SetValue(progress); + return result; +} + +ComputedTiming AnimationEffect::GetComputedTiming( + const TimingParams* aTiming) const { + const double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1; + const auto progressTimelinePosition = + mAnimation ? mAnimation->AtProgressTimelineBoundary() + : Animation::ProgressTimelinePosition::NotBoundary; + return GetComputedTimingAt(GetLocalTime(), + aTiming ? *aTiming : NormalizedTiming(), + playbackRate, progressTimelinePosition); +} + +// Helper function for generating an (Computed)EffectTiming dictionary +static void GetEffectTimingDictionary(const TimingParams& aTiming, + EffectTiming& aRetVal) { + aRetVal.mDelay = aTiming.Delay().ToMilliseconds(); + aRetVal.mEndDelay = aTiming.EndDelay().ToMilliseconds(); + aRetVal.mFill = aTiming.Fill(); + aRetVal.mIterationStart = aTiming.IterationStart(); + aRetVal.mIterations = aTiming.Iterations(); + if (aTiming.Duration()) { + aRetVal.mDuration.SetAsUnrestrictedDouble() = + aTiming.Duration()->ToMilliseconds(); + } + aRetVal.mDirection = aTiming.Direction(); + if (aTiming.TimingFunction()) { + aRetVal.mEasing.Truncate(); + aTiming.TimingFunction()->AppendToString(aRetVal.mEasing); + } +} + +void AnimationEffect::GetTiming(EffectTiming& aRetVal) const { + GetEffectTimingDictionary(SpecifiedTiming(), aRetVal); +} + +void AnimationEffect::GetComputedTimingAsDict( + ComputedEffectTiming& aRetVal) const { + // Specified timing + GetEffectTimingDictionary(SpecifiedTiming(), aRetVal); + + // Computed timing + double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1; + const Nullable currentTime = GetLocalTime(); + const auto progressTimelinePosition = + mAnimation ? mAnimation->AtProgressTimelineBoundary() + : Animation::ProgressTimelinePosition::NotBoundary; + ComputedTiming computedTiming = GetComputedTimingAt( + currentTime, SpecifiedTiming(), playbackRate, progressTimelinePosition); + + aRetVal.mDuration.SetAsUnrestrictedDouble() = + computedTiming.mDuration.ToMilliseconds(); + aRetVal.mFill = computedTiming.mFill; + aRetVal.mActiveDuration = computedTiming.mActiveDuration.ToMilliseconds(); + aRetVal.mEndTime = computedTiming.mEndTime.ToMilliseconds(); + aRetVal.mLocalTime = + AnimationUtils::TimeDurationToDouble(currentTime, mRTPCallerType); + aRetVal.mProgress = computedTiming.mProgress; + + if (!aRetVal.mProgress.IsNull()) { + // Convert the returned currentIteration into Infinity if we set + // (uint64_t) computedTiming.mCurrentIteration to UINT64_MAX + double iteration = + computedTiming.mCurrentIteration == UINT64_MAX + ? PositiveInfinity() + : static_cast(computedTiming.mCurrentIteration); + aRetVal.mCurrentIteration.SetValue(iteration); + } +} + +void AnimationEffect::UpdateTiming(const OptionalEffectTiming& aTiming, + ErrorResult& aRv) { + TimingParams timing = + TimingParams::MergeOptionalEffectTiming(mTiming, aTiming, aRv); + if (aRv.Failed()) { + return; + } + + SetSpecifiedTiming(std::move(timing)); +} + +void AnimationEffect::UpdateNormalizedTiming() { + mNormalizedTiming.reset(); + + if (!mAnimation || !mAnimation->UsingScrollTimeline()) { + return; + } + + // Since `mAnimation` has a scroll timeline, we can be sure `GetTimeline()` + // and `TimelineDuration()` will not return null. + mNormalizedTiming.emplace( + mTiming.Normalize(mAnimation->GetTimeline()->TimelineDuration().Value())); +} + +Nullable AnimationEffect::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->GetCurrentTimeAsDuration(); + } + return result; +} + +} // namespace mozilla::dom diff --git a/dom/animation/AnimationEffect.h b/dom/animation/AnimationEffect.h new file mode 100644 index 0000000000..7dbd51c2bf --- /dev/null +++ b/dom/animation/AnimationEffect.h @@ -0,0 +1,117 @@ +/* -*- 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_AnimationEffect_h +#define mozilla_dom_AnimationEffect_h + +#include "mozilla/ComputedTiming.h" +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/ScrollTimeline.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/TimingParams.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class KeyframeEffect; +struct ComputedEffectTiming; +struct EffectTiming; +struct OptionalEffectTiming; +class Document; + +class AnimationEffect : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationEffect) + + AnimationEffect(Document* aDocument, TimingParams&& aTiming); + + virtual KeyframeEffect* AsKeyframeEffect() { return nullptr; } + + nsISupports* GetParentObject() const; + + bool IsCurrent() const; + bool IsInEffect() const; + bool HasFiniteActiveDuration() const { + return NormalizedTiming().ActiveDuration() != TimeDuration::Forever(); + } + + // AnimationEffect interface + virtual void GetTiming(EffectTiming& aRetVal) const; + virtual void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const; + virtual void UpdateTiming(const OptionalEffectTiming& aTiming, + ErrorResult& aRv); + + const TimingParams& SpecifiedTiming() const { return mTiming; } + void SetSpecifiedTiming(TimingParams&& aTiming); + + const TimingParams& NormalizedTiming() const { + MOZ_ASSERT((mAnimation && mAnimation->UsingScrollTimeline() && + mNormalizedTiming) || + !mNormalizedTiming, + "We do normalization only for progress-based timeline"); + return mNormalizedTiming ? *mNormalizedTiming : mTiming; + } + + // There are 3 conditions where we have to update the normalized timing: + // 1. mAnimation is changed, or + // 2. the timeline of mAnimation is changed, or + // 3. mTiming is changed. + void UpdateNormalizedTiming(); + + // 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, + Animation::ProgressTimelinePosition aProgressTimelinePosition); + // Shortcut that gets the computed timing using the current local time as + // calculated from the timeline time. + ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const; + + virtual void SetAnimation(Animation* aAnimation) = 0; + Animation* GetAnimation() const { return mAnimation; }; + + /** + * Returns true if this effect animates one of the properties we consider + * geometric properties, e.g. properties such as 'width' or 'margin-left' + * that we try to synchronize with transform animations, on a valid target + * element. + */ + virtual bool AffectsGeometry() const = 0; + + protected: + virtual ~AnimationEffect(); + + Nullable GetLocalTime() const; + + protected: + RefPtr mDocument; + RefPtr mAnimation; + TimingParams mTiming; + Maybe mNormalizedTiming; + + // Whether the Animation is System, ResistFingerprinting, or neither + enum RTPCallerType mRTPCallerType; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AnimationEffect_h diff --git a/dom/animation/AnimationEventDispatcher.cpp b/dom/animation/AnimationEventDispatcher.cpp new file mode 100644 index 0000000000..b4c55cddce --- /dev/null +++ b/dom/animation/AnimationEventDispatcher.cpp @@ -0,0 +1,62 @@ +/* -*- 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/AnimationEventDispatcher.h" + +#include "mozilla/EventDispatcher.h" +#include "nsPresContext.h" +#include "nsRefreshDriver.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEventDispatcher) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEventDispatcher) + tmp->ClearEventQueue(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEventDispatcher) + for (auto& info : tmp->mPendingEvents) { + ImplCycleCollectionTraverse( + cb, info.mTarget, + "mozilla::AnimationEventDispatcher.mPendingEvents.mTarget"); + ImplCycleCollectionTraverse( + cb, info.mAnimation, + "mozilla::AnimationEventDispatcher.mPendingEvents.mAnimation"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void AnimationEventDispatcher::Disconnect() { + if (mIsObserving) { + MOZ_ASSERT(mPresContext && mPresContext->RefreshDriver(), + "The pres context and the refresh driver should be still " + "alive if we haven't disassociated from the refresh driver"); + mPresContext->RefreshDriver()->CancelPendingAnimationEvents(this); + mIsObserving = false; + } + mPresContext = nullptr; +} + +void AnimationEventDispatcher::QueueEvent(AnimationEventInfo&& aEvent) { + mPendingEvents.AppendElement(std::move(aEvent)); + mIsSorted = false; + ScheduleDispatch(); +} + +void AnimationEventDispatcher::QueueEvents( + nsTArray&& aEvents) { + mPendingEvents.AppendElements(std::move(aEvents)); + mIsSorted = false; + ScheduleDispatch(); +} + +void AnimationEventDispatcher::ScheduleDispatch() { + MOZ_ASSERT(mPresContext, "The pres context should be valid"); + if (!mIsObserving) { + mPresContext->RefreshDriver()->ScheduleAnimationEventDispatch(this); + mIsObserving = true; + } +} + +} // namespace mozilla diff --git a/dom/animation/AnimationEventDispatcher.h b/dom/animation/AnimationEventDispatcher.h new file mode 100644 index 0000000000..3fe0663829 --- /dev/null +++ b/dom/animation/AnimationEventDispatcher.h @@ -0,0 +1,407 @@ +/* -*- 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_AnimationEventDispatcher_h +#define mozilla_AnimationEventDispatcher_h + +#include // For +#include "mozilla/AnimationComparator.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/AnimationEffect.h" +#include "mozilla/dom/AnimationPlaybackEvent.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsCSSProps.h" +#include "nsCycleCollectionParticipant.h" +#include "nsPresContext.h" + +class nsRefreshDriver; + +namespace geckoprofiler::markers { + +using namespace mozilla; + +struct CSSAnimationMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("CSSAnimation"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const nsCString& aName, + const nsCString& aTarget, + nsCSSPropertyIDSet aPropertySet) { + aWriter.StringProperty("Name", aName); + aWriter.StringProperty("Target", aTarget); + nsAutoCString properties; + nsAutoCString oncompositor; + for (nsCSSPropertyID property : aPropertySet) { + if (!properties.IsEmpty()) { + properties.AppendLiteral(", "); + oncompositor.AppendLiteral(", "); + } + properties.Append(nsCSSProps::GetStringValue(property)); + oncompositor.Append(nsCSSProps::PropHasFlags( + property, CSSPropFlags::CanAnimateOnCompositor) + ? "true" + : "false"); + } + + aWriter.StringProperty("properties", properties); + aWriter.StringProperty("oncompositor", oncompositor); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyFormatSearchable("Name", MS::Format::String, + MS::Searchable::Searchable); + schema.AddKeyLabelFormat("properties", "Animated Properties", + MS::Format::String); + schema.AddKeyLabelFormat("oncompositor", "Can Run on Compositor", + MS::Format::String); + schema.AddKeyFormat("Target", MS::Format::String); + schema.SetChartLabel("{marker.data.Name}"); + schema.SetTableLabel( + "{marker.name} - {marker.data.Name}: {marker.data.properties}"); + return schema; + } +}; + +struct CSSTransitionMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("CSSTransition"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const nsCString& aTarget, + nsCSSPropertyID aProperty, bool aCanceled) { + aWriter.StringProperty("Target", aTarget); + aWriter.StringProperty("property", nsCSSProps::GetStringValue(aProperty)); + aWriter.BoolProperty("oncompositor", + nsCSSProps::PropHasFlags( + aProperty, CSSPropFlags::CanAnimateOnCompositor)); + if (aCanceled) { + aWriter.BoolProperty("Canceled", aCanceled); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormat("property", "Animated Property", + MS::Format::String); + schema.AddKeyLabelFormat("oncompositor", "Can Run on Compositor", + MS::Format::String); + schema.AddKeyFormat("Canceled", MS::Format::String); + schema.AddKeyFormat("Target", MS::Format::String); + schema.SetChartLabel("{marker.data.property}"); + schema.SetTableLabel("{marker.name} - {marker.data.property}"); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +namespace mozilla { + +struct AnimationEventInfo { + RefPtr mTarget; + RefPtr mAnimation; + TimeStamp mScheduledEventTimeStamp; + + typedef Variant> + EventVariant; + EventVariant mEvent; + + // For CSS animation events + AnimationEventInfo(nsAtom* aAnimationName, + const NonOwningAnimationTarget& aTarget, + EventMessage aMessage, double aElapsedTime, + const TimeStamp& aScheduledEventTimeStamp, + dom::Animation* aAnimation) + : mTarget(aTarget.mElement), + mAnimation(aAnimation), + mScheduledEventTimeStamp(aScheduledEventTimeStamp), + mEvent(EventVariant(InternalAnimationEvent(true, aMessage))) { + InternalAnimationEvent& event = mEvent.as(); + + aAnimationName->ToString(event.mAnimationName); + // XXX Looks like nobody initialize WidgetEvent::time + event.mElapsedTime = aElapsedTime; + event.mPseudoElement = + nsCSSPseudoElements::PseudoTypeAsString(aTarget.mPseudoType); + + if ((aMessage == eAnimationCancel || aMessage == eAnimationEnd || + aMessage == eAnimationIteration) && + profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString name; + aAnimationName->ToUTF8String(name); + + const TimeStamp startTime = [&] { + if (aMessage == eAnimationIteration) { + if (auto* effect = aAnimation->GetEffect()) { + return aScheduledEventTimeStamp - + TimeDuration(effect->GetComputedTiming().mDuration); + } + } + return aScheduledEventTimeStamp - + TimeDuration::FromSeconds(aElapsedTime); + }(); + + nsCSSPropertyIDSet propertySet; + nsAutoString target; + if (dom::AnimationEffect* effect = aAnimation->GetEffect()) { + if (dom::KeyframeEffect* keyFrameEffect = effect->AsKeyframeEffect()) { + keyFrameEffect->GetTarget()->Describe(target, true); + for (const AnimationProperty& property : + keyFrameEffect->Properties()) { + propertySet.AddProperty(property.mProperty); + } + } + } + + PROFILER_MARKER( + aMessage == eAnimationIteration + ? ProfilerString8View("CSS animation iteration") + : ProfilerString8View("CSS animation"), + DOM, + MarkerOptions( + MarkerTiming::Interval(startTime, aScheduledEventTimeStamp), + aAnimation->GetOwner() + ? MarkerInnerWindowId(aAnimation->GetOwner()->WindowID()) + : MarkerInnerWindowId::NoId()), + CSSAnimationMarker, name, NS_ConvertUTF16toUTF8(target), propertySet); + } + } + + // For CSS transition events + AnimationEventInfo(nsCSSPropertyID aProperty, + const NonOwningAnimationTarget& aTarget, + EventMessage aMessage, double aElapsedTime, + const TimeStamp& aScheduledEventTimeStamp, + dom::Animation* aAnimation) + : mTarget(aTarget.mElement), + mAnimation(aAnimation), + mScheduledEventTimeStamp(aScheduledEventTimeStamp), + mEvent(EventVariant(InternalTransitionEvent(true, aMessage))) { + InternalTransitionEvent& event = mEvent.as(); + + event.mPropertyName = + NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(aProperty)); + // XXX Looks like nobody initialize WidgetEvent::time + event.mElapsedTime = aElapsedTime; + event.mPseudoElement = + nsCSSPseudoElements::PseudoTypeAsString(aTarget.mPseudoType); + + if ((aMessage == eTransitionEnd || aMessage == eTransitionCancel) && + profiler_thread_is_being_profiled_for_markers()) { + nsAutoString target; + if (dom::AnimationEffect* effect = aAnimation->GetEffect()) { + if (dom::KeyframeEffect* keyFrameEffect = effect->AsKeyframeEffect()) { + keyFrameEffect->GetTarget()->Describe(target, true); + } + } + PROFILER_MARKER( + "CSS transition", DOM, + MarkerOptions( + MarkerTiming::Interval( + aScheduledEventTimeStamp - + TimeDuration::FromSeconds(aElapsedTime), + aScheduledEventTimeStamp), + aAnimation->GetOwner() + ? MarkerInnerWindowId(aAnimation->GetOwner()->WindowID()) + : MarkerInnerWindowId::NoId()), + CSSTransitionMarker, NS_ConvertUTF16toUTF8(target), aProperty, + aMessage == eTransitionCancel); + } + } + + // For web animation events + AnimationEventInfo(const nsAString& aName, + RefPtr&& aEvent, + TimeStamp&& aScheduledEventTimeStamp, + dom::Animation* aAnimation) + : mTarget(aAnimation), + mAnimation(aAnimation), + mScheduledEventTimeStamp(std::move(aScheduledEventTimeStamp)), + mEvent(std::move(aEvent)) {} + + AnimationEventInfo(const AnimationEventInfo& aOther) = delete; + AnimationEventInfo& operator=(const AnimationEventInfo& aOther) = delete; + AnimationEventInfo(AnimationEventInfo&& aOther) = default; + AnimationEventInfo& operator=(AnimationEventInfo&& aOther) = default; + + bool IsWebAnimationEvent() const { + return mEvent.is>(); + } + +#ifdef DEBUG + bool IsStale() const { + const WidgetEvent* widgetEvent = AsWidgetEvent(); + return widgetEvent->mFlags.mIsBeingDispatched || + widgetEvent->mFlags.mDispatchedAtLeastOnce; + } + + const WidgetEvent* AsWidgetEvent() const { + return const_cast(this)->AsWidgetEvent(); + } +#endif + + WidgetEvent* AsWidgetEvent() { + if (mEvent.is()) { + return &mEvent.as(); + } + if (mEvent.is()) { + return &mEvent.as(); + } + if (mEvent.is>()) { + return mEvent.as>()->WidgetEventPtr(); + } + + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unexpected event type"); + return nullptr; + } + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch(nsPresContext* aPresContext) { + RefPtr target = mTarget; + if (mEvent.is>()) { + auto playbackEvent = mEvent.as>(); + EventDispatcher::DispatchDOMEvent(target, nullptr /* WidgetEvent */, + playbackEvent, aPresContext, + nullptr /* nsEventStatus */); + return; + } + + MOZ_ASSERT(mEvent.is() || + mEvent.is()); + + if (mEvent.is() && target->IsNode()) { + nsPIDOMWindowInner* inner = + target->AsNode()->OwnerDoc()->GetInnerWindow(); + if (inner && !inner->HasTransitionEventListeners()) { + MOZ_ASSERT(AsWidgetEvent()->mMessage == eTransitionStart || + AsWidgetEvent()->mMessage == eTransitionRun || + AsWidgetEvent()->mMessage == eTransitionEnd || + AsWidgetEvent()->mMessage == eTransitionCancel); + return; + } + } + + EventDispatcher::Dispatch(target, aPresContext, AsWidgetEvent()); + } +}; + +class AnimationEventDispatcher final { + public: + explicit AnimationEventDispatcher(nsPresContext* aPresContext) + : mPresContext(aPresContext), mIsSorted(true), mIsObserving(false) {} + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEventDispatcher) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(AnimationEventDispatcher) + + void Disconnect(); + + void QueueEvent(AnimationEventInfo&& aEvent); + void QueueEvents(nsTArray&& aEvents); + + // This will call SortEvents automatically if it has not already been + // called. + void DispatchEvents() { + mIsObserving = false; + if (!mPresContext || mPendingEvents.IsEmpty()) { + return; + } + + SortEvents(); + + EventArray events = std::move(mPendingEvents); + // mIsSorted will be set to true by SortEvents above, and we leave it + // that way since mPendingEvents is now empty + for (AnimationEventInfo& info : events) { + MOZ_ASSERT(!info.IsStale(), "The event shouldn't be stale"); + info.Dispatch(mPresContext); + + // Bail out if our mPresContext was nullified due to destroying the pres + // context. + if (!mPresContext) { + break; + } + } + } + + void ClearEventQueue() { + mPendingEvents.Clear(); + mIsSorted = true; + } + bool HasQueuedEvents() const { return !mPendingEvents.IsEmpty(); } + + private: +#ifndef DEBUG + ~AnimationEventDispatcher() = default; +#else + ~AnimationEventDispatcher() { + MOZ_ASSERT(!mIsObserving, + "AnimationEventDispatcher should have disassociated from " + "nsRefreshDriver"); + } +#endif + + class AnimationEventInfoLessThan { + public: + bool operator()(const AnimationEventInfo& a, + const AnimationEventInfo& b) const { + if (a.mScheduledEventTimeStamp != b.mScheduledEventTimeStamp) { + // Null timestamps sort first + if (a.mScheduledEventTimeStamp.IsNull() || + b.mScheduledEventTimeStamp.IsNull()) { + return a.mScheduledEventTimeStamp.IsNull(); + } else { + return a.mScheduledEventTimeStamp < b.mScheduledEventTimeStamp; + } + } + + // Events in the Web Animations spec are prior to CSS events. + if (a.IsWebAnimationEvent() != b.IsWebAnimationEvent()) { + return a.IsWebAnimationEvent(); + } + + AnimationPtrComparator> comparator; + return comparator.LessThan(a.mAnimation, b.mAnimation); + } + }; + + // Sort all pending CSS animation/transition events by scheduled event time + // and composite order. + // https://drafts.csswg.org/web-animations/#update-animations-and-send-events + void SortEvents() { + if (mIsSorted) { + return; + } + + for (auto& pending : mPendingEvents) { + pending.mAnimation->CachedChildIndexRef().reset(); + } + + // FIXME: Replace with mPendingEvents.StableSort when bug 1147091 is + // fixed. + std::stable_sort(mPendingEvents.begin(), mPendingEvents.end(), + AnimationEventInfoLessThan()); + mIsSorted = true; + } + void ScheduleDispatch(); + + nsPresContext* mPresContext; + typedef nsTArray EventArray; + EventArray mPendingEvents; + bool mIsSorted; + bool mIsObserving; +}; + +} // namespace mozilla + +#endif // mozilla_AnimationEventDispatcher_h diff --git a/dom/animation/AnimationPerformanceWarning.cpp b/dom/animation/AnimationPerformanceWarning.cpp new file mode 100644 index 0000000000..e57beff51f --- /dev/null +++ b/dom/animation/AnimationPerformanceWarning.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 "AnimationPerformanceWarning.h" + +#include "nsContentUtils.h" + +namespace mozilla { + +template +nsresult AnimationPerformanceWarning::ToLocalizedStringWithIntParams( + const char* aKey, nsAString& aLocalizedString) const { + AutoTArray strings; + + MOZ_DIAGNOSTIC_ASSERT(mParams->Length() == N); + for (size_t i = 0, n = mParams->Length(); i < n; i++) { + strings.AppendElement()->AppendInt((*mParams)[i]); + } + + return nsContentUtils::FormatLocalizedString( + nsContentUtils::eLAYOUT_PROPERTIES, aKey, strings, aLocalizedString); +} + +bool AnimationPerformanceWarning::ToLocalizedString( + nsAString& aLocalizedString) const { + const char* key = nullptr; + + switch (mType) { + case Type::ContentTooLarge: + MOZ_ASSERT(mParams && mParams->Length() == 6, + "Parameter's length should be 6 for ContentTooLarge2"); + + return NS_SUCCEEDED(ToLocalizedStringWithIntParams<6>( + "CompositorAnimationWarningContentTooLarge2", aLocalizedString)); + case Type::ContentTooLargeArea: + MOZ_ASSERT(mParams && mParams->Length() == 2, + "Parameter's length should be 2 for ContentTooLargeArea"); + + return NS_SUCCEEDED(ToLocalizedStringWithIntParams<2>( + "CompositorAnimationWarningContentTooLargeArea", aLocalizedString)); + case Type::TransformBackfaceVisibilityHidden: + key = "CompositorAnimationWarningTransformBackfaceVisibilityHidden"; + break; + case Type::TransformSVG: + key = "CompositorAnimationWarningTransformSVG"; + break; + case Type::TransformWithGeometricProperties: + key = "CompositorAnimationWarningTransformWithGeometricProperties"; + break; + case Type::TransformWithSyncGeometricAnimations: + key = "CompositorAnimationWarningTransformWithSyncGeometricAnimations"; + break; + case Type::TransformFrameInactive: + key = "CompositorAnimationWarningTransformFrameInactive"; + break; + case Type::TransformIsBlockedByImportantRules: + key = "CompositorAnimationWarningTransformIsBlockedByImportantRules"; + break; + case Type::OpacityFrameInactive: + key = "CompositorAnimationWarningOpacityFrameInactive"; + break; + case Type::HasRenderingObserver: + key = "CompositorAnimationWarningHasRenderingObserver"; + break; + case Type::HasCurrentColor: + key = "CompositorAnimationWarningHasCurrentColor"; + break; + case Type::None: + MOZ_ASSERT_UNREACHABLE("Uninitialized type shouldn't be used"); + return false; + } + + 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 0000000000..e59eade9e6 --- /dev/null +++ b/dom/animation/AnimationPerformanceWarning.h @@ -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/. */ + +#ifndef mozilla_dom_AnimationPerformanceWarning_h +#define mozilla_dom_AnimationPerformanceWarning_h + +#include + +#include "mozilla/Maybe.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +namespace mozilla { + +// Represents the reason why we can't run the CSS property on the compositor. +struct AnimationPerformanceWarning { + enum class Type : uint8_t { + None, + ContentTooLarge, + ContentTooLargeArea, + TransformBackfaceVisibilityHidden, + TransformSVG, + TransformWithGeometricProperties, + TransformWithSyncGeometricAnimations, + TransformFrameInactive, + TransformIsBlockedByImportantRules, + OpacityFrameInactive, + HasRenderingObserver, + HasCurrentColor, + }; + + explicit AnimationPerformanceWarning(Type aType) : mType(aType) { + MOZ_ASSERT(mType != Type::None); + } + + AnimationPerformanceWarning(Type aType, + std::initializer_list aParams) + : mType(aType) { + MOZ_ASSERT(mType != Type::None); + // 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(nsAString& aLocalizedString) const; + template + nsresult ToLocalizedStringWithIntParams(const char* aKey, + nsAString& 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/AnimationPropertySegment.h b/dom/animation/AnimationPropertySegment.h new file mode 100644 index 0000000000..fb4ea7ee26 --- /dev/null +++ b/dom/animation/AnimationPropertySegment.h @@ -0,0 +1,55 @@ +/* -*- 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_AnimationPropertySegment_h +#define mozilla_dom_AnimationPropertySegment_h + +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/Maybe.h" +#include "mozilla/StyleAnimationValue.h" // For AnimationValue +#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For CompositeOperation + +namespace mozilla { + +struct AnimationPropertySegment { + float mFromKey, mToKey; + // NOTE: In the case that no keyframe for 0 or 1 offset is specified + // the unit of mFromValue or mToValue is eUnit_Null. + AnimationValue mFromValue, mToValue; + + Maybe mTimingFunction; + dom::CompositeOperation mFromComposite = dom::CompositeOperation::Replace; + dom::CompositeOperation mToComposite = dom::CompositeOperation::Replace; + + bool HasReplaceableValues() const { + return HasReplaceableFromValue() && HasReplaceableToValue(); + } + + bool HasReplaceableFromValue() const { + return !mFromValue.IsNull() && + mFromComposite == dom::CompositeOperation::Replace; + } + + bool HasReplaceableToValue() const { + return !mToValue.IsNull() && + mToComposite == dom::CompositeOperation::Replace; + } + + bool operator==(const AnimationPropertySegment& aOther) const { + return mFromKey == aOther.mFromKey && mToKey == aOther.mToKey && + mFromValue == aOther.mFromValue && mToValue == aOther.mToValue && + mTimingFunction == aOther.mTimingFunction && + mFromComposite == aOther.mFromComposite && + mToComposite == aOther.mToComposite; + } + bool operator!=(const AnimationPropertySegment& aOther) const { + return !(*this == aOther); + } +}; + +} // namespace mozilla + +#endif // mozilla_dom_AnimationPropertySegment_h diff --git a/dom/animation/AnimationTarget.h b/dom/animation/AnimationTarget.h new file mode 100644 index 0000000000..3a0dacb9aa --- /dev/null +++ b/dom/animation/AnimationTarget.h @@ -0,0 +1,108 @@ +/* -*- 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/HashFunctions.h" // For HashNumber, AddToHash +#include "mozilla/HashTable.h" // For DefaultHasher, PointerHasher +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "nsCSSPseudoElements.h" + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +struct OwningAnimationTarget { + OwningAnimationTarget() = default; + + OwningAnimationTarget(dom::Element* aElement, PseudoStyleType 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; + } + + explicit operator bool() const { return !!mElement; } + + // mElement represents the parent element of a pseudo-element, not the + // generated content element. + RefPtr mElement; + PseudoStyleType mPseudoType = PseudoStyleType::NotPseudo; +}; + +struct NonOwningAnimationTarget { + NonOwningAnimationTarget() = default; + + NonOwningAnimationTarget(dom::Element* aElement, PseudoStyleType aType) + : mElement(aElement), mPseudoType(aType) {} + + explicit NonOwningAnimationTarget(const OwningAnimationTarget& aOther) + : mElement(aOther.mElement), mPseudoType(aOther.mPseudoType) {} + + bool operator==(const NonOwningAnimationTarget& aOther) const { + return mElement == aOther.mElement && mPseudoType == aOther.mPseudoType; + } + + NonOwningAnimationTarget& operator=(const OwningAnimationTarget& aOther) { + mElement = aOther.mElement; + mPseudoType = aOther.mPseudoType; + return *this; + } + + explicit operator bool() const { return !!mElement; } + + // mElement represents the parent element of a pseudo-element, not the + // generated content element. + dom::Element* MOZ_NON_OWNING_REF mElement = nullptr; + PseudoStyleType mPseudoType = PseudoStyleType::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); + } +} + +// A DefaultHasher specialization for OwningAnimationTarget. +template <> +struct DefaultHasher { + using Key = OwningAnimationTarget; + using Lookup = OwningAnimationTarget; + using PtrHasher = PointerHasher; + + static HashNumber hash(const Lookup& aLookup) { + return AddToHash(PtrHasher::hash(aLookup.mElement.get()), + static_cast(aLookup.mPseudoType)); + } + + static bool match(const Key& aKey, const Lookup& aLookup) { + return PtrHasher::match(aKey.mElement.get(), aLookup.mElement.get()) && + aKey.mPseudoType == aLookup.mPseudoType; + } + + static void rekey(Key& aKey, Key&& aNewKey) { aKey = std::move(aNewKey); } +}; + +} // namespace mozilla + +#endif // mozilla_AnimationTarget_h diff --git a/dom/animation/AnimationTimeline.cpp b/dom/animation/AnimationTimeline.cpp new file mode 100644 index 0000000000..8be0554683 --- /dev/null +++ b/dom/animation/AnimationTimeline.cpp @@ -0,0 +1,116 @@ +/* -*- 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::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_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_END + +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 + +AnimationTimeline::AnimationTimeline(nsIGlobalObject* aWindow, + RTPCallerType aRTPCallerType) + : mWindow(aWindow), mRTPCallerType(aRTPCallerType) { + MOZ_ASSERT(mWindow); +} + +AnimationTimeline::~AnimationTimeline() { mAnimationOrder.clear(); } + +bool AnimationTimeline::Tick() { + bool needsTicks = false; + + nsTArray animationsToRemove; + + for (Animation* animation = mAnimationOrder.getFirst(); animation; + animation = + static_cast*>(animation)->getNext()) { + MOZ_ASSERT(mAnimations.Contains(animation), + "The sampling order list should be a subset of the hashset"); + MOZ_ASSERT(!animation->IsHiddenByContentVisibility(), + "The sampling order list should not contain any animations " + "that are hidden by content-visibility"); + + // 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->NeedsTicks()) { + animationsToRemove.AppendElement(animation); + } + } + + for (Animation* animation : animationsToRemove) { + RemoveAnimation(animation); + } + + return needsTicks; +} + +void AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation) { + if (mAnimations.EnsureInserted(&aAnimation)) { + if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) { + aAnimation.GetTimeline()->RemoveAnimation(&aAnimation); + } + if (!aAnimation.IsHiddenByContentVisibility()) { + mAnimationOrder.insertBack(&aAnimation); + } + } +} + +void AnimationTimeline::RemoveAnimation(Animation* aAnimation) { + MOZ_ASSERT(!aAnimation->GetTimeline() || aAnimation->GetTimeline() == this); + if (static_cast*>(aAnimation)->isInList()) { + MOZ_ASSERT(mAnimations.Contains(aAnimation), + "The sampling order list should be a subset of the hashset"); + static_cast*>(aAnimation)->remove(); + } + mAnimations.Remove(aAnimation); +} + +void AnimationTimeline::NotifyAnimationContentVisibilityChanged( + Animation* aAnimation, bool aIsVisible) { + bool inList = + static_cast*>(aAnimation)->isInList(); + MOZ_ASSERT(!inList || mAnimations.Contains(aAnimation), + "The sampling order list should be a subset of the hashset"); + if (aIsVisible && !inList && mAnimations.Contains(aAnimation)) { + mAnimationOrder.insertBack(aAnimation); + } else if (!aIsVisible && inList) { + static_cast*>(aAnimation)->remove(); + } +} + +} // namespace mozilla::dom diff --git a/dom/animation/AnimationTimeline.h b/dom/animation/AnimationTimeline.h new file mode 100644 index 0000000000..715cb28969 --- /dev/null +++ b/dom/animation/AnimationTimeline.h @@ -0,0 +1,143 @@ +/* -*- 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 "nsTHashSet.h" + +namespace mozilla::dom { + +class Animation; +class Document; +class ScrollTimeline; + +class AnimationTimeline : public nsISupports, public nsWrapperCache { + public: + explicit AnimationTimeline(nsIGlobalObject* aWindow, + RTPCallerType aRTPCallerType); + + protected: + virtual ~AnimationTimeline(); + + // Tick animations and may remove them from the list if we don't need to + // tick it. Return true if any animations need to be ticked. + bool Tick(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AnimationTimeline) + + nsIGlobalObject* GetParentObject() const { return mWindow; } + + // AnimationTimeline methods + virtual Nullable GetCurrentTimeAsDuration() const = 0; + + // Wrapper functions for AnimationTimeline DOM methods when called from + // script. + Nullable GetCurrentTimeAsDouble() const { + return AnimationUtils::TimeDurationToDouble(GetCurrentTimeAsDuration(), + mRTPCallerType); + } + + TimeStamp GetCurrentTimeAsTimeStamp() const { + Nullable currentTime = GetCurrentTimeAsDuration(); + return !currentTime.IsNull() ? ToTimeStamp(currentTime.Value()) + : TimeStamp(); + } + + /** + * Returns true if the times returned by GetCurrentTimeAsDuration() 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); + virtual void NotifyAnimationContentVisibilityChanged(Animation* aAnimation, + bool aIsVisible); + + virtual Document* GetDocument() const = 0; + + virtual bool IsMonotonicallyIncreasing() const = 0; + + RTPCallerType GetRTPCallerType() const { return mRTPCallerType; } + + virtual bool IsScrollTimeline() const { return false; } + virtual const ScrollTimeline* AsScrollTimeline() const { return nullptr; } + virtual bool IsViewTimeline() const { return false; } + + // For a monotonic timeline, there is no upper bound on current time, and + // timeline duration is unresolved. For a non-monotonic (e.g. scroll) + // timeline, the duration has a fixed upper bound. + // + // https://drafts.csswg.org/web-animations-2/#timeline-duration + virtual Nullable TimelineDuration() const { return nullptr; } + + protected: + nsCOMPtr mWindow; + + // Animations observing this timeline + // + // We store them in (a) a hashset for quick lookup, and (b) a LinkedList + // to maintain a fixed sampling order. Animations that are hidden by + // `content-visibility` are not sampled and will only be in the hashset. + // The LinkedList should always be a subset of the hashset. + // + // The hashset keeps a strong reference to each animation since + // dealing with addref/release with LinkedList is difficult. + typedef nsTHashSet> AnimationSet; + AnimationSet mAnimations; + LinkedList mAnimationOrder; + + // Whether the Timeline is System, ResistFingerprinting, or neither + enum RTPCallerType mRTPCallerType; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AnimationTimeline_h diff --git a/dom/animation/AnimationUtils.cpp b/dom/animation/AnimationUtils.cpp new file mode 100644 index 0000000000..8d00d9ebcf --- /dev/null +++ b/dom/animation/AnimationUtils.cpp @@ -0,0 +1,145 @@ +/* -*- 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 "mozilla/dom/Animation.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/EffectSet.h" +#include "nsDebug.h" +#include "nsAtom.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "nsGlobalWindow.h" +#include "nsString.h" +#include "xpcpublic.h" // For xpc::NativeGlobal + +using namespace mozilla::dom; + +namespace mozilla { + +/* static */ +void AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage, + const nsIContent* aContent) { + if (aContent) { + aMessage.AppendLiteral(" ["); + aMessage.Append(nsAtomCString(aContent->NodeInfo()->NameAtom())); + + nsAtom* 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 */ +Document* AnimationUtils::GetCurrentRealmDocument(JSContext* aCx) { + nsGlobalWindowInner* win = xpc::CurrentWindowOrNull(aCx); + if (!win) { + return nullptr; + } + return win->GetDoc(); +} + +/* static */ +Document* AnimationUtils::GetDocumentFromGlobal(JSObject* aGlobalObject) { + nsGlobalWindowInner* win = xpc::WindowOrNull(aGlobalObject); + if (!win) { + return nullptr; + } + return win->GetDoc(); +} + +/* static */ +bool AnimationUtils::FrameHasAnimatedScale(const nsIFrame* aFrame) { + EffectSet* effectSet = EffectSet::GetForFrame( + aFrame, nsCSSPropertyIDSet::TransformLikeProperties()); + if (!effectSet) { + return false; + } + + for (const dom::KeyframeEffect* effect : *effectSet) { + if (effect->ContainsAnimatedScale(aFrame)) { + return true; + } + } + + return false; +} + +/* static */ +bool AnimationUtils::HasCurrentTransitions(const Element* aElement, + PseudoStyleType aPseudoType) { + MOZ_ASSERT(aElement); + + EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType); + if (!effectSet) { + return false; + } + + for (const dom::KeyframeEffect* effect : *effectSet) { + // If |effect| is current, it must have an associated Animation + // so we don't need to null-check the result of GetAnimation(). + if (effect->IsCurrent() && effect->GetAnimation()->AsCSSTransition()) { + return true; + } + } + + return false; +} + +/*static*/ Element* AnimationUtils::GetElementForRestyle( + Element* aElement, PseudoStyleType aPseudoType) { + if (aPseudoType == PseudoStyleType::NotPseudo) { + return aElement; + } + + if (aPseudoType == PseudoStyleType::before) { + return nsLayoutUtils::GetBeforePseudo(aElement); + } + + if (aPseudoType == PseudoStyleType::after) { + return nsLayoutUtils::GetAfterPseudo(aElement); + } + + if (aPseudoType == PseudoStyleType::marker) { + return nsLayoutUtils::GetMarkerPseudo(aElement); + } + + MOZ_ASSERT_UNREACHABLE( + "Should not try to get the element to restyle for a pseudo other that " + ":before, :after or ::marker"); + return nullptr; +} + +/*static*/ std::pair +AnimationUtils::GetElementPseudoPair(const Element* aElementOrPseudo) { + MOZ_ASSERT(aElementOrPseudo); + + if (aElementOrPseudo->IsGeneratedContentContainerForBefore()) { + return {aElementOrPseudo->GetParent()->AsElement(), + PseudoStyleType::before}; + } + + if (aElementOrPseudo->IsGeneratedContentContainerForAfter()) { + return {aElementOrPseudo->GetParent()->AsElement(), PseudoStyleType::after}; + } + + if (aElementOrPseudo->IsGeneratedContentContainerForMarker()) { + return {aElementOrPseudo->GetParent()->AsElement(), + PseudoStyleType::marker}; + } + + return {aElementOrPseudo, PseudoStyleType::NotPseudo}; +} + +} // namespace mozilla diff --git a/dom/animation/AnimationUtils.h b/dom/animation/AnimationUtils.h new file mode 100644 index 0000000000..0d8c53afa0 --- /dev/null +++ b/dom/animation/AnimationUtils.h @@ -0,0 +1,134 @@ +/* -*- 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/PseudoStyleType.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/Nullable.h" +#include "nsRFPService.h" +#include "nsStringFwd.h" + +class nsIContent; +class nsIFrame; +struct JSContext; + +namespace mozilla { + +class EffectSet; + +namespace dom { +class Document; +class Element; +} // namespace dom + +class AnimationUtils { + public: + using Document = dom::Document; + + static dom::Nullable TimeDurationToDouble( + const dom::Nullable& aTime, RTPCallerType aRTPCallerType) { + dom::Nullable result; + + if (!aTime.IsNull()) { + // 0 is an inappropriate mixin for this this area; however CSS Animations + // needs to have it's Time Reduction Logic refactored, so it's currently + // only clamping for RFP mode. RFP mode gives a much lower time precision, + // so we accept the security leak here for now + result.SetValue(nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly( + aTime.Value().ToMilliseconds(), 0, aRTPCallerType)); + } + + 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 Document* GetCurrentRealmDocument(JSContext* aCx); + + /** + * Get the document from the global object, or nullptr if the document has + * no window, to use when constructing DOM object without entering the + * target window's compartment (see KeyframeEffect constructor). + */ + static Document* GetDocumentFromGlobal(JSObject* aGlobalObject); + + /** + * Returns true if the given frame has an animated scale. + */ + static bool FrameHasAnimatedScale(const nsIFrame* aFrame); + + /** + * Returns true if the given (pseudo-)element has any transitions that are + * current (playing or waiting to play) or in effect (e.g. filling forwards). + */ + static bool HasCurrentTransitions(const dom::Element* aElement, + PseudoStyleType aPseudoType); + + /** + * Returns true if this pseudo style type is supported by animations. + * Note: This doesn't include PseudoStyleType::NotPseudo. + */ + static bool IsSupportedPseudoForAnimations(PseudoStyleType aType) { + // FIXME: Bug 1615469: Support first-line and first-letter for Animation. + return aType == PseudoStyleType::before || + aType == PseudoStyleType::after || aType == PseudoStyleType::marker; + } + + /** + * Returns true if the difference between |aFirst| and |aSecond| is within + * the animation time tolerance (i.e. 1 microsecond). + */ + static bool IsWithinAnimationTimeTolerance(const TimeDuration& aFirst, + const TimeDuration& aSecond) { + if (aFirst == TimeDuration::Forever() || + aSecond == TimeDuration::Forever()) { + return aFirst == aSecond; + } + + TimeDuration diff = aFirst >= aSecond ? aFirst - aSecond : aSecond - aFirst; + return diff <= TimeDuration::FromMicroseconds(1); + } + + // Returns the target element for restyling. + // + // If |aPseudoType| is ::after, ::before or ::marker, returns the generated + // content element of which |aElement| is the parent. If |aPseudoType| is any + // other pseudo type (other than PseudoStyleType::NotPseudo) returns nullptr. + // Otherwise, returns |aElement|. + static dom::Element* GetElementForRestyle(dom::Element* aElement, + PseudoStyleType aPseudoType); + + // Returns the pair of |Element, PseudoStyleType| from an element which could + // be an element or a pseudo element (i.e. an element used for restyling and + // DOM tree). + // + // Animation module usually uses a pair of (Element*, PseudoStyleType) to + // represent the animation target, and the |Element| in the pair is the + // generated content container if it's a pseudo element. + static std::pair GetElementPseudoPair( + const dom::Element* aElementOrPseudo); +}; + +} // namespace mozilla + +#endif diff --git a/dom/animation/CSSAnimation.cpp b/dom/animation/CSSAnimation.cpp new file mode 100644 index 0000000000..ed48d24806 --- /dev/null +++ b/dom/animation/CSSAnimation.cpp @@ -0,0 +1,376 @@ +/* -*- 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 "CSSAnimation.h" + +#include "mozilla/AnimationEventDispatcher.h" +#include "mozilla/dom/CSSAnimationBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/TimeStamp.h" +#include "nsPresContext.h" + +namespace mozilla::dom { + +using AnimationPhase = ComputedTiming::AnimationPhase; + +JSObject* CSSAnimation::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto); +} + +void CSSAnimation::SetEffect(AnimationEffect* aEffect) { + Animation::SetEffect(aEffect); + + AddOverriddenProperties(CSSAnimationProperties::Effect); +} + +void CSSAnimation::SetStartTimeAsDouble(const Nullable& aStartTime) { + // Note that we always compare with the paused state since for the purposes + // of determining if play control is being overridden or not, we want to + // treat the finished state as running. + bool wasPaused = PlayState() == AnimationPlayState::Paused; + + Animation::SetStartTimeAsDouble(aStartTime); + + bool isPaused = PlayState() == AnimationPlayState::Paused; + + if (wasPaused != isPaused) { + AddOverriddenProperties(CSSAnimationProperties::PlayState); + } +} + +mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) { + FlushUnanimatedStyle(); + return Animation::GetReady(aRv); +} + +void CSSAnimation::Reverse(ErrorResult& aRv) { + // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in + // the paused state. + bool wasPaused = PlayState() == AnimationPlayState::Paused; + + Animation::Reverse(aRv); + if (aRv.Failed()) { + return; + } + + bool isPaused = PlayState() == AnimationPlayState::Paused; + + if (wasPaused != isPaused) { + AddOverriddenProperties(CSSAnimationProperties::PlayState); + } +} + +AnimationPlayState CSSAnimation::PlayStateFromJS() const { + // Flush style to ensure that any properties controlling animation state + // (e.g. animation-play-state) are fully updated. + FlushUnanimatedStyle(); + return Animation::PlayStateFromJS(); +} + +bool CSSAnimation::PendingFromJS() const { + // Flush style since, for example, if the animation-play-state was just + // changed its possible we should now be pending. + FlushUnanimatedStyle(); + return Animation::PendingFromJS(); +} + +void CSSAnimation::PlayFromJS(ErrorResult& aRv) { + // Note that flushing style below might trigger calls to + // PlayFromStyle()/PauseFromStyle() on this object. + FlushUnanimatedStyle(); + Animation::PlayFromJS(aRv); + if (aRv.Failed()) { + return; + } + + AddOverriddenProperties(CSSAnimationProperties::PlayState); +} + +void CSSAnimation::PauseFromJS(ErrorResult& aRv) { + Animation::PauseFromJS(aRv); + if (aRv.Failed()) { + return; + } + + AddOverriddenProperties(CSSAnimationProperties::PlayState); +} + +void CSSAnimation::PlayFromStyle() { + ErrorResult rv; + Animation::Play(rv, Animation::LimitBehavior::Continue); + // play() should not throw when LimitBehavior is Continue + MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation"); +} + +void CSSAnimation::PauseFromStyle() { + ErrorResult rv; + Animation::Pause(rv); + // pause() should only throw when *all* of the following conditions are true: + // - we are in the idle state, and + // - we have a negative playback rate, and + // - we have an infinitely repeating animation + // The first two conditions will never happen under regular style processing + // but could happen if an author made modifications to the Animation object + // and then updated animation-play-state. It's an unusual case and there's + // no obvious way to pass on the exception information so we just silently + // fail for now. + if (rv.Failed()) { + NS_WARNING("Unexpected exception pausing animation - silently failing"); + } +} + +void CSSAnimation::Tick() { + Animation::Tick(); + QueueEvents(); +} + +bool CSSAnimation::HasLowerCompositeOrderThan( + const CSSAnimation& aOther) const { + MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), + "Should only be called for CSS animations that are sorted " + "as CSS animations (i.e. tied to CSS markup)"); + + // 0. Object-equality case + if (&aOther == this) { + return false; + } + + // 1. Sort by document order + if (!mOwningElement.Equals(aOther.mOwningElement)) { + return mOwningElement.LessThan( + const_cast(this)->CachedChildIndexRef(), + aOther.mOwningElement, + const_cast(&aOther)->CachedChildIndexRef()); + } + + // 2. (Same element and pseudo): Sort by position in animation-name + return mAnimationIndex < aOther.mAnimationIndex; +} + +void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) { + // If the animation is pending, we ignore animation events until we finish + // pending. + if (mPendingState != PendingState::NotPending) { + return; + } + + // CSS animations dispatch events at their owning element. This allows + // script to repurpose a CSS animation to target a different element, + // to use a group effect (which has no obvious "target element"), or + // to remove the animation effect altogether whilst still getting + // animation events. + // + // It does mean, however, that for a CSS animation that has no owning + // element (e.g. it was created using the CSSAnimation constructor or + // disassociated from CSS) no events are fired. If it becomes desirable + // for these animations to still fire events we should spec the concept + // of the "original owning element" or "event target" and allow script + // to set it when creating a CSSAnimation object. + if (!mOwningElement.IsSet()) { + return; + } + + nsPresContext* presContext = mOwningElement.GetPresContext(); + if (!presContext) { + return; + } + + uint64_t currentIteration = 0; + ComputedTiming::AnimationPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + StickyTimeDuration iterationStartTime; + + if (!mEffect) { + currentPhase = + GetAnimationPhaseWithoutEffect(*this); + if (currentPhase == mPreviousPhase) { + return; + } + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + currentPhase = computedTiming.mPhase; + currentIteration = computedTiming.mCurrentIteration; + if (currentPhase == mPreviousPhase && + currentIteration == mPreviousIteration) { + return; + } + intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); + intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); + + uint64_t iterationBoundary = mPreviousIteration > currentIteration + ? currentIteration + 1 + : currentIteration; + double multiplier = iterationBoundary - computedTiming.mIterationStart; + if (multiplier != 0.0) { + iterationStartTime = computedTiming.mDuration.MultDouble(multiplier); + } + } + + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); + + AutoTArray events; + + auto appendAnimationEvent = [&](EventMessage aMessage, + const StickyTimeDuration& aElapsedTime, + const TimeStamp& aScheduledEventTimeStamp) { + double elapsedTime = aElapsedTime.ToSeconds(); + if (aMessage == eAnimationCancel) { + // 0 is an inappropriate value for this callsite. What we need to do is + // use a single random value for all increasing times reportable. + // That is to say, whenever elapsedTime goes negative (because an + // animation restarts, something rewinds the animation, or otherwise) + // a new random value for the mix-in must be generated. + elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( + elapsedTime, 0, mRTPCallerType); + } + events.AppendElement( + AnimationEventInfo(mAnimationName, mOwningElement.Target(), aMessage, + elapsedTime, aScheduledEventTimeStamp, this)); + }; + + // Handle cancel event first + if ((mPreviousPhase != AnimationPhase::Idle && + mPreviousPhase != AnimationPhase::After) && + currentPhase == AnimationPhase::Idle) { + appendAnimationEvent(eAnimationCancel, aActiveTime, + GetTimelineCurrentTimeAsTimeStamp()); + } + + switch (mPreviousPhase) { + case AnimationPhase::Idle: + case AnimationPhase::Before: + if (currentPhase == AnimationPhase::Active) { + appendAnimationEvent(eAnimationStart, intervalStartTime, + startTimeStamp); + } else if (currentPhase == AnimationPhase::After) { + appendAnimationEvent(eAnimationStart, intervalStartTime, + startTimeStamp); + appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp); + } + break; + case AnimationPhase::Active: + if (currentPhase == AnimationPhase::Before) { + appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp); + } else if (currentPhase == AnimationPhase::Active) { + // The currentIteration must have changed or element we would have + // returned early above. + MOZ_ASSERT(currentIteration != mPreviousIteration); + appendAnimationEvent(eAnimationIteration, iterationStartTime, + iterationTimeStamp); + } else if (currentPhase == AnimationPhase::After) { + appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp); + } + break; + case AnimationPhase::After: + if (currentPhase == AnimationPhase::Before) { + appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp); + appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp); + } else if (currentPhase == AnimationPhase::Active) { + appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp); + } + break; + } + mPreviousPhase = currentPhase; + mPreviousIteration = currentIteration; + + if (!events.IsEmpty()) { + presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); + } +} + +void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) { + if (mNeedsNewAnimationIndexWhenRun && + PlayState() != AnimationPlayState::Idle) { + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = false; + } + + Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); +} + +/////////////////////// CSSAnimationKeyframeEffect //////////////////////// + +void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const { + MaybeFlushUnanimatedStyle(); + KeyframeEffect::GetTiming(aRetVal); +} + +void CSSAnimationKeyframeEffect::GetComputedTimingAsDict( + ComputedEffectTiming& aRetVal) const { + MaybeFlushUnanimatedStyle(); + KeyframeEffect::GetComputedTimingAsDict(aRetVal); +} + +void CSSAnimationKeyframeEffect::UpdateTiming( + const OptionalEffectTiming& aTiming, ErrorResult& aRv) { + KeyframeEffect::UpdateTiming(aTiming, aRv); + + if (aRv.Failed()) { + return; + } + + if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { + CSSAnimationProperties updatedProperties = CSSAnimationProperties::None; + if (aTiming.mDuration.WasPassed()) { + updatedProperties |= CSSAnimationProperties::Duration; + } + if (aTiming.mIterations.WasPassed()) { + updatedProperties |= CSSAnimationProperties::IterationCount; + } + if (aTiming.mDirection.WasPassed()) { + updatedProperties |= CSSAnimationProperties::Direction; + } + if (aTiming.mDelay.WasPassed()) { + updatedProperties |= CSSAnimationProperties::Delay; + } + if (aTiming.mFill.WasPassed()) { + updatedProperties |= CSSAnimationProperties::FillMode; + } + + cssAnimation->AddOverriddenProperties(updatedProperties); + } +} + +void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext, + JS::Handle aKeyframes, + ErrorResult& aRv) { + KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv); + + if (aRv.Failed()) { + return; + } + + if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { + cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes); + } +} + +void CSSAnimationKeyframeEffect::SetComposite( + const CompositeOperation& aComposite) { + KeyframeEffect::SetComposite(aComposite); + + if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { + cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Composition); + } +} + +void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const { + if (!GetOwningCSSAnimation()) { + return; + } + + if (dom::Document* doc = GetRenderedDocument()) { + doc->FlushPendingNotifications( + ChangesToFlush(FlushType::Style, false /* flush animations */)); + } +} + +} // namespace mozilla::dom diff --git a/dom/animation/CSSAnimation.h b/dom/animation/CSSAnimation.h new file mode 100644 index 0000000000..e1c45c49b9 --- /dev/null +++ b/dom/animation/CSSAnimation.h @@ -0,0 +1,238 @@ +/* -*- 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_CSSAnimation_h +#define mozilla_dom_CSSAnimation_h + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/dom/MutationObservers.h" +#include "mozilla/StyleAnimationValue.h" +#include "AnimationCommon.h" + +namespace mozilla { +// Properties of CSS Animations that can be overridden by the Web Animations API +// in a manner that means we should ignore subsequent changes to markup for that +// property. +enum class CSSAnimationProperties { + None = 0, + Keyframes = 1 << 0, + Duration = 1 << 1, + IterationCount = 1 << 2, + Direction = 1 << 3, + Delay = 1 << 4, + FillMode = 1 << 5, + Composition = 1 << 6, + Effect = Keyframes | Duration | IterationCount | Direction | Delay | + FillMode | Composition, + PlayState = 1 << 7, +}; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CSSAnimationProperties) + +namespace dom { + +class CSSAnimation final : public Animation { + public: + explicit CSSAnimation(nsIGlobalObject* aGlobal, nsAtom* aAnimationName) + : dom::Animation(aGlobal), + mAnimationName(aAnimationName), + mNeedsNewAnimationIndexWhenRun(false), + mPreviousPhase(ComputedTiming::AnimationPhase::Idle), + mPreviousIteration(0) { + // We might need to drop this assertion once we add a script-accessible + // constructor but for animations generated from CSS markup the + // animation-name should never be empty. + MOZ_ASSERT(mAnimationName != nsGkAtoms::_empty, + "animation-name should not be 'none'"); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + CSSAnimation* AsCSSAnimation() override { return this; } + const CSSAnimation* AsCSSAnimation() const override { return this; } + + // CSSAnimation interface + void GetAnimationName(nsString& aRetVal) const { + mAnimationName->ToString(aRetVal); + } + + nsAtom* AnimationName() const { return mAnimationName; } + + // Animation interface overrides + void SetEffect(AnimationEffect* aEffect) override; + void SetStartTimeAsDouble(const Nullable& aStartTime) override; + Promise* GetReady(ErrorResult& aRv) override; + void Reverse(ErrorResult& aRv) override; + + // NOTE: tabbrowser.xml currently relies on the fact that reading the + // currentTime of a CSSAnimation does *not* flush style (whereas reading the + // playState does). If CSS Animations 2 specifies that reading currentTime + // also flushes style we will need to find another way to detect canceled + // animations in tabbrowser.xml. On the other hand, if CSS Animations 2 + // specifies that reading playState does *not* flush style (and we drop the + // following override), then we should update tabbrowser.xml to check + // the playState instead. + AnimationPlayState PlayStateFromJS() const override; + bool PendingFromJS() const override; + void PlayFromJS(ErrorResult& aRv) override; + void PauseFromJS(ErrorResult& aRv) override; + + void PlayFromStyle(); + void PauseFromStyle(); + void CancelFromStyle(PostRestyleMode aPostRestyle) { + // When an animation is disassociated with style it enters an odd state + // where its composite order is undefined until it first transitions + // out of the idle state. + // + // Even if the composite order isn't defined we don't want it to be random + // in case we need to determine the order to dispatch events associated + // with an animation in this state. To solve this we treat the animation as + // if it had been added to the end of the global animation list so that + // its sort order is defined. We'll update this index again once the + // animation leaves the idle state. + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = true; + + Animation::Cancel(aPostRestyle); + + // We need to do this *after* calling Cancel() since + // Cancel() might synchronously trigger a cancel event for which + // we need an owning element to target the event at. + mOwningElement = OwningElementRef(); + } + + void Tick() override; + void QueueEvents( + const StickyTimeDuration& aActiveTime = StickyTimeDuration()); + + bool HasLowerCompositeOrderThan(const CSSAnimation& aOther) const; + + void SetAnimationIndex(uint64_t aIndex) { + MOZ_ASSERT(IsTiedToMarkup()); + if (IsRelevant() && mAnimationIndex != aIndex) { + MutationObservers::NotifyAnimationChanged(this); + PostUpdate(); + } + mAnimationIndex = aIndex; + } + + // Sets the owning element which is used for determining the composite + // order of CSSAnimation objects generated from CSS markup. + // + // @see mOwningElement + void SetOwningElement(const OwningElementRef& aElement) { + mOwningElement = aElement; + } + // True for animations that are generated from CSS markup and continue to + // reflect changes to that markup. + bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } + + void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override { + QueueEvents(aActiveTime); + } + + CSSAnimationProperties GetOverriddenProperties() const { + return mOverriddenProperties; + } + void AddOverriddenProperties(CSSAnimationProperties aProperties) { + mOverriddenProperties |= aProperties; + } + + protected: + virtual ~CSSAnimation() { + MOZ_ASSERT(!mOwningElement.IsSet(), + "Owning element should be cleared " + "before a CSS animation is destroyed"); + } + + // Animation overrides + void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) override; + + // Returns the duration from the start of the animation's source effect's + // active interval to the point where the animation actually begins playback. + // This is zero unless the animation's source effect has a negative delay in + // which case it is the absolute value of that delay. + // This is used for setting the elapsedTime member of CSS AnimationEvents. + TimeDuration InitialAdvance() const { + return mEffect ? std::max(TimeDuration(), + mEffect->NormalizedTiming().Delay() * -1) + : TimeDuration(); + } + + RefPtr mAnimationName; + + // The (pseudo-)element whose computed animation-name refers to this + // animation (if any). + // + // This is used for determining the relative composite order of animations + // generated from CSS markup. + // + // Typically this will be the same as the target element of the keyframe + // effect associated with this animation. However, it can differ in the + // following circumstances: + // + // a) If script removes or replaces the effect of this animation, + // b) If this animation is cancelled (e.g. by updating the + // animation-name property or removing the owning element from the + // document), + // c) If this object is generated from script using the CSSAnimation + // constructor. + // + // For (b) and (c) the owning element will return !IsSet(). + OwningElementRef mOwningElement; + + // When true, indicates that when this animation next leaves the idle state, + // its animation index should be updated. + bool mNeedsNewAnimationIndexWhenRun; + + // Phase and current iteration from the previous time we queued events. + // This is used to determine what new events to dispatch. + ComputedTiming::AnimationPhase mPreviousPhase; + uint64_t mPreviousIteration; + + // Properties that would normally be defined by the cascade but which have + // since been explicitly set via the Web Animations API. + CSSAnimationProperties mOverriddenProperties = CSSAnimationProperties::None; +}; + +// A subclass of KeyframeEffect that reports when specific properties have been +// overridden via the Web Animations API. +class CSSAnimationKeyframeEffect : public KeyframeEffect { + public: + CSSAnimationKeyframeEffect(Document* aDocument, + OwningAnimationTarget&& aTarget, + TimingParams&& aTiming, + const KeyframeEffectParams& aOptions) + : KeyframeEffect(aDocument, std::move(aTarget), std::move(aTiming), + aOptions) {} + + void GetTiming(EffectTiming& aRetVal) const override; + void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const override; + void UpdateTiming(const OptionalEffectTiming& aTiming, + ErrorResult& aRv) override; + void SetKeyframes(JSContext* aContext, JS::Handle aKeyframes, + ErrorResult& aRv) override; + void SetComposite(const CompositeOperation& aComposite) override; + + private: + CSSAnimation* GetOwningCSSAnimation() { + return mAnimation ? mAnimation->AsCSSAnimation() : nullptr; + } + const CSSAnimation* GetOwningCSSAnimation() const { + return mAnimation ? mAnimation->AsCSSAnimation() : nullptr; + } + + // Flushes styles if our owning animation is a CSSAnimation + void MaybeFlushUnanimatedStyle() const; +}; + +} // namespace dom + +} // namespace mozilla + +#endif // mozilla_dom_CSSAnimation_h diff --git a/dom/animation/CSSPseudoElement.cpp b/dom/animation/CSSPseudoElement.cpp new file mode 100644 index 0000000000..9dfdd9d4f6 --- /dev/null +++ b/dom/animation/CSSPseudoElement.cpp @@ -0,0 +1,90 @@ +/* -*- 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/CSSPseudoElement.h" +#include "mozilla/dom/CSSPseudoElementBinding.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/AnimationComparator.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CSSPseudoElement, mOriginatingElement) + +CSSPseudoElement::CSSPseudoElement(dom::Element* aElement, + PseudoStyleType aType) + : mOriginatingElement(aElement), mPseudoType(aType) { + MOZ_ASSERT(aElement); + MOZ_ASSERT(AnimationUtils::IsSupportedPseudoForAnimations(aType), + "Unexpected Pseudo Type"); +} + +CSSPseudoElement::~CSSPseudoElement() { + // Element might have been unlinked already, so we have to do null check. + if (mOriginatingElement) { + mOriginatingElement->RemoveProperty( + GetCSSPseudoElementPropertyAtom(mPseudoType)); + } +} + +ParentObject CSSPseudoElement::GetParentObject() const { + return mOriginatingElement->GetParentObject(); +} + +JSObject* CSSPseudoElement::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return CSSPseudoElement_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed CSSPseudoElement::GetCSSPseudoElement( + dom::Element* aElement, PseudoStyleType aType) { + if (!aElement) { + return nullptr; + } + + nsAtom* 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 */ +nsAtom* CSSPseudoElement::GetCSSPseudoElementPropertyAtom( + PseudoStyleType aType) { + switch (aType) { + case PseudoStyleType::before: + return nsGkAtoms::cssPseudoElementBeforeProperty; + + case PseudoStyleType::after: + return nsGkAtoms::cssPseudoElementAfterProperty; + + case PseudoStyleType::marker: + return nsGkAtoms::cssPseudoElementMarkerProperty; + + default: + MOZ_ASSERT_UNREACHABLE( + "Should not try to get CSSPseudoElement " + "other than ::before, ::after or ::marker"); + return nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/animation/CSSPseudoElement.h b/dom/animation/CSSPseudoElement.h new file mode 100644 index 0000000000..4b5fbc917c --- /dev/null +++ b/dom/animation/CSSPseudoElement.h @@ -0,0 +1,73 @@ +/* -*- 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_CSSPseudoElement_h +#define mozilla_dom_CSSPseudoElement_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/RefPtr.h" +#include "nsCSSPseudoElements.h" +#include "nsWrapperCache.h" + +namespace mozilla::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_NATIVE_WRAPPERCACHE_CLASS(CSSPseudoElement) + + protected: + virtual ~CSSPseudoElement(); + + public: + ParentObject GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + PseudoStyleType 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))); + } + dom::Element* Element() const { return mOriginatingElement.get(); } + + // 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( + dom::Element* aElement, PseudoStyleType aType); + + private: + // Only ::before, ::after and ::marker are supported. + CSSPseudoElement(dom::Element* aElement, PseudoStyleType aType); + + static nsAtom* GetCSSPseudoElementPropertyAtom(PseudoStyleType aType); + + // mOriginatingElement 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 originating element. + RefPtr mOriginatingElement; + PseudoStyleType mPseudoType; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CSSPseudoElement_h diff --git a/dom/animation/CSSTransition.cpp b/dom/animation/CSSTransition.cpp new file mode 100644 index 0000000000..38f9c9386a --- /dev/null +++ b/dom/animation/CSSTransition.cpp @@ -0,0 +1,333 @@ +/* -*- 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 "CSSTransition.h" + +#include "mozilla/AnimationEventDispatcher.h" +#include "mozilla/dom/CSSTransitionBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/TimeStamp.h" +#include "nsPresContext.h" + +namespace mozilla::dom { + +JSObject* CSSTransition::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto); +} + +void CSSTransition::GetTransitionProperty(nsString& aRetVal) const { + MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, + "Transition Property should be initialized"); + aRetVal = + NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty)); +} + +AnimationPlayState CSSTransition::PlayStateFromJS() const { + FlushUnanimatedStyle(); + return Animation::PlayStateFromJS(); +} + +bool CSSTransition::PendingFromJS() const { + // Transitions don't become pending again after they start running but, if + // while the transition is still pending, style is updated in such a way + // that the transition will be canceled, we need to report false here. + // Hence we need to flush, but only when we're pending. + if (Pending()) { + FlushUnanimatedStyle(); + } + return Animation::PendingFromJS(); +} + +void CSSTransition::PlayFromJS(ErrorResult& aRv) { + FlushUnanimatedStyle(); + Animation::PlayFromJS(aRv); +} + +void CSSTransition::UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) { + if (mNeedsNewAnimationIndexWhenRun && + PlayState() != AnimationPlayState::Idle) { + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = false; + } + + Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); +} + +void CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) { + if (!mOwningElement.IsSet()) { + return; + } + + nsPresContext* presContext = mOwningElement.GetPresContext(); + if (!presContext) { + return; + } + + static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); + + TransitionPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect(*this); + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast(computedTiming.mPhase); + intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); + intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); + } + + if (mPendingState != PendingState::NotPending && + (mPreviousTransitionPhase == TransitionPhase::Idle || + mPreviousTransitionPhase == TransitionPhase::Pending)) { + currentPhase = TransitionPhase::Pending; + } + + if (currentPhase == mPreviousTransitionPhase) { + return; + } + + // TimeStamps to use for ordering the events when they are dispatched. We + // use a TimeStamp so we can compare events produced by different elements, + // perhaps even with different timelines. + // The zero timestamp is for transitionrun events where we ignore the delay + // for the purpose of ordering events. + TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + + AutoTArray events; + + auto appendTransitionEvent = [&](EventMessage aMessage, + const StickyTimeDuration& aElapsedTime, + const TimeStamp& aScheduledEventTimeStamp) { + double elapsedTime = aElapsedTime.ToSeconds(); + if (aMessage == eTransitionCancel) { + // 0 is an inappropriate value for this callsite. What we need to do is + // use a single random value for all increasing times reportable. + // That is to say, whenever elapsedTime goes negative (because an + // animation restarts, something rewinds the animation, or otherwise) + // a new random value for the mix-in must be generated. + elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( + elapsedTime, 0, mRTPCallerType); + } + events.AppendElement(AnimationEventInfo( + TransitionProperty(), mOwningElement.Target(), aMessage, elapsedTime, + aScheduledEventTimeStamp, this)); + }; + + // Handle cancel events first + if ((mPreviousTransitionPhase != TransitionPhase::Idle && + mPreviousTransitionPhase != TransitionPhase::After) && + currentPhase == TransitionPhase::Idle) { + appendTransitionEvent(eTransitionCancel, aActiveTime, + GetTimelineCurrentTimeAsTimeStamp()); + } + + // All other events + switch (mPreviousTransitionPhase) { + case TransitionPhase::Idle: + if (currentPhase == TransitionPhase::Pending || + currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + } else if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } + break; + + case TransitionPhase::Pending: + case TransitionPhase::Before: + if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionStart, intervalStartTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } + break; + + case TransitionPhase::Active: + if (currentPhase == TransitionPhase::After) { + appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); + } else if (currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionEnd, intervalStartTime, + startTimeStamp); + } + break; + + case TransitionPhase::After: + if (currentPhase == TransitionPhase::Active) { + appendTransitionEvent(eTransitionStart, intervalEndTime, + startTimeStamp); + } else if (currentPhase == TransitionPhase::Before) { + appendTransitionEvent(eTransitionStart, intervalEndTime, + startTimeStamp); + appendTransitionEvent(eTransitionEnd, intervalStartTime, endTimeStamp); + } + break; + } + mPreviousTransitionPhase = currentPhase; + + if (!events.IsEmpty()) { + presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); + } +} + +void CSSTransition::Tick() { + Animation::Tick(); + QueueEvents(); +} + +nsCSSPropertyID CSSTransition::TransitionProperty() const { + MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, + "Transition property should be initialized"); + return mTransitionProperty; +} + +AnimationValue CSSTransition::ToValue() const { + MOZ_ASSERT(!mTransitionToValue.IsNull(), + "Transition ToValue should be initialized"); + return mTransitionToValue; +} + +bool CSSTransition::HasLowerCompositeOrderThan( + const CSSTransition& aOther) const { + MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), + "Should only be called for CSS transitions that are sorted " + "as CSS transitions (i.e. tied to CSS markup)"); + + // 0. Object-equality case + if (&aOther == this) { + return false; + } + + // 1. Sort by document order + if (!mOwningElement.Equals(aOther.mOwningElement)) { + return mOwningElement.LessThan( + const_cast(this)->CachedChildIndexRef(), + aOther.mOwningElement, + const_cast(&aOther)->CachedChildIndexRef()); + } + + // 2. (Same element and pseudo): Sort by transition generation + if (mAnimationIndex != aOther.mAnimationIndex) { + return mAnimationIndex < aOther.mAnimationIndex; + } + + // 3. (Same transition generation): Sort by transition property + return nsCSSProps::GetStringValue(TransitionProperty()) < + nsCSSProps::GetStringValue(aOther.TransitionProperty()); +} + +/* static */ +Nullable CSSTransition::GetCurrentTimeAt( + const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, + const TimeDuration& aStartTime, double aPlaybackRate) { + Nullable result; + + Nullable timelineTime = aTimeline.ToTimelineTime(aBaseTime); + if (!timelineTime.IsNull()) { + result.SetValue( + (timelineTime.Value() - aStartTime).MultDouble(aPlaybackRate)); + } + + return result; +} + +double CSSTransition::CurrentValuePortion() const { + if (!GetEffect()) { + return 0.0; + } + + // Transitions use a fill mode of 'backwards' so GetComputedTiming will + // never return a null time progress due to being *before* the animation + // interval. However, it might be possible that we're behind on flushing + // causing us to get called *after* the animation interval. So, just in + // case, we override the fill mode to 'both' to ensure the progress + // is never null. + TimingParams timingToUse = GetEffect()->SpecifiedTiming(); + timingToUse.SetFill(dom::FillMode::Both); + ComputedTiming computedTiming = GetEffect()->GetComputedTiming(&timingToUse); + + if (computedTiming.mProgress.IsNull()) { + return 0.0; + } + + // 'transition-timing-function' corresponds to the effect timing while + // the transition keyframes have a linear timing function so we can ignore + // them for the purposes of calculating the value portion. + return computedTiming.mProgress.Value(); +} + +void CSSTransition::UpdateStartValueFromReplacedTransition() { + MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() && + mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet( + nsCSSPropertyIDSet::CompositorAnimatables()), + "Should be called for compositor-runnable transitions"); + + if (!mReplacedTransition) { + return; + } + + // We don't set |mReplacedTransition| if the timeline of this transition is + // different from the document timeline. The timeline of Animation may be + // null via script, so if it's null, it must be different from the document + // timeline (because document timeline is readonly so we cannot change it by + // script). Therefore, we check this assertion if mReplacedTransition is + // valid. + MOZ_ASSERT(mTimeline, + "Should have a timeline if we are replacing transition start " + "values"); + + ComputedTiming computedTiming = AnimationEffect::GetComputedTimingAt( + CSSTransition::GetCurrentTimeAt(*mTimeline, TimeStamp::Now(), + mReplacedTransition->mStartTime, + mReplacedTransition->mPlaybackRate), + mReplacedTransition->mTiming, mReplacedTransition->mPlaybackRate, + Animation::ProgressTimelinePosition::NotBoundary); + + if (!computedTiming.mProgress.IsNull()) { + double valuePosition = StyleComputedTimingFunction::GetPortion( + mReplacedTransition->mTimingFunction, computedTiming.mProgress.Value(), + computedTiming.mBeforeFlag); + + const AnimationValue& replacedFrom = mReplacedTransition->mFromValue; + const AnimationValue& replacedTo = mReplacedTransition->mToValue; + AnimationValue startValue; + startValue.mServo = + Servo_AnimationValues_Interpolate(replacedFrom.mServo, + replacedTo.mServo, valuePosition) + .Consume(); + + mEffect->AsKeyframeEffect()->ReplaceTransitionStartValue( + std::move(startValue)); + } + + mReplacedTransition.reset(); +} + +void CSSTransition::SetEffectFromStyle(KeyframeEffect* aEffect) { + MOZ_ASSERT(aEffect->IsValidTransition()); + + Animation::SetEffectNoUpdate(aEffect); + mTransitionProperty = aEffect->Properties()[0].mProperty; + mTransitionToValue = aEffect->Properties()[0].mSegments[0].mToValue; +} + +} // namespace mozilla::dom diff --git a/dom/animation/CSSTransition.h b/dom/animation/CSSTransition.h new file mode 100644 index 0000000000..ec246f8c26 --- /dev/null +++ b/dom/animation/CSSTransition.h @@ -0,0 +1,230 @@ +/* -*- 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_CSSTransition_h +#define mozilla_dom_CSSTransition_h + +#include "mozilla/ComputedTiming.h" +#include "mozilla/dom/Animation.h" +#include "mozilla/StyleAnimationValue.h" +#include "AnimationCommon.h" + +class nsIGlobalObject; + +namespace mozilla { +namespace dom { + +class CSSTransition final : public Animation { + public: + explicit CSSTransition(nsIGlobalObject* aGlobal) + : Animation(aGlobal), + mPreviousTransitionPhase(TransitionPhase::Idle), + mNeedsNewAnimationIndexWhenRun(false), + mTransitionProperty(eCSSProperty_UNKNOWN) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + CSSTransition* AsCSSTransition() override { return this; } + const CSSTransition* AsCSSTransition() const override { return this; } + + // CSSTransition interface + void GetTransitionProperty(nsString& aRetVal) const; + + // Animation interface overrides + AnimationPlayState PlayStateFromJS() const override; + bool PendingFromJS() const override; + void PlayFromJS(ErrorResult& aRv) override; + + // A variant of Play() that avoids posting style updates since this method + // is expected to be called whilst already updating style. + void PlayFromStyle() { + ErrorResult rv; + PlayNoUpdate(rv, Animation::LimitBehavior::Continue); + // play() should not throw when LimitBehavior is Continue + MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing transition"); + } + + void CancelFromStyle(PostRestyleMode aPostRestyle) { + // The animation index to use for compositing will be established when + // this transition next transitions out of the idle state but we still + // update it now so that the sort order of this transition remains + // defined until that moment. + // + // See longer explanation in CSSAnimation::CancelFromStyle. + mAnimationIndex = sNextAnimationIndex++; + mNeedsNewAnimationIndexWhenRun = true; + + Animation::Cancel(aPostRestyle); + + // It is important we do this *after* calling Cancel(). + // This is because Cancel() will end up posting a restyle and + // that restyle should target the *transitions* level of the cascade. + // However, once we clear the owning element, CascadeLevel() will begin + // returning CascadeLevel::Animations. + mOwningElement = OwningElementRef(); + } + + void SetEffectFromStyle(KeyframeEffect*); + + void Tick() override; + + nsCSSPropertyID TransitionProperty() const; + AnimationValue ToValue() const; + + bool HasLowerCompositeOrderThan(const CSSTransition& aOther) const; + EffectCompositor::CascadeLevel CascadeLevel() const override { + return IsTiedToMarkup() ? EffectCompositor::CascadeLevel::Transitions + : EffectCompositor::CascadeLevel::Animations; + } + + void SetCreationSequence(uint64_t aIndex) { + MOZ_ASSERT(IsTiedToMarkup()); + mAnimationIndex = aIndex; + } + + // Sets the owning element which is used for determining the composite + // oder of CSSTransition objects generated from CSS markup. + // + // @see mOwningElement + void SetOwningElement(const OwningElementRef& aElement) { + mOwningElement = aElement; + } + // True for transitions that are generated from CSS markup and continue to + // reflect changes to that markup. + bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } + + // Return the animation current time based on a given TimeStamp, a given + // start time and a given playbackRate on a given timeline. This is useful + // when we estimate the current animated value running on the compositor + // because the animation on the compositor may be running ahead while + // main-thread is busy. + static Nullable GetCurrentTimeAt( + const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, + const TimeDuration& aStartTime, double aPlaybackRate); + + void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override { + QueueEvents(aActiveTime); + } + + // Compute the portion of the *value* space that we should be through + // at the current time. (The input to the transition timing function + // has time units, the output has value units.) + double CurrentValuePortion() const; + + const AnimationValue& StartForReversingTest() const { + return mStartForReversingTest; + } + double ReversePortion() const { return mReversePortion; } + + void SetReverseParameters(AnimationValue&& aStartForReversingTest, + double aReversePortion) { + mStartForReversingTest = std::move(aStartForReversingTest); + mReversePortion = aReversePortion; + } + + struct ReplacedTransitionProperties { + TimeDuration mStartTime; + double mPlaybackRate; + TimingParams mTiming; + Maybe mTimingFunction; + AnimationValue mFromValue, mToValue; + }; + void SetReplacedTransition( + ReplacedTransitionProperties&& aReplacedTransition) { + mReplacedTransition.emplace(std::move(aReplacedTransition)); + } + + // For a new transition interrupting an existing transition on the + // compositor, update the start value to match the value of the replaced + // transitions at the current time. + void UpdateStartValueFromReplacedTransition(); + + protected: + virtual ~CSSTransition() { + MOZ_ASSERT(!mOwningElement.IsSet(), + "Owning element should be cleared " + "before a CSS transition is destroyed"); + } + + // Animation overrides + void UpdateTiming(SeekFlag aSeekFlag, + SyncNotifyFlag aSyncNotifyFlag) override; + + void QueueEvents(const StickyTimeDuration& activeTime = StickyTimeDuration()); + + enum class TransitionPhase; + + // The (pseudo-)element whose computed transition-property refers to this + // transition (if any). + // + // This is used for determining the relative composite order of transitions + // generated from CSS markup. + // + // Typically this will be the same as the target element of the keyframe + // effect associated with this transition. However, it can differ in the + // following circumstances: + // + // a) If script removes or replaces the effect of this transition, + // b) If this transition is cancelled (e.g. by updating the + // transition-property or removing the owning element from the document), + // c) If this object is generated from script using the CSSTransition + // constructor. + // + // For (b) and (c) the owning element will return !IsSet(). + OwningElementRef mOwningElement; + + // The 'transition phase' used to determine which transition events need + // to be queued on this tick. + // See: https://drafts.csswg.org/css-transitions-2/#transition-phase + enum class TransitionPhase { + Idle = static_cast(ComputedTiming::AnimationPhase::Idle), + Before = static_cast(ComputedTiming::AnimationPhase::Before), + Active = static_cast(ComputedTiming::AnimationPhase::Active), + After = static_cast(ComputedTiming::AnimationPhase::After), + Pending + }; + TransitionPhase mPreviousTransitionPhase; + + // When true, indicates that when this transition next leaves the idle state, + // its animation index should be updated. + bool mNeedsNewAnimationIndexWhenRun; + + // Store the transition property and to-value here since we need that + // information in order to determine if there is an existing transition + // for a given style change. We can't store that information on the + // effect however since it can be replaced using the Web Animations API. + nsCSSPropertyID mTransitionProperty; + AnimationValue mTransitionToValue; + + // This is the start value to be used for a check for whether a + // transition is being reversed. Normally the same as + // mEffect->mProperties[0].mSegments[0].mFromValue, except when this + // transition started as the reversal of another in-progress transition + // or when the effect has been mutated using the Web Animations API. + // + // Needed so we can handle two reverses in a row. + AnimationValue mStartForReversingTest; + + // Likewise, the portion (in value space) of the "full" reversed + // transition that we're actually covering. For example, if a :hover + // effect has a transition that moves the element 10px to the right + // (by changing 'left' from 0px to 10px), and the mouse moves in to + // the element (starting the transition) but then moves out after the + // transition has advanced 4px, the second transition (from 10px/4px + // to 0px) will have mReversePortion of 0.4. (If the mouse then moves + // in again when the transition is back to 2px, the mReversePortion + // for the third transition (from 0px/2px to 10px) will be 0.8. + double mReversePortion = 1.0; + + Maybe mReplacedTransition; +}; + +} // namespace dom + +} // namespace mozilla + +#endif // mozilla_dom_CSSTransition_h diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h new file mode 100644 index 0000000000..fe98277bb3 --- /dev/null +++ b/dom/animation/ComputedTiming.h @@ -0,0 +1,71 @@ +/* -*- 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/dom/AnimationEffectBinding.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 time within the active interval. + StickyTimeDuration mActiveTime; + // 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. + dom::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 { + Idle, // 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::Idle; + + bool mBeforeFlag = false; +}; + +} // namespace mozilla + +#endif // mozilla_ComputedTiming_h diff --git a/dom/animation/DocumentTimeline.cpp b/dom/animation/DocumentTimeline.cpp new file mode 100644 index 0000000000..1ccd65365a --- /dev/null +++ b/dom/animation/DocumentTimeline.cpp @@ -0,0 +1,311 @@ +/* -*- 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/DocumentInlines.h" +#include "mozilla/dom/DocumentTimelineBinding.h" +#include "AnimationUtils.h" +#include "nsContentUtils.h" +#include "nsDOMMutationObserver.h" +#include "nsDOMNavigationTiming.h" +#include "nsPresContext.h" +#include "nsRefreshDriver.h" + +namespace mozilla::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(DocumentTimeline) +NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline) + +NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline) +NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline) + +DocumentTimeline::DocumentTimeline(Document* aDocument, + const TimeDuration& aOriginTime) + : AnimationTimeline(aDocument->GetParentObject(), + aDocument->GetScopeObject()->GetRTPCallerType()), + mDocument(aDocument), + mIsObservingRefreshDriver(false), + mOriginTime(aOriginTime) { + if (mDocument) { + mDocument->Timelines().insertBack(this); + } + // Ensure mLastRefreshDriverTime is valid. + UpdateLastRefreshDriverTime(); +} + +DocumentTimeline::~DocumentTimeline() { + MOZ_ASSERT(!mIsObservingRefreshDriver, + "Timeline should have disassociated" + " from the refresh driver before being destroyed"); + if (isInList()) { + remove(); + } +} + +JSObject* DocumentTimeline::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return DocumentTimeline_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed DocumentTimeline::Constructor( + const GlobalObject& aGlobal, const DocumentTimelineOptions& aOptions, + ErrorResult& aRv) { + Document* 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("Origin time"); + return nullptr; + } + RefPtr timeline = new DocumentTimeline(doc, originTime); + + return timeline.forget(); +} + +Nullable DocumentTimeline::GetCurrentTimeAsDuration() const { + return ToTimelineTime(GetCurrentTimeStamp()); +} + +bool DocumentTimeline::TracksWallclockTime() const { + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + return !refreshDriver || !refreshDriver->IsTestControllingRefreshesEnabled(); +} + +TimeStamp DocumentTimeline::GetCurrentTimeStamp() const { + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + return refreshDriver ? refreshDriver->MostRecentRefresh() + : mLastRefreshDriverTime; +} + +void DocumentTimeline::UpdateLastRefreshDriverTime() { + 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; + + nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming(); + // If we don't have a refresh driver and we've never had one use the + // timeline's zero time. + // In addition, it's possible that our refresh driver's timestamp is behind + // from the navigation start time because the refresh driver timestamp is + // sent through an IPC call whereas the navigation time is set by calling + // TimeStamp::Now() directly. In such cases we also use the timeline's zero + // time. + if (timing && + (result.IsNull() || result < timing->GetNavigationStartTimeStamp())) { + 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 start time each time. + refreshTime = result; + } + + if (!refreshTime.IsNull()) { + mLastRefreshDriverTime = refreshTime; + } +} + +Nullable DocumentTimeline::ToTimelineTime( + const TimeStamp& aTimeStamp) const { + Nullable result; // Initializes to null + if (aTimeStamp.IsNull()) { + return result; + } + + nsDOMNavigationTiming* 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 && !mAnimationOrder.isEmpty()) { + 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"); + + ObserveRefreshDriver(refreshDriver); + } + } +} + +void DocumentTimeline::MostRecentRefreshTimeUpdated() { + MOZ_ASSERT(mIsObservingRefreshDriver); + MOZ_ASSERT(GetRefreshDriver(), + "Should be able to reach refresh driver from within WillRefresh"); + + nsAutoAnimationMutationBatch mb(mDocument); + + bool ticked = Tick(); + if (!ticked) { + // 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::WillRefresh(mozilla::TimeStamp aTime) { + UpdateLastRefreshDriverTime(); + MostRecentRefreshTimeUpdated(); +} + +void DocumentTimeline::NotifyTimerAdjusted(TimeStamp aTime) { + MostRecentRefreshTimeUpdated(); +} + +void DocumentTimeline::ObserveRefreshDriver(nsRefreshDriver* aDriver) { + MOZ_ASSERT(!mIsObservingRefreshDriver); + // Set the mIsObservingRefreshDriver flag before calling AddRefreshObserver + // since it might end up calling NotifyTimerAdjusted which calls + // MostRecentRefreshTimeUpdated which has an assertion for + // mIsObserveingRefreshDriver check. + mIsObservingRefreshDriver = true; + aDriver->AddRefreshObserver(this, FlushType::Style, + "DocumentTimeline animations"); + aDriver->AddTimerAdjustmentObserver(this); +} + +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"); + ObserveRefreshDriver(aDriver); + // Although we have started observing the refresh driver, it's possible we + // could perform a paint before the first refresh driver tick happens. To + // ensure we're in a consistent state in that case we run the first tick + // manually. + MostRecentRefreshTimeUpdated(); + } +} + +void DocumentTimeline::DisconnectRefreshDriver(nsRefreshDriver* aDriver) { + MOZ_ASSERT(mIsObservingRefreshDriver); + + aDriver->RemoveRefreshObserver(this, FlushType::Style); + aDriver->RemoveTimerAdjustmentObserver(this); + mIsObservingRefreshDriver = false; +} + +void DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver) { + if (!mIsObservingRefreshDriver) { + return; + } + + DisconnectRefreshDriver(aDriver); +} + +void DocumentTimeline::RemoveAnimation(Animation* aAnimation) { + AnimationTimeline::RemoveAnimation(aAnimation); + + if (!mIsObservingRefreshDriver || !mAnimationOrder.isEmpty()) { + return; + } + + UnregisterFromRefreshDriver(); +} + +void DocumentTimeline::NotifyAnimationContentVisibilityChanged( + Animation* aAnimation, bool aIsVisible) { + AnimationTimeline::NotifyAnimationContentVisibilityChanged(aAnimation, + aIsVisible); + + if (mIsObservingRefreshDriver && mAnimationOrder.isEmpty()) { + UnregisterFromRefreshDriver(); + } + + if (!mIsObservingRefreshDriver && !mAnimationOrder.isEmpty()) { + 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"); + + ObserveRefreshDriver(refreshDriver); + } + } +} + +TimeStamp DocumentTimeline::ToTimeStamp( + const TimeDuration& aTimeDuration) const { + TimeStamp result; + nsDOMNavigationTiming* timing = mDocument->GetNavigationTiming(); + if (MOZ_UNLIKELY(!timing)) { + return result; + } + + result = + timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime); + return result; +} + +nsRefreshDriver* DocumentTimeline::GetRefreshDriver() const { + nsPresContext* presContext = mDocument->GetPresContext(); + if (MOZ_UNLIKELY(!presContext)) { + return nullptr; + } + + return presContext->RefreshDriver(); +} + +void DocumentTimeline::UnregisterFromRefreshDriver() { + if (!mIsObservingRefreshDriver) { + return; + } + + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (!refreshDriver) { + return; + } + DisconnectRefreshDriver(refreshDriver); +} + +} // namespace mozilla::dom diff --git a/dom/animation/DocumentTimeline.h b/dom/animation/DocumentTimeline.h new file mode 100644 index 0000000000..b0c8c86546 --- /dev/null +++ b/dom/animation/DocumentTimeline.h @@ -0,0 +1,97 @@ +/* -*- 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/Document.h" +#include "mozilla/dom/DocumentTimelineBinding.h" +#include "mozilla/LinkedList.h" +#include "mozilla/TimeStamp.h" +#include "AnimationTimeline.h" +#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp +#include "nsRefreshDriver.h" +#include "nsRefreshObservers.h" + +struct JSContext; + +namespace mozilla::dom { + +class DocumentTimeline final : public AnimationTimeline, + public nsARefreshObserver, + public nsATimerAdjustmentObserver, + public LinkedListElement { + public: + DocumentTimeline(Document* aDocument, const TimeDuration& aOriginTime); + + protected: + virtual ~DocumentTimeline(); + + 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 + + // This is deliberately _not_ called GetCurrentTime since that would clash + // with a macro defined in winbase.h + virtual Nullable GetCurrentTimeAsDuration() const override; + + bool TracksWallclockTime() const override; + Nullable ToTimelineTime( + const TimeStamp& aTimeStamp) const override; + TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override; + + void NotifyAnimationUpdated(Animation& aAnimation) override; + + void RemoveAnimation(Animation* aAnimation) override; + void NotifyAnimationContentVisibilityChanged(Animation* aAnimation, + bool aIsVisible) override; + + // nsARefreshObserver methods + void WillRefresh(TimeStamp aTime) override; + // nsATimerAdjustmentObserver methods + void NotifyTimerAdjusted(TimeStamp aTime) override; + + void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver); + + Document* GetDocument() const override { return mDocument; } + + void UpdateLastRefreshDriverTime(); + + bool IsMonotonicallyIncreasing() const override { return true; } + + protected: + TimeStamp GetCurrentTimeStamp() const; + nsRefreshDriver* GetRefreshDriver() const; + void UnregisterFromRefreshDriver(); + void MostRecentRefreshTimeUpdated(); + void ObserveRefreshDriver(nsRefreshDriver* aDriver); + void DisconnectRefreshDriver(nsRefreshDriver* aDriver); + + RefPtr 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). + TimeStamp mLastRefreshDriverTime; + bool mIsObservingRefreshDriver; + + TimeDuration mOriginTime; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_DocumentTimeline_h diff --git a/dom/animation/EffectCompositor.cpp b/dom/animation/EffectCompositor.cpp new file mode 100644 index 0000000000..095ec63baf --- /dev/null +++ b/dom/animation/EffectCompositor.cpp @@ -0,0 +1,969 @@ +/* -*- 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 +#include + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "mozilla/AnimationComparator.h" +#include "mozilla/AnimationPerformanceWarning.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/ComputedStyleInlines.h" +#include "mozilla/EffectSet.h" +#include "mozilla/LayerAnimationInfo.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/RestyleManager.h" +#include "mozilla/ServoBindings.h" // Servo_GetProperties_Overriding_Animation +#include "mozilla/ServoStyleSet.h" +#include "mozilla/StaticPrefs_layers.h" +#include "mozilla/StyleAnimationValue.h" +#include "nsContentUtils.h" +#include "nsCSSPseudoElements.h" +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" +#include "nsDisplayItemTypes.h" +#include "nsAtom.h" +#include "nsLayoutUtils.h" +#include "nsTArray.h" +#include "PendingAnimationTracker.h" + +using mozilla::dom::Animation; +using mozilla::dom::Element; +using mozilla::dom::KeyframeEffect; + +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 (const auto& elementSet : tmp->mElementsToRestyle) { + for (const auto& key : elementSet.Keys()) { + CycleCollectionNoteChild(cb, key.mElement, + "EffectCompositor::mElementsToRestyle[]", + cb.Flags()); + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/* static */ +bool EffectCompositor::AllowCompositorAnimationsOnFrame( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aWarning /* out */) { + if (aFrame->RefusedAsyncAnimation()) { + return false; + } + + if (!nsLayoutUtils::AreAsyncAnimationsEnabled()) { + if (StaticPrefs::layers_offmainthreadcomposition_log_animations()) { + 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()) { + aWarning = AnimationPerformanceWarning::Type::HasRenderingObserver; + return false; + } + content = content->GetParent(); + } + + return true; +} + +// 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, const nsCSSPropertyIDSet& aPropertySet, + nsTArray>* aMatches /*out*/) { + // Do not process any animations on the compositor when in print or print + // preview. + if (aFrame->PresContext()->IsPrintingOrPrintPreview()) { + return false; + } + + MOZ_ASSERT( + aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_TRANSFORM)) || + aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_OPACITY)) || + aPropertySet.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_BACKGROUND_COLOR)), + "Should be the subset of transform-like properties, or opacity, " + "or background color"); + + MOZ_ASSERT(!aMatches || aMatches->IsEmpty(), + "Matches array, if provided, should be empty"); + + EffectSet* effects = EffectSet::GetForFrame(aFrame, aPropertySet); + if (!effects || effects->IsEmpty()) { + return false; + } + + // First check for newly-started transform animations that should be + // synchronized with geometric animations. We need to do this before any + // other early returns (the one above is ok) since we can only check this + // state when the animation is newly-started. + if (aPropertySet.Intersects(LayerAnimationInfo::GetCSSPropertiesFor( + DisplayItemType::TYPE_TRANSFORM))) { + PendingAnimationTracker* tracker = + aFrame->PresContext()->Document()->GetPendingAnimationTracker(); + if (tracker) { + tracker->MarkAnimationsThatMightNeedSynchronization(); + } + } + + AnimationPerformanceWarning::Type warning = + AnimationPerformanceWarning::Type::None; + if (!EffectCompositor::AllowCompositorAnimationsOnFrame(aFrame, warning)) { + if (warning != AnimationPerformanceWarning::Type::None) { + EffectCompositor::SetPerformanceWarning( + aFrame, aPropertySet, AnimationPerformanceWarning(warning)); + } + 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( + nsLayoutUtils::GetStyleFrame(aFrame)); + MOZ_ASSERT(pseudoElement, + "We have a valid element for the frame, if we don't we should " + "have bailed out at above the call to EffectSet::Get"); + EffectCompositor::MaybeUpdateCascadeResults(pseudoElement->mElement, + pseudoElement->mPseudoType); + + bool foundRunningAnimations = false; + for (KeyframeEffect* effect : *effects) { + AnimationPerformanceWarning::Type effectWarning = + AnimationPerformanceWarning::Type::None; + KeyframeEffect::MatchForCompositor matchResult = + effect->IsMatchForCompositor(aPropertySet, aFrame, *effects, + effectWarning); + if (effectWarning != AnimationPerformanceWarning::Type::None) { + EffectCompositor::SetPerformanceWarning( + aFrame, aPropertySet, AnimationPerformanceWarning(effectWarning)); + } + + if (matchResult == + KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty) { + // For a given |aFrame|, we don't want some animations of |aPropertySet| + // to run on the compositor and others to run on the main thread, so if + // any need to be synchronized with the main thread, run them all there. + if (aMatches) { + aMatches->Clear(); + } + return false; + } + + if (matchResult == KeyframeEffect::MatchForCompositor::No) { + continue; + } + + if (aMatches) { + aMatches->AppendElement(effect->GetAnimation()); + } + + if (matchResult == KeyframeEffect::MatchForCompositor::Yes) { + foundRunningAnimations = true; + } + } + + // If all animations we added were not currently playing animations, don't + // send them to the compositor. + if (aMatches && !foundRunningAnimations) { + aMatches->Clear(); + } + + MOZ_ASSERT(!foundRunningAnimations || !aMatches || !aMatches->IsEmpty(), + "If return value is true, matches array should be non-empty"); + + if (aMatches && foundRunningAnimations) { + aMatches->Sort(AnimationPtrComparator>()); + } + + return foundRunningAnimations; +} + +void EffectCompositor::RequestRestyle(dom::Element* aElement, + PseudoStyleType aPseudoType, + RestyleType aRestyleType, + CascadeLevel aCascadeLevel) { + if (!mPresContext) { + // Pres context will be null after the effect compositor is disconnected. + return; + } + + // Ignore animations on orphaned elements and elements in documents without + // a pres shell (e.g. XMLHttpRequest responseXML documents). + if (!nsContentUtils::GetPresShellForContent(aElement)) { + return; + } + + auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel]; + PseudoElementHashEntry::KeyType key = {aElement, aPseudoType}; + + bool& restyleEntry = elementsToRestyle.LookupOrInsert(key, false); + if (aRestyleType == RestyleType::Throttled) { + mPresContext->PresShell()->SetNeedThrottledAnimationFlush(); + } else { + // Update hashtable first in case PostRestyleForAnimation mutates it + // and invalidates the restyleEntry reference. + // (It shouldn't, but just to be sure.) + bool skipRestyle = std::exchange(restyleEntry, true); + if (!skipRestyle) { + PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel); + } + } + + if (aRestyleType == RestyleType::Layer) { + mPresContext->RestyleManager()->IncrementAnimationGeneration(); + if (auto* effectSet = EffectSet::Get(aElement, aPseudoType)) { + effectSet->UpdateAnimationGeneration(mPresContext); + } + } +} + +void EffectCompositor::PostRestyleForAnimation(dom::Element* aElement, + PseudoStyleType aPseudoType, + CascadeLevel aCascadeLevel) { + if (!mPresContext) { + return; + } + + // FIXME: Bug 1615083 KeyframeEffect::SetTarget() and + // KeyframeEffect::SetPseudoElement() may set a non-existing pseudo element, + // and we still have to update its style, based on the wpt. However, we don't + // have the generated element here, so we failed the wpt. + // + // See wpt for more info: web-animations/interfaces/KeyframeEffect/target.html + Element* element = + AnimationUtils::GetElementForRestyle(aElement, aPseudoType); + if (!element) { + return; + } + + RestyleHint hint = aCascadeLevel == CascadeLevel::Transitions + ? RestyleHint::RESTYLE_CSS_TRANSITIONS + : RestyleHint::RESTYLE_CSS_ANIMATIONS; + + MOZ_ASSERT(NS_IsMainThread(), + "Restyle request during restyling should be requested only on " + "the main-thread. e.g. after the parallel traversal"); + if (ServoStyleSet::IsInServoTraversal() || mIsInPreTraverse) { + MOZ_ASSERT(hint == RestyleHint::RESTYLE_CSS_ANIMATIONS || + hint == RestyleHint::RESTYLE_CSS_TRANSITIONS); + + // We can't call Servo_NoteExplicitHints here since AtomicRefCell does not + // allow us mutate ElementData of the |aElement| in SequentialTask. + // Instead we call Servo_NoteExplicitHints for the element in PreTraverse() + // which will be called right before the second traversal that we do for + // updating CSS animations. + // In that case PreTraverse() will return true so that we know to do the + // second traversal so we don't need to post any restyle requests to the + // PresShell. + return; + } + + MOZ_ASSERT(!mPresContext->RestyleManager()->IsInStyleRefresh()); + + 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::ClearRestyleRequestsFor(Element* aElement) { + MOZ_ASSERT(aElement); + + auto& elementsToRestyle = mElementsToRestyle[CascadeLevel::Animations]; + + PseudoStyleType pseudoType = aElement->GetPseudoElementType(); + if (pseudoType == PseudoStyleType::NotPseudo) { + PseudoElementHashEntry::KeyType notPseudoKey = {aElement, + PseudoStyleType::NotPseudo}; + PseudoElementHashEntry::KeyType beforePseudoKey = {aElement, + PseudoStyleType::before}; + PseudoElementHashEntry::KeyType afterPseudoKey = {aElement, + PseudoStyleType::after}; + PseudoElementHashEntry::KeyType markerPseudoKey = {aElement, + PseudoStyleType::marker}; + + elementsToRestyle.Remove(notPseudoKey); + elementsToRestyle.Remove(beforePseudoKey); + elementsToRestyle.Remove(afterPseudoKey); + elementsToRestyle.Remove(markerPseudoKey); + } else if (AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) { + Element* parentElement = aElement->GetParentElement(); + MOZ_ASSERT(parentElement); + PseudoElementHashEntry::KeyType key = {parentElement, pseudoType}; + elementsToRestyle.Remove(key); + } +} + +void EffectCompositor::UpdateEffectProperties(const ComputedStyle* aStyle, + Element* aElement, + PseudoStyleType aPseudoType) { + EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType); + if (!effectSet) { + return; + } + + // Style context (Gecko) or computed values (Stylo) change might cause CSS + // cascade level, e.g removing !important, so we should update the cascading + // result. + effectSet->MarkCascadeNeedsUpdate(); + + for (KeyframeEffect* effect : *effectSet) { + effect->UpdateProperties(aStyle); + } +} + +namespace { +class EffectCompositeOrderComparator { + public: + bool Equals(const KeyframeEffect* a, const KeyframeEffect* b) const { + return a == b; + } + + bool LessThan(const KeyframeEffect* a, const KeyframeEffect* 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()); + } +}; +} // namespace + +static void ComposeSortedEffects( + const nsTArray& aSortedEffects, + const EffectSet* aEffectSet, EffectCompositor::CascadeLevel aCascadeLevel, + StyleAnimationValueMap* aAnimationValues) { + const bool isTransition = + aCascadeLevel == EffectCompositor::CascadeLevel::Transitions; + nsCSSPropertyIDSet propertiesToSkip; + // Transitions should be overridden by running animations of the same + // property per https://drafts.csswg.org/css-transitions/#application: + // + // > Implementations must add this value to the cascade if and only if that + // > property is not currently undergoing a CSS Animation on the same element. + // + // FIXME(emilio, bug 1606176): This should assert that + // aEffectSet->PropertiesForAnimationsLevel() is up-to-date, and it may not + // follow the spec in those cases. There are various places where we get style + // without flushing that would trigger the below assertion. + // + // MOZ_ASSERT_IF(aEffectSet, !aEffectSet->CascadeNeedsUpdate()); + if (aEffectSet) { + propertiesToSkip = + isTransition ? aEffectSet->PropertiesForAnimationsLevel() + : aEffectSet->PropertiesForAnimationsLevel().Inverse(); + } + + for (KeyframeEffect* effect : aSortedEffects) { + auto* animation = effect->GetAnimation(); + MOZ_ASSERT(!isTransition || animation->CascadeLevel() == aCascadeLevel); + animation->ComposeStyle(*aAnimationValues, propertiesToSkip); + } +} + +bool EffectCompositor::GetServoAnimationRule( + const dom::Element* aElement, PseudoStyleType aPseudoType, + CascadeLevel aCascadeLevel, StyleAnimationValueMap* aAnimationValues) { + MOZ_ASSERT(aAnimationValues); + // Gecko_GetAnimationRule should have already checked this + MOZ_ASSERT(nsContentUtils::GetPresShellForContent(aElement), + "Should not be trying to run animations on elements in documents" + " without a pres shell (e.g. XMLHttpRequest documents)"); + + EffectSet* effectSet = EffectSet::Get(aElement, aPseudoType); + if (!effectSet) { + return false; + } + + const bool isTransition = aCascadeLevel == CascadeLevel::Transitions; + + // Get a list of effects sorted by composite order. + nsTArray sortedEffectList(effectSet->Count()); + for (KeyframeEffect* effect : *effectSet) { + if (isTransition && + effect->GetAnimation()->CascadeLevel() != aCascadeLevel) { + // We may need to use transition rules for the animations level for the + // case of missing keyframes in animations, but we don't ever need to look + // at non-transition levels to build a transition rule. When the effect + // set information is out of date (see above), this avoids creating bogus + // transition rules, see bug 1605610. + continue; + } + sortedEffectList.AppendElement(effect); + } + + if (sortedEffectList.IsEmpty()) { + return false; + } + + sortedEffectList.Sort(EffectCompositeOrderComparator()); + + ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel, + aAnimationValues); + + MOZ_ASSERT(effectSet == EffectSet::Get(aElement, aPseudoType), + "EffectSet should not change while composing style"); + + return true; +} + +bool EffectCompositor::ComposeServoAnimationRuleForEffect( + KeyframeEffect& aEffect, CascadeLevel aCascadeLevel, + StyleAnimationValueMap* aAnimationValues) { + MOZ_ASSERT(aAnimationValues); + MOZ_ASSERT(mPresContext && mPresContext->IsDynamic(), + "Should not be in print preview"); + + NonOwningAnimationTarget target = aEffect.GetAnimationTarget(); + if (!target) { + return false; + } + + // Don't try to compose animations for elements in documents without a pres + // shell (e.g. XMLHttpRequest documents). + if (!nsContentUtils::GetPresShellForContent(target.mElement)) { + return false; + } + + // GetServoAnimationRule is called as part of the regular style resolution + // where the cascade results are updated in the pre-traversal as needed. + // This function, however, is only called when committing styles so we + // need to ensure the cascade results are up-to-date manually. + MaybeUpdateCascadeResults(target.mElement, target.mPseudoType); + + EffectSet* effectSet = EffectSet::Get(target.mElement, target.mPseudoType); + + // Get a list of effects sorted by composite order up to and including + // |aEffect|, even if it is not in the EffectSet. + auto comparator = EffectCompositeOrderComparator(); + nsTArray sortedEffectList(effectSet ? effectSet->Count() + 1 + : 1); + if (effectSet) { + for (KeyframeEffect* effect : *effectSet) { + if (comparator.LessThan(effect, &aEffect)) { + sortedEffectList.AppendElement(effect); + } + } + sortedEffectList.Sort(comparator); + } + sortedEffectList.AppendElement(&aEffect); + + ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel, + aAnimationValues); + + MOZ_ASSERT(effectSet == EffectSet::Get(target.mElement, target.mPseudoType), + "EffectSet should not change while composing style"); + + return true; +} + +bool EffectCompositor::HasPendingStyleUpdates() const { + for (auto& elementSet : mElementsToRestyle) { + if (elementSet.Count()) { + return true; + } + } + + return false; +} + +/* static */ +bool EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame, + DisplayItemType aType) { + return FindAnimationsForCompositor( + aFrame, LayerAnimationInfo::GetCSSPropertiesFor(aType), nullptr); +} + +/* static */ +nsTArray> EffectCompositor::GetAnimationsForCompositor( + const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet) { + nsTArray> result; + +#ifdef DEBUG + bool foundSome = +#endif + FindAnimationsForCompositor(aFrame, aPropertySet, &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, + DisplayItemType aType) { + EffectSet* effects = EffectSet::GetForFrame(aFrame, aType); + if (!effects) { + return; + } + + const nsCSSPropertyIDSet& propertySet = + LayerAnimationInfo::GetCSSPropertiesFor(aType); + for (KeyframeEffect* effect : *effects) { + effect->SetIsRunningOnCompositor(propertySet, false); + } +} + +/* static */ +void EffectCompositor::MaybeUpdateCascadeResults(Element* aElement, + PseudoStyleType aPseudoType) { + EffectSet* effects = EffectSet::Get(aElement, aPseudoType); + if (!effects || !effects->CascadeNeedsUpdate()) { + return; + } + + UpdateCascadeResults(*effects, aElement, aPseudoType); + + MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state"); +} + +/* static */ +Maybe +EffectCompositor::GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame) { + // Always return the same object to benefit from return-value optimization. + Maybe result; + + PseudoStyleType pseudoType = aFrame->Style()->GetPseudoType(); + + if (pseudoType != PseudoStyleType::NotPseudo && + !AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) { + return result; + } + + nsIContent* content = aFrame->GetContent(); + if (!content) { + return result; + } + + if (AnimationUtils::IsSupportedPseudoForAnimations(pseudoType)) { + content = content->GetParent(); + if (!content) { + return result; + } + } + + if (!content->IsElement()) { + return result; + } + + result.emplace(content->AsElement(), pseudoType); + + return result; +} + +/* static */ +nsCSSPropertyIDSet EffectCompositor::GetOverriddenProperties( + EffectSet& aEffectSet, Element* aElement, PseudoStyleType aPseudoType) { + MOZ_ASSERT(aElement, "Should have an element to get style data from"); + + nsCSSPropertyIDSet result; + + Element* elementForRestyle = + AnimationUtils::GetElementForRestyle(aElement, aPseudoType); + if (!elementForRestyle) { + return result; + } + + static constexpr size_t compositorAnimatableCount = + nsCSSPropertyIDSet::CompositorAnimatableCount(); + AutoTArray propertiesToTrack; + { + nsCSSPropertyIDSet propertiesToTrackAsSet; + for (KeyframeEffect* effect : aEffectSet) { + for (const AnimationProperty& property : effect->Properties()) { + if (nsCSSProps::PropHasFlags(property.mProperty, + CSSPropFlags::CanAnimateOnCompositor) && + !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() == compositorAnimatableCount) { + break; + } + } + } + + if (propertiesToTrack.IsEmpty()) { + return result; + } + + Servo_GetProperties_Overriding_Animation(elementForRestyle, + &propertiesToTrack, &result); + return result; +} + +/* static */ +void EffectCompositor::UpdateCascadeResults(EffectSet& aEffectSet, + Element* aElement, + PseudoStyleType aPseudoType) { + MOZ_ASSERT(EffectSet::Get(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 (KeyframeEffect* 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 = + GetOverriddenProperties(aEffectSet, aElement, aPseudoType); + + nsCSSPropertyIDSet& propertiesWithImportantRules = + aEffectSet.PropertiesWithImportantRules(); + nsCSSPropertyIDSet& propertiesForAnimationsLevel = + aEffectSet.PropertiesForAnimationsLevel(); + + static constexpr nsCSSPropertyIDSet compositorAnimatables = + nsCSSPropertyIDSet::CompositorAnimatables(); + // Record which compositor-animatable properties were originally set so we can + // compare for changes later. + nsCSSPropertyIDSet prevCompositorPropertiesWithImportantRules = + propertiesWithImportantRules.Intersect(compositorAnimatables); + + nsCSSPropertyIDSet prevPropertiesForAnimationsLevel = + propertiesForAnimationsLevel; + + propertiesWithImportantRules.Empty(); + propertiesForAnimationsLevel.Empty(); + + nsCSSPropertyIDSet propertiesForTransitionsLevel; + + for (const KeyframeEffect* 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); + } + + switch (cascadeLevel) { + case EffectCompositor::CascadeLevel::Animations: + propertiesForAnimationsLevel.AddProperty(prop.mProperty); + break; + case EffectCompositor::CascadeLevel::Transitions: + propertiesForTransitionsLevel.AddProperty(prop.mProperty); + break; + } + } + } + + aEffectSet.MarkCascadeUpdated(); + + nsPresContext* presContext = nsContentUtils::GetContextForContent(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.Equals( + propertiesWithImportantRules.Intersect(compositorAnimatables))) { + presContext->EffectCompositor()->RequestRestyle( + aElement, aPseudoType, EffectCompositor::RestyleType::Layer, + EffectCompositor::CascadeLevel::Animations); + } + + // If we have transition properties and if the same propery for animations + // level is newly added or removed, we need to update the transition level + // rule since the it will be added/removed from the rule tree. + nsCSSPropertyIDSet changedPropertiesForAnimationLevel = + prevPropertiesForAnimationsLevel.Xor(propertiesForAnimationsLevel); + nsCSSPropertyIDSet commonProperties = propertiesForTransitionsLevel.Intersect( + changedPropertiesForAnimationLevel); + if (!commonProperties.IsEmpty()) { + EffectCompositor::RestyleType restyleType = + changedPropertiesForAnimationLevel.Intersects(compositorAnimatables) + ? EffectCompositor::RestyleType::Standard + : EffectCompositor::RestyleType::Layer; + presContext->EffectCompositor()->RequestRestyle( + aElement, aPseudoType, restyleType, + EffectCompositor::CascadeLevel::Transitions); + } +} + +/* static */ +void EffectCompositor::SetPerformanceWarning( + const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet, + const AnimationPerformanceWarning& aWarning) { + EffectSet* effects = EffectSet::GetForFrame(aFrame, aPropertySet); + if (!effects) { + return; + } + + for (KeyframeEffect* effect : *effects) { + effect->SetPerformanceWarning(aPropertySet, aWarning); + } +} + +bool EffectCompositor::PreTraverse(ServoTraversalFlags aFlags) { + return PreTraverseInSubtree(aFlags, nullptr); +} + +bool EffectCompositor::PreTraverseInSubtree(ServoTraversalFlags aFlags, + Element* aRoot) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aRoot || nsContentUtils::GetPresShellForContent(aRoot), + "Traversal root, if provided, should be bound to a display " + "document"); + + // Convert the root element to the parent element if the root element is + // pseudo since we check each element in mElementsToRestyle is in the subtree + // of the root element later in this function, but for pseudo elements the + // element in mElementsToRestyle is the parent of the pseudo. + if (aRoot && (aRoot->IsGeneratedContentContainerForBefore() || + aRoot->IsGeneratedContentContainerForAfter() || + aRoot->IsGeneratedContentContainerForMarker())) { + aRoot = aRoot->GetParentElement(); + } + + AutoRestore guard(mIsInPreTraverse); + mIsInPreTraverse = true; + + // We need to force flush all throttled animations if we also have + // non-animation restyles (since we'll want the up-to-date animation style + // when we go to process them so we can trigger transitions correctly), and + // if we are currently flushing all throttled animation restyles. + bool flushThrottledRestyles = + (aRoot && aRoot->HasDirtyDescendantsForServo()) || + (aFlags & ServoTraversalFlags::FlushThrottledAnimations); + + using ElementsToRestyleIterType = + nsTHashMap::ConstIterator; + auto getNeededRestyleTarget = + [&](const ElementsToRestyleIterType& aIter) -> NonOwningAnimationTarget { + NonOwningAnimationTarget returnTarget; + + // If aIter.Data() is false, the element only requested a throttled + // (skippable) restyle, so we can skip it if flushThrottledRestyles is not + // true. + if (!flushThrottledRestyles && !aIter.Data()) { + return returnTarget; + } + + const NonOwningAnimationTarget& target = aIter.Key(); + + // Skip elements in documents without a pres shell. Normally we filter out + // such elements in RequestRestyle but it can happen that, after adding + // them to mElementsToRestyle, they are transferred to a different document. + // + // We will drop them from mElementsToRestyle at the end of the next full + // document restyle (at the end of this function) but for consistency with + // how we treat such elements in RequestRestyle, we just ignore them here. + if (!nsContentUtils::GetPresShellForContent(target.mElement)) { + return returnTarget; + } + + // Ignore restyles that aren't in the flattened tree subtree rooted at + // aRoot. + if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOfForStyle( + target.mElement, aRoot)) { + return returnTarget; + } + + returnTarget = target; + return returnTarget; + }; + + bool foundElementsNeedingRestyle = false; + + nsTArray elementsWithCascadeUpdates; + for (size_t i = 0; i < kCascadeLevelCount; ++i) { + CascadeLevel cascadeLevel = CascadeLevel(i); + auto& elementSet = mElementsToRestyle[cascadeLevel]; + for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) { + const NonOwningAnimationTarget& target = getNeededRestyleTarget(iter); + if (!target.mElement) { + continue; + } + + EffectSet* effects = EffectSet::Get(target.mElement, target.mPseudoType); + if (!effects || !effects->CascadeNeedsUpdate()) { + continue; + } + + elementsWithCascadeUpdates.AppendElement(target); + } + } + + for (const NonOwningAnimationTarget& target : elementsWithCascadeUpdates) { + MaybeUpdateCascadeResults(target.mElement, target.mPseudoType); + } + elementsWithCascadeUpdates.Clear(); + + 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()) { + const NonOwningAnimationTarget& target = getNeededRestyleTarget(iter); + if (!target.mElement) { + continue; + } + + if (target.mElement->GetComposedDoc() != mPresContext->Document()) { + iter.Remove(); + continue; + } + + // We need to post restyle hints even if the target is not in EffectSet to + // ensure the final restyling for removed animations. + // We can't call PostRestyleEvent directly here since we are still in the + // middle of the servo traversal. + mPresContext->RestyleManager()->PostRestyleEventForAnimations( + target.mElement, target.mPseudoType, + cascadeLevel == CascadeLevel::Transitions + ? RestyleHint::RESTYLE_CSS_TRANSITIONS + : RestyleHint::RESTYLE_CSS_ANIMATIONS); + + foundElementsNeedingRestyle = true; + + auto* effects = EffectSet::Get(target.mElement, target.mPseudoType); + if (!effects) { + // Drop EffectSets that have been destroyed. + iter.Remove(); + continue; + } + + for (KeyframeEffect* effect : *effects) { + effect->GetAnimation()->WillComposeStyle(); + } + + // Remove the element from the list of elements to restyle since we are + // about to restyle it. + iter.Remove(); + } + + // If this is a full document restyle, then unconditionally clear + // elementSet in case there are any elements that didn't match above + // because they were moved to a document without a pres shell after + // posting an animation restyle. + if (!aRoot && flushThrottledRestyles) { + elementSet.Clear(); + } + } + + return foundElementsNeedingRestyle; +} + +void EffectCompositor::NoteElementForReducing( + const NonOwningAnimationTarget& aTarget) { + if (!StaticPrefs::dom_animations_api_autoremove_enabled()) { + return; + } + + Unused << mElementsToReduce.put( + OwningAnimationTarget{aTarget.mElement, aTarget.mPseudoType}); +} + +static void ReduceEffectSet(EffectSet& aEffectSet) { + // Get a list of effects sorted by composite order. + nsTArray sortedEffectList(aEffectSet.Count()); + for (KeyframeEffect* effect : aEffectSet) { + sortedEffectList.AppendElement(effect); + } + sortedEffectList.Sort(EffectCompositeOrderComparator()); + + nsCSSPropertyIDSet setProperties; + + // Iterate in reverse + for (auto iter = sortedEffectList.rbegin(); iter != sortedEffectList.rend(); + ++iter) { + MOZ_ASSERT(*iter && (*iter)->GetAnimation(), + "Effect in an EffectSet should have an animation"); + KeyframeEffect& effect = **iter; + Animation& animation = *effect.GetAnimation(); + if (animation.IsRemovable() && + effect.GetPropertySet().IsSubsetOf(setProperties)) { + animation.Remove(); + } else if (animation.IsReplaceable()) { + setProperties |= effect.GetPropertySet(); + } + } +} + +void EffectCompositor::ReduceAnimations() { + for (auto iter = mElementsToReduce.iter(); !iter.done(); iter.next()) { + const OwningAnimationTarget& target = iter.get(); + auto* effectSet = EffectSet::Get(target.mElement, target.mPseudoType); + if (effectSet) { + ReduceEffectSet(*effectSet); + } + } + + mElementsToReduce.clear(); +} + +} // namespace mozilla diff --git a/dom/animation/EffectCompositor.h b/dom/animation/EffectCompositor.h new file mode 100644 index 0000000000..34c2688486 --- /dev/null +++ b/dom/animation/EffectCompositor.h @@ -0,0 +1,258 @@ +/* -*- 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/AnimationPerformanceWarning.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/HashTable.h" +#include "mozilla/Maybe.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/PseudoElementHashEntry.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ServoTypes.h" +#include "nsCSSPropertyID.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsTArray.h" + +class nsCSSPropertyIDSet; +class nsAtom; +class nsIFrame; +class nsPresContext; +enum class DisplayItemType : uint8_t; + +namespace mozilla { + +class ComputedStyle; +class EffectSet; +class RestyleTracker; +struct StyleAnimationValue; +struct StyleAnimationValueMap; +struct AnimationProperty; +struct NonOwningAnimationTarget; + +namespace dom { +class Animation; +class Element; +class KeyframeEffect; +} // namespace dom + +class EffectCompositor { + public: + explicit EffectCompositor(nsPresContext* aPresContext) + : mPresContext(aPresContext) {} + + 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 : uint32_t { + // The animations sheet (CSS animations, script-generated animations, + // and CSS transitions that are no longer tied to CSS markup) + Animations = 0, + // The transitions sheet (CSS transitions that are tied to CSS markup) + Transitions = 1 + }; + // 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, PseudoStyleType 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, + PseudoStyleType 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(); + + // Clear all pending restyle requests for the given (pseudo-) element (and its + // ::before, ::after and ::marker elements if the given element is not + // pseudo). + void ClearRestyleRequestsFor(dom::Element* aElement); + + // Called when computed style 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(const ComputedStyle* aStyle, + dom::Element* aElement, + PseudoStyleType aPseudoType); + + // Get the animation rule for the appropriate level of the cascade for + // a (pseudo-)element. Called from the Servo side. + // + // The animation rule is stored in |StyleAnimationValueMap|. + // We need to be careful while doing any modification because it may cause + // some thread-safe issues. + bool GetServoAnimationRule(const dom::Element* aElement, + PseudoStyleType aPseudoType, + CascadeLevel aCascadeLevel, + StyleAnimationValueMap* aAnimationValues); + + // A variant on GetServoAnimationRule that composes all the effects for an + // element up to and including |aEffect|. + // + // Note that |aEffect| might not be in the EffectSet since we can use this for + // committing the computed style of a removed Animation. + bool ComposeServoAnimationRuleForEffect( + dom::KeyframeEffect& aEffect, CascadeLevel aCascadeLevel, + StyleAnimationValueMap* aAnimationValues); + + bool HasPendingStyleUpdates() const; + + static bool HasAnimationsForCompositor(const nsIFrame* aFrame, + DisplayItemType aType); + + static nsTArray> GetAnimationsForCompositor( + const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet); + + static void ClearIsRunningOnCompositor(const nsIFrame* aFrame, + DisplayItemType aType); + + // 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. + // + // 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, + PseudoStyleType aPseudoType); + + // Update the mPropertiesWithImportantRules and + // mPropertiesForAnimationsLevel members of the given EffectSet, and also + // request any restyles required by changes to the cascade result. + // + // NOTE: 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. + // + // This is typically reserved for internal callers but is public here since + // when we detect changes to the cascade on the Servo side we can't call + // MarkCascadeNeedsUpdate during the traversal so instead we call this as part + // of a follow-up sequential task. + static void UpdateCascadeResults(EffectSet& aEffectSet, + dom::Element* aElement, + PseudoStyleType aPseudoType); + + // 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. + // + // For display:table content, which maintains a distinction between primary + // frame (table wrapper frame) and style frame (inner table frame), animations + // are stored on the content associated with the _style_ frame even though + // some (particularly transform-like animations) may be applied to the + // _primary_ frame. As a result, callers will typically want to pass the style + // frame to this function. + // + // 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 animate + // properties in |aPropertySet|. + static void SetPerformanceWarning( + const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet, + const AnimationPerformanceWarning& aWarning); + + // Do a bunch of stuff that we should avoid doing during the parallel + // traversal (e.g. changing member variables) for all elements that we expect + // to restyle on the next traversal. + // + // Returns true if there are elements needing a restyle for animation. + bool PreTraverse(ServoTraversalFlags aFlags); + + // Similar to the above but for all elements in the subtree rooted + // at aElement. + bool PreTraverseInSubtree(ServoTraversalFlags aFlags, dom::Element* aRoot); + + // Record a (pseudo-)element that may have animations that can be removed. + void NoteElementForReducing(const NonOwningAnimationTarget& aTarget); + + bool NeedsReducing() const { return !mElementsToReduce.empty(); } + void ReduceAnimations(); + + // Returns true if any type of compositor animations on |aFrame| allow + // runnning on the compositor. + // Sets the reason in |aWarning| if the result is false. + static bool AllowCompositorAnimationsOnFrame( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aWarning /* out */); + + private: + ~EffectCompositor() = default; + + // 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. + static nsCSSPropertyIDSet GetOverriddenProperties( + EffectSet& aEffectSet, dom::Element* aElement, + PseudoStyleType aPseudoType); + + 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; + + bool mIsInPreTraverse = false; + + HashSet mElementsToReduce; +}; + +} // namespace mozilla + +#endif // mozilla_EffectCompositor_h diff --git a/dom/animation/EffectSet.cpp b/dom/animation/EffectSet.cpp new file mode 100644 index 0000000000..160b1322e8 --- /dev/null +++ b/dom/animation/EffectSet.cpp @@ -0,0 +1,132 @@ +/* -*- 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/RestyleManager.h" +#include "mozilla/LayerAnimationInfo.h" +#include "nsCSSPseudoElements.h" // For PseudoStyleType +#include "nsCycleCollectionNoteChild.h" // For CycleCollectionNoteChild +#include "nsPresContext.h" +#include "nsLayoutUtils.h" +#include "ElementAnimationData.h" + +namespace mozilla { + +void EffectSet::Traverse(nsCycleCollectionTraversalCallback& aCallback) { + for (const auto& key : mEffects) { + CycleCollectionNoteChild(aCallback, key, "EffectSet::mEffects[]", + aCallback.Flags()); + } +} + +/* static */ +EffectSet* EffectSet::Get(const dom::Element* aElement, + PseudoStyleType aPseudoType) { + if (auto* data = aElement->GetAnimationData()) { + return data->GetEffectSetFor(aPseudoType); + } + return nullptr; +} + +/* static */ +EffectSet* EffectSet::GetOrCreate(dom::Element* aElement, + PseudoStyleType aPseudoType) { + return &aElement->EnsureAnimationData().EnsureEffectSetFor(aPseudoType); +} + +/* static */ +EffectSet* EffectSet::GetForFrame(const nsIFrame* aFrame, + const nsCSSPropertyIDSet& aProperties) { + MOZ_ASSERT(aFrame); + + // Transform animations are run on the primary frame (but stored on the + // content associated with the style frame). + const nsIFrame* frameToQuery = nullptr; + if (aProperties.IsSubsetOf(nsCSSPropertyIDSet::TransformLikeProperties())) { + // Make sure to return nullptr if we're looking for transform animations on + // the inner table frame. + if (!aFrame->IsFrameOfType(nsIFrame::eSupportsCSSTransforms)) { + return nullptr; + } + frameToQuery = nsLayoutUtils::GetStyleFrame(aFrame); + } else { + MOZ_ASSERT( + !aProperties.Intersects(nsCSSPropertyIDSet::TransformLikeProperties()), + "We should have only transform properties or no transform properties"); + // We don't need to explicitly return nullptr when |aFrame| is NOT the style + // frame since there will be no effect set in that case. + frameToQuery = aFrame; + } + + Maybe target = + EffectCompositor::GetAnimationElementAndPseudoForFrame(frameToQuery); + if (!target) { + return nullptr; + } + + return Get(target->mElement, target->mPseudoType); +} + +/* static */ +EffectSet* EffectSet::GetForFrame(const nsIFrame* aFrame, + DisplayItemType aDisplayItemType) { + return EffectSet::GetForFrame( + aFrame, LayerAnimationInfo::GetCSSPropertiesFor(aDisplayItemType)); +} + +/* static */ +EffectSet* EffectSet::GetForStyleFrame(const nsIFrame* aStyleFrame) { + Maybe target = + EffectCompositor::GetAnimationElementAndPseudoForFrame(aStyleFrame); + + if (!target) { + return nullptr; + } + + return Get(target->mElement, target->mPseudoType); +} + +/* static */ +EffectSet* EffectSet::GetForEffect(const dom::KeyframeEffect* aEffect) { + NonOwningAnimationTarget target = aEffect->GetAnimationTarget(); + if (!target) { + return nullptr; + } + + return EffectSet::Get(target.mElement, target.mPseudoType); +} + +/* static */ +void EffectSet::DestroyEffectSet(dom::Element* aElement, + PseudoStyleType aPseudoType) { + if (auto* data = aElement->GetAnimationData()) { + data->ClearEffectSetFor(aPseudoType); + } +} + +void EffectSet::UpdateAnimationGeneration(nsPresContext* aPresContext) { + mAnimationGeneration = + aPresContext->RestyleManager()->GetAnimationGeneration(); +} + +void EffectSet::AddEffect(dom::KeyframeEffect& aEffect) { + if (!mEffects.EnsureInserted(&aEffect)) { + return; + } + + MarkCascadeNeedsUpdate(); +} + +void EffectSet::RemoveEffect(dom::KeyframeEffect& aEffect) { + if (!mEffects.EnsureRemoved(&aEffect)) { + return; + } + + MarkCascadeNeedsUpdate(); +} + +} // namespace mozilla diff --git a/dom/animation/EffectSet.h b/dom/animation/EffectSet.h new file mode 100644 index 0000000000..c743676794 --- /dev/null +++ b/dom/animation/EffectSet.h @@ -0,0 +1,246 @@ +/* -*- 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/DebugOnly.h" +#include "mozilla/EffectCompositor.h" +#include "mozilla/EnumeratedArray.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/KeyframeEffect.h" +#include "nsHashKeys.h" // For nsPtrHashKey +#include "nsTHashSet.h" + +class nsPresContext; +enum class DisplayItemType : uint8_t; + +namespace mozilla { + +namespace dom { +class Element; +} // namespace dom + +enum class PseudoStyleType : 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), + mMayHaveOpacityAnim(false), + mMayHaveTransformAnim(false) { + MOZ_COUNT_CTOR(EffectSet); + } + + ~EffectSet() { + MOZ_ASSERT(!IsBeingEnumerated(), + "Effect set should not be destroyed while it is being " + "enumerated"); + MOZ_COUNT_DTOR(EffectSet); + } + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback& aCallback); + + static EffectSet* Get(const dom::Element* aElement, + PseudoStyleType aPseudoType); + static EffectSet* GetOrCreate(dom::Element* aElement, + PseudoStyleType aPseudoType); + + static EffectSet* GetForFrame(const nsIFrame* aFrame, + const nsCSSPropertyIDSet& aProperties); + static EffectSet* GetForFrame(const nsIFrame* aFrame, + DisplayItemType aDisplayItemType); + // Gets the EffectSet associated with the specified frame's content. + // + // Typically the specified frame should be a "style frame". + // + // That is because display:table content: + // + // - makes a distinction between the primary frame and style frame, + // - associates the EffectSet with the style frame's content, + // - applies transform animations to the primary frame. + // + // In such a situation, passing in the primary frame here will return nullptr + // despite the fact that it has a transform animation applied to it. + // + // GetForFrame, above, handles this by automatically looking up the + // EffectSet on the corresponding style frame when querying transform + // properties. Unless you are sure you know what you are doing, you should + // try using GetForFrame first. + // + // If you decide to use this, consider documenting why you are sure it is ok + // to use this. + static EffectSet* GetForStyleFrame(const nsIFrame* aStyleFrame); + + static EffectSet* GetForEffect(const dom::KeyframeEffect* aEffect); + + static void DestroyEffectSet(dom::Element* aElement, + PseudoStyleType aPseudoType); + + void AddEffect(dom::KeyframeEffect& aEffect); + void RemoveEffect(dom::KeyframeEffect& aEffect); + + void SetMayHaveOpacityAnimation() { mMayHaveOpacityAnim = true; } + bool MayHaveOpacityAnimation() const { return mMayHaveOpacityAnim; } + void SetMayHaveTransformAnimation() { mMayHaveTransformAnim = true; } + bool MayHaveTransformAnimation() const { return mMayHaveTransformAnim; } + + private: + using OwningEffectSet = nsTHashSet>; + + 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(). + // + // XXX Except for the active iterator checks, this could be replaced by the + // STL-style iterators of nsTHashSet directly now. + class Iterator { + public: + explicit Iterator(EffectSet& aEffectSet) + : Iterator(aEffectSet, aEffectSet.mEffects.begin()) {} + + Iterator() = delete; + Iterator(const Iterator&) = delete; + Iterator(Iterator&&) = delete; + Iterator& operator=(const Iterator&) = delete; + Iterator& operator=(Iterator&&) = delete; + + static Iterator EndIterator(EffectSet& aEffectSet) { + return {aEffectSet, aEffectSet.mEffects.end()}; + } + +#ifdef DEBUG + ~Iterator() { + MOZ_ASSERT(mEffectSet.mActiveIterators > 0); + mEffectSet.mActiveIterators--; + } +#endif + + bool operator!=(const Iterator& aOther) const { + return mHashIterator != aOther.mHashIterator; + } + + Iterator& operator++() { + ++mHashIterator; + return *this; + } + + dom::KeyframeEffect* operator*() { return *mHashIterator; } + + private: + Iterator(EffectSet& aEffectSet, + OwningEffectSet::const_iterator aHashIterator) + : +#ifdef DEBUG + mEffectSet(aEffectSet), +#endif + mHashIterator(std::move(aHashIterator)) { +#ifdef DEBUG + mEffectSet.mActiveIterators++; +#endif + } + +#ifdef DEBUG + EffectSet& mEffectSet; +#endif + OwningEffectSet::const_iterator mHashIterator; + }; + + 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(); } + + const TimeStamp& LastOverflowAnimationSyncTime() const { + return mLastOverflowAnimationSyncTime; + } + void UpdateLastOverflowAnimationSyncTime(const TimeStamp& aRefreshTime) { + mLastOverflowAnimationSyncTime = aRefreshTime; + } + + bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; } + void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; } + void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; } + + void UpdateAnimationGeneration(nsPresContext* aPresContext); + uint64_t GetAnimationGeneration() const { return mAnimationGeneration; } + + const nsCSSPropertyIDSet& PropertiesWithImportantRules() const { + return mPropertiesWithImportantRules; + } + nsCSSPropertyIDSet& PropertiesWithImportantRules() { + return mPropertiesWithImportantRules; + } + nsCSSPropertyIDSet PropertiesForAnimationsLevel() const { + return mPropertiesForAnimationsLevel; + } + nsCSSPropertyIDSet& PropertiesForAnimationsLevel() { + return mPropertiesForAnimationsLevel; + } + + private: + OwningEffectSet mEffects; + + // Refresh driver timestamp from the moment when the animations which produce + // overflow change hints in this effect set were last updated. + + // This is used for animations whose main-thread restyling is throttled either + // because they are running on the compositor or because they are not visible. + // We still need to update them on the main thread periodically, however (e.g. + // so scrollbars can be updated), so this tracks the last time we did that. + TimeStamp mLastOverflowAnimationSyncTime; + + // 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 = 0; + + // 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 = 0; +#endif + + // 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 = false; + + bool mMayHaveOpacityAnim = false; + bool mMayHaveTransformAnim = false; +}; + +} // namespace mozilla + +#endif // mozilla_EffectSet_h diff --git a/dom/animation/ElementAnimationData.cpp b/dom/animation/ElementAnimationData.cpp new file mode 100644 index 0000000000..d87caf8a58 --- /dev/null +++ b/dom/animation/ElementAnimationData.cpp @@ -0,0 +1,126 @@ +/* -*- 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 "ElementAnimationData.h" +#include "mozilla/AnimationCollection.h" +#include "mozilla/TimelineCollection.h" +#include "mozilla/EffectSet.h" +#include "mozilla/dom/CSSTransition.h" +#include "mozilla/dom/CSSAnimation.h" +#include "mozilla/dom/ScrollTimeline.h" +#include "mozilla/dom/ViewTimeline.h" + +namespace mozilla { + +void ElementAnimationData::Traverse(nsCycleCollectionTraversalCallback& cb) { + mElementData.Traverse(cb); + mBeforeData.Traverse(cb); + mAfterData.Traverse(cb); + mMarkerData.Traverse(cb); +} + +void ElementAnimationData::ClearAllAnimationCollections() { + for (auto* data : {&mElementData, &mBeforeData, &mAfterData, &mMarkerData}) { + data->mAnimations = nullptr; + data->mTransitions = nullptr; + data->mScrollTimelines = nullptr; + data->mViewTimelines = nullptr; + data->mProgressTimelineScheduler = nullptr; + } +} + +ElementAnimationData::PerElementOrPseudoData::PerElementOrPseudoData() = + default; +ElementAnimationData::PerElementOrPseudoData::~PerElementOrPseudoData() = + default; + +void ElementAnimationData::PerElementOrPseudoData::Traverse( + nsCycleCollectionTraversalCallback& cb) { + // We only care about mEffectSet. The animation collections are managed by the + // pres context and go away when presentation of the document goes away. + if (mEffectSet) { + mEffectSet->Traverse(cb); + } +} + +EffectSet& ElementAnimationData::PerElementOrPseudoData::DoEnsureEffectSet() { + MOZ_ASSERT(!mEffectSet); + mEffectSet = MakeUnique(); + return *mEffectSet; +} + +CSSTransitionCollection& +ElementAnimationData::PerElementOrPseudoData::DoEnsureTransitions( + dom::Element& aOwner, PseudoStyleType aType) { + MOZ_ASSERT(!mTransitions); + mTransitions = MakeUnique(aOwner, aType); + return *mTransitions; +} + +CSSAnimationCollection& +ElementAnimationData::PerElementOrPseudoData::DoEnsureAnimations( + dom::Element& aOwner, PseudoStyleType aType) { + MOZ_ASSERT(!mAnimations); + mAnimations = MakeUnique(aOwner, aType); + return *mAnimations; +} + +ScrollTimelineCollection& +ElementAnimationData::PerElementOrPseudoData::DoEnsureScrollTimelines( + dom::Element& aOwner, PseudoStyleType aType) { + MOZ_ASSERT(!mScrollTimelines); + mScrollTimelines = MakeUnique(aOwner, aType); + return *mScrollTimelines; +} + +ViewTimelineCollection& +ElementAnimationData::PerElementOrPseudoData::DoEnsureViewTimelines( + dom::Element& aOwner, PseudoStyleType aType) { + MOZ_ASSERT(!mViewTimelines); + mViewTimelines = MakeUnique(aOwner, aType); + return *mViewTimelines; +} + +dom::ProgressTimelineScheduler& +ElementAnimationData::PerElementOrPseudoData::DoEnsureProgressTimelineScheduler( + dom::Element& aOwner, PseudoStyleType aType) { + MOZ_ASSERT(!mProgressTimelineScheduler); + mProgressTimelineScheduler = MakeUnique(); + return *mProgressTimelineScheduler; +} + +void ElementAnimationData::PerElementOrPseudoData::DoClearEffectSet() { + MOZ_ASSERT(mEffectSet); + mEffectSet = nullptr; +} + +void ElementAnimationData::PerElementOrPseudoData::DoClearTransitions() { + MOZ_ASSERT(mTransitions); + mTransitions = nullptr; +} + +void ElementAnimationData::PerElementOrPseudoData::DoClearAnimations() { + MOZ_ASSERT(mAnimations); + mAnimations = nullptr; +} + +void ElementAnimationData::PerElementOrPseudoData::DoClearScrollTimelines() { + MOZ_ASSERT(mScrollTimelines); + mScrollTimelines = nullptr; +} + +void ElementAnimationData::PerElementOrPseudoData::DoClearViewTimelines() { + MOZ_ASSERT(mViewTimelines); + mViewTimelines = nullptr; +} + +void ElementAnimationData::PerElementOrPseudoData:: + DoClearProgressTimelineScheduler() { + MOZ_ASSERT(mProgressTimelineScheduler); + mProgressTimelineScheduler = nullptr; +} + +} // namespace mozilla diff --git a/dom/animation/ElementAnimationData.h b/dom/animation/ElementAnimationData.h new file mode 100644 index 0000000000..9183f0f008 --- /dev/null +++ b/dom/animation/ElementAnimationData.h @@ -0,0 +1,262 @@ +/* -*- 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_ElementAnimationData_h +#define mozilla_ElementAnimationData_h + +#include "mozilla/UniquePtr.h" +#include "mozilla/PseudoStyleType.h" + +class nsCycleCollectionTraversalCallback; + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class EffectSet; +template +class AnimationCollection; +template +class TimelineCollection; +namespace dom { +class Element; +class CSSAnimation; +class CSSTransition; +class ProgressTimelineScheduler; +class ScrollTimeline; +class ViewTimeline; +} // namespace dom +using CSSAnimationCollection = AnimationCollection; +using CSSTransitionCollection = AnimationCollection; +using ScrollTimelineCollection = TimelineCollection; +using ViewTimelineCollection = TimelineCollection; + +// The animation data for a given element (and its pseudo-elements). +class ElementAnimationData { + struct PerElementOrPseudoData { + UniquePtr mEffectSet; + UniquePtr mAnimations; + UniquePtr mTransitions; + + // Note: scroll-timeline-name is applied to elements which could be + // scroll containers, or replaced elements. view-timeline-name is applied to + // all elements. However, the named timeline is referenceable in + // animation-timeline by the tree order scope. + // Spec: https://drafts.csswg.org/scroll-animations-1/#timeline-scope. + // + // So it should be fine to create timeline objects only on the elements and + // pseudo elements which support animations. + UniquePtr mScrollTimelines; + UniquePtr mViewTimelines; + + // This is different from |mScrollTimelines|. We use this to schedule all + // scroll-driven animations (which use anonymous/named scroll timelines or + // anonymous/name view timelines) for a specific scroll source (which is the + // element with nsIScrollableFrame). + // + // TimelineCollection owns and manages the named progress timeline generated + // by specifying scroll-timeline-name property and view-timeline-name + // property on this element. However, the anonymous progress timelines (e.g. + // animation-timeline:scroll()) are owned by Animation objects only. + // + // Note: + // 1. For named scroll timelines. The element which specifies + // scroll-timeline-name is the scroll source. However, for named view + // timelines, the element which specifies view-timeline-name may not be + // the scroll source because we use its nearest scroll container as the + // scroll source. + // 2. For anonymous progress timelines, we don't keep their timeline obejcts + // in TimelineCollection. + // So, per 1) and 2), we use |mProgressTimelineScheduler| for the scroll + // source element to schedule scroll-driven animations. + UniquePtr mProgressTimelineScheduler; + + PerElementOrPseudoData(); + ~PerElementOrPseudoData(); + + EffectSet& DoEnsureEffectSet(); + CSSTransitionCollection& DoEnsureTransitions(dom::Element&, + PseudoStyleType); + CSSAnimationCollection& DoEnsureAnimations(dom::Element&, PseudoStyleType); + ScrollTimelineCollection& DoEnsureScrollTimelines(dom::Element&, + PseudoStyleType); + ViewTimelineCollection& DoEnsureViewTimelines(dom::Element&, + PseudoStyleType); + dom::ProgressTimelineScheduler& DoEnsureProgressTimelineScheduler( + dom::Element&, PseudoStyleType); + + void DoClearEffectSet(); + void DoClearTransitions(); + void DoClearAnimations(); + void DoClearScrollTimelines(); + void DoClearViewTimelines(); + void DoClearProgressTimelineScheduler(); + + void Traverse(nsCycleCollectionTraversalCallback&); + }; + + PerElementOrPseudoData mElementData; + + // TODO(emilio): Maybe this should be a hash map eventually, once we allow + // animating all pseudo-elements. + PerElementOrPseudoData mBeforeData; + PerElementOrPseudoData mAfterData; + PerElementOrPseudoData mMarkerData; + + const PerElementOrPseudoData& DataFor(PseudoStyleType aType) const { + switch (aType) { + case PseudoStyleType::NotPseudo: + break; + case PseudoStyleType::before: + return mBeforeData; + case PseudoStyleType::after: + return mAfterData; + case PseudoStyleType::marker: + return mMarkerData; + default: + MOZ_ASSERT_UNREACHABLE( + "Should not try to get animation effects for " + "a pseudo other that :before, :after or ::marker"); + break; + } + return mElementData; + } + + PerElementOrPseudoData& DataFor(PseudoStyleType aType) { + const auto& data = + const_cast(this)->DataFor(aType); + return const_cast(data); + } + + public: + void Traverse(nsCycleCollectionTraversalCallback&); + + void ClearAllAnimationCollections(); + + EffectSet* GetEffectSetFor(PseudoStyleType aType) const { + return DataFor(aType).mEffectSet.get(); + } + + void ClearEffectSetFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mEffectSet) { + data.DoClearEffectSet(); + } + } + + EffectSet& EnsureEffectSetFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* set = data.mEffectSet.get()) { + return *set; + } + return data.DoEnsureEffectSet(); + } + + CSSTransitionCollection* GetTransitionCollection(PseudoStyleType aType) { + return DataFor(aType).mTransitions.get(); + } + + void ClearTransitionCollectionFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mTransitions) { + data.DoClearTransitions(); + } + } + + CSSTransitionCollection& EnsureTransitionCollection(dom::Element& aOwner, + PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* collection = data.mTransitions.get()) { + return *collection; + } + return data.DoEnsureTransitions(aOwner, aType); + } + + CSSAnimationCollection* GetAnimationCollection(PseudoStyleType aType) { + return DataFor(aType).mAnimations.get(); + } + + void ClearAnimationCollectionFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mAnimations) { + data.DoClearAnimations(); + } + } + + CSSAnimationCollection& EnsureAnimationCollection(dom::Element& aOwner, + PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* collection = data.mAnimations.get()) { + return *collection; + } + return data.DoEnsureAnimations(aOwner, aType); + } + + ScrollTimelineCollection* GetScrollTimelineCollection(PseudoStyleType aType) { + return DataFor(aType).mScrollTimelines.get(); + } + + void ClearScrollTimelineCollectionFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mScrollTimelines) { + data.DoClearScrollTimelines(); + } + } + + ScrollTimelineCollection& EnsureScrollTimelineCollection( + dom::Element& aOwner, PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* collection = data.mScrollTimelines.get()) { + return *collection; + } + return data.DoEnsureScrollTimelines(aOwner, aType); + } + + ViewTimelineCollection* GetViewTimelineCollection(PseudoStyleType aType) { + return DataFor(aType).mViewTimelines.get(); + } + + void ClearViewTimelineCollectionFor(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mViewTimelines) { + data.DoClearViewTimelines(); + } + } + + ViewTimelineCollection& EnsureViewTimelineCollection(dom::Element& aOwner, + PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* collection = data.mViewTimelines.get()) { + return *collection; + } + return data.DoEnsureViewTimelines(aOwner, aType); + } + + dom::ProgressTimelineScheduler* GetProgressTimelineScheduler( + PseudoStyleType aType) { + return DataFor(aType).mProgressTimelineScheduler.get(); + } + + void ClearProgressTimelineScheduler(PseudoStyleType aType) { + auto& data = DataFor(aType); + if (data.mProgressTimelineScheduler) { + data.DoClearProgressTimelineScheduler(); + } + } + + dom::ProgressTimelineScheduler& EnsureProgressTimelineScheduler( + dom::Element& aOwner, PseudoStyleType aType) { + auto& data = DataFor(aType); + if (auto* collection = data.mProgressTimelineScheduler.get()) { + return *collection; + } + return data.DoEnsureProgressTimelineScheduler(aOwner, aType); + } + + ElementAnimationData() = default; +}; + +} // namespace mozilla + +#endif diff --git a/dom/animation/Keyframe.h b/dom/animation/Keyframe.h new file mode 100644 index 0000000000..304358d61e --- /dev/null +++ b/dom/animation/Keyframe.h @@ -0,0 +1,83 @@ +/* -*- 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_Keyframe_h +#define mozilla_dom_Keyframe_h + +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" +#include "nsTArray.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" // CompositeOperationOrAuto +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +struct StyleLockedDeclarationBlock; + +/** + * A property-value pair specified on a keyframe. + */ +struct PropertyValuePair { + explicit PropertyValuePair(nsCSSPropertyID aProperty) + : mProperty(aProperty) {} + PropertyValuePair(nsCSSPropertyID aProperty, + RefPtr&& aValue) + : mProperty(aProperty), mServoDeclarationBlock(std::move(aValue)) { + MOZ_ASSERT(mServoDeclarationBlock, "Should be valid property value"); + } + + nsCSSPropertyID mProperty; + + // The specified value when using the Servo backend. + RefPtr mServoDeclarationBlock; + +#ifdef DEBUG + // Flag to indicate that when we call StyleAnimationValue::ComputeValues on + // this value we should behave as if that function had failed. + bool mSimulateComputeValuesFailure = false; +#endif + + 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 computed style 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) = default; + + Keyframe& operator=(const Keyframe& aOther) = default; + Keyframe& operator=(Keyframe&& aOther) = default; + + Maybe mOffset; + static constexpr double kComputedOffsetNotSet = -1.0; + double mComputedOffset = kComputedOffsetNotSet; + Maybe mTimingFunction; // Nothing() here means + // "linear" + dom::CompositeOperationOrAuto mComposite = + dom::CompositeOperationOrAuto::Auto; + CopyableTArray mPropertyValues; +}; + +} // namespace mozilla + +#endif // mozilla_dom_Keyframe_h diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp new file mode 100644 index 0000000000..2f0d886ac1 --- /dev/null +++ b/dom/animation/KeyframeEffect.cpp @@ -0,0 +1,2117 @@ +/* -*- 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/Animation.h" +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" +// For UnrestrictedDoubleOrKeyframeAnimationOptions; +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/dom/MutationObservers.h" +#include "mozilla/layers/AnimationInfo.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/ComputedStyleInlines.h" +#include "mozilla/EffectSet.h" +#include "mozilla/FloatingPoint.h" // For IsFinite +#include "mozilla/LayerAnimationInfo.h" +#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt +#include "mozilla/KeyframeUtils.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/StaticPrefs_layers.h" +#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetComputedStyle +#include "nsContentUtils.h" +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags +#include "nsCSSPseudoElements.h" // For PseudoStyleType +#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsPresContextInlines.h" +#include "nsRefreshDriver.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "WindowRenderer.h" + +namespace mozilla { + +void AnimationProperty::SetPerformanceWarning( + const AnimationPerformanceWarning& aWarning, const dom::Element* aElement) { + if (mPerformanceWarning && *mPerformanceWarning == aWarning) { + return; + } + + mPerformanceWarning = Some(aWarning); + + nsAutoString localizedString; + if (StaticPrefs::layers_offmainthreadcomposition_log_animations() && + mPerformanceWarning->ToLocalizedString(localizedString)) { + nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString); + AnimationUtils::LogAsyncAnimationFailure(logMessage, aElement); + } +} + +bool PropertyValuePair::operator==(const PropertyValuePair& aOther) const { + if (mProperty != aOther.mProperty) { + 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(KeyframeEffect, AnimationEffect, + mTarget.mElement) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffect, AnimationEffect) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(KeyframeEffect) +NS_INTERFACE_MAP_END_INHERITING(AnimationEffect) + +NS_IMPL_ADDREF_INHERITED(KeyframeEffect, AnimationEffect) +NS_IMPL_RELEASE_INHERITED(KeyframeEffect, AnimationEffect) + +KeyframeEffect::KeyframeEffect(Document* aDocument, + OwningAnimationTarget&& aTarget, + TimingParams&& aTiming, + const KeyframeEffectParams& aOptions) + : AnimationEffect(aDocument, std::move(aTiming)), + mTarget(std::move(aTarget)), + mEffectOptions(aOptions) {} + +KeyframeEffect::KeyframeEffect(Document* aDocument, + OwningAnimationTarget&& aTarget, + const KeyframeEffect& aOther) + : AnimationEffect(aDocument, TimingParams{aOther.SpecifiedTiming()}), + mTarget(std::move(aTarget)), + mEffectOptions{aOther.IterationComposite(), aOther.Composite(), + mTarget.mPseudoType}, + mKeyframes(aOther.mKeyframes.Clone()), + mProperties(aOther.mProperties.Clone()), + mBaseValues(aOther.mBaseValues.Clone()) {} + +JSObject* KeyframeEffect::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return KeyframeEffect_Binding::Wrap(aCx, this, aGivenProto); +} + +IterationCompositeOperation KeyframeEffect::IterationComposite() const { + return mEffectOptions.mIterationComposite; +} + +void KeyframeEffect::SetIterationComposite( + const IterationCompositeOperation& aIterationComposite) { + if (mEffectOptions.mIterationComposite == aIterationComposite) { + return; + } + + if (mAnimation && mAnimation->IsRelevant()) { + MutationObservers::NotifyAnimationChanged(mAnimation); + } + + mEffectOptions.mIterationComposite = aIterationComposite; + RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +CompositeOperation KeyframeEffect::Composite() const { + return mEffectOptions.mComposite; +} + +void KeyframeEffect::SetComposite(const CompositeOperation& aComposite) { + if (mEffectOptions.mComposite == aComposite) { + return; + } + + mEffectOptions.mComposite = aComposite; + + if (mAnimation && mAnimation->IsRelevant()) { + MutationObservers::NotifyAnimationChanged(mAnimation); + } + + if (mTarget) { + RefPtr computedStyle = + GetTargetComputedStyle(Flush::None); + if (computedStyle) { + UpdateProperties(computedStyle); + } + } +} + +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()) { + MutationObservers::NotifyAnimationChanged(mAnimation); + } + + RequestRestyle(EffectCompositor::RestyleType::Layer); + } +} + +void KeyframeEffect::NotifyAnimationTimingUpdated( + PostRestyleMode aPostRestyle) { + 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(); + } + + // Request restyle if necessary. + if (aPostRestyle == PostRestyleMode::IfNeeded && mAnimation && + !mProperties.IsEmpty() && HasComputedTimingChanged()) { + EffectCompositor::RestyleType restyleType = + CanThrottle() ? EffectCompositor::RestyleType::Throttled + : EffectCompositor::RestyleType::Standard; + RequestRestyle(restyleType); + } + + // Detect changes to "in effect" status since we need to recalculate the + // animation cascade for this element whenever that changes. + // Note that updating mInEffectOnLastAnimationTimingUpdate has to be done + // after above CanThrottle() call since the function uses the flag inside it. + bool inEffect = IsInEffect(); + if (inEffect != mInEffectOnLastAnimationTimingUpdate) { + MarkCascadeNeedsUpdate(); + mInEffectOnLastAnimationTimingUpdate = inEffect; + } + + // 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://drafts.csswg.org/web-animations/#dom-keyframeeffect-setkeyframes +void KeyframeEffect::SetKeyframes(JSContext* aContext, + JS::Handle aKeyframes, + ErrorResult& aRv) { + nsTArray keyframes = KeyframeUtils::GetKeyframesFromObject( + aContext, mDocument, aKeyframes, "KeyframeEffect.setKeyframes", aRv); + if (aRv.Failed()) { + return; + } + + RefPtr style = GetTargetComputedStyle(Flush::None); + SetKeyframes(std::move(keyframes), style, nullptr /* AnimationTimeline */); +} + +void KeyframeEffect::SetKeyframes(nsTArray&& aKeyframes, + const ComputedStyle* aStyle, + const AnimationTimeline* aTimeline) { + if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) { + return; + } + + mKeyframes = std::move(aKeyframes); + KeyframeUtils::DistributeKeyframes(mKeyframes); + + if (mAnimation && mAnimation->IsRelevant()) { + MutationObservers::NotifyAnimationChanged(mAnimation); + } + + // We need to call UpdateProperties() unless the target element doesn't have + // style (e.g. the target element is not associated with any document). + if (aStyle) { + UpdateProperties(aStyle, aTimeline); + } +} + +void KeyframeEffect::ReplaceTransitionStartValue(AnimationValue&& aStartValue) { + if (!aStartValue.mServo) { + return; + } + + // A typical transition should have a single property and a single segment. + // + // (And for atypical transitions, that is, those updated by script, we don't + // apply the replacing behavior.) + if (mProperties.Length() != 1 || mProperties[0].mSegments.Length() != 1) { + return; + } + + // Likewise, check that the keyframes are of the expected shape. + if (mKeyframes.Length() != 2 || mKeyframes[0].mPropertyValues.Length() != 1) { + return; + } + + // Check that the value we are about to substitute in is actually for the + // same property. + if (Servo_AnimationValue_GetPropertyId(aStartValue.mServo) != + mProperties[0].mProperty) { + return; + } + + mKeyframes[0].mPropertyValues[0].mServoDeclarationBlock = + Servo_AnimationValue_Uncompute(aStartValue.mServo).Consume(); + mProperties[0].mSegments[0].mFromValue = std::move(aStartValue); +} + +static bool IsEffectiveProperty(const EffectSet& aEffects, + nsCSSPropertyID aProperty) { + return !aEffects.PropertiesWithImportantRules().HasProperty(aProperty) || + !aEffects.PropertiesForAnimationsLevel().HasProperty(aProperty); +} + +const AnimationProperty* KeyframeEffect::GetEffectiveAnimationOfProperty( + nsCSSPropertyID aProperty, const EffectSet& aEffects) const { + MOZ_ASSERT(mTarget && &aEffects == EffectSet::Get(mTarget.mElement, + mTarget.mPseudoType)); + + for (const AnimationProperty& property : mProperties) { + if (aProperty != property.mProperty) { + continue; + } + + const AnimationProperty* result = nullptr; + // Only include the property if it is not overridden by !important rules in + // the transitions level. + if (IsEffectiveProperty(aEffects, property.mProperty)) { + result = &property; + } + return result; + } + return nullptr; +} + +bool KeyframeEffect::HasEffectiveAnimationOfPropertySet( + const nsCSSPropertyIDSet& aPropertySet, const EffectSet& aEffectSet) const { + for (const AnimationProperty& property : mProperties) { + if (aPropertySet.HasProperty(property.mProperty) && + IsEffectiveProperty(aEffectSet, property.mProperty)) { + return true; + } + } + return false; +} + +nsCSSPropertyIDSet KeyframeEffect::GetPropertiesForCompositor( + EffectSet& aEffects, const nsIFrame* aFrame) const { + MOZ_ASSERT(&aEffects == + EffectSet::Get(mTarget.mElement, mTarget.mPseudoType)); + + nsCSSPropertyIDSet properties; + + if (!mAnimation || !mAnimation->IsRelevant()) { + return properties; + } + + static constexpr nsCSSPropertyIDSet compositorAnimatables = + nsCSSPropertyIDSet::CompositorAnimatables(); + static constexpr nsCSSPropertyIDSet transformLikeProperties = + nsCSSPropertyIDSet::TransformLikeProperties(); + + nsCSSPropertyIDSet transformSet; + AnimationPerformanceWarning::Type dummyWarning; + + for (const AnimationProperty& property : mProperties) { + if (!compositorAnimatables.HasProperty(property.mProperty)) { + continue; + } + + // Transform-like properties are combined together on the compositor so we + // need to evaluate them as a group. We build up a separate set here then + // evaluate it as a separate step below. + if (transformLikeProperties.HasProperty(property.mProperty)) { + transformSet.AddProperty(property.mProperty); + continue; + } + + KeyframeEffect::MatchForCompositor matchResult = IsMatchForCompositor( + nsCSSPropertyIDSet{property.mProperty}, aFrame, aEffects, dummyWarning); + if (matchResult == + KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty || + matchResult == KeyframeEffect::MatchForCompositor::No) { + continue; + } + properties.AddProperty(property.mProperty); + } + + if (!transformSet.IsEmpty()) { + KeyframeEffect::MatchForCompositor matchResult = + IsMatchForCompositor(transformSet, aFrame, aEffects, dummyWarning); + if (matchResult == KeyframeEffect::MatchForCompositor::Yes || + matchResult == KeyframeEffect::MatchForCompositor::IfNeeded) { + properties |= transformSet; + } + } + + return properties; +} + +nsCSSPropertyIDSet KeyframeEffect::GetPropertySet() const { + nsCSSPropertyIDSet result; + + for (const AnimationProperty& property : mProperties) { + result.AddProperty(property.mProperty); + } + + return result; +} + +#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 + +static bool HasCurrentColor( + const nsTArray& aSegments) { + for (const AnimationPropertySegment& segment : aSegments) { + if ((!segment.mFromValue.IsNull() && segment.mFromValue.IsCurrentColor()) || + (!segment.mToValue.IsNull() && segment.mToValue.IsCurrentColor())) { + return true; + } + } + return false; +} +void KeyframeEffect::UpdateProperties(const ComputedStyle* aStyle, + const AnimationTimeline* aTimeline) { + MOZ_ASSERT(aStyle); + + nsTArray properties = BuildProperties(aStyle); + + bool propertiesChanged = mProperties != properties; + + // We need to update base styles even if any properties are not changed at all + // since base styles might have been changed due to parent style changes, etc. + bool baseStylesChanged = false; + EnsureBaseStyles(aStyle, properties, aTimeline, + !propertiesChanged ? &baseStylesChanged : nullptr); + + if (!propertiesChanged) { + if (baseStylesChanged) { + RequestRestyle(EffectCompositor::RestyleType::Layer); + } + // Check if we need to update the cumulative change hint because we now have + // style data. + if (mNeedsStyleData && mTarget && mTarget.mElement->HasServoData()) { + CalculateCumulativeChangeHint(aStyle); + } + return; + } + + // Preserve the state of the mIsRunningOnCompositor flag. + nsCSSPropertyIDSet runningOnCompositorProperties; + + for (const AnimationProperty& property : mProperties) { + if (property.mIsRunningOnCompositor) { + runningOnCompositorProperties.AddProperty(property.mProperty); + } + } + + mProperties = std::move(properties); + UpdateEffectSet(); + + mHasCurrentColor = false; + + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = + runningOnCompositorProperties.HasProperty(property.mProperty); + + if (property.mProperty == eCSSProperty_background_color && + !mHasCurrentColor) { + if (HasCurrentColor(property.mSegments)) { + mHasCurrentColor = true; + break; + } + } + } + + CalculateCumulativeChangeHint(aStyle); + + MarkCascadeNeedsUpdate(); + + if (mAnimation) { + mAnimation->NotifyEffectPropertiesUpdated(); + } + + RequestRestyle(EffectCompositor::RestyleType::Layer); +} + +void KeyframeEffect::EnsureBaseStyles( + const ComputedStyle* aComputedValues, + const nsTArray& aProperties, + const AnimationTimeline* aTimeline, bool* aBaseStylesChanged) { + if (aBaseStylesChanged != nullptr) { + *aBaseStylesChanged = false; + } + + if (!mTarget) { + return; + } + + BaseValuesHashmap previousBaseStyles; + if (aBaseStylesChanged != nullptr) { + previousBaseStyles = std::move(mBaseValues); + } + + mBaseValues.Clear(); + + nsPresContext* presContext = + nsContentUtils::GetContextForContent(mTarget.mElement); + // If |aProperties| is empty we're not going to dereference |presContext| so + // we don't care if it is nullptr. + // + // We could just return early when |aProperties| is empty and save looking up + // the pres context, but that won't save any effort normally since we don't + // call this function if we have no keyframes to begin with. Furthermore, the + // case where |presContext| is nullptr is so rare (we've only ever seen in + // fuzzing, and even then we've never been able to reproduce it reliably) + // it's not worth the runtime cost of an extra branch. + MOZ_ASSERT(presContext || aProperties.IsEmpty(), + "Typically presContext should not be nullptr but if it is" + " we should have also failed to calculate the computed values" + " passed-in as aProperties"); + + if (!aTimeline) { + // If we pass a valid timeline, we use it (note: this happens when we create + // a new animation or replace the old one, for CSS Animations and CSS + // Transitions). Otherwise, we check the timeline from |mAnimation|. + aTimeline = mAnimation ? mAnimation->GetTimeline() : nullptr; + } + + RefPtr baseComputedStyle; + for (const AnimationProperty& property : aProperties) { + EnsureBaseStyle(property, presContext, aComputedValues, aTimeline, + baseComputedStyle); + } + + if (aBaseStylesChanged != nullptr && + std::any_of( + mBaseValues.cbegin(), mBaseValues.cend(), [&](const auto& entry) { + return AnimationValue(entry.GetData()) != + AnimationValue(previousBaseStyles.Get(entry.GetKey())); + })) { + *aBaseStylesChanged = true; + } +} + +void KeyframeEffect::EnsureBaseStyle( + const AnimationProperty& aProperty, nsPresContext* aPresContext, + const ComputedStyle* aComputedStyle, const AnimationTimeline* aTimeline, + RefPtr& aBaseComputedStyle) { + auto needBaseStyleForScrollTimeline = + [this](const AnimationProperty& aProperty, + const AnimationTimeline* aTimeline) { + static constexpr TimeDuration zeroDuration; + const TimingParams& timing = NormalizedTiming(); + // For scroll-timeline with a positive delay, it's possible to scroll + // back and forth between delay phase and active phase, so we need to + // keep its base style and maybe use it to override the animations in + // delay on the compositor. + return aTimeline && aTimeline->IsScrollTimeline() && + nsCSSPropertyIDSet::CompositorAnimatables().HasProperty( + aProperty.mProperty) && + (timing.Delay() > zeroDuration || + timing.EndDelay() > zeroDuration); + }; + auto hasAdditiveValues = [](const AnimationProperty& aProperty) { + for (const AnimationPropertySegment& segment : aProperty.mSegments) { + if (!segment.HasReplaceableValues()) { + return true; + } + } + return false; + }; + + // Note: Check base style for compositor (i.e. for scroll-driven animations) + // first because it is much cleaper. + const bool needBaseStyle = + needBaseStyleForScrollTimeline(aProperty, aTimeline) || + hasAdditiveValues(aProperty); + if (!needBaseStyle) { + return; + } + + if (!aBaseComputedStyle) { + MOZ_ASSERT(mTarget, "Should have a valid target"); + + Element* animatingElement = AnimationUtils::GetElementForRestyle( + mTarget.mElement, mTarget.mPseudoType); + if (!animatingElement) { + return; + } + aBaseComputedStyle = aPresContext->StyleSet()->GetBaseContextForElement( + animatingElement, aComputedStyle); + } + RefPtr baseValue = + Servo_ComputedValues_ExtractAnimationValue(aBaseComputedStyle, + aProperty.mProperty) + .Consume(); + mBaseValues.InsertOrUpdate(aProperty.mProperty, std::move(baseValue)); +} + +void KeyframeEffect::WillComposeStyle() { + ComputedTiming computedTiming = GetComputedTiming(); + mProgressOnLastCompose = computedTiming.mProgress; + mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration; +} + +void KeyframeEffect::ComposeStyleRule(StyleAnimationValueMap& aAnimationValues, + const AnimationProperty& aProperty, + const AnimationPropertySegment& aSegment, + const ComputedTiming& aComputedTiming) { + auto* opaqueTable = + reinterpret_cast(&mBaseValues); + Servo_AnimationCompose(&aAnimationValues, opaqueTable, aProperty.mProperty, + &aSegment, &aProperty.mSegments.LastElement(), + &aComputedTiming, mEffectOptions.mIterationComposite); +} + +void KeyframeEffect::ComposeStyle(StyleAnimationValueMap& aComposeResult, + const nsCSSPropertyIDSet& aPropertiesToSkip) { + ComputedTiming computedTiming = GetComputedTiming(); + + // 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"); + + ComposeStyleRule(aComposeResult, prop, *segment, computedTiming); + } + + // If the animation produces a change hint that affects the overflow region, + // we need to record the current time to unthrottle the animation + // periodically when the animation is being throttled because it's scrolled + // out of view. + if (HasPropertiesThatMightAffectOverflow()) { + nsPresContext* presContext = + nsContentUtils::GetContextForContent(mTarget.mElement); + EffectSet* effectSet = + EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + if (presContext && effectSet) { + TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh(); + effectSet->UpdateLastOverflowAnimationSyncTime(now); + } + } +} + +bool KeyframeEffect::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 KeyframeEffect::SetIsRunningOnCompositor(nsCSSPropertyID aProperty, + bool aIsRunning) { + MOZ_ASSERT( + nsCSSProps::PropHasFlags(aProperty, CSSPropFlags::CanAnimateOnCompositor), + "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(); + } else if (mAnimation && mAnimation->IsPartialPrerendered()) { + ResetPartialPrerendered(); + } + return; + } + } +} + +void KeyframeEffect::SetIsRunningOnCompositor( + const nsCSSPropertyIDSet& aPropertySet, bool aIsRunning) { + for (AnimationProperty& property : mProperties) { + if (aPropertySet.HasProperty(property.mProperty)) { + MOZ_ASSERT(nsCSSProps::PropHasFlags(property.mProperty, + CSSPropFlags::CanAnimateOnCompositor), + "Property being animated on compositor is a recognized " + "compositor-animatable property"); + 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(); + } + } + } + + if (!aIsRunning && mAnimation && mAnimation->IsPartialPrerendered()) { + ResetPartialPrerendered(); + } +} + +void KeyframeEffect::ResetIsRunningOnCompositor() { + for (AnimationProperty& property : mProperties) { + property.mIsRunningOnCompositor = false; + } + + if (mAnimation && mAnimation->IsPartialPrerendered()) { + ResetPartialPrerendered(); + } +} + +void KeyframeEffect::ResetPartialPrerendered() { + MOZ_ASSERT(mAnimation && mAnimation->IsPartialPrerendered()); + + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) { + return; + } + + nsIWidget* widget = frame->GetNearestWidget(); + if (!widget) { + return; + } + + if (WindowRenderer* windowRenderer = widget->GetWindowRenderer()) { + windowRenderer->RemovePartialPrerenderedAnimation( + mAnimation->IdOnCompositor(), mAnimation); + } +} + +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, CallerType aCallerType, ErrorResult& aRv) { + KeyframeEffectParams result; + if (aOptions.IsUnrestrictedDouble()) { + return result; + } + + const KeyframeEffectOptions& options = + KeyframeEffectOptionsFromUnion(aOptions); + + // If dom.animations-api.compositing.enabled is turned off, + // iterationComposite and composite are the default value 'replace' in the + // dictionary. + result.mIterationComposite = options.mIterationComposite; + result.mComposite = options.mComposite; + + result.mPseudoType = PseudoStyleType::NotPseudo; + if (DOMStringIsNull(options.mPseudoElement)) { + return result; + } + + Maybe pseudoType = + nsCSSPseudoElements::GetPseudoType(options.mPseudoElement); + if (!pseudoType) { + // Per the spec, we throw SyntaxError for syntactically invalid pseudos. + aRv.ThrowSyntaxError( + nsPrintfCString("'%s' is a syntactically invalid pseudo-element.", + NS_ConvertUTF16toUTF8(options.mPseudoElement).get())); + return result; + } + + result.mPseudoType = *pseudoType; + if (!AnimationUtils::IsSupportedPseudoForAnimations(result.mPseudoType)) { + // Per the spec, we throw SyntaxError for unsupported pseudos. + aRv.ThrowSyntaxError( + nsPrintfCString("'%s' is an unsupported pseudo-element.", + NS_ConvertUTF16toUTF8(options.mPseudoElement).get())); + } + + return result; +} + +template +/* static */ +already_AddRefed KeyframeEffect::ConstructKeyframeEffect( + const GlobalObject& aGlobal, Element* aTarget, + JS::Handle aKeyframes, const OptionsType& aOptions, + ErrorResult& aRv) { + // We should get the document from `aGlobal` instead of the current Realm + // to make this works in Xray case. + // + // In all non-Xray cases, `aGlobal` matches the current Realm, so this + // matches the spec behavior. + // + // In Xray case, the new objects should be created using the document of + // the target global, but the KeyframeEffect constructors are called in the + // caller's compartment to access `aKeyframes` object. + Document* doc = AnimationUtils::GetDocumentFromGlobal(aGlobal.Get()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + KeyframeEffectParams effectOptions = + KeyframeEffectParamsFromUnion(aOptions, aGlobal.CallerType(), aRv); + // An invalid Pseudo-element aborts all further steps. + if (aRv.Failed()) { + return nullptr; + } + + TimingParams timingParams = TimingParams::FromOptionsUnion(aOptions, aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr effect = new KeyframeEffect( + doc, OwningAnimationTarget(aTarget, effectOptions.mPseudoType), + std::move(timingParams), effectOptions); + + effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return effect.forget(); +} + +nsTArray KeyframeEffect::BuildProperties( + const ComputedStyle* aStyle) { + MOZ_ASSERT(aStyle); + + nsTArray result; + // If mTarget is false (i.e. mTarget.mElement 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.Clone()); + + result = KeyframeUtils::GetAnimationPropertiesFromKeyframes( + keyframesCopy, mTarget.mElement, mTarget.mPseudoType, aStyle, + mEffectOptions.mComposite); + +#ifdef DEBUG + MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy), + "Apart from the computed offset members, the keyframes array" + " should not be modified"); +#endif + + mKeyframes = std::move(keyframesCopy); + return result; +} + +template +static void EnumerateContinuationsOrIBSplitSiblings(nsIFrame* aFrame, + FrameEnumFunc&& aFunc) { + while (aFrame) { + aFunc(aFrame); + aFrame = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(aFrame); + } +} + +void KeyframeEffect::UpdateTarget(Element* aElement, + PseudoStyleType aPseudoType) { + OwningAnimationTarget newTarget(aElement, aPseudoType); + + if (mTarget == newTarget) { + // Assign the same target, skip it. + return; + } + + if (mTarget) { + // Call ResetIsRunningOnCompositor() prior to UnregisterTarget() since + // ResetIsRunningOnCompositor() might try to get the EffectSet associated + // with this keyframe effect to remove partial pre-render animation from + // the layer manager. + ResetIsRunningOnCompositor(); + UnregisterTarget(); + + RequestRestyle(EffectCompositor::RestyleType::Layer); + + nsAutoAnimationMutationBatch mb(mTarget.mElement->OwnerDoc()); + if (mAnimation) { + MutationObservers::NotifyAnimationRemoved(mAnimation); + } + } + + mTarget = newTarget; + + if (mTarget) { + UpdateTargetRegistration(); + RefPtr computedStyle = + GetTargetComputedStyle(Flush::None); + if (computedStyle) { + UpdateProperties(computedStyle); + } + + RequestRestyle(EffectCompositor::RestyleType::Layer); + + nsAutoAnimationMutationBatch mb(mTarget.mElement->OwnerDoc()); + if (mAnimation) { + MutationObservers::NotifyAnimationAdded(mAnimation); + mAnimation->ReschedulePendingTasks(); + } + } + + if (mAnimation) { + mAnimation->NotifyEffectTargetUpdated(); + } +} + +void KeyframeEffect::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()) && mAnimation && + mAnimation->ReplaceState() != AnimationReplaceState::Removed), + "Out of date Animation::IsRelevant value"); + + if (isRelevant && !mInEffectSet) { + EffectSet* effectSet = + EffectSet::GetOrCreate(mTarget.mElement, mTarget.mPseudoType); + effectSet->AddEffect(*this); + mInEffectSet = true; + UpdateEffectSet(effectSet); + nsIFrame* frame = GetPrimaryFrame(); + EnumerateContinuationsOrIBSplitSiblings( + frame, [](nsIFrame* aFrame) { aFrame->MarkNeedsDisplayItemRebuild(); }); + } else if (!isRelevant && mInEffectSet) { + UnregisterTarget(); + } +} + +void KeyframeEffect::UnregisterTarget() { + if (!mInEffectSet) { + return; + } + + EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + MOZ_ASSERT(effectSet, + "If mInEffectSet is true, there must be an EffectSet" + " on the target element"); + mInEffectSet = false; + if (effectSet) { + effectSet->RemoveEffect(*this); + + if (effectSet->IsEmpty()) { + EffectSet::DestroyEffectSet(mTarget.mElement, mTarget.mPseudoType); + } + } + nsIFrame* frame = GetPrimaryFrame(); + EnumerateContinuationsOrIBSplitSiblings( + frame, [](nsIFrame* aFrame) { aFrame->MarkNeedsDisplayItemRebuild(); }); +} + +void KeyframeEffect::RequestRestyle( + EffectCompositor::RestyleType aRestyleType) { + if (!mTarget) { + return; + } + nsPresContext* presContext = + nsContentUtils::GetContextForContent(mTarget.mElement); + if (presContext && mAnimation) { + presContext->EffectCompositor()->RequestRestyle( + mTarget.mElement, mTarget.mPseudoType, aRestyleType, + mAnimation->CascadeLevel()); + } +} + +already_AddRefed KeyframeEffect::GetTargetComputedStyle( + Flush aFlushType) const { + if (!GetRenderedDocument()) { + return nullptr; + } + + MOZ_ASSERT(mTarget, + "Should only have a document when we have a target element"); + + OwningAnimationTarget kungfuDeathGrip(mTarget.mElement, mTarget.mPseudoType); + + return aFlushType == Flush::Style + ? nsComputedDOMStyle::GetComputedStyle(mTarget.mElement, + mTarget.mPseudoType) + : nsComputedDOMStyle::GetComputedStyleNoFlush(mTarget.mElement, + mTarget.mPseudoType); +} + +#ifdef DEBUG +void DumpAnimationProperties( + const StylePerDocumentStyleData* aRawData, + nsTArray& aAnimationProperties) { + for (auto& p : aAnimationProperties) { + printf("%s\n", nsCString(nsCSSProps::GetStringValue(p.mProperty)).get()); + for (auto& s : p.mSegments) { + nsAutoCString fromValue, toValue; + s.mFromValue.SerializeSpecifiedValue(p.mProperty, aRawData, fromValue); + s.mToValue.SerializeSpecifiedValue(p.mProperty, aRawData, toValue); + printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey, fromValue.get(), + toValue.get()); + } + } +} +#endif + +/* static */ +already_AddRefed KeyframeEffect::Constructor( + const GlobalObject& aGlobal, Element* aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv) { + return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, aOptions, aRv); +} + +/* static */ +already_AddRefed KeyframeEffect::Constructor( + const GlobalObject& aGlobal, Element* aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv) { + return ConstructKeyframeEffect(aGlobal, aTarget, aKeyframes, aOptions, aRv); +} + +/* static */ +already_AddRefed KeyframeEffect::Constructor( + const GlobalObject& aGlobal, KeyframeEffect& aSource, ErrorResult& aRv) { + Document* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context()); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Create a new KeyframeEffect object with aSource's target, + // iteration composite operation, composite operation, and spacing mode. + // The constructor creates a new AnimationEffect 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 KeyframeEffect(doc, OwningAnimationTarget{aSource.mTarget}, aSource); + // Copy cumulative change hint. mCumulativeChangeHint should be the same as + // the source one because both of targets are the same. + effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint; + + return effect.forget(); +} + +void KeyframeEffect::SetPseudoElement(const nsAString& aPseudoElement, + ErrorResult& aRv) { + if (DOMStringIsNull(aPseudoElement)) { + UpdateTarget(mTarget.mElement, PseudoStyleType::NotPseudo); + return; + } + + // Note: GetPseudoType() returns Some(NotPseudo) for the null string, + // so we handle null case before this. + Maybe pseudoType = + nsCSSPseudoElements::GetPseudoType(aPseudoElement); + if (!pseudoType || *pseudoType == PseudoStyleType::NotPseudo) { + // Per the spec, we throw SyntaxError for syntactically invalid pseudos. + aRv.ThrowSyntaxError( + nsPrintfCString("'%s' is a syntactically invalid pseudo-element.", + NS_ConvertUTF16toUTF8(aPseudoElement).get())); + return; + } + + if (!AnimationUtils::IsSupportedPseudoForAnimations(*pseudoType)) { + // Per the spec, we throw SyntaxError for unsupported pseudos. + aRv.ThrowSyntaxError( + nsPrintfCString("'%s' is an unsupported pseudo-element.", + NS_ConvertUTF16toUTF8(aPseudoElement).get())); + return; + } + + UpdateTarget(mTarget.mElement, *pseudoType); +} + +static void CreatePropertyValue( + nsCSSPropertyID aProperty, float aOffset, + const Maybe& aTimingFunction, + const AnimationValue& aValue, dom::CompositeOperation aComposite, + const StylePerDocumentStyleData* aRawData, + AnimationPropertyValueDetails& aResult) { + aResult.mOffset = aOffset; + + if (!aValue.IsNull()) { + nsAutoCString stringValue; + aValue.SerializeSpecifiedValue(aProperty, aRawData, stringValue); + aResult.mValue.Construct(stringValue); + } + + if (aTimingFunction) { + aResult.mEasing.Construct(); + aTimingFunction->AppendToString(aResult.mEasing.Value()); + } else { + aResult.mEasing.Construct("linear"_ns); + } + + aResult.mComposite = aComposite; +} + +void KeyframeEffect::GetProperties( + nsTArray& aProperties, ErrorResult& aRv) const { + const StylePerDocumentStyleData* rawData = + mDocument->StyleSetForPresShellOrMediaQueryEvaluation()->RawData(); + + for (const AnimationProperty& property : mProperties) { + AnimationPropertyDetails propertyDetails; + propertyDetails.mProperty = + NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty)); + propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor; + + nsAutoString 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, + segment.mFromComposite, rawData, 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(); + } + // Even though we called SetCapacity before, this could fail, since we + // might add multiple elements to propertyDetails.mValues for an element + // of property.mSegments in the cases mentioned below. + if (!propertyDetails.mValues.AppendElement(fromValue, + mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + // 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, segment.mToComposite, rawData, + 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(); + if (!propertyDetails.mValues.AppendElement(toValue, + mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + } + + aProperties.AppendElement(propertyDetails); + } +} + +void KeyframeEffect::GetKeyframes(JSContext* aCx, nsTArray& aResult, + ErrorResult& aRv) const { + MOZ_ASSERT(aResult.IsEmpty()); + MOZ_ASSERT(!aRv.Failed()); + + if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + bool isCSSAnimation = mAnimation && mAnimation->AsCSSAnimation(); + + // For Servo, when we have CSS Animation @keyframes with variables, we convert + // shorthands to longhands if needed, and store a reference to the unparsed + // value. When it comes time to serialize, however, what do you serialize for + // a longhand that comes from a variable reference in a shorthand? Servo says, + // "an empty string" which is not particularly helpful. + // + // We should just store shorthands as-is (bug 1391537) and then return the + // variable references, but for now, since we don't do that, and in order to + // be consistent with Gecko, we just expand the variables (assuming we have + // enough context to do so). For that we need to grab the ComputedStyle so we + // know what custom property values to provide. + RefPtr computedStyle; + if (isCSSAnimation) { + // The following will flush style but that's ok since if you update + // a variable's computed value, you expect to see that updated value in the + // result of getKeyframes(). + // + // If we don't have a target, the following will return null. In that case + // we might end up returning variables as-is or empty string. That should be + // acceptable however, since such a case is rare and this is only + // short-term (and unshipped) behavior until bug 1391537 is fixed. + computedStyle = GetTargetComputedStyle(Flush::Style); + } + + const StylePerDocumentStyleData* rawData = + mDocument->StyleSetForPresShellOrMediaQueryEvaluation()->RawData(); + + 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". + + // With the pref off (i.e. dom.animations-api.compositing.enabled:false), + // the dictionary-to-JS conversion will skip this member entirely. + keyframeDict.mComposite = keyframe.mComposite; + + JS::Rooted keyframeJSValue(aCx); + if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr customProperties; + // A workaround for CSS Animations in servo backend, custom properties in + // keyframe are stored in a servo's declaration block. Find the declaration + // block to resolve CSS variables in the keyframe. + // This workaround will be solved by bug 1391537. + if (isCSSAnimation) { + for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) { + if (propertyValue.mProperty == + nsCSSPropertyID::eCSSPropertyExtra_variable) { + customProperties = propertyValue.mServoDeclarationBlock; + break; + } + } + } + + JS::Rooted keyframeObject(aCx, &keyframeJSValue.toObject()); + for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) { + nsAutoCString stringValue; + // Don't serialize the custom properties for this keyframe. + if (propertyValue.mProperty == + nsCSSPropertyID::eCSSPropertyExtra_variable) { + continue; + } + if (propertyValue.mServoDeclarationBlock) { + Servo_DeclarationBlock_SerializeOneValue( + propertyValue.mServoDeclarationBlock, propertyValue.mProperty, + &stringValue, computedStyle, customProperties, rawData); + } else { + if (auto* value = mBaseValues.GetWeak(propertyValue.mProperty)) { + Servo_AnimationValue_Serialize(value, propertyValue.mProperty, + rawData, &stringValue); + } + } + + // Basically, we need to do the mapping: + // * eCSSProperty_offset => "cssOffset" + // * eCSSProperty_float => "cssFloat" + // This means if property refers to the CSS "offset"/"float" property, + // return the string "cssOffset"/"cssFloat". (So avoid overlapping + // "offset" property in BaseKeyframe.) + // https://drafts.csswg.org/web-animations/#property-name-conversion + const char* name = nullptr; + switch (propertyValue.mProperty) { + case nsCSSPropertyID::eCSSProperty_offset: + name = "cssOffset"; + break; + case nsCSSPropertyID::eCSSProperty_float: + // FIXME: Bug 1582314: Should handle cssFloat manually if we remove it + // from nsCSSProps::PropertyIDLName(). + default: + name = nsCSSProps::PropertyIDLName(propertyValue.mProperty); + } + + JS::Rooted value(aCx); + if (!NonVoidUTF8StringToJsval(aCx, stringValue, &value) || + !JS_DefineProperty(aCx, keyframeObject, name, value, + JSPROP_ENUMERATE)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + + aResult.AppendElement(keyframeObject); + } +} + +/* static */ const TimeDuration +KeyframeEffect::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; +} + +static bool IsDefinitivelyInvisibleDueToOpacity(const nsIFrame& aFrame) { + if (!aFrame.Style()->IsInOpacityZeroSubtree()) { + return false; + } + + // Find the root of the opacity: 0 subtree. + const nsIFrame* root = &aFrame; + while (true) { + auto* parent = root->GetInFlowParent(); + if (!parent || !parent->Style()->IsInOpacityZeroSubtree()) { + break; + } + root = parent; + } + + MOZ_ASSERT(root && root->Style()->IsInOpacityZeroSubtree()); + + // If aFrame is the root of the opacity: zero subtree, we can't prove we can + // optimize it away, because it may have an opacity animation itself. + if (root == &aFrame) { + return false; + } + + // Even if we're in an opacity: zero subtree, if the root of the subtree may + // have an opacity animation, we can't optimize us away, as we may become + // visible ourselves. + return !root->HasAnimationOfOpacity(); +} + +static bool CanOptimizeAwayDueToOpacity(const KeyframeEffect& aEffect, + const nsIFrame& aFrame) { + if (!aFrame.Style()->IsInOpacityZeroSubtree()) { + return false; + } + if (IsDefinitivelyInvisibleDueToOpacity(aFrame)) { + return true; + } + return !aEffect.HasOpacityChange() && !aFrame.HasAnimationOfOpacity(); +} + +bool KeyframeEffect::CanThrottleIfNotVisible(nsIFrame& aFrame) const { + // Unless we are newly in-effect, 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 (!mInEffectOnLastAnimationTimingUpdate || !CanIgnoreIfNotVisible()) { + return false; + } + + PresShell* presShell = GetPresShell(); + if (presShell && !presShell->IsActive()) { + return true; + } + + const bool isVisibilityHidden = + !aFrame.IsVisibleOrMayHaveVisibleDescendants(); + const bool canOptimizeAwayVisibility = + isVisibilityHidden && !HasVisibilityChange(); + + const bool invisible = canOptimizeAwayVisibility || + CanOptimizeAwayDueToOpacity(*this, aFrame) || + aFrame.IsScrolledOutOfView(); + if (!invisible) { + return false; + } + + // If there are no overflow change hints, we don't need to worry about + // unthrottling the animation periodically to update scrollbar positions for + // the overflow region. + if (!HasPropertiesThatMightAffectOverflow()) { + return true; + } + + // Don't throttle finite animations since the animation might suddenly + // come into view and if it was throttled it will be out-of-sync. + if (HasFiniteActiveDuration()) { + return false; + } + + return isVisibilityHidden ? CanThrottleOverflowChangesInScrollable(aFrame) + : CanThrottleOverflowChanges(aFrame); +} + +bool KeyframeEffect::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* const frame = GetStyleFrame(); + 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; + } + + // Do not throttle any animations during print preview. + if (frame->PresContext()->IsPrintingOrPrintPreview()) { + return false; + } + + if (CanThrottleIfNotVisible(*frame)) { + return true; + } + + EffectSet* effectSet = nullptr; + for (const AnimationProperty& property : mProperties) { + if (!property.mIsRunningOnCompositor) { + return false; + } + + MOZ_ASSERT(nsCSSPropertyIDSet::CompositorAnimatables().HasProperty( + property.mProperty), + "The property should be able to run on the compositor"); + if (!effectSet) { + effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + MOZ_ASSERT(effectSet, + "CanThrottle should be called on an effect " + "associated with a target element"); + } + MOZ_ASSERT(HasEffectiveAnimationOfProperty(property.mProperty, *effectSet), + "There should be an effective animation of the property while " + "it is marked as being run on the compositor"); + + DisplayItemType displayItemType = + LayerAnimationInfo::GetDisplayItemTypeForProperty(property.mProperty); + + // Note that AnimationInfo::GetGenarationFromFrame() is supposed to work + // with the primary frame instead of the style frame. + Maybe generation = layers::AnimationInfo::GetGenerationFromFrame( + GetPrimaryFrame(), displayItemType); + // Unthrottle if the animation needs to be brought up to date + if (!generation || effectSet->GetAnimationGeneration() != *generation) { + return false; + } + + // If this is a transform animation that affects the overflow region, + // we should unthrottle the animation periodically. + if (HasPropertiesThatMightAffectOverflow() && + !CanThrottleOverflowChangesInScrollable(*frame)) { + return false; + } + } + + return true; +} + +bool KeyframeEffect::CanThrottleOverflowChanges(const nsIFrame& aFrame) const { + TimeStamp now = aFrame.PresContext()->RefreshDriver()->MostRecentRefresh(); + + EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + MOZ_ASSERT(effectSet, + "CanOverflowTransformChanges is expected to be called" + " on an effect in an effect set"); + MOZ_ASSERT(mAnimation, + "CanOverflowTransformChanges is expected to be called" + " on an effect with a parent animation"); + TimeStamp lastSyncTime = effectSet->LastOverflowAnimationSyncTime(); + // If this animation can cause overflow, we can throttle some of the ticks. + return (!lastSyncTime.IsNull() && + (now - lastSyncTime) < OverflowRegionRefreshInterval()); +} + +bool KeyframeEffect::CanThrottleOverflowChangesInScrollable( + nsIFrame& aFrame) const { + // If the target element is not associated with any documents, we don't care + // it. + Document* doc = GetRenderedDocument(); + if (!doc) { + return true; + } + + // If we know that the animation cannot cause overflow, + // we can just disable flushes for this animation. + + // If we have no intersection observers, we don't care about overflow. + if (!doc->HasIntersectionObservers()) { + return true; + } + + if (CanThrottleOverflowChanges(aFrame)) { + 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; + } + + ScrollStyles ss = scrollable->GetScrollStyles(); + if (ss.mVertical == StyleOverflow::Hidden && + ss.mHorizontal == StyleOverflow::Hidden && + scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) { + return true; + } + + return false; +} + +nsIFrame* KeyframeEffect::GetStyleFrame() const { + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) { + return nullptr; + } + + return nsLayoutUtils::GetStyleFrame(frame); +} + +nsIFrame* KeyframeEffect::GetPrimaryFrame() const { + nsIFrame* frame = nullptr; + if (!mTarget) { + return frame; + } + + if (mTarget.mPseudoType == PseudoStyleType::before) { + frame = nsLayoutUtils::GetBeforeFrame(mTarget.mElement); + } else if (mTarget.mPseudoType == PseudoStyleType::after) { + frame = nsLayoutUtils::GetAfterFrame(mTarget.mElement); + } else if (mTarget.mPseudoType == PseudoStyleType::marker) { + frame = nsLayoutUtils::GetMarkerFrame(mTarget.mElement); + } else { + frame = mTarget.mElement->GetPrimaryFrame(); + MOZ_ASSERT(mTarget.mPseudoType == PseudoStyleType::NotPseudo, + "unknown mTarget.mPseudoType"); + } + + return frame; +} + +Document* KeyframeEffect::GetRenderedDocument() const { + if (!mTarget) { + return nullptr; + } + return mTarget.mElement->GetComposedDoc(); +} + +PresShell* KeyframeEffect::GetPresShell() const { + Document* doc = GetRenderedDocument(); + if (!doc) { + return nullptr; + } + return doc->GetPresShell(); +} + +/* static */ +bool KeyframeEffect::IsGeometricProperty(const nsCSSPropertyID aProperty) { + MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty), + "Property should be a longhand property"); + + switch (aProperty) { + case eCSSProperty_bottom: + case eCSSProperty_height: + case eCSSProperty_left: + case eCSSProperty_margin_bottom: + case eCSSProperty_margin_left: + case eCSSProperty_margin_right: + case eCSSProperty_margin_top: + case eCSSProperty_padding_bottom: + case eCSSProperty_padding_left: + case eCSSProperty_padding_right: + case eCSSProperty_padding_top: + case eCSSProperty_right: + case eCSSProperty_top: + case eCSSProperty_width: + return true; + default: + return false; + } +} + +/* static */ +bool KeyframeEffect::CanAnimateTransformOnCompositor( + const nsIFrame* aFrame, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) { + // In some cases, such as when we are simply collecting all the compositor + // animations regardless of the frame on which they run in order to calculate + // change hints, |aFrame| will be the style frame. However, even in that case + // we should look at the primary frame since that is where the transform will + // be applied. + const nsIFrame* primaryFrame = + nsLayoutUtils::GetPrimaryFrameFromStyleFrame(aFrame); + + // 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 (primaryFrame->BackfaceIsHidden()) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden; + return false; + } + // Async 'transform' animations of aFrames with SVG transforms is not + // supported. See bug 779599. + if (primaryFrame->IsSVGTransformed()) { + aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG; + return false; + } + + return true; +} + +bool KeyframeEffect::ShouldBlockAsyncTransformAnimations( + const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const { + EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + // The various transform properties ('transform', 'scale' etc.) get combined + // on the compositor. + // + // As a result, if we have an animation of 'scale' and 'translate', but the + // 'translate' property is covered by an !important rule, we will not be + // able to combine the result on the compositor since we won't have the + // !important rule to incorporate. In that case we should run all the + // transform-related animations on the main thread (where we have the + // !important rule). + nsCSSPropertyIDSet blockedProperties = + effectSet->PropertiesWithImportantRules().Intersect( + effectSet->PropertiesForAnimationsLevel()); + if (blockedProperties.Intersects(aPropertySet)) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformIsBlockedByImportantRules; + return true; + } + + MOZ_ASSERT(mAnimation); + // Note: If the geometric animations are using scroll-timeline, we don't need + // to synchronize transform animations with them. + const bool enableMainthreadSynchronizationWithGeometricAnimations = + StaticPrefs:: + dom_animations_mainthread_synchronization_with_geometric_animations() && + !mAnimation->UsingScrollTimeline(); + + 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 (enableMainthreadSynchronizationWithGeometricAnimations && + IsGeometricProperty(property.mProperty)) { + aPerformanceWarning = + AnimationPerformanceWarning::Type::TransformWithGeometricProperties; + return true; + } + + // Check for unsupported transform animations + if (LayerAnimationInfo::GetCSSPropertiesFor(DisplayItemType::TYPE_TRANSFORM) + .HasProperty(property.mProperty)) { + if (!CanAnimateTransformOnCompositor(aFrame, aPerformanceWarning)) { + return true; + } + } + } + + return false; +} + +bool KeyframeEffect::HasGeometricProperties() const { + for (const AnimationProperty& property : mProperties) { + if (IsGeometricProperty(property.mProperty)) { + return true; + } + } + + return false; +} + +void KeyframeEffect::SetPerformanceWarning( + const nsCSSPropertyIDSet& aPropertySet, + const AnimationPerformanceWarning& aWarning) { + nsCSSPropertyIDSet curr = aPropertySet; + for (AnimationProperty& property : mProperties) { + if (!curr.HasProperty(property.mProperty)) { + continue; + } + property.SetPerformanceWarning(aWarning, mTarget.mElement); + curr.RemoveProperty(property.mProperty); + if (curr.IsEmpty()) { + return; + } + } +} + +already_AddRefed +KeyframeEffect::CreateComputedStyleForAnimationValue( + nsCSSPropertyID aProperty, const AnimationValue& aValue, + nsPresContext* aPresContext, const ComputedStyle* aBaseComputedStyle) { + MOZ_ASSERT(aBaseComputedStyle, + "CreateComputedStyleForAnimationValue needs to be called " + "with a valid ComputedStyle"); + + Element* elementForResolve = AnimationUtils::GetElementForRestyle( + mTarget.mElement, mTarget.mPseudoType); + // The element may be null if, for example, we target a pseudo-element that no + // longer exists. + if (!elementForResolve) { + return nullptr; + } + + ServoStyleSet* styleSet = aPresContext->StyleSet(); + return styleSet->ResolveServoStyleByAddingAnimation( + elementForResolve, aBaseComputedStyle, aValue.mServo); +} + +void KeyframeEffect::CalculateCumulativeChangeHint( + const ComputedStyle* aComputedStyle) { + mCumulativeChangeHint = nsChangeHint(0); + mNeedsStyleData = false; + + nsPresContext* presContext = + mTarget ? nsContentUtils::GetContextForContent(mTarget.mElement) + : nullptr; + if (!presContext) { + // Change hints make no sense if we're not rendered. + // + // Actually, we cannot even post them anywhere. + mNeedsStyleData = true; + return; + } + + for (const AnimationProperty& property : mProperties) { + // For opacity property we don't produce any change hints that are not + // included in nsChangeHint_Hints_CanIgnoreIfNotVisible so we can throttle + // opacity animations regardless of the change they produce. This + // optimization is particularly important since it allows us to throttle + // opacity animations with missing 0%/100% keyframes. + if (property.mProperty == eCSSProperty_opacity) { + continue; + } + + for (const AnimationPropertySegment& segment : property.mSegments) { + // In case composite operation is not 'replace' or value is null, + // we can't throttle animations which will not cause any layout changes + // on invisible elements because we can't calculate the change hint for + // such properties until we compose it. + if (!segment.HasReplaceableValues()) { + if (!nsCSSPropertyIDSet::TransformLikeProperties().HasProperty( + property.mProperty)) { + mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible; + return; + } + // We try a little harder to optimize transform animations simply + // because they are so common (the second-most commonly animated + // property at the time of writing). So if we encounter a transform + // segment that needs composing with the underlying value, we just add + // all the change hints a transform animation is known to be able to + // generate. + mCumulativeChangeHint |= + nsChangeHint_ComprehensiveAddOrRemoveTransform | + nsChangeHint_UpdatePostTransformOverflow | + nsChangeHint_UpdateTransformLayer; + continue; + } + + RefPtr fromContext = + CreateComputedStyleForAnimationValue(property.mProperty, + segment.mFromValue, presContext, + aComputedStyle); + if (!fromContext) { + mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible; + mNeedsStyleData = true; + return; + } + + RefPtr toContext = + CreateComputedStyleForAnimationValue(property.mProperty, + segment.mToValue, presContext, + aComputedStyle); + if (!toContext) { + mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible; + mNeedsStyleData = true; + return; + } + + uint32_t equalStructs = 0; + nsChangeHint changeHint = + fromContext->CalcStyleDifference(*toContext, &equalStructs); + + mCumulativeChangeHint |= changeHint; + } + } +} + +void KeyframeEffect::SetAnimation(Animation* aAnimation) { + if (mAnimation == aAnimation) { + return; + } + + // Restyle for the old animation. + RequestRestyle(EffectCompositor::RestyleType::Layer); + + mAnimation = aAnimation; + + UpdateNormalizedTiming(); + + // 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(PostRestyleMode::IfNeeded); + if (mAnimation) { + MarkCascadeNeedsUpdate(); + } +} + +bool KeyframeEffect::CanIgnoreIfNotVisible() const { + if (!StaticPrefs::dom_animations_offscreen_throttling()) { + 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 KeyframeEffect::MarkCascadeNeedsUpdate() { + if (!mTarget) { + return; + } + + EffectSet* effectSet = EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + if (!effectSet) { + return; + } + effectSet->MarkCascadeNeedsUpdate(); +} + +/* static */ +bool KeyframeEffect::HasComputedTimingChanged( + const ComputedTiming& aComputedTiming, + IterationCompositeOperation aIterationComposite, + const Nullable& aProgressOnLastCompose, + uint64_t aCurrentIterationOnLastCompose) { + // 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. + return aComputedTiming.mProgress != aProgressOnLastCompose || + (aIterationComposite == IterationCompositeOperation::Accumulate && + aComputedTiming.mCurrentIteration != aCurrentIterationOnLastCompose); +} + +bool KeyframeEffect::HasComputedTimingChanged() const { + ComputedTiming computedTiming = GetComputedTiming(); + return HasComputedTimingChanged( + computedTiming, mEffectOptions.mIterationComposite, + mProgressOnLastCompose, mCurrentIterationOnLastCompose); +} + +bool KeyframeEffect::ContainsAnimatedScale(const nsIFrame* aFrame) const { + // For display:table content, transform animations run on the table wrapper + // frame. If we are being passed a frame that doesn't support transforms + // (i.e. the inner table frame) we could just return false, but it possibly + // means we looked up the wrong EffectSet so for now we just assert instead. + MOZ_ASSERT(aFrame && aFrame->IsFrameOfType(nsIFrame::eSupportsCSSTransforms), + "We should be passed a frame that supports transforms"); + + if (!IsCurrent()) { + return false; + } + + if (!mAnimation || + mAnimation->ReplaceState() == AnimationReplaceState::Removed) { + return false; + } + + for (const AnimationProperty& prop : mProperties) { + if (prop.mProperty != eCSSProperty_transform && + prop.mProperty != eCSSProperty_scale && + prop.mProperty != eCSSProperty_rotate) { + continue; + } + + AnimationValue baseStyle = BaseStyle(prop.mProperty); + if (!baseStyle.IsNull()) { + gfx::MatrixScales size = baseStyle.GetScaleValue(aFrame); + if (size != gfx::MatrixScales()) { + return true; + } + } + + // This is actually overestimate because there are some cases that combining + // the base value and from/to value produces 1:1 scale. But it doesn't + // really matter. + for (const AnimationPropertySegment& segment : prop.mSegments) { + if (!segment.mFromValue.IsNull()) { + gfx::MatrixScales from = segment.mFromValue.GetScaleValue(aFrame); + if (from != gfx::MatrixScales()) { + return true; + } + } + if (!segment.mToValue.IsNull()) { + gfx::MatrixScales to = segment.mToValue.GetScaleValue(aFrame); + if (to != gfx::MatrixScales()) { + return true; + } + } + } + } + + return false; +} + +void KeyframeEffect::UpdateEffectSet(EffectSet* aEffectSet) const { + if (!mInEffectSet) { + return; + } + + EffectSet* effectSet = + aEffectSet ? aEffectSet + : EffectSet::Get(mTarget.mElement, mTarget.mPseudoType); + if (!effectSet) { + return; + } + + nsIFrame* styleFrame = GetStyleFrame(); + if (HasAnimationOfPropertySet(nsCSSPropertyIDSet::OpacityProperties())) { + effectSet->SetMayHaveOpacityAnimation(); + EnumerateContinuationsOrIBSplitSiblings(styleFrame, [](nsIFrame* aFrame) { + aFrame->SetMayHaveOpacityAnimation(); + }); + } + + nsIFrame* primaryFrame = GetPrimaryFrame(); + if (HasAnimationOfPropertySet( + nsCSSPropertyIDSet::TransformLikeProperties())) { + effectSet->SetMayHaveTransformAnimation(); + // For table frames, it's not clear if we should iterate over the + // continuations of the table wrapper or the inner table frame. + // + // Fortunately, this is not currently an issue because we only split tables + // when printing (page breaks) where we don't run animations. + EnumerateContinuationsOrIBSplitSiblings(primaryFrame, [](nsIFrame* aFrame) { + aFrame->SetMayHaveTransformAnimation(); + }); + } +} + +KeyframeEffect::MatchForCompositor KeyframeEffect::IsMatchForCompositor( + const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame, + const EffectSet& aEffects, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const { + MOZ_ASSERT(mAnimation); + + if (!mAnimation->IsRelevant()) { + return KeyframeEffect::MatchForCompositor::No; + } + + if (mAnimation->ShouldBeSynchronizedWithMainThread(aPropertySet, aFrame, + aPerformanceWarning)) { + // For a given |aFrame|, we don't want some animations of |aProperty| to + // run on the compositor and others to run on the main thread, so if any + // need to be synchronized with the main thread, run them all there. + return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty; + } + + if (mAnimation->UsingScrollTimeline()) { + const ScrollTimeline* scrollTimeline = + mAnimation->GetTimeline()->AsScrollTimeline(); + // We don't send this animation to the compositor if + // 1. the APZ is disabled entirely or for the source, or + // 2. the associated scroll-timeline is inactive, or + // 3. the scrolling direction is not available (i.e. no scroll range). + // 4. the scroll style of the scroller is overflow:hidden. + if (!scrollTimeline->APZIsActiveForSource() || + !scrollTimeline->IsActive() || + !scrollTimeline->ScrollingDirectionIsAvailable() || + scrollTimeline->SourceScrollStyle() == StyleOverflow::Hidden) { + return KeyframeEffect::MatchForCompositor::No; + } + + // FIXME: Bug 1818346. Support OMTA for view-timeline. We disable it for now + // because we need to make view-timeline-inset animations run on the OMTA as + // well before enable this. + if (scrollTimeline->IsViewTimeline()) { + return KeyframeEffect::MatchForCompositor::No; + } + } + + if (!HasEffectiveAnimationOfPropertySet(aPropertySet, aEffects)) { + return KeyframeEffect::MatchForCompositor::No; + } + + // If we know that the animation is not visible, we don't need to send the + // animation to the compositor. + if (!aFrame->IsVisibleOrMayHaveVisibleDescendants() || + CanOptimizeAwayDueToOpacity(*this, *aFrame) || + aFrame->IsScrolledOutOfView()) { + return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty; + } + + if (aPropertySet.HasProperty(eCSSProperty_background_color)) { + if (!StaticPrefs::gfx_omta_background_color()) { + return KeyframeEffect::MatchForCompositor::No; + } + + // We don't yet support off-main-thread background-color animations on + // canvas frame or on or which genarate + // nsDisplayCanvasBackgroundColor or nsDisplaySolidColor display item. + if (aFrame->IsCanvasFrame() || + (aFrame->GetContent() && + (aFrame->GetContent()->IsHTMLElement(nsGkAtoms::body) || + aFrame->GetContent()->IsHTMLElement(nsGkAtoms::html)))) { + return KeyframeEffect::MatchForCompositor::No; + } + } + + // We can't run this background color animation on the compositor if there + // is any `current-color` keyframe. + if (mHasCurrentColor) { + aPerformanceWarning = AnimationPerformanceWarning::Type::HasCurrentColor; + return KeyframeEffect::MatchForCompositor::NoAndBlockThisProperty; + } + + return mAnimation->IsPlaying() ? KeyframeEffect::MatchForCompositor::Yes + : KeyframeEffect::MatchForCompositor::IfNeeded; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/animation/KeyframeEffect.h b/dom/animation/KeyframeEffect.h new file mode 100644 index 0000000000..8f8466369e --- /dev/null +++ b/dom/animation/KeyframeEffect.h @@ -0,0 +1,529 @@ +/* -*- 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 "nsChangeHint.h" +#include "nsCSSPropertyID.h" +#include "nsCSSPropertyIDSet.h" +#include "nsCSSValue.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefPtrHashtable.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" +#include "mozilla/AnimationPerformanceWarning.h" +#include "mozilla/AnimationPropertySegment.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/Attributes.h" +#include "mozilla/EffectCompositor.h" +#include "mozilla/Keyframe.h" +#include "mozilla/KeyframeEffectParams.h" +#include "mozilla/PostRestyleMode.h" +// StyleLockedDeclarationBlock and associated RefPtrTraits +#include "mozilla/ServoBindingTypes.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/AnimationEffect.h" +#include "mozilla/dom/BindingDeclarations.h" + +struct JSContext; +class JSObject; +class nsIContent; +class nsIFrame; + +namespace mozilla { + +class AnimValuesStyleRule; +class ErrorResult; +struct AnimationRule; +struct TimingParams; +class EffectSet; +class ComputedStyle; +class PresShell; + +namespace dom { +class Element; +class GlobalObject; +class UnrestrictedDoubleOrKeyframeAnimationOptions; +class UnrestrictedDoubleOrKeyframeEffectOptions; +enum class IterationCompositeOperation : uint8_t; +enum class CompositeOperation : uint8_t; +struct AnimationPropertyDetails; +} // namespace dom + +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; + + nsTArray mSegments; + + // The copy constructor/assignment doesn't copy mIsRunningOnCompositor and + // mPerformanceWarning. + AnimationProperty() = default; + AnimationProperty(const AnimationProperty& aOther) + : mProperty(aOther.mProperty), mSegments(aOther.mSegments.Clone()) {} + AnimationProperty& operator=(const AnimationProperty& aOther) { + mProperty = aOther.mProperty; + mSegments = aOther.mSegments.Clone(); + 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); + } + + void SetPerformanceWarning(const AnimationPerformanceWarning& aWarning, + const dom::Element* aElement); +}; + +namespace dom { + +class Animation; +class Document; + +class KeyframeEffect : public AnimationEffect { + public: + KeyframeEffect(Document* aDocument, OwningAnimationTarget&& aTarget, + TimingParams&& aTiming, const KeyframeEffectParams& aOptions); + + KeyframeEffect(Document* aDocument, OwningAnimationTarget&& aTarget, + const KeyframeEffect& aOther); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffect, + AnimationEffect) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + KeyframeEffect* AsKeyframeEffect() override { return this; } + + bool IsValidTransition() const { + return Properties().Length() == 1 && + Properties()[0].mSegments.Length() == 1; + } + + // KeyframeEffect interface + static already_AddRefed Constructor( + const GlobalObject& aGlobal, Element* aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv); + + static already_AddRefed Constructor( + const GlobalObject& aGlobal, KeyframeEffect& 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, Element* aTarget, + JS::Handle aKeyframes, + const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv); + + Element* GetTarget() const { return mTarget.mElement.get(); } + NonOwningAnimationTarget GetAnimationTarget() const { + return NonOwningAnimationTarget(mTarget.mElement, mTarget.mPseudoType); + } + void GetPseudoElement(nsAString& aRetVal) const { + if (mTarget.mPseudoType == PseudoStyleType::NotPseudo) { + SetDOMStringToNull(aRetVal); + return; + } + aRetVal = nsCSSPseudoElements::PseudoTypeAsString(mTarget.mPseudoType); + } + + // These two setters call GetTargetComputedStyle 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 ComputedStyle into this method and use + // that to update the properties rather than calling + // GetComputedStyle. + void SetTarget(Element* aTarget) { + UpdateTarget(aTarget, mTarget.mPseudoType); + } + void SetPseudoElement(const nsAString& aPseudoElement, ErrorResult& aRv); + + void GetKeyframes(JSContext* aCx, nsTArray& aResult, + ErrorResult& aRv) const; + void GetProperties(nsTArray& aProperties, + ErrorResult& aRv) const; + + IterationCompositeOperation IterationComposite() const; + void SetIterationComposite( + const IterationCompositeOperation& aIterationComposite); + + CompositeOperation Composite() const; + virtual void SetComposite(const CompositeOperation& aComposite); + void SetCompositeFromStyle(const CompositeOperation& aComposite) { + KeyframeEffect::SetComposite(aComposite); + } + + void NotifySpecifiedTimingUpdated(); + void NotifyAnimationTimingUpdated(PostRestyleMode aPostRestyle); + void RequestRestyle(EffectCompositor::RestyleType aRestyleType); + void SetAnimation(Animation* aAnimation) override; + virtual void SetKeyframes(JSContext* aContext, + JS::Handle aKeyframes, ErrorResult& aRv); + void SetKeyframes(nsTArray&& aKeyframes, + const ComputedStyle* aStyle, + const AnimationTimeline* aTimeline); + + // Replace the start value of the transition. This is used for updating + // transitions running on the compositor. + void ReplaceTransitionStartValue(AnimationValue&& aStartValue); + + // Returns the set of properties affected by this effect regardless of + // whether any of these properties is overridden by an !important rule. + nsCSSPropertyIDSet GetPropertySet() const; + + // Returns true if the effect includes a property in |aPropertySet| regardless + // of whether any property in the set is overridden by an !important rule. + bool HasAnimationOfPropertySet(const nsCSSPropertyIDSet& aPropertySet) const { + return GetPropertySet().Intersects(aPropertySet); + } + + // 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. + // + // |aEffect| should be the EffectSet containing this KeyframeEffect since + // this function is typically called for all KeyframeEffects on an element + // so that we can avoid multiple calls of EffectSet::GetEffect(). + // + // Note that does not consider the interaction between related transform + // properties where an !important rule on another transform property may + // cause all transform properties to be run on the main thread. That check is + // performed by GetPropertiesForCompositor. + bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty, + const EffectSet& aEffect) const { + return GetEffectiveAnimationOfProperty(aProperty, aEffect) != nullptr; + } + const AnimationProperty* GetEffectiveAnimationOfProperty( + nsCSSPropertyID aProperty, const EffectSet& aEffect) const; + + // Similar to HasEffectiveAnimationOfProperty, above, but for + // an nsCSSPropertyIDSet. Returns true if this keyframe effect has at least + // one property in |aPropertySet| that is not overridden by an !important + // rule. + // + // Note that does not consider the interaction between related transform + // properties where an !important rule on another transform property may + // cause all transform properties to be run on the main thread. That check is + // performed by GetPropertiesForCompositor. + bool HasEffectiveAnimationOfPropertySet( + const nsCSSPropertyIDSet& aPropertySet, + const EffectSet& aEffectSet) const; + + // Returns all the effective animated CSS properties that can be animated on + // the compositor and are not overridden by a higher cascade level. + // + // NOTE: This function is basically called for all KeyframeEffects on an + // element thus it takes |aEffects| to avoid multiple calls of + // EffectSet::GetEffect(). + // + // NOTE(2): This function does NOT check that animations are permitted on + // |aFrame|. It is the responsibility of the caller to first call + // EffectCompositor::AllowCompositorAnimationsOnFrame for |aFrame|, or use + // nsLayoutUtils::GetAnimationPropertiesForCompositor instead. + nsCSSPropertyIDSet GetPropertiesForCompositor(EffectSet& aEffects, + const nsIFrame* aFrame) const; + + const nsTArray& Properties() const { return mProperties; } + + // Update |mProperties| by recalculating from |mKeyframes| using + // |aComputedStyle| to resolve specified values. + // Note: we use |aTimeline| to check if we need to ensure the base styles. + // If it is nullptr, we use the timeline from |mAnimation|. + void UpdateProperties(const ComputedStyle* aStyle, + const AnimationTimeline* aTimeline = nullptr); + + // Update various bits of state related to running ComposeStyle(). + // We need to update this outside ComposeStyle() because we should avoid + // mutating any state in ComposeStyle() since it might be called during + // parallel traversal. + void WillComposeStyle(); + + // Updates |aComposeResult| with the animation values produced by this + // AnimationEffect for the current time except any properties contained + // in |aPropertiesToSkip|. + void ComposeStyle(StyleAnimationValueMap& aComposeResult, + 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 SetIsRunningOnCompositor(const nsCSSPropertyIDSet& aPropertySet, + bool aIsRunning); + void ResetIsRunningOnCompositor(); + + void ResetPartialPrerendered(); + + // 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, const nsCSSPropertyIDSet& aPropertySet, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const; + bool HasGeometricProperties() const; + bool AffectsGeometry() const override { + return mTarget && HasGeometricProperties(); + } + + Document* GetRenderedDocument() const; + PresShell* GetPresShell() const; + + // Associates a warning with the animated property set 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(const nsCSSPropertyIDSet& aPropertySet, + const AnimationPerformanceWarning& aWarning); + + // Cumulative change hint on each segment for each property. + // This is used for deciding the animation is paint-only. + void CalculateCumulativeChangeHint(const ComputedStyle* aStyle); + + // 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; + + // Returns true if the effect is current state and has scale animation. + // |aFrame| is used for calculation of scale values. + bool ContainsAnimatedScale(const nsIFrame* aFrame) const; + + AnimationValue BaseStyle(nsCSSPropertyID aProperty) const { + AnimationValue result; + bool hasProperty = false; + // We cannot use getters_AddRefs on StyleAnimationValue because it is + // an incomplete type, so Get() doesn't work. Instead, use GetWeak, and + // then assign the raw pointer to a RefPtr. + result.mServo = mBaseValues.GetWeak(aProperty, &hasProperty); + MOZ_ASSERT(hasProperty || result.IsNull()); + return result; + } + + enum class MatchForCompositor { + // This animation matches and should run on the compositor if possible. + Yes, + // This (not currently playing) animation matches and can be run on the + // compositor if there are other animations for this property that return + // 'Yes'. + IfNeeded, + // This animation does not match or can't be run on the compositor. + No, + // This animation does not match or can't be run on the compositor and, + // furthermore, its presence means we should not run any animations for this + // property on the compositor. + NoAndBlockThisProperty + }; + + MatchForCompositor IsMatchForCompositor( + const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame, + const EffectSet& aEffects, + AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const; + + static bool HasComputedTimingChanged( + const ComputedTiming& aComputedTiming, + IterationCompositeOperation aIterationComposite, + const Nullable& aProgressOnLastCompose, + uint64_t aCurrentIterationOnLastCompose); + + bool HasOpacityChange() const { + return mCumulativeChangeHint & nsChangeHint_UpdateOpacityLayer; + } + + protected: + ~KeyframeEffect() override = default; + + template + static already_AddRefed ConstructKeyframeEffect( + const GlobalObject& aGlobal, Element* aTarget, + JS::Handle aKeyframes, const OptionsType& aOptions, + ErrorResult& aRv); + + // Build properties by recalculating from |mKeyframes| using |aComputedStyle| + // to resolve specified values. This function also applies paced spacing if + // needed. + nsTArray BuildProperties(const ComputedStyle* aStyle); + + // Helper for SetTarget() and SetPseudoElement(). + void UpdateTarget(Element* aElement, PseudoStyleType aPseudoType); + + // 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(); + + // Looks up the ComputedStyle 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 GetComputedStyle when we are in the process + // of building a ComputedStyle may trigger various forms of infinite + // recursion. + enum class Flush { + Style, + None, + }; + already_AddRefed GetTargetComputedStyle(Flush) const; + + // A wrapper for marking cascade update according to the current + // target and its effectSet. + void MarkCascadeNeedsUpdate(); + + void EnsureBaseStyles(const ComputedStyle* aComputedValues, + const nsTArray& aProperties, + const AnimationTimeline* aTimeline, + bool* aBaseStylesChanged); + void EnsureBaseStyle(const AnimationProperty& aProperty, + nsPresContext* aPresContext, + const ComputedStyle* aComputedValues, + const AnimationTimeline* aTimeline, + RefPtr& aBaseComputedValues); + + OwningAnimationTarget 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 = false; + + // True if this effect is in the EffectSet for its target element. This is + // used as an optimization to avoid unnecessary hashmap lookups on the + // EffectSet. + bool mInEffectSet = false; + + // True if the last time we tried to update the cumulative change hint we + // didn't have style data to do so. We set this flag so that the next time + // our style context changes we know to update the cumulative change hint even + // if our properties haven't changed. + bool mNeedsStyleData = false; + + // True if there is any current-color for background color in this keyframes. + bool mHasCurrentColor = false; + + // The non-animated values for properties in this effect that contain at + // least one animation value that is composited with the underlying value + // (i.e. it uses the additive or accumulate composite mode). + using BaseValuesHashmap = + nsRefPtrHashtable; + BaseValuesHashmap mBaseValues; + + private: + nsChangeHint mCumulativeChangeHint = nsChangeHint{0}; + + void ComposeStyleRule(StyleAnimationValueMap& aAnimationValues, + const AnimationProperty& aProperty, + const AnimationPropertySegment& aSegment, + const ComputedTiming& aComputedTiming); + + already_AddRefed CreateComputedStyleForAnimationValue( + nsCSSPropertyID aProperty, const AnimationValue& aValue, + nsPresContext* aPresContext, const ComputedStyle* aBaseComputedStyle); + + // Return the primary frame for the target (pseudo-)element. + nsIFrame* GetPrimaryFrame() const; + // Returns the frame which is used for styling. + nsIFrame* GetStyleFrame() const; + + bool CanThrottle() const; + bool CanThrottleOverflowChanges(const nsIFrame& aFrame) const; + bool CanThrottleOverflowChangesInScrollable(nsIFrame& aFrame) const; + bool CanThrottleIfNotVisible(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 /* out */); + static bool IsGeometricProperty(const nsCSSPropertyID aProperty); + + static const TimeDuration OverflowRegionRefreshInterval(); + + void UpdateEffectSet(mozilla::EffectSet* aEffectSet = nullptr) const; + + // Returns true if this effect has properties that might affect the overflow + // region. + // This function is used for updating scroll bars or notifying intersection + // observers reflected by the transform. + bool HasPropertiesThatMightAffectOverflow() const { + return mCumulativeChangeHint & + (nsChangeHint_AddOrRemoveTransform | nsChangeHint_UpdateOverflow | + nsChangeHint_UpdatePostTransformOverflow | + nsChangeHint_UpdateTransformLayer); + } + + // Returns true if this effect causes visibility change. + // (i.e. 'visibility: hidden' -> 'visibility: visible' and vice versa.) + bool HasVisibilityChange() const { + return mCumulativeChangeHint & nsChangeHint_VisibilityChange; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_KeyframeEffect_h diff --git a/dom/animation/KeyframeEffectParams.h b/dom/animation/KeyframeEffectParams.h new file mode 100644 index 0000000000..08bb22e08e --- /dev/null +++ b/dom/animation/KeyframeEffectParams.h @@ -0,0 +1,34 @@ +/* -*- 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 "mozilla/dom/KeyframeEffectBinding.h" // IterationCompositeOperation +#include "mozilla/PseudoStyleType.h" // PseudoStyleType + +namespace mozilla { + +struct KeyframeEffectParams { + KeyframeEffectParams() = default; + KeyframeEffectParams(dom::IterationCompositeOperation aIterationComposite, + dom::CompositeOperation aComposite, + PseudoStyleType aPseudoType) + : mIterationComposite(aIterationComposite), + mComposite(aComposite), + mPseudoType(aPseudoType) {} + explicit KeyframeEffectParams(dom::CompositeOperation aComposite) + : mComposite(aComposite) {} + + dom::IterationCompositeOperation mIterationComposite = + dom::IterationCompositeOperation::Replace; + dom::CompositeOperation mComposite = dom::CompositeOperation::Replace; + PseudoStyleType mPseudoType = PseudoStyleType::NotPseudo; +}; + +} // namespace mozilla + +#endif // mozilla_KeyframeEffectParams_h diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp new file mode 100644 index 0000000000..35234e0a9d --- /dev/null +++ b/dom/animation/KeyframeUtils.cpp @@ -0,0 +1,1255 @@ +/* -*- 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/KeyframeUtils.h" + +#include // For std::stable_sort, std::min +#include + +#include "jsapi.h" // For most JSAPI +#include "js/ForOfIterator.h" // For JS::ForOfIterator +#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetProperty, JS_GetPropertyById +#include "mozilla/ComputedStyle.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/RangedArray.h" +#include "mozilla/ServoBindingTypes.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/ServoCSSParser.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/TimingParams.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc. +#include "mozilla/dom/BindingCallContext.h" +#include "mozilla/dom/Document.h" // For Document::AreWebAnimationsImplicitKeyframesEnabled +#include "mozilla/dom/Element.h" +#include "mozilla/dom/KeyframeEffect.h" // For PropertyValuesPair etc. +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/dom/Nullable.h" +#include "nsCSSPropertyIDSet.h" +#include "nsCSSProps.h" +#include "nsCSSPseudoElements.h" // For PseudoStyleType +#include "nsClassHashtable.h" +#include "nsContentUtils.h" // For GetContextForContent +#include "nsIScriptError.h" +#include "nsPresContextInlines.h" +#include "nsTArray.h" + +using mozilla::dom::Nullable; + +namespace mozilla { + +// ------------------------------------------------------------------ +// +// Internal data types +// +// ------------------------------------------------------------------ + +// For the aAllowList parameter of AppendStringOrStringSequence and +// GetPropertyValuesPairs. +enum class ListAllowance { eDisallow, eAllow }; + +/** + * 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; +}; + +/** + * 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; + AnimationValue mValue; + + float mOffset; + Maybe mTimingFunction; + dom::CompositeOperation mComposite; + + struct PropertyOffsetComparator { + static bool Equals(const KeyframeValueEntry& aLhs, + const KeyframeValueEntry& aRhs) { + return aLhs.mProperty == aRhs.mProperty && aLhs.mOffset == aRhs.mOffset; + } + static bool LessThan(const KeyframeValueEntry& aLhs, + const KeyframeValueEntry& aRhs) { + // First, sort by property IDL name. + int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) - + nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty); + if (order != 0) { + return order < 0; + } + + // Then, by offset. + return aLhs.mOffset < aRhs.mOffset; + } + }; +}; + +class ComputedOffsetComparator { + public: + static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) { + return aLhs.mComputedOffset == aRhs.mComputedOffset; + } + + static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) { + return aLhs.mComputedOffset < aRhs.mComputedOffset; + } +}; + +// ------------------------------------------------------------------ +// +// Internal helper method declarations +// +// ------------------------------------------------------------------ + +static void GetKeyframeListFromKeyframeSequence( + JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator, + nsTArray& aResult, const char* aContext, ErrorResult& aRv); + +static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument, + JS::ForOfIterator& aIterator, + const char* aContext, + 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 Maybe MakePropertyValuePair( + nsCSSPropertyID aProperty, const nsACString& aStringValue, + dom::Document* aDocument); + +static bool HasValidOffsets(const nsTArray& aKeyframes); + +#ifdef DEBUG +static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair); + +#endif + +static nsTArray GetComputedKeyframeValues( + const nsTArray& aKeyframes, dom::Element* aElement, + PseudoStyleType aPseudoType, const ComputedStyle* aComputedValues); + +static void BuildSegmentsFromValueEntries( + nsTArray& aEntries, + nsTArray& aResult); + +static void GetKeyframeListFromPropertyIndexedKeyframe( + JSContext* aCx, dom::Document* aDocument, JS::Handle aValue, + nsTArray& aResult, ErrorResult& aRv); + +static bool HasImplicitKeyframeValues(const nsTArray& aKeyframes, + dom::Document* aDocument); + +static void DistributeRange(const Range& aRange); + +// ------------------------------------------------------------------ +// +// Public API +// +// ------------------------------------------------------------------ + +/* static */ +nsTArray KeyframeUtils::GetKeyframesFromObject( + JSContext* aCx, dom::Document* aDocument, JS::Handle aFrames, + const char* aContext, 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, + aContext, 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; + } + + if (!dom::Document::AreWebAnimationsImplicitKeyframesEnabled(aCx, nullptr) && + HasImplicitKeyframeValues(keyframes, aDocument)) { + keyframes.Clear(); + aRv.ThrowNotSupportedError( + "Animation to or from an underlying value is not yet supported"); + } + + return keyframes; +} + +/* static */ +void KeyframeUtils::DistributeKeyframes(nsTArray& aKeyframes) { + if (aKeyframes.IsEmpty()) { + return; + } + + // 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.LastElement(); + const RangedPtr begin(aKeyframes.Elements(), 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). + DistributeRange(Range(keyframeA, keyframeB + 1)); + keyframeA = keyframeB; + } +} + +/* static */ +nsTArray KeyframeUtils::GetAnimationPropertiesFromKeyframes( + const nsTArray& aKeyframes, dom::Element* aElement, + PseudoStyleType aPseudoType, const ComputedStyle* aStyle, + dom::CompositeOperation aEffectComposite) { + nsTArray result; + + const nsTArray computedValues = + GetComputedKeyframeValues(aKeyframes, aElement, aPseudoType, aStyle); + if (computedValues.IsEmpty()) { + // In rare cases GetComputedKeyframeValues might fail and return an empty + // array, in which case we likewise return an empty array from here. + return result; + } + + MOZ_ASSERT(aKeyframes.Length() == computedValues.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 : computedValues[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; + // The following assumes that CompositeOperation is a strict subset of + // CompositeOperationOrAuto. + entry->mComposite = + frame.mComposite == dom::CompositeOperationOrAuto::Auto + ? aEffectComposite + : static_cast(frame.mComposite); + } + } + + BuildSegmentsFromValueEntries(entries, result); + return result; +} + +/* static */ +bool KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty) { + // Regardless of the backend type, treat the 'display' property as not + // animatable. (Servo will report it as being animatable, since it is + // in fact animatable by SMIL.) + if (aProperty == eCSSProperty_display) { + return false; + } + return Servo_Property_IsAnimatable(aProperty); +} + +// ------------------------------------------------------------------ +// +// 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 aContext The context string to prepend to thrown exceptions. + * @param aRv Out param to store any errors thrown by this function. + */ +static void GetKeyframeListFromKeyframeSequence( + JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator, + nsTArray& aResult, const char* aContext, 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, aContext, aResult)) { + aResult.Clear(); + aRv.NoteJSContextException(aCx); + 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)) { + aResult.Clear(); + aRv.ThrowTypeError(); + 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, dom::Document* aDocument, + JS::ForOfIterator& aIterator, + const char* aContext, + nsTArray& aResult) { + JS::Rooted value(aCx); + // Parsing errors should only be reported after we have finished iterating + // through all values. If we have any early returns while iterating, we should + // ignore parsing errors. + IgnoredErrorResult parseEasingResult; + + 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, aContext, "Element of sequence argument"); + return false; + } + + // Convert the JS value into a BaseKeyframe dictionary value. + dom::binding_detail::FastBaseKeyframe keyframeDict; + dom::BindingCallContext callCx(aCx, aContext); + if (!keyframeDict.Init(callCx, value, + "Element of sequence argument")) { + // This may happen if the value type of the member of BaseKeyframe is + // invalid. e.g. `offset` only accept a double value, so if we provide a + // string, we enter this branch. + // Besides, keyframeDict.Init() should throw a Type Error message already, + // so we don't have to do it again. + return false; + } + + Keyframe* keyframe = aResult.AppendElement(fallible); + if (!keyframe) { + return false; + } + + if (!keyframeDict.mOffset.IsNull()) { + keyframe->mOffset.emplace(keyframeDict.mOffset.Value()); + } + + if (StaticPrefs::dom_animations_api_compositing_enabled()) { + keyframe->mComposite = keyframeDict.mComposite; + } + + // 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; + } + } + + if (!parseEasingResult.Failed()) { + keyframe->mTimingFunction = + TimingParams::ParseEasing(keyframeDict.mEasing, parseEasingResult); + // Even if the above fails, we still need to continue reading off all the + // properties since checking the validity of easing should be treated as + // a separate step that happens *after* all the other processing in this + // loop since (since it is never likely to be handled by WebIDL unlike the + // rest of this loop). + } + + for (PropertyValuesPair& pair : propertyValuePairs) { + MOZ_ASSERT(pair.mValues.Length() == 1); + + Maybe valuePair = + MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument); + if (!valuePair) { + continue; + } + keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref())); + +#ifdef DEBUG + // 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()); + } +#endif + } + } + + // Throw any errors we encountered while parsing 'easing' properties. + if (parseEasingResult.MaybeSetPendingException(aCx)) { + return false; + } + + return true; +} + +/** + * Reads the property-values pairs from the specified JS object. + * + * @param aObject The JS object to look at. + * @param aAllowLists If eAllow, values will be converted to + * (DOMString or sequence aObject, + ListAllowance aAllowLists, + nsTArray& aResult) { + nsTArray properties; + + // Iterate over all the properties on aObject and append an + // entry to properties for them. + // + // We don't compare the jsids that we encounter with those for + // the explicit dictionary members, since we know that none + // of the CSS property IDL names clash with them. + JS::Rooted ids(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, aObject, &ids)) { + return false; + } + for (size_t i = 0, n = ids.length(); i < n; i++) { + nsAutoJSCString propName; + if (!propName.init(aCx, ids[i])) { + return false; + } + + // Basically, we have to handle "cssOffset" and "cssFloat" specially here: + // "cssOffset" => eCSSProperty_offset + // "cssFloat" => eCSSProperty_float + // This means if the attribute is the string "cssOffset"/"cssFloat", we use + // CSS "offset"/"float" property. + // https://drafts.csswg.org/web-animations/#property-name-conversion + nsCSSPropertyID property = nsCSSPropertyID::eCSSProperty_UNKNOWN; + if (propName.EqualsLiteral("cssOffset")) { + property = nsCSSPropertyID::eCSSProperty_offset; + } else if (propName.EqualsLiteral("cssFloat")) { + property = nsCSSPropertyID::eCSSProperty_float; + } else if (!propName.EqualsLiteral("offset") && + !propName.EqualsLiteral("float")) { + property = nsCSSProps::LookupPropertyByIDLName( + propName, CSSEnabledState::ForAllContent); + } + + 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()); +} + +static void ReportInvalidPropertyValueToConsole( + nsCSSPropertyID aProperty, const nsACString& aInvalidPropertyValue, + dom::Document* aDoc) { + AutoTArray params; + params.AppendElement(NS_ConvertUTF8toUTF16(aInvalidPropertyValue)); + CopyASCIItoUTF16(nsCSSProps::GetStringValue(aProperty), + *params.AppendElement()); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Animation"_ns, + aDoc, nsContentUtils::eDOM_PROPERTIES, + "InvalidKeyframePropertyValue", params); +} + +/** + * 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 aDocument The document to use when parsing. + * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is + * an invalid property value. + */ +static Maybe MakePropertyValuePair( + nsCSSPropertyID aProperty, const nsACString& aStringValue, + dom::Document* aDocument) { + MOZ_ASSERT(aDocument); + Maybe result; + + ServoCSSParser::ParsingEnvironment env = + ServoCSSParser::GetParsingEnvironment(aDocument); + RefPtr servoDeclarationBlock = + ServoCSSParser::ParseProperty(aProperty, aStringValue, env); + + if (servoDeclarationBlock) { + result.emplace(aProperty, std::move(servoDeclarationBlock)); + } else { + ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument); + } + 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; +} + +#ifdef DEBUG +/** + * 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"); + + aPair.mSimulateComputeValuesFailure = true; +} + +#endif + +/** + * The variation of the above function. This is for Servo backend. + */ +static nsTArray GetComputedKeyframeValues( + const nsTArray& aKeyframes, dom::Element* aElement, + PseudoStyleType aPseudoType, const ComputedStyle* aComputedStyle) { + MOZ_ASSERT(aElement); + + nsTArray result; + + nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement); + if (!presContext) { + // This has been reported to happen with some combinations of content + // (particularly involving resize events and layout flushes? See bug 1407898 + // and bug 1408420) but no reproducible steps have been found. + // For now we just return an empty array. + return result; + } + + result = presContext->StyleSet()->GetComputedKeyframeValuesFor( + aKeyframes, aElement, aPseudoType, aComputedStyle); + return result; +} + +static void AppendInitialSegment(AnimationProperty* aAnimationProperty, + const KeyframeValueEntry& aFirstEntry) { + AnimationPropertySegment* segment = + aAnimationProperty->mSegments.AppendElement(); + segment->mFromKey = 0.0f; + segment->mToKey = aFirstEntry.mOffset; + segment->mToValue = aFirstEntry.mValue; + segment->mToComposite = aFirstEntry.mComposite; +} + +static void AppendFinalSegment(AnimationProperty* aAnimationProperty, + const KeyframeValueEntry& aLastEntry) { + AnimationPropertySegment* segment = + aAnimationProperty->mSegments.AppendElement(); + segment->mFromKey = aLastEntry.mOffset; + segment->mFromValue = aLastEntry.mValue; + segment->mFromComposite = aLastEntry.mComposite; + segment->mToKey = 1.0f; + segment->mTimingFunction = aLastEntry.mTimingFunction; +} + +// Returns a newly created AnimationProperty if one was created to fill-in the +// missing keyframe, nullptr otherwise (if we decided not to fill the keyframe +// becase we don't support implicit keyframes). +static AnimationProperty* HandleMissingInitialKeyframe( + nsTArray& aResult, const KeyframeValueEntry& aEntry) { + MOZ_ASSERT(aEntry.mOffset != 0.0f, + "The offset of the entry should not be 0.0"); + + // If the preference for implicit keyframes is not enabled, don't fill in the + // missing keyframe. + if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) { + return nullptr; + } + + AnimationProperty* result = aResult.AppendElement(); + result->mProperty = aEntry.mProperty; + + AppendInitialSegment(result, aEntry); + + return result; +} + +static void HandleMissingFinalKeyframe( + nsTArray& aResult, const KeyframeValueEntry& aEntry, + AnimationProperty* aCurrentAnimationProperty) { + MOZ_ASSERT(aEntry.mOffset != 1.0f, + "The offset of the entry should not be 1.0"); + + // If the preference for implicit keyframes is not enabled, don't fill + // in the missing keyframe. + if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled()) { + // If we have already appended a new entry for the property so we have to + // remove it. + if (aCurrentAnimationProperty) { + aResult.RemoveLastElement(); + } + return; + } + + // If |aCurrentAnimationProperty| is nullptr, that means this is the first + // entry for the property, we have to append a new AnimationProperty for this + // property. + if (!aCurrentAnimationProperty) { + aCurrentAnimationProperty = aResult.AppendElement(); + aCurrentAnimationProperty->mProperty = aEntry.mProperty; + + // If we have only one entry whose offset is neither 1 nor 0 for this + // property, we need to append the initial segment as well. + if (aEntry.mOffset != 0.0f) { + AppendInitialSegment(aCurrentAnimationProperty, aEntry); + } + } + AppendFinalSegment(aCurrentAnimationProperty, aEntry); +} + +/** + * 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, if additive animation is disabled, 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) { + // If we've reached the end of the array of entries, synthesize a final (and + // initial) segment if necessary. + if (i + 1 == n) { + if (aEntries[i].mOffset != 1.0f) { + HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty); + } else if (aEntries[i].mOffset == 1.0f && !animationProperty) { + // If the last entry with offset 1 and no animation property, that means + // it is the only entry for this property so append a single segment + // from 0 offset to |aEntry[i].offset|. + Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]); + } + animationProperty = nullptr; + break; + } + + MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN && + aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN, + "Each entry should specify a valid property"); + + // No keyframe for this property at offset 0. + if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) { + // If we don't support additive animation we can't fill in the missing + // keyframes and we should just skip this property altogether. 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. + animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]); + if (animationProperty) { + lastProperty = aEntries[i].mProperty; + } else { + // Skip this entry if we did not handle the missing entry. + ++i; + continue; + } + } + + // Skip this entry if the next entry has the same offset except for initial + // and final ones. We will handle missing keyframe in the next loop + // if the property is changed on the next entry. + if (aEntries[i].mProperty == aEntries[i + 1].mProperty && + aEntries[i].mOffset == aEntries[i + 1].mOffset && + aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) { + ++i; + continue; + } + + // No keyframe for this property at offset 1. + if (aEntries[i].mProperty != aEntries[i + 1].mProperty && + aEntries[i].mOffset != 1.0f) { + HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty); + // Move on to new property. + animationProperty = nullptr; + ++i; + continue; + } + + // Starting from i + 1, determine the next [i, j] interval from which to + // generate a segment. Basically, j is i + 1, but there are some special + // cases for offset 0 and 1, so we need to handle them specifically. + // Note: From this moment, we make sure [i + 1] is valid and + // there must be an initial entry (i.e. mOffset = 0.0) and + // a final entry (i.e. mOffset = 1.0). Besides, all the entries + // with the same offsets except for initial/final ones are filtered + // out already. + size_t j = i + 1; + 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); + while (j + 1 < n && 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. + 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; + } + } + + // 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; + segment->mFromComposite = aEntries[i].mComposite; + segment->mToComposite = aEntries[j].mComposite; + + 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, dom::Document* 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; + // XXXbz Pass in the method name from callers and set up a BindingCallContext? + if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument")) { + aRv.Throw(NS_ERROR_FAILURE); + 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. + nsTHashMap processedKeyframes; + for (const PropertyValuesPair& pair : propertyValuesPairs) { + size_t count = pair.mValues.Length(); + if (count == 0) { + // No animation values for this property. + continue; + } + + // If we only have one value, we should animate from the underlying value + // but not if the pref for supporting implicit keyframes is disabled. + if (!StaticPrefs::dom_animations_api_implicit_keyframes_enabled() && + count == 1) { + aRv.ThrowNotSupportedError( + "Animation to or from an underlying value is not yet supported"); + return; + } + + size_t n = pair.mValues.Length() - 1; + size_t i = 0; + + for (const nsCString& stringValue : pair.mValues) { + // For single-valued lists, the single value should be added to a + // keyframe with offset 1. + double offset = n ? i++ / double(n) : 1; + Keyframe& keyframe = processedKeyframes.LookupOrInsert(offset); + if (keyframe.mPropertyValues.IsEmpty()) { + keyframe.mComputedOffset = offset; + } + + Maybe valuePair = + MakePropertyValuePair(pair.mProperty, stringValue, aDocument); + if (!valuePair) { + continue; + } + keyframe.mPropertyValues.AppendElement(std::move(valuePair.ref())); + } + } + + aResult.SetCapacity(processedKeyframes.Count()); + std::transform(processedKeyframes.begin(), processedKeyframes.end(), + MakeBackInserter(aResult), [](auto& entry) { + return std::move(*entry.GetModifiableData()); + }); + + aResult.Sort(ComputedOffsetComparator()); + + // Fill in any specified offsets + // + // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of + // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument + const FallibleTArray>* offsets = nullptr; + AutoTArray, 1> singleOffset; + auto& offset = keyframeDict.mOffset; + if (offset.IsDouble()) { + singleOffset.AppendElement(offset.GetAsDouble()); + // dom::Sequence is a fallible but AutoTArray is infallible and we need to + // point to one or the other. Fortunately, fallible and infallible array + // types can be implicitly converted provided they are const. + const FallibleTArray>& asFallibleArray = singleOffset; + offsets = &asFallibleArray; + } else if (offset.IsDoubleOrNullSequence()) { + offsets = &offset.GetAsDoubleOrNullSequence(); + } + // If offset.IsNull() is true, then we want to leave the mOffset member of + // each keyframe with its initialized value of null. By leaving |offsets| + // as nullptr here, we skip updating mOffset below. + + size_t offsetsToFill = + offsets ? std::min(offsets->Length(), aResult.Length()) : 0; + for (size_t i = 0; i < offsetsToFill; i++) { + if (!offsets->ElementAt(i).IsNull()) { + aResult[i].mOffset.emplace(offsets->ElementAt(i).Value()); + } + } + + // Check that the keyframes are loosely sorted and that any specified offsets + // are between 0.0 and 1.0 inclusive. + // + // This corresponds to steps 6-7 of + // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument + // + // In the spec, TypeErrors arising from invalid offsets and easings are thrown + // at the end of the procedure since it assumes we initially store easing + // values as strings and then later parse them. + // + // However, we will parse easing members immediately when we process them + // below. In order to maintain the relative order in which TypeErrors are + // thrown according to the spec, namely exceptions arising from invalid + // offsets are thrown before exceptions arising from invalid easings, we check + // the offsets here. + if (!HasValidOffsets(aResult)) { + aResult.Clear(); + aRv.ThrowTypeError(); + return; + } + + // Fill in any easings. + // + // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of + // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument + FallibleTArray> easings; + auto parseAndAppendEasing = [&](const nsACString& easingString, + ErrorResult& aRv) { + auto easing = TimingParams::ParseEasing(easingString, aRv); + if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } + }; + + auto& easing = keyframeDict.mEasing; + if (easing.IsUTF8String()) { + parseAndAppendEasing(easing.GetAsUTF8String(), aRv); + if (aRv.Failed()) { + aResult.Clear(); + return; + } + } else { + for (const auto& easingString : easing.GetAsUTF8StringSequence()) { + parseAndAppendEasing(easingString, aRv); + if (aRv.Failed()) { + aResult.Clear(); + return; + } + } + } + + // If |easings| is empty, then we are supposed to fill it in with the value + // "linear" and then repeat the list as necessary. + // + // However, for Keyframe.mTimingFunction we represent "linear" as a None + // value. Since we have not assigned 'mTimingFunction' for any of the + // keyframes in |aResult| they will already have their initial None value + // (i.e. linear). As a result, if |easings| is empty, we don't need to do + // anything. + if (!easings.IsEmpty()) { + for (size_t i = 0; i < aResult.Length(); i++) { + aResult[i].mTimingFunction = easings[i % easings.Length()]; + } + } + + // Fill in any composite operations. + // + // This corresponds to step 5, "Otherwise," branch, substep 12 of + // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument + if (StaticPrefs::dom_animations_api_compositing_enabled()) { + const FallibleTArray* compositeOps = nullptr; + AutoTArray singleCompositeOp; + auto& composite = keyframeDict.mComposite; + if (composite.IsCompositeOperationOrAuto()) { + singleCompositeOp.AppendElement( + composite.GetAsCompositeOperationOrAuto()); + const FallibleTArray& asFallibleArray = + singleCompositeOp; + compositeOps = &asFallibleArray; + } else if (composite.IsCompositeOperationOrAutoSequence()) { + compositeOps = &composite.GetAsCompositeOperationOrAutoSequence(); + } + + // Fill in and repeat as needed. + if (compositeOps && !compositeOps->IsEmpty()) { + size_t length = compositeOps->Length(); + for (size_t i = 0; i < aResult.Length(); i++) { + aResult[i].mComposite = compositeOps->ElementAt(i % length); + } + } + } +} + +/** + * 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. 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 HasImplicitKeyframeValues(const nsTArray& aKeyframes, + dom::Document* 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 ship implicit keyframes and + // remove the corresponding pref. + // 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); + } + }; + + for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) { + const Keyframe& frame = aKeyframes[i]; + + // We won't have called DistributeKeyframes when this is called so + // we can't use frame.mComputedOffset. Instead we do a rough version + // of that algorithm that substitutes null offsets with 0.0 for the first + // frame, 1.0 for the last frame, and 0.5 for everything else. + double computedOffset = i == len - 1 ? 1.0 : i == 0 ? 0.0 : 0.5; + double offsetToUse = frame.mOffset ? frame.mOffset.value() : computedOffset; + + for (const PropertyValuePair& pair : frame.mPropertyValues) { + if (nsCSSProps::IsShorthand(pair.mProperty)) { + MOZ_ASSERT(pair.mServoDeclarationBlock); + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(prop, pair.mProperty, + CSSEnabledState::ForAllContent) { + addToPropertySets(*prop, offsetToUse); + } + } else { + addToPropertySets(pair.mProperty, offsetToUse); + } + } + } + + return !propertiesWithFromValue.Equals(properties) || + !propertiesWithToValue.Equals(properties); +} + +/** + * Distribute the offsets of all keyframes in between the endpoints of the + * given range. + * + * @param aRange The sequence of keyframes between whose endpoints we should + * distribute offsets. + */ +static void DistributeRange(const Range& aRange) { + const Range rangeToAdjust = + Range(aRange.begin() + 1, aRange.end() - 1); + const size_t n = aRange.length() - 1; + const double startOffset = aRange[0].mComputedOffset; + const double diffOffset = aRange[n].mComputedOffset - startOffset; + for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) { + size_t index = iter - aRange.begin(); + iter->mComputedOffset = startOffset + double(index) / n * diffOffset; + } +} + +} // namespace mozilla diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h new file mode 100644 index 0000000000..311a091df8 --- /dev/null +++ b/dom/animation/KeyframeUtils.h @@ -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/. */ + +#ifndef mozilla_KeyframeUtils_h +#define mozilla_KeyframeUtils_h + +#include "mozilla/KeyframeEffectParams.h" // For CompositeOperation +#include "nsCSSPropertyID.h" +#include "nsTArrayForwardDeclare.h" // For nsTArray +#include "js/RootingAPI.h" // For JS::Handle + +struct JSContext; +class JSObject; + +namespace mozilla { +struct AnimationProperty; +class ComputedStyle; + +enum class PseudoStyleType : uint8_t; +class ErrorResult; +struct Keyframe; +struct PropertyStyleAnimationValuePair; + +namespace dom { +class Document; +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 aContext Information about who is trying to get keyframes from the + * object, for use in error reporting. This must be be a non-null + * pointer representing a null-terminated ASCII string. + * @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, dom::Document* aDocument, JS::Handle aFrames, + const char* aContext, ErrorResult& aRv); + + /** + * Calculate the computed offset of keyframes by evenly distributing keyframes + * with a missing offset. + * + * @see + * https://drafts.csswg.org/web-animations/#calculating-computed-keyframes + * + * @param aKeyframes The set of keyframes to adjust. + */ + static void DistributeKeyframes(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 aElement The context element. + * @param aStyle The computed style values. + * @param aEffectComposite The composite operation specified on the effect. + * For any keyframes in |aKeyframes| that do not specify a composite + * operation, this value will be used. + * @return The set of animation properties. If an error occurs, the returned + * array will be empty. + */ + static nsTArray GetAnimationPropertiesFromKeyframes( + const nsTArray& aKeyframes, dom::Element* aElement, + PseudoStyleType aPseudoType, const ComputedStyle* aStyle, + dom::CompositeOperation aEffectComposite); + + /** + * Check if the property or, for shorthands, one or more of + * its subproperties, is animatable. + * + * @param aProperty The property to check. + * @param aBackend The style backend, Servo or Gecko, that should determine + * if the property is animatable or not. + * @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 0000000000..9993f0888a --- /dev/null +++ b/dom/animation/PendingAnimationTracker.cpp @@ -0,0 +1,193 @@ +/* -*- 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/PresShell.h" +#include "mozilla/dom/AnimationEffect.h" +#include "mozilla/dom/AnimationTimeline.h" +#include "mozilla/dom/Nullable.h" +#include "nsIFrame.h" +#include "nsTransitionManager.h" // For CSSTransition + +using mozilla::dom::Nullable; + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker, mPlayPendingSet, + mPausePendingSet, mDocument) + +PendingAnimationTracker::PendingAnimationTracker(dom::Document* aDocument) + : mDocument(aDocument) {} + +void PendingAnimationTracker::AddPending(dom::Animation& aAnimation, + AnimationSet& aSet) { + aSet.Insert(&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.Remove(&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.begin(), end = aAnimationSet.end(); + iter != end; ++iter) { + dom::Animation* animation = *iter; + 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) { + aAnimationSet.Remove(iter); + continue; + } + + MOZ_ASSERT(timeline->IsMonotonicallyIncreasing(), + "The non-monotonicially-increasing timeline should be in " + "ScrollTimelineAnimationTracker"); + + // 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); + + aAnimationSet.Remove(iter); + } + }; + + triggerAnimationsAtReadyTime(mPlayPendingSet); + triggerAnimationsAtReadyTime(mPausePendingSet); + + mHasPlayPendingGeometricAnimations = + mPlayPendingSet.Count() ? CheckState::Indeterminate : CheckState::Absent; +} + +void PendingAnimationTracker::TriggerPendingAnimationsNow() { + auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) { + for (const auto& animation : aAnimationSet) { + animation->TriggerNow(); + } + aAnimationSet.Clear(); + }; + + triggerAndClearAnimations(mPlayPendingSet); + triggerAndClearAnimations(mPausePendingSet); + + mHasPlayPendingGeometricAnimations = CheckState::Absent; +} + +static bool IsTransition(const dom::Animation& aAnimation) { + const dom::CSSTransition* transition = aAnimation.AsCSSTransition(); + return transition && transition->IsTiedToMarkup(); +} + +void PendingAnimationTracker::MarkAnimationsThatMightNeedSynchronization() { + // We only set mHasPlayPendingGeometricAnimations to "present" in this method + // and nowhere else. After setting the state to "present", if there is any + // change to the set of play-pending animations we will reset + // mHasPlayPendingGeometricAnimations to either "indeterminate" or "absent". + // + // As a result, if mHasPlayPendingGeometricAnimations is "present", we can + // assume that this method has already been called for the current set of + // play-pending animations and it is not necessary to run this method again. + // + // If mHasPlayPendingGeometricAnimations is "absent", then we can also skip + // the body of this method since there are no notifications to be sent. + // + // Therefore, the only case we need to be concerned about is the + // "indeterminate" case. For all other cases we can return early. + // + // Note that *without* this optimization, starting animations would become + // O(n^2) in the case where each animation is on a different element and + // contains a compositor-animatable property since we would end up iterating + // over all animations in the play-pending set for each target element. + if (mHasPlayPendingGeometricAnimations != CheckState::Indeterminate) { + return; + } + + // We only synchronize CSS transitions with other CSS transitions (and we only + // synchronize non-transition animations with non-transition animations) + // since typically the author will not trigger both CSS animations and + // CSS transitions simultaneously and expect them to be synchronized. + // + // If we try to synchronize CSS transitions with non-transitions then for some + // content we will end up degrading performance by forcing animations to run + // on the main thread that really don't need to. + + mHasPlayPendingGeometricAnimations = CheckState::Absent; + for (const auto& animation : mPlayPendingSet) { + if (animation->GetEffect() && animation->GetEffect()->AffectsGeometry()) { + mHasPlayPendingGeometricAnimations &= ~CheckState::Absent; + mHasPlayPendingGeometricAnimations |= IsTransition(*animation) + ? CheckState::TransitionsPresent + : CheckState::AnimationsPresent; + + // If we have both transitions and animations we don't need to look any + // further. + if (mHasPlayPendingGeometricAnimations == + (CheckState::TransitionsPresent | CheckState::AnimationsPresent)) { + break; + } + } + } + + if (mHasPlayPendingGeometricAnimations == CheckState::Absent) { + return; + } + + for (const auto& animation : mPlayPendingSet) { + bool isTransition = IsTransition(*animation); + if ((isTransition && + mHasPlayPendingGeometricAnimations & CheckState::TransitionsPresent) || + (!isTransition && + mHasPlayPendingGeometricAnimations & CheckState::AnimationsPresent)) { + animation->NotifyGeometricAnimationsStartingThisFrame(); + } + } +} + +void PendingAnimationTracker::EnsurePaintIsScheduled() { + if (!mDocument) { + return; + } + + PresShell* presShell = mDocument->GetPresShell(); + if (!presShell) { + return; + } + + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) { + return; + } + + rootFrame->SchedulePaintWithoutInvalidatingObservers(); +} + +} // namespace mozilla diff --git a/dom/animation/PendingAnimationTracker.h b/dom/animation/PendingAnimationTracker.h new file mode 100644 index 0000000000..5fb277b7ef --- /dev/null +++ b/dom/animation/PendingAnimationTracker.h @@ -0,0 +1,113 @@ +/* -*- 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_PendingAnimationTracker_h +#define mozilla_PendingAnimationTracker_h + +#include "mozilla/dom/Animation.h" +#include "mozilla/TypedEnumBits.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashSet.h" + +class nsIFrame; + +namespace mozilla { + +namespace dom { +class Document; +} + +/** + * Handle the pending animations which use document-timeline or null-timeline + * while playing or pausing. + */ +class PendingAnimationTracker final { + public: + explicit PendingAnimationTracker(dom::Document* aDocument); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker) + + void AddPlayPending(dom::Animation& aAnimation) { + // We'd like to assert here that IsWaitingToPause(aAnimation) is false but + // if |aAnimation| was tracked here as a pause-pending animation when it was + // removed from |mDocument|, then re-attached to |mDocument|, and then + // played again, we could end up here with IsWaitingToPause returning true. + // + // However, that should be harmless since all it means is that we'll call + // Animation::TriggerOnNextTick or Animation::TriggerNow twice, both of + // which will handle the redundant call gracefully. + AddPending(aAnimation, mPlayPendingSet); + mHasPlayPendingGeometricAnimations = CheckState::Indeterminate; + } + void RemovePlayPending(dom::Animation& aAnimation) { + RemovePending(aAnimation, mPlayPendingSet); + mHasPlayPendingGeometricAnimations = CheckState::Indeterminate; + } + bool IsWaitingToPlay(const dom::Animation& aAnimation) const { + return IsWaiting(aAnimation, mPlayPendingSet); + } + + void AddPausePending(dom::Animation& aAnimation) { + // As with AddPausePending, we'd like to assert that + // IsWaitingToPlay(aAnimation) is false but there are some circumstances + // where this can be true. Fortunately adding the animation to both pending + // sets should be harmless. + 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; + } + + /** + * Looks amongst the set of play-pending animations, and, if there are + * animations that affect geometric properties, notifies all play-pending + * animations so that they can be synchronized, if needed. + */ + void MarkAnimationsThatMightNeedSynchronization(); + + private: + ~PendingAnimationTracker() = default; + + void EnsurePaintIsScheduled(); + + using AnimationSet = nsTHashSet>; + + 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; + RefPtr mDocument; + + public: + enum class CheckState { + Indeterminate = 0, + Absent = 1 << 0, + AnimationsPresent = 1 << 1, + TransitionsPresent = 1 << 2, + }; + + private: + CheckState mHasPlayPendingGeometricAnimations = CheckState::Indeterminate; +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(PendingAnimationTracker::CheckState) + +} // namespace mozilla + +#endif // mozilla_PendingAnimationTracker_h diff --git a/dom/animation/PostRestyleMode.h b/dom/animation/PostRestyleMode.h new file mode 100644 index 0000000000..7ddf8df778 --- /dev/null +++ b/dom/animation/PostRestyleMode.h @@ -0,0 +1,16 @@ +/* -*- 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_PostRestyleMode_h +#define mozilla_PostRestyleMode_h + +namespace mozilla { + +enum class PostRestyleMode { IfNeeded, Never }; + +} // namespace mozilla + +#endif // mozilla_PostRestyleMode_h diff --git a/dom/animation/PseudoElementHashEntry.h b/dom/animation/PseudoElementHashEntry.h new file mode 100644 index 0000000000..b532d86105 --- /dev/null +++ b/dom/animation/PseudoElementHashEntry.h @@ -0,0 +1,51 @@ +/* -*- 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, PseudoStyleType pair +class PseudoElementHashEntry : public PLDHashEntryHdr { + public: + typedef NonOwningAnimationTarget KeyType; + typedef const NonOwningAnimationTarget* KeyTypePointer; + + explicit PseudoElementHashEntry(KeyTypePointer aKey) + : mElement(aKey->mElement), mPseudoType(aKey->mPseudoType) {} + PseudoElementHashEntry(PseudoElementHashEntry&& aOther) = 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. + static_assert(sizeof(PseudoStyleType) == sizeof(uint8_t), ""); + return mozilla::HashGeneric(aKey->mElement, + static_cast(aKey->mPseudoType)); + } + enum { ALLOW_MEMMOVE = true }; + + RefPtr mElement; + PseudoStyleType mPseudoType; +}; + +} // namespace mozilla + +#endif // mozilla_PseudoElementHashEntry_h diff --git a/dom/animation/ScrollTimeline.cpp b/dom/animation/ScrollTimeline.cpp new file mode 100644 index 0000000000..eeb9571494 --- /dev/null +++ b/dom/animation/ScrollTimeline.cpp @@ -0,0 +1,295 @@ +/* -*- 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 "ScrollTimeline.h" + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/ElementInlines.h" +#include "mozilla/AnimationTarget.h" +#include "mozilla/DisplayPortUtils.h" +#include "mozilla/ElementAnimationData.h" +#include "mozilla/PresShell.h" +#include "nsIFrame.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" + +namespace mozilla::dom { + +// --------------------------------- +// Methods of ScrollTimeline +// --------------------------------- + +NS_IMPL_CYCLE_COLLECTION_CLASS(ScrollTimeline) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ScrollTimeline, + AnimationTimeline) + tmp->Teardown(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSource.mElement) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ScrollTimeline, + AnimationTimeline) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSource.mElement) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ScrollTimeline, + AnimationTimeline) + +ScrollTimeline::ScrollTimeline(Document* aDocument, const Scroller& aScroller, + StyleScrollAxis aAxis) + : AnimationTimeline(aDocument->GetParentObject(), + aDocument->GetScopeObject()->GetRTPCallerType()), + mDocument(aDocument), + mSource(aScroller), + mAxis(aAxis) { + MOZ_ASSERT(aDocument); + RegisterWithScrollSource(); +} + +/* static */ std::pair +ScrollTimeline::FindNearestScroller(Element* aSubject, + PseudoStyleType aPseudoType) { + MOZ_ASSERT(aSubject); + Element* subject = + AnimationUtils::GetElementForRestyle(aSubject, aPseudoType); + + Element* curr = subject->GetFlattenedTreeParentElement(); + Element* root = subject->OwnerDoc()->GetDocumentElement(); + while (curr && curr != root) { + const ComputedStyle* style = Servo_Element_GetMaybeOutOfDateStyle(curr); + MOZ_ASSERT(style, "The ancestor should be styled."); + if (style->StyleDisplay()->IsScrollableOverflow()) { + break; + } + curr = curr->GetFlattenedTreeParentElement(); + } + // If there is no scroll container, we use root. + if (!curr) { + return {root, PseudoStyleType::NotPseudo}; + } + return AnimationUtils::GetElementPseudoPair(curr); +} + +/* static */ +already_AddRefed ScrollTimeline::MakeAnonymous( + Document* aDocument, const NonOwningAnimationTarget& aTarget, + StyleScrollAxis aAxis, StyleScroller aScroller) { + MOZ_ASSERT(aTarget); + Scroller scroller; + switch (aScroller) { + case StyleScroller::Root: + scroller = Scroller::Root(aTarget.mElement->OwnerDoc()); + break; + + case StyleScroller::Nearest: { + auto [element, pseudo] = + FindNearestScroller(aTarget.mElement, aTarget.mPseudoType); + scroller = Scroller::Nearest(const_cast(element), pseudo); + break; + } + case StyleScroller::SelfElement: + scroller = Scroller::Self(aTarget.mElement, aTarget.mPseudoType); + break; + } + + // Each use of scroll() corresponds to its own instance of ScrollTimeline in + // the Web Animations API, even if multiple elements use scroll() to refer to + // the same scroll container with the same arguments. + // https://drafts.csswg.org/scroll-animations-1/#scroll-notation + return MakeAndAddRef(aDocument, scroller, aAxis); +} + +/* static*/ already_AddRefed ScrollTimeline::MakeNamed( + Document* aDocument, Element* aReferenceElement, + PseudoStyleType aPseudoType, const StyleScrollTimeline& aStyleTimeline) { + MOZ_ASSERT(NS_IsMainThread()); + + Scroller scroller = Scroller::Named(aReferenceElement, aPseudoType); + return MakeAndAddRef(aDocument, std::move(scroller), + aStyleTimeline.GetAxis()); +} + +Nullable ScrollTimeline::GetCurrentTimeAsDuration() const { + // If no layout box, this timeline is inactive. + if (!mSource || !mSource.mElement->GetPrimaryFrame()) { + return nullptr; + } + + // if this is not a scroller container, this timeline is inactive. + const nsIScrollableFrame* scrollFrame = GetScrollFrame(); + if (!scrollFrame) { + return nullptr; + } + + const auto orientation = Axis(); + + // If there is no scrollable overflow, then the ScrollTimeline is inactive. + // https://drafts.csswg.org/scroll-animations-1/#scrolltimeline-interface + if (!scrollFrame->GetAvailableScrollingDirections().contains(orientation)) { + return nullptr; + } + + const bool isHorizontal = orientation == layers::ScrollDirection::eHorizontal; + const nsPoint& scrollPosition = scrollFrame->GetScrollPosition(); + const Maybe& offsets = + ComputeOffsets(scrollFrame, orientation); + if (!offsets) { + return nullptr; + } + + // Note: For RTL, scrollPosition.x or scrollPosition.y may be negative, + // e.g. the range of its value is [0, -range], so we have to use the + // absolute value. + nscoord position = + std::abs(isHorizontal ? scrollPosition.x : scrollPosition.y); + double progress = static_cast(position - offsets->mStart) / + static_cast(offsets->mEnd - offsets->mStart); + return TimeDuration::FromMilliseconds(progress * + PROGRESS_TIMELINE_DURATION_MILLISEC); +} + +layers::ScrollDirection ScrollTimeline::Axis() const { + MOZ_ASSERT(mSource && mSource.mElement->GetPrimaryFrame()); + + const WritingMode wm = mSource.mElement->GetPrimaryFrame()->GetWritingMode(); + return mAxis == StyleScrollAxis::Horizontal || + (!wm.IsVertical() && mAxis == StyleScrollAxis::Inline) || + (wm.IsVertical() && mAxis == StyleScrollAxis::Block) + ? layers::ScrollDirection::eHorizontal + : layers::ScrollDirection::eVertical; +} + +StyleOverflow ScrollTimeline::SourceScrollStyle() const { + MOZ_ASSERT(mSource && mSource.mElement->GetPrimaryFrame()); + + const nsIScrollableFrame* scrollFrame = GetScrollFrame(); + MOZ_ASSERT(scrollFrame); + + const ScrollStyles scrollStyles = scrollFrame->GetScrollStyles(); + + return Axis() == layers::ScrollDirection::eHorizontal + ? scrollStyles.mHorizontal + : scrollStyles.mVertical; +} + +bool ScrollTimeline::APZIsActiveForSource() const { + MOZ_ASSERT(mSource); + return gfxPlatform::AsyncPanZoomEnabled() && + !nsLayoutUtils::ShouldDisableApzForElement(mSource.mElement) && + DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(mSource.mElement); +} + +bool ScrollTimeline::ScrollingDirectionIsAvailable() const { + const nsIScrollableFrame* scrollFrame = GetScrollFrame(); + MOZ_ASSERT(scrollFrame); + return scrollFrame->GetAvailableScrollingDirections().contains(Axis()); +} + +void ScrollTimeline::ReplacePropertiesWith(const Element* aReferenceElement, + PseudoStyleType aPseudoType, + const StyleScrollTimeline& aNew) { + MOZ_ASSERT(aReferenceElement == mSource.mElement && + aPseudoType == mSource.mPseudoType); + mAxis = aNew.GetAxis(); + + for (auto* anim = mAnimationOrder.getFirst(); anim; + anim = static_cast*>(anim)->getNext()) { + MOZ_ASSERT(anim->GetTimeline() == this); + // Set this so we just PostUpdate() for this animation. + anim->SetTimeline(this); + } +} + +Maybe ScrollTimeline::ComputeOffsets( + const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const { + const nsRect& scrollRange = aScrollFrame->GetScrollRange(); + nscoord range = aOrientation == layers::ScrollDirection::eHorizontal + ? scrollRange.width + : scrollRange.height; + MOZ_ASSERT(range > 0); + return Some(ScrollOffsets{0, range}); +} + +void ScrollTimeline::RegisterWithScrollSource() { + if (!mSource) { + return; + } + + auto& scheduler = + ProgressTimelineScheduler::Ensure(mSource.mElement, mSource.mPseudoType); + scheduler.AddTimeline(this); +} + +void ScrollTimeline::UnregisterFromScrollSource() { + if (!mSource) { + return; + } + + auto* scheduler = + ProgressTimelineScheduler::Get(mSource.mElement, mSource.mPseudoType); + if (!scheduler) { + return; + } + + scheduler->RemoveTimeline(this); + if (scheduler->IsEmpty()) { + ProgressTimelineScheduler::Destroy(mSource.mElement, mSource.mPseudoType); + } +} + +const nsIScrollableFrame* ScrollTimeline::GetScrollFrame() const { + if (!mSource) { + return nullptr; + } + + switch (mSource.mType) { + case Scroller::Type::Root: + if (const PresShell* presShell = + mSource.mElement->OwnerDoc()->GetPresShell()) { + return presShell->GetRootScrollFrameAsScrollable(); + } + return nullptr; + case Scroller::Type::Nearest: + case Scroller::Type::Name: + case Scroller::Type::Self: + return nsLayoutUtils::FindScrollableFrameFor(mSource.mElement); + } + + MOZ_ASSERT_UNREACHABLE("Unsupported scroller type"); + return nullptr; +} + +// ------------------------------------ +// Methods of ProgressTimelineScheduler +// ------------------------------------ +/* static */ ProgressTimelineScheduler* ProgressTimelineScheduler::Get( + const Element* aElement, PseudoStyleType aPseudoType) { + MOZ_ASSERT(aElement); + auto* data = aElement->GetAnimationData(); + if (!data) { + return nullptr; + } + + return data->GetProgressTimelineScheduler(aPseudoType); +} + +/* static */ ProgressTimelineScheduler& ProgressTimelineScheduler::Ensure( + Element* aElement, PseudoStyleType aPseudoType) { + MOZ_ASSERT(aElement); + return aElement->EnsureAnimationData().EnsureProgressTimelineScheduler( + *aElement, aPseudoType); +} + +/* static */ +void ProgressTimelineScheduler::Destroy(const Element* aElement, + PseudoStyleType aPseudoType) { + auto* data = aElement->GetAnimationData(); + MOZ_ASSERT(data); + data->ClearProgressTimelineScheduler(aPseudoType); +} + +} // namespace mozilla::dom diff --git a/dom/animation/ScrollTimeline.h b/dom/animation/ScrollTimeline.h new file mode 100644 index 0000000000..129872f61b --- /dev/null +++ b/dom/animation/ScrollTimeline.h @@ -0,0 +1,281 @@ +/* -*- 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_ScrollTimeline_h +#define mozilla_dom_ScrollTimeline_h + +#include "mozilla/dom/AnimationTimeline.h" +#include "mozilla/dom/Document.h" +#include "mozilla/HashTable.h" +#include "mozilla/PairHash.h" +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/WritingModes.h" + +#define PROGRESS_TIMELINE_DURATION_MILLISEC 100000 + +class nsIScrollableFrame; + +namespace mozilla { +class ElementAnimationData; +struct NonOwningAnimationTarget; +namespace dom { +class Element; + +/** + * Implementation notes + * -------------------- + * + * ScrollTimelines do not observe refreshes the way DocumentTimelines do. + * This is because the refresh driver keeps ticking while it has registered + * refresh observers. For a DocumentTimeline, it's appropriate to keep the + * refresh driver ticking as long as there are active animations, since the + * animations need to be sampled on every frame. Scroll-linked animations, + * however, only need to be sampled when scrolling has occurred, so keeping + * the refresh driver ticking is wasteful. + * + * As a result, we schedule an animation restyle when + * 1) there are any scroll offsets updated (from APZ or script), via + * nsIScrollableFrame, or + * 2) there are any possible scroll range updated during the frame reflow. + * + * ------------- + * | Animation | + * ------------- + * ^ + * | Call Animation::Tick() if there are any scroll updates. + * | + * ------------------ + * | ScrollTimeline | + * ------------------ + * ^ + * | Try schedule the scroll-driven animations, if there are any scroll + * | offsets changed or the scroll range changed [1]. + * | + * ---------------------- + * | nsIScrollableFrame | + * ---------------------- + * + * [1] nsIScrollableFrame uses its associated dom::Element to lookup the + * ScrollTimelineSet, and iterates the set to schedule the animations + * linked to the ScrollTimelines. + */ +class ScrollTimeline : public AnimationTimeline { + template + friend already_AddRefed mozilla::MakeAndAddRef(Args&&... aArgs); + + public: + struct Scroller { + // FIXME: Bug 1765211. Perhaps we only need root and a specific element. + // This depends on how we fix this bug. + enum class Type : uint8_t { + Root, + Nearest, + Name, + Self, + }; + Type mType = Type::Root; + RefPtr mElement; + PseudoStyleType mPseudoType; + + // We use the owner doc of the animation target. This may be different from + // |mDocument| after we implement ScrollTimeline interface for script. + static Scroller Root(const Document* aOwnerDoc) { + // For auto, we use scrolling element as the default scroller. + // However, it's mutable, and we would like to keep things simple, so + // we always register the ScrollTimeline to the document element (i.e. + // root element) because the content of the root scroll frame is the root + // element. + return {Type::Root, aOwnerDoc->GetDocumentElement(), + PseudoStyleType::NotPseudo}; + } + + static Scroller Nearest(Element* aElement, PseudoStyleType aPseudoType) { + return {Type::Nearest, aElement, aPseudoType}; + } + + static Scroller Named(Element* aElement, PseudoStyleType aPseudoType) { + return {Type::Name, aElement, aPseudoType}; + } + + static Scroller Self(Element* aElement, PseudoStyleType aPseudoType) { + return {Type::Self, aElement, aPseudoType}; + } + + explicit operator bool() const { return mElement; } + bool operator==(const Scroller& aOther) const { + return mType == aOther.mType && mElement == aOther.mElement && + mPseudoType == aOther.mPseudoType; + } + }; + + static already_AddRefed MakeAnonymous( + Document* aDocument, const NonOwningAnimationTarget& aTarget, + StyleScrollAxis aAxis, StyleScroller aScroller); + + // Note: |aReferfenceElement| is used as the scroller which specifies + // scroll-timeline-name property. + static already_AddRefed MakeNamed( + Document* aDocument, Element* aReferenceElement, + PseudoStyleType aPseudoType, const StyleScrollTimeline& aStyleTimeline); + + bool operator==(const ScrollTimeline& aOther) const { + return mDocument == aOther.mDocument && mSource == aOther.mSource && + mAxis == aOther.mAxis; + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ScrollTimeline, AnimationTimeline) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override { + // FIXME: Bug 1676794: Implement ScrollTimeline interface. + return nullptr; + } + + // AnimationTimeline methods. + Nullable GetCurrentTimeAsDuration() const override; + bool TracksWallclockTime() const override { return false; } + Nullable ToTimelineTime( + const TimeStamp& aTimeStamp) const override { + // It's unclear to us what should we do for this function now, so return + // nullptr. + return nullptr; + } + TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override { + // It's unclear to us what should we do for this function now, so return + // zero time. + return {}; + } + Document* GetDocument() const override { return mDocument; } + bool IsMonotonicallyIncreasing() const override { return false; } + bool IsScrollTimeline() const override { return true; } + const ScrollTimeline* AsScrollTimeline() const override { return this; } + bool IsViewTimeline() const override { return false; } + + Nullable TimelineDuration() const override { + // We are using this magic number for progress-based timeline duration + // because we don't support percentage for duration. + return TimeDuration::FromMilliseconds(PROGRESS_TIMELINE_DURATION_MILLISEC); + } + + void ScheduleAnimations() { + // FIXME: Bug 1737927: Need to check the animation mutation observers for + // animations with scroll timelines. + // nsAutoAnimationMutationBatch mb(mDocument); + + Tick(); + } + + // If the source of a ScrollTimeline is an element whose principal box does + // not exist or is not a scroll container, then its phase is the timeline + // inactive phase. It is otherwise in the active phase. This returns true if + // the timeline is in active phase. + // https://drafts.csswg.org/web-animations-1/#inactive-timeline + // Note: This function is called only for compositor animations, so we must + // have the primary frame (principal box) for the source element if it exists. + bool IsActive() const { return GetScrollFrame(); } + + Element* SourceElement() const { + MOZ_ASSERT(mSource); + return mSource.mElement; + } + + // A helper to get the physical orientation of this scroll-timeline. + layers::ScrollDirection Axis() const; + + StyleOverflow SourceScrollStyle() const; + + bool APZIsActiveForSource() const; + + bool ScrollingDirectionIsAvailable() const; + + void ReplacePropertiesWith(const Element* aReferenceElement, + PseudoStyleType aPseudoType, + const StyleScrollTimeline& aNew); + + protected: + virtual ~ScrollTimeline() { Teardown(); } + ScrollTimeline() = delete; + ScrollTimeline(Document* aDocument, const Scroller& aScroller, + StyleScrollAxis aAxis); + + struct ScrollOffsets { + nscoord mStart = 0; + nscoord mEnd = 0; + }; + virtual Maybe ComputeOffsets( + const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const; + + // Note: This function is required to be idempotent, as it can be called from + // both cycleCollection::Unlink() and ~ScrollTimeline(). When modifying this + // function, be sure to preserve this property. + void Teardown() { UnregisterFromScrollSource(); } + + // Register this scroll timeline to the element property. + void RegisterWithScrollSource(); + + // Unregister this scroll timeline from the element property. + void UnregisterFromScrollSource(); + + const nsIScrollableFrame* GetScrollFrame() const; + + static std::pair FindNearestScroller( + Element* aSubject, PseudoStyleType aPseudoType); + + RefPtr mDocument; + + // FIXME: Bug 1765211: We may have to update the source element once the + // overflow property of the scroll-container is updated when we are using + // nearest scroller. + Scroller mSource; + StyleScrollAxis mAxis; +}; + +/** + * A wrapper around a hashset of ScrollTimeline objects to handle the scheduling + * of scroll driven animations. This is used for all kinds of progress + * timelines, i.e. anonymous/named scroll timelines and anonymous/named view + * timelines. And this object is owned by the scroll source (See + * ElementAnimationData and nsGfxScrollFrame for the usage). + */ +class ProgressTimelineScheduler { + public: + ProgressTimelineScheduler() { MOZ_COUNT_CTOR(ProgressTimelineScheduler); } + ~ProgressTimelineScheduler() { MOZ_COUNT_DTOR(ProgressTimelineScheduler); } + + static ProgressTimelineScheduler* Get(const Element* aElement, + PseudoStyleType aPseudoType); + static ProgressTimelineScheduler& Ensure(Element* aElement, + PseudoStyleType aPseudoType); + static void Destroy(const Element* aElement, PseudoStyleType aPseudoType); + + void AddTimeline(ScrollTimeline* aScrollTimeline) { + Unused << mTimelines.put(aScrollTimeline); + } + void RemoveTimeline(ScrollTimeline* aScrollTimeline) { + mTimelines.remove(aScrollTimeline); + } + + bool IsEmpty() const { return mTimelines.empty(); } + + void ScheduleAnimations() const { + for (auto iter = mTimelines.iter(); !iter.done(); iter.next()) { + iter.get()->ScheduleAnimations(); + } + } + + private: + // We let Animations own its scroll timeline or view timeline if it is + // anonymous. For named progress timelines, they are created and destroyed by + // TimelineCollection. + HashSet mTimelines; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScrollTimeline_h diff --git a/dom/animation/ScrollTimelineAnimationTracker.cpp b/dom/animation/ScrollTimelineAnimationTracker.cpp new file mode 100644 index 0000000000..ae46bca62b --- /dev/null +++ b/dom/animation/ScrollTimelineAnimationTracker.cpp @@ -0,0 +1,48 @@ +/* -*- 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 "ScrollTimelineAnimationTracker.h" + +#include "mozilla/dom/Document.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION(ScrollTimelineAnimationTracker, mPendingSet, mDocument) + +void ScrollTimelineAnimationTracker::TriggerPendingAnimations() { + for (auto iter = mPendingSet.begin(), end = mPendingSet.end(); iter != end; + ++iter) { + dom::Animation* animation = *iter; + + MOZ_ASSERT(animation->GetTimeline() && + !animation->GetTimeline()->IsMonotonicallyIncreasing()); + + // FIXME: Trigger now may not be correct because the spec says: + // If a user agent determines that animation is immediately ready, it may + // schedule the task (i.e. ResumeAt()) as a microtask such that it runs at + // the next microtask checkpoint, but it must not perform the task + // synchronously. + // Note: So, for now, we put the animation into the tracker, and trigger + // them immediately until the frames are ready. Using TriggerOnNextTick() + // for scroll-driven animations may have issues because we don't tick if + // no one does scroll. + if (!animation->TryTriggerNowForFiniteTimeline()) { + // Note: We keep this animation pending even if its timeline is always + // inactive. It's pretty hard to tell its future status, for example, it's + // possible that the scroll container is in display:none subtree but the + // animating element isn't the subtree, then we need to keep tracking the + // situation until the scroll container gets framed. so in general we make + // this animation be pending (i.e. not ready) if its scroll-timeline is + // inactive, and this also matches the current spec definition. + continue; + } + + // Note: Remove() is legitimately called once per entry during the loop. + mPendingSet.Remove(iter); + } +} + +} // namespace mozilla diff --git a/dom/animation/ScrollTimelineAnimationTracker.h b/dom/animation/ScrollTimelineAnimationTracker.h new file mode 100644 index 0000000000..fec2814cec --- /dev/null +++ b/dom/animation/ScrollTimelineAnimationTracker.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_ScrollTimelineAnimationTracker_h +#define mozilla_ScrollTimelineAnimationTracker_h + +#include "mozilla/dom/Animation.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashSet.h" + +namespace mozilla { + +namespace dom { +class Document; +} + +/** + * Handle the pending animations which use scroll timeline while playing or + * pausing. + */ +class ScrollTimelineAnimationTracker final { + public: + explicit ScrollTimelineAnimationTracker(dom::Document* aDocument) + : mDocument(aDocument) {} + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING( + ScrollTimelineAnimationTracker) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ScrollTimelineAnimationTracker) + + void AddPending(dom::Animation& aAnimation) { + mPendingSet.Insert(&aAnimation); + } + + void RemovePending(dom::Animation& aAnimation) { + mPendingSet.Remove(&aAnimation); + } + + bool HasPendingAnimations() const { return mPendingSet.Count() > 0; } + + bool IsWaiting(const dom::Animation& aAnimation) const { + return mPendingSet.Contains(const_cast(&aAnimation)); + } + + void TriggerPendingAnimations(); + + private: + ~ScrollTimelineAnimationTracker() = default; + + nsTHashSet> mPendingSet; + RefPtr mDocument; +}; + +} // namespace mozilla + +#endif // mozilla_ScrollTimelineAnimationTracker_h diff --git a/dom/animation/TimingParams.cpp b/dom/animation/TimingParams.cpp new file mode 100644 index 0000000000..7ab78f52f1 --- /dev/null +++ b/dom/animation/TimingParams.cpp @@ -0,0 +1,291 @@ +/* -*- 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/Document.h" +#include "mozilla/dom/KeyframeAnimationOptionsBinding.h" +#include "mozilla/dom/KeyframeEffectBinding.h" +#include "mozilla/ServoCSSParser.h" + +namespace mozilla { + +template +static const dom::EffectTiming& GetTimingProperties( + const OptionsType& aOptions); + +template <> +/* static */ +const dom::EffectTiming& GetTimingProperties( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions) { + MOZ_ASSERT(aOptions.IsKeyframeEffectOptions()); + return aOptions.GetAsKeyframeEffectOptions(); +} + +template <> +/* static */ +const dom::EffectTiming& GetTimingProperties( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions) { + MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions()); + return aOptions.GetAsKeyframeAnimationOptions(); +} + +template +/* static */ +TimingParams TimingParams::FromOptionsType(const OptionsType& aOptions, + ErrorResult& aRv) { + TimingParams result; + + if (aOptions.IsUnrestrictedDouble()) { + double durationInMs = aOptions.GetAsUnrestrictedDouble(); + if (durationInMs >= 0) { + result.mDuration.emplace( + StickyTimeDuration::FromMilliseconds(durationInMs)); + } else { + nsPrintfCString error("Duration value %g is less than 0", durationInMs); + aRv.ThrowTypeError(error); + return result; + } + result.Update(); + } else { + const dom::EffectTiming& timing = GetTimingProperties(aOptions); + result = FromEffectTiming(timing, aRv); + } + + return result; +} + +/* static */ +TimingParams TimingParams::FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv) { + return FromOptionsType(aOptions, aRv); +} + +/* static */ +TimingParams TimingParams::FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv) { + return FromOptionsType(aOptions, aRv); +} + +/* static */ +TimingParams TimingParams::FromEffectTiming( + const dom::EffectTiming& aEffectTiming, ErrorResult& aRv) { + TimingParams result; + + Maybe duration = + TimingParams::ParseDuration(aEffectTiming.mDuration, aRv); + if (aRv.Failed()) { + return result; + } + TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart, aRv); + if (aRv.Failed()) { + return result; + } + TimingParams::ValidateIterations(aEffectTiming.mIterations, aRv); + if (aRv.Failed()) { + return result; + } + Maybe easing = + ParseEasing(aEffectTiming.mEasing, aRv); + if (aRv.Failed()) { + return result; + } + + result.mDuration = duration; + result.mDelay = TimeDuration::FromMilliseconds(aEffectTiming.mDelay); + result.mEndDelay = TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay); + result.mIterations = aEffectTiming.mIterations; + result.mIterationStart = aEffectTiming.mIterationStart; + result.mDirection = aEffectTiming.mDirection; + result.mFill = aEffectTiming.mFill; + result.mFunction = std::move(easing); + + result.Update(); + + return result; +} + +/* static */ +TimingParams TimingParams::MergeOptionalEffectTiming( + const TimingParams& aSource, const dom::OptionalEffectTiming& aEffectTiming, + ErrorResult& aRv) { + MOZ_ASSERT(!aRv.Failed(), "Initially return value should be ok"); + + TimingParams result = aSource; + + // Check for errors first + + Maybe duration; + if (aEffectTiming.mDuration.WasPassed()) { + duration = + TimingParams::ParseDuration(aEffectTiming.mDuration.Value(), aRv); + if (aRv.Failed()) { + return result; + } + } + + if (aEffectTiming.mIterationStart.WasPassed()) { + TimingParams::ValidateIterationStart(aEffectTiming.mIterationStart.Value(), + aRv); + if (aRv.Failed()) { + return result; + } + } + + if (aEffectTiming.mIterations.WasPassed()) { + TimingParams::ValidateIterations(aEffectTiming.mIterations.Value(), aRv); + if (aRv.Failed()) { + return result; + } + } + + Maybe easing; + if (aEffectTiming.mEasing.WasPassed()) { + easing = ParseEasing(aEffectTiming.mEasing.Value(), aRv); + if (aRv.Failed()) { + return result; + } + } + + // Assign values + + if (aEffectTiming.mDuration.WasPassed()) { + result.mDuration = duration; + } + if (aEffectTiming.mDelay.WasPassed()) { + result.mDelay = + TimeDuration::FromMilliseconds(aEffectTiming.mDelay.Value()); + } + if (aEffectTiming.mEndDelay.WasPassed()) { + result.mEndDelay = + TimeDuration::FromMilliseconds(aEffectTiming.mEndDelay.Value()); + } + if (aEffectTiming.mIterations.WasPassed()) { + result.mIterations = aEffectTiming.mIterations.Value(); + } + if (aEffectTiming.mIterationStart.WasPassed()) { + result.mIterationStart = aEffectTiming.mIterationStart.Value(); + } + if (aEffectTiming.mDirection.WasPassed()) { + result.mDirection = aEffectTiming.mDirection.Value(); + } + if (aEffectTiming.mFill.WasPassed()) { + result.mFill = aEffectTiming.mFill.Value(); + } + if (aEffectTiming.mEasing.WasPassed()) { + result.mFunction = easing; + } + + result.Update(); + + return result; +} + +/* static */ +Maybe TimingParams::ParseEasing( + const nsACString& aEasing, ErrorResult& aRv) { + auto timingFunction = StyleComputedTimingFunction::LinearKeyword(); + if (!ServoCSSParser::ParseEasing(aEasing, timingFunction)) { + aRv.ThrowTypeError(aEasing); + return Nothing(); + } + + if (timingFunction.IsLinearKeyword()) { + return Nothing(); + } + + return Some(std::move(timingFunction)); +} + +bool TimingParams::operator==(const TimingParams& aOther) const { + // We don't compare mActiveDuration and mEndTime because they are calculated + // from other timing parameters. + return mDuration == aOther.mDuration && mDelay == aOther.mDelay && + mEndDelay == aOther.mEndDelay && mIterations == aOther.mIterations && + mIterationStart == aOther.mIterationStart && + mDirection == aOther.mDirection && mFill == aOther.mFill && + mFunction == aOther.mFunction; +} + +// FIXME: This is a tentative way to normalize the timing which is defined in +// [web-animations-2] [1]. I borrow this implementation and some concepts for +// the edge cases from Chromium [2] so we can match the behavior with them. The +// implementation here ignores the case of percentage of start delay, end delay, +// and duration because Gecko doesn't support them. We may have to update the +// calculation if the spec issue [3] gets any update. +// +// [1] +// https://drafts.csswg.org/web-animations-2/#time-based-animation-to-a-proportional-animation +// [2] https://chromium-review.googlesource.com/c/chromium/src/+/2992387 +// [3] https://github.com/w3c/csswg-drafts/issues/4862 +TimingParams TimingParams::Normalize( + const TimeDuration& aTimelineDuration) const { + MOZ_ASSERT(aTimelineDuration, + "the timeline duration of scroll-timeline is always non-zero now"); + + TimingParams normalizedTiming(*this); + + // Handle iteration duration value of "auto" first. + // FIXME: Bug 1676794: Gecko doesn't support `animation-duration:auto` and we + // don't support JS-generated scroll animations, so we don't fall into this + // case for now. Need to check this again after we support ScrollTimeline + // interface. + if (!mDuration) { + // If the iteration duration is auto, then: + // Set start delay and end delay to 0, as it is not possible to mix time + // and proportions. + normalizedTiming.mDelay = TimeDuration(); + normalizedTiming.mEndDelay = TimeDuration(); + normalizedTiming.Update(); + return normalizedTiming; + } + + if (mEndTime.IsZero()) { + // mEndTime of zero causes division by zero so we handle it here. + // + // FIXME: The spec doesn't mention this case, so we might have to update + // this based on the spec issue, + // https://github.com/w3c/csswg-drafts/issues/7459. + normalizedTiming.mDelay = TimeDuration(); + normalizedTiming.mEndDelay = TimeDuration(); + normalizedTiming.mDuration = Some(TimeDuration()); + } else if (mEndTime == TimeDuration::Forever()) { + // The iteration count or duration may be infinite; however, start and + // end delays are strictly finite. Thus, in the limit when end time + // approaches infinity: + // start delay / end time = finite / infinite = 0 + // end delay / end time = finite / infinite = 0 + // iteration duration / end time = 1 / iteration count + // This condition can be reached by switching to a scroll timeline on + // an existing infinite duration animation. + // + // FIXME: The spec doesn't mention this case, so we might have to update + // this based on the spec issue, + // https://github.com/w3c/csswg-drafts/issues/7459. + normalizedTiming.mDelay = TimeDuration(); + normalizedTiming.mEndDelay = TimeDuration(); + normalizedTiming.mDuration = + Some(aTimelineDuration.MultDouble(1.0 / mIterations)); + } else { + // Convert to percentages then multiply by the timeline duration. + const double endTimeInSec = mEndTime.ToSeconds(); + normalizedTiming.mDelay = + aTimelineDuration.MultDouble(mDelay.ToSeconds() / endTimeInSec); + normalizedTiming.mEndDelay = + aTimelineDuration.MultDouble(mEndDelay.ToSeconds() / endTimeInSec); + normalizedTiming.mDuration = Some(StickyTimeDuration( + aTimelineDuration.MultDouble(mDuration->ToSeconds() / endTimeInSec))); + } + + normalizedTiming.Update(); + return normalizedTiming; +} + +} // namespace mozilla diff --git a/dom/animation/TimingParams.h b/dom/animation/TimingParams.h new file mode 100644 index 0000000000..4745631006 --- /dev/null +++ b/dom/animation/TimingParams.h @@ -0,0 +1,265 @@ +/* -*- 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 "X11UndefineNone.h" +#include "nsPrintfCString.h" +#include "nsStringFwd.h" +#include "nsPrintfCString.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString +#include "mozilla/Maybe.h" +#include "mozilla/StickyTimeDuration.h" +#include "mozilla/TimeStamp.h" // for TimeDuration +#include "mozilla/ServoStyleConsts.h" + +#include "mozilla/dom/AnimationEffectBinding.h" // for FillMode + // and PlaybackDirection + +namespace mozilla { + +namespace dom { +class UnrestrictedDoubleOrKeyframeEffectOptions; +class UnrestrictedDoubleOrKeyframeAnimationOptions; +} // namespace dom + +struct TimingParams { + TimingParams() = default; + + TimingParams(float aDuration, float aDelay, float aIterationCount, + dom::PlaybackDirection aDirection, dom::FillMode aFillMode) + : mIterations(aIterationCount), mDirection(aDirection), mFill(aFillMode) { + mDuration.emplace(StickyTimeDuration::FromMilliseconds(aDuration)); + mDelay = TimeDuration::FromMilliseconds(aDelay); + Update(); + } + + TimingParams(const TimeDuration& aDuration, const TimeDuration& aDelay, + const TimeDuration& aEndDelay, float aIterations, + float aIterationStart, dom::PlaybackDirection aDirection, + dom::FillMode aFillMode, + const Maybe& aFunction) + : mDelay(aDelay), + mEndDelay(aEndDelay), + mIterations(aIterations), + mIterationStart(aIterationStart), + mDirection(aDirection), + mFill(aFillMode), + mFunction(aFunction) { + mDuration.emplace(aDuration); + Update(); + } + + template + static TimingParams FromOptionsType(const OptionsType& aOptions, + ErrorResult& aRv); + static TimingParams FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions, + ErrorResult& aRv); + static TimingParams FromOptionsUnion( + const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions, + ErrorResult& aRv); + static TimingParams FromEffectTiming(const dom::EffectTiming& aEffectTiming, + ErrorResult& aRv); + // Returns a copy of |aSource| where each timing property in |aSource| that + // is also specified in |aEffectTiming| is replaced with the value from + // |aEffectTiming|. + // + // If any of the values in |aEffectTiming| are invalid, |aRv.Failed()| will be + // true and an unmodified copy of |aSource| will be returned. + static TimingParams MergeOptionalEffectTiming( + const TimingParams& aSource, + const dom::OptionalEffectTiming& aEffectTiming, 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 { + nsPrintfCString err("Duration (%g) must be nonnegative", durationInMs); + aRv.ThrowTypeError(err); + } + } else if (!aDuration.GetAsString().EqualsLiteral("auto")) { + aRv.ThrowTypeError( + NS_ConvertUTF16toUTF8(aDuration.GetAsString())); + } + return result; + } + + static void ValidateIterationStart(double aIterationStart, ErrorResult& aRv) { + if (aIterationStart < 0) { + nsPrintfCString err("Iteration start (%g) must not be negative", + aIterationStart); + aRv.ThrowTypeError(err); + } + } + + static void ValidateIterations(double aIterations, ErrorResult& aRv) { + if (std::isnan(aIterations)) { + aRv.ThrowTypeError("Iterations must not be NaN"); + return; + } + + if (aIterations < 0) { + nsPrintfCString err("Iterations (%g) must not be negative", aIterations); + aRv.ThrowTypeError(err); + } + } + + static Maybe ParseEasing(const nsACString&, + ErrorResult&); + + static StickyTimeDuration CalcActiveDuration( + const Maybe& aDuration, double aIterations) { + // 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 (!aDuration || aDuration->IsZero() || aIterations == 0.0) { + return zeroDuration; + } + + MOZ_ASSERT(*aDuration >= zeroDuration && aIterations >= 0.0, + "Both animation duration and ieration count should be greater " + "than zero"); + + StickyTimeDuration result = aDuration->MultDouble(aIterations); + if (result < zeroDuration) { + // If the result of multiplying above is less than zero, it's likely an + // overflow happened. we consider it's +Inf here. + return StickyTimeDuration::Forever(); + } + return result; + } + // Return the duration of the active interval calculated by duration and + // iteration count. + StickyTimeDuration ActiveDuration() const { + MOZ_ASSERT(CalcActiveDuration(mDuration, mIterations) == mActiveDuration, + "Cached value of active duration should be up to date"); + return mActiveDuration; + } + + StickyTimeDuration EndTime() const { + MOZ_ASSERT(mEndTime == CalcEndTime(), + "Cached value of end time should be up to date"); + return mEndTime; + } + + StickyTimeDuration CalcBeforeActiveBoundary() const { + static constexpr StickyTimeDuration zeroDuration; + // https://drafts.csswg.org/web-animations-1/#before-active-boundary-time + return std::max(std::min(StickyTimeDuration(mDelay), mEndTime), + zeroDuration); + } + + StickyTimeDuration CalcActiveAfterBoundary() const { + if (mActiveDuration == StickyTimeDuration::Forever()) { + return StickyTimeDuration::Forever(); + } + + static constexpr StickyTimeDuration zeroDuration; + // https://drafts.csswg.org/web-animations-1/#active-after-boundary-time + return std::max( + std::min(StickyTimeDuration(mDelay + mActiveDuration), mEndTime), + zeroDuration); + } + + bool operator==(const TimingParams& aOther) const; + bool operator!=(const TimingParams& aOther) const { + return !(*this == aOther); + } + + void SetDuration(Maybe&& aDuration) { + mDuration = std::move(aDuration); + Update(); + } + void SetDuration(const Maybe& aDuration) { + mDuration = aDuration; + Update(); + } + const Maybe& Duration() const { return mDuration; } + + void SetDelay(const TimeDuration& aDelay) { + mDelay = aDelay; + Update(); + } + const TimeDuration& Delay() const { return mDelay; } + + void SetEndDelay(const TimeDuration& aEndDelay) { + mEndDelay = aEndDelay; + Update(); + } + const TimeDuration& EndDelay() const { return mEndDelay; } + + void SetIterations(double aIterations) { + mIterations = aIterations; + Update(); + } + double Iterations() const { return mIterations; } + + void SetIterationStart(double aIterationStart) { + mIterationStart = aIterationStart; + } + double IterationStart() const { return mIterationStart; } + + void SetDirection(dom::PlaybackDirection aDirection) { + mDirection = aDirection; + } + dom::PlaybackDirection Direction() const { return mDirection; } + + void SetFill(dom::FillMode aFill) { mFill = aFill; } + dom::FillMode Fill() const { return mFill; } + + void SetTimingFunction(Maybe&& aFunction) { + mFunction = std::move(aFunction); + } + const Maybe& TimingFunction() const { + return mFunction; + } + + // This is called only for progress-based timeline (i.e. non-monotonic + // timeline). That is, |aTimelineDuration| should be resolved already. + TimingParams Normalize(const TimeDuration& aTimelineDuration) const; + + private: + void Update() { + mActiveDuration = CalcActiveDuration(mDuration, mIterations); + mEndTime = CalcEndTime(); + } + + StickyTimeDuration CalcEndTime() const { + if (mActiveDuration == StickyTimeDuration::Forever()) { + return StickyTimeDuration::Forever(); + } + return std::max(mDelay + mActiveDuration + mEndDelay, StickyTimeDuration()); + } + + // 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; + StickyTimeDuration mActiveDuration = StickyTimeDuration(); + StickyTimeDuration mEndTime = StickyTimeDuration(); +}; + +} // namespace mozilla + +#endif // mozilla_TimingParams_h diff --git a/dom/animation/ViewTimeline.cpp b/dom/animation/ViewTimeline.cpp new file mode 100644 index 0000000000..5727c5f08e --- /dev/null +++ b/dom/animation/ViewTimeline.cpp @@ -0,0 +1,166 @@ +/* -*- 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 "ViewTimeline.h" + +#include "mozilla/dom/Animation.h" +#include "mozilla/dom/ElementInlines.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ViewTimeline, ScrollTimeline, mSubject) +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(ViewTimeline, ScrollTimeline) + +/* static */ +already_AddRefed ViewTimeline::MakeNamed( + Document* aDocument, Element* aSubject, PseudoStyleType aPseudoType, + const StyleViewTimeline& aStyleTimeline) { + MOZ_ASSERT(NS_IsMainThread()); + + // 1. Lookup scroller. We have to find the nearest scroller from |aSubject| + // and |aPseudoType|. + auto [element, pseudo] = FindNearestScroller(aSubject, aPseudoType); + auto scroller = Scroller::Nearest(const_cast(element), pseudo); + + // 2. Create timeline. + return MakeAndAddRef(aDocument, scroller, + aStyleTimeline.GetAxis(), aSubject, + aPseudoType, aStyleTimeline.GetInset()); +} + +/* static */ +already_AddRefed ViewTimeline::MakeAnonymous( + Document* aDocument, const NonOwningAnimationTarget& aTarget, + StyleScrollAxis aAxis, const StyleViewTimelineInset& aInset) { + // view() finds the nearest scroll container from the animation target. + auto [element, pseudo] = + FindNearestScroller(aTarget.mElement, aTarget.mPseudoType); + Scroller scroller = Scroller::Nearest(const_cast(element), pseudo); + return MakeAndAddRef(aDocument, scroller, aAxis, + aTarget.mElement, aTarget.mPseudoType, + aInset); +} + +void ViewTimeline::ReplacePropertiesWith(Element* aSubjectElement, + PseudoStyleType aPseudoType, + const StyleViewTimeline& aNew) { + mSubject = aSubjectElement; + mSubjectPseudoType = aPseudoType; + mAxis = aNew.GetAxis(); + // FIXME: Bug 1817073. We assume it is a non-animatable value for now. + mInset = aNew.GetInset(); + + for (auto* anim = mAnimationOrder.getFirst(); anim; + anim = static_cast*>(anim)->getNext()) { + MOZ_ASSERT(anim->GetTimeline() == this); + // Set this so we just PostUpdate() for this animation. + anim->SetTimeline(this); + } +} + +Maybe ViewTimeline::ComputeOffsets( + const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const { + MOZ_ASSERT(mSubject); + MOZ_ASSERT(aScrollFrame); + + const Element* subjectElement = + AnimationUtils::GetElementForRestyle(mSubject, mSubjectPseudoType); + const nsIFrame* subject = subjectElement->GetPrimaryFrame(); + if (!subject) { + // No principal box of the subject, so we cannot compute the offset. This + // may happen when we clear all animation collections during unbinding from + // the tree. + return Nothing(); + } + + // In order to get the distance between the subject and the scrollport + // properly, we use the position based on the domain of the scrolled frame, + // instead of the scrollable frame. + const nsIFrame* scrolledFrame = aScrollFrame->GetScrolledFrame(); + MOZ_ASSERT(scrolledFrame); + const nsRect subjectRect(subject->GetOffsetTo(scrolledFrame), + subject->GetSize()); + + // Use scrollport size (i.e. padding box size - scrollbar size), which is used + // for calculating the view progress visibility range. + // https://drafts.csswg.org/scroll-animations/#view-progress-visibility-range + const nsRect scrollPort = aScrollFrame->GetScrollPortRect(); + + // Adjuct the positions and sizes based on the physical axis. + nscoord subjectPosition = subjectRect.y; + nscoord subjectSize = subjectRect.height; + nscoord scrollPortSize = scrollPort.height; + if (aOrientation == layers::ScrollDirection::eHorizontal) { + // |subjectPosition| should be the position of the start border edge of the + // subject, so for R-L case, we have to use XMost() as the start border + // edge of the subject, and compute its position by using the x-most side of + // the scrolled frame as the origin on the horizontal axis. + subjectPosition = scrolledFrame->GetWritingMode().IsPhysicalRTL() + ? scrolledFrame->GetSize().width - subjectRect.XMost() + : subjectRect.x; + subjectSize = subjectRect.width; + scrollPortSize = scrollPort.width; + } + + // |sideInsets.mEnd| is used to adjust the start offset, and + // |sideInsets.mStart| is used to adjust the end offset. This is because + // |sideInsets.mStart| refers to logical start side [1] of the source box + // (i.e. the box of the scrollport), where as |startOffset| refers to the + // start of the timeline, and similarly for end side/offset. [1] + // https://drafts.csswg.org/css-writing-modes-4/#css-start + const auto sideInsets = ComputeInsets(aScrollFrame, aOrientation); + + // Basically, we are computing the "cover" timeline range name, which + // represents the full range of the view progress timeline. + // https://drafts.csswg.org/scroll-animations-1/#valdef-animation-timeline-range-cover + + // Note: `subjectPosition - scrollPortSize` means the distance between the + // start border edge of the subject and the end edge of the scrollport. + nscoord startOffset = subjectPosition - scrollPortSize + sideInsets.mEnd; + // Note: `subjectPosition + subjectSize` means the position of the end border + // edge of the subject. When it touches the start edge of the scrollport, it + // is 100%. + nscoord endOffset = subjectPosition + subjectSize - sideInsets.mStart; + return Some(ScrollOffsets{startOffset, endOffset}); +} + +ScrollTimeline::ScrollOffsets ViewTimeline::ComputeInsets( + const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const { + // If view-timeline-inset is auto, it indicates to use the value of + // scroll-padding. We use logical dimension to map that start/end offset to + // the corresponding scroll-padding-{inline|block}-{start|end} values. + const WritingMode wm = aScrollFrame->GetScrolledFrame()->GetWritingMode(); + const auto& scrollPadding = + LogicalMargin(wm, aScrollFrame->GetScrollPadding()); + const bool isBlockAxis = + mAxis == StyleScrollAxis::Block || + (mAxis == StyleScrollAxis::Horizontal && wm.IsVertical()) || + (mAxis == StyleScrollAxis::Vertical && !wm.IsVertical()); + + // The percentages of view-timelne-inset is relative to the corresponding + // dimension of the relevant scrollport. + // https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset + const nsRect scrollPort = aScrollFrame->GetScrollPortRect(); + const nscoord percentageBasis = + aOrientation == layers::ScrollDirection::eHorizontal ? scrollPort.width + : scrollPort.height; + + nscoord startInset = + mInset.start.IsAuto() + ? (isBlockAxis ? scrollPadding.BStart(wm) : scrollPadding.IStart(wm)) + : mInset.start.AsLengthPercentage().Resolve(percentageBasis); + nscoord endInset = + mInset.end.IsAuto() + ? (isBlockAxis ? scrollPadding.BEnd(wm) : scrollPadding.IEnd(wm)) + : mInset.end.AsLengthPercentage().Resolve(percentageBasis); + return {startInset, endInset}; +} + +} // namespace mozilla::dom diff --git a/dom/animation/ViewTimeline.h b/dom/animation/ViewTimeline.h new file mode 100644 index 0000000000..a5ede6345d --- /dev/null +++ b/dom/animation/ViewTimeline.h @@ -0,0 +1,86 @@ +/* -*- 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_ViewTimeline_h +#define mozilla_dom_ViewTimeline_h + +#include "mozilla/dom/ScrollTimeline.h" + +namespace mozilla::dom { + +/* + * A view progress timeline is a segment of a scroll progress timeline that are + * scoped to the scroll positions in which any part of the associated element’s + * principal box intersects its nearest ancestor scrollport. So ViewTimeline + * is a special case of ScrollTimeline. + */ +class ViewTimeline final : public ScrollTimeline { + template + friend already_AddRefed mozilla::MakeAndAddRef(Args&&... aArgs); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ViewTimeline, ScrollTimeline) + + ViewTimeline() = delete; + + // Note: |aSubject| is used as the subject which specifies view-timeline-name + // property, and we use this subject to look up its nearest scroll container. + static already_AddRefed MakeNamed( + Document* aDocument, Element* aSubject, PseudoStyleType aPseudoType, + const StyleViewTimeline& aStyleTimeline); + + static already_AddRefed MakeAnonymous( + Document* aDocument, const NonOwningAnimationTarget& aTarget, + StyleScrollAxis aAxis, const StyleViewTimelineInset& aInset); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override { + return nullptr; + } + + bool IsViewTimeline() const override { return true; } + + void ReplacePropertiesWith(Element* aSubjectElement, + PseudoStyleType aPseudoType, + const StyleViewTimeline& aNew); + + private: + ~ViewTimeline() = default; + ViewTimeline(Document* aDocument, const Scroller& aScroller, + StyleScrollAxis aAxis, Element* aSubject, + PseudoStyleType aSubjectPseudoType, + const StyleViewTimelineInset& aInset) + : ScrollTimeline(aDocument, aScroller, aAxis), + mSubject(aSubject), + mSubjectPseudoType(aSubjectPseudoType), + mInset(aInset) {} + + Maybe ComputeOffsets( + const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const override; + + ScrollOffsets ComputeInsets(const nsIScrollableFrame* aScrollFrame, + layers::ScrollDirection aOrientation) const; + + // The subject element. + // 1. For view(), the subject element is the animation target. + // 2. For view-timeline property, the subject element is the element who + // defines this property. + RefPtr mSubject; + PseudoStyleType mSubjectPseudoType; + + // FIXME: Bug 1817073. view-timeline-inset is an animatable property. However, + // the inset from view() is not animatable, so for named view timeline, this + // value depends on the animation style. Therefore, we have to check its style + // value when using it. For now, in order to simplify the implementation, we + // make |mInset| be fixed. + StyleViewTimelineInset mInset; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ViewTimeline_h diff --git a/dom/animation/moz.build b/dom/animation/moz.build new file mode 100644 index 0000000000..94f0f8625f --- /dev/null +++ b/dom/animation/moz.build @@ -0,0 +1,79 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Animation") + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] + +EXPORTS.mozilla.dom += [ + "Animation.h", + "AnimationEffect.h", + "AnimationTimeline.h", + "CSSAnimation.h", + "CSSPseudoElement.h", + "CSSTransition.h", + "DocumentTimeline.h", + "KeyframeEffect.h", + "ScrollTimeline.h", + "ViewTimeline.h", +] + +EXPORTS.mozilla += [ + "AnimationComparator.h", + "AnimationEventDispatcher.h", + "AnimationPerformanceWarning.h", + "AnimationPropertySegment.h", + "AnimationTarget.h", + "AnimationUtils.h", + "ComputedTiming.h", + "EffectCompositor.h", + "EffectSet.h", + "ElementAnimationData.h", + "Keyframe.h", + "KeyframeEffectParams.h", + "KeyframeUtils.h", + "PendingAnimationTracker.h", + "PostRestyleMode.h", + "PseudoElementHashEntry.h", + "ScrollTimelineAnimationTracker.h", + "TimingParams.h", +] + +UNIFIED_SOURCES += [ + "Animation.cpp", + "AnimationEffect.cpp", + "AnimationEventDispatcher.cpp", + "AnimationPerformanceWarning.cpp", + "AnimationTimeline.cpp", + "AnimationUtils.cpp", + "CSSAnimation.cpp", + "CSSPseudoElement.cpp", + "CSSTransition.cpp", + "DocumentTimeline.cpp", + "EffectCompositor.cpp", + "EffectSet.cpp", + "ElementAnimationData.cpp", + "KeyframeEffect.cpp", + "KeyframeUtils.cpp", + "PendingAnimationTracker.cpp", + "ScrollTimeline.cpp", + "ScrollTimelineAnimationTracker.cpp", + "TimingParams.cpp", + "ViewTimeline.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/layout/base", + "/layout/painting", + "/layout/style", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/animation/test/chrome.ini b/dom/animation/test/chrome.ini new file mode 100644 index 0000000000..4001899a50 --- /dev/null +++ b/dom/animation/test/chrome.ini @@ -0,0 +1,29 @@ +[DEFAULT] +prefs = + dom.animations-api.autoremove.enabled=true + dom.animations-api.compositing.enabled=true + dom.animations.mainthread-synchronization-with-geometric-animations=true + gfx.omta.background-color=true + layout.css.individual-transform.enabled=true + layout.css.motion-path.enabled=true +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_keyframe_effect_xrays.html] +[chrome/test_animation_observers_async.html] +[chrome/test_animation_observers_sync.html] +[chrome/test_animation_performance_warning.html] +[chrome/test_animation_properties.html] +[chrome/test_animation_properties_display.html] +[chrome/test_cssanimation_missing_keyframes.html] +[chrome/test_generated_content_getAnimations.html] +[chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html] +[chrome/test_running_on_compositor.html] +[chrome/test_simulate_compute_values_failure.html] +skip-if = !debug 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 0000000000..2fa15b1764 --- /dev/null +++ b/dom/animation/test/chrome/file_animate_xrays.html @@ -0,0 +1,18 @@ + + + + + + +
+ + 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 0000000000..64df6db720 --- /dev/null +++ b/dom/animation/test/chrome/test_animate_xrays.html @@ -0,0 +1,40 @@ + + + + + + + + +Mozilla Bug 1414674 +
+ + + diff --git a/dom/animation/test/chrome/test_animation_observers_async.html b/dom/animation/test/chrome/test_animation_observers_async.html new file mode 100644 index 0000000000..7505baca53 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers_async.html @@ -0,0 +1,665 @@ + + + +Test chrome-only MutationObserver animation notifications (async tests) + + + + + +
+ +
+ diff --git a/dom/animation/test/chrome/test_animation_observers_sync.html b/dom/animation/test/chrome/test_animation_observers_sync.html new file mode 100644 index 0000000000..7a890fd2f4 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_observers_sync.html @@ -0,0 +1,1587 @@ + + + +Test chrome-only MutationObserver animation notifications (sync tests) + + + + + +
+ + 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 0000000000..ca0572cfaa --- /dev/null +++ b/dom/animation/test/chrome/test_animation_performance_warning.html @@ -0,0 +1,1692 @@ + + + +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 0000000000..b9d6dcbef1 --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties.html @@ -0,0 +1,838 @@ + + + +Bug 1254419 - Test the values returned by + KeyframeEffect.getProperties() + + + + + +Mozilla Bug 1254419 +
+ + + diff --git a/dom/animation/test/chrome/test_animation_properties_display.html b/dom/animation/test/chrome/test_animation_properties_display.html new file mode 100644 index 0000000000..184fc9d3bc --- /dev/null +++ b/dom/animation/test/chrome/test_animation_properties_display.html @@ -0,0 +1,43 @@ + + + +Bug 1536688 - Test that 'display' is not included in + KeyframeEffect.getProperties() when using shorthand 'all' + + + + + +Mozilla Bug 1536688 +
+ + diff --git a/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html new file mode 100644 index 0000000000..1d19386712 --- /dev/null +++ b/dom/animation/test/chrome/test_cssanimation_missing_keyframes.html @@ -0,0 +1,73 @@ + + + +Bug 1339332 - Test for missing keyframes in CSS Animation + + + + + +Mozilla Bug 1339332 +
+ + + 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 0000000000..08de27f57d --- /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_keyframe_effect_xrays.html b/dom/animation/test/chrome/test_keyframe_effect_xrays.html new file mode 100644 index 0000000000..ca3e712ac5 --- /dev/null +++ b/dom/animation/test/chrome/test_keyframe_effect_xrays.html @@ -0,0 +1,45 @@ + + + + + + + + +Mozilla Bug 1414674 +
+ + + diff --git a/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html new file mode 100644 index 0000000000..f8efaa6baf --- /dev/null +++ b/dom/animation/test/chrome/test_mutation_observer_for_element_removal_in_shadow_tree.html @@ -0,0 +1,45 @@ + + + + + +
+ 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 0000000000..532c11258f --- /dev/null +++ b/dom/animation/test/chrome/test_running_on_compositor.html @@ -0,0 +1,1516 @@ + + + +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/chrome/test_simulate_compute_values_failure.html b/dom/animation/test/chrome/test_simulate_compute_values_failure.html new file mode 100644 index 0000000000..2ee041c9f8 --- /dev/null +++ b/dom/animation/test/chrome/test_simulate_compute_values_failure.html @@ -0,0 +1,371 @@ + + + +Bug 1276688 - Test for properties that parse correctly but which we fail + to convert to computed values + + + + + +Mozilla Bug 1276688 +
+ + diff --git a/dom/animation/test/crashtests/1134538.html b/dom/animation/test/crashtests/1134538.html new file mode 100644 index 0000000000..136d63deee --- /dev/null +++ b/dom/animation/test/crashtests/1134538.html @@ -0,0 +1,8 @@ + +
+ diff --git a/dom/animation/test/crashtests/1216842-1.html b/dom/animation/test/crashtests/1216842-1.html new file mode 100644 index 0000000000..6ac40b2fb8 --- /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 0000000000..7bae8a3116 --- /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 0000000000..1bc2179a86 --- /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 0000000000..eba13c396a --- /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 0000000000..73b6f22c4b --- /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 0000000000..aaf80eeec3 --- /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 0000000000..aa10ff3ab8 --- /dev/null +++ b/dom/animation/test/crashtests/1239889-1.html @@ -0,0 +1,16 @@ + + + + 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 0000000000..13b2e2d7e7 --- /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 0000000000..e0b0495388 --- /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 0000000000..da0e8605bd --- /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 0000000000..2565aa6eb8 --- /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 0000000000..71b6c24148 --- /dev/null +++ b/dom/animation/test/crashtests/1277272-1.html @@ -0,0 +1,25 @@ + + + + + + + + +
+ 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 0000000000..3da4e4deb2 --- /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 0000000000..bd4bfe8a61 --- /dev/null +++ b/dom/animation/test/mochitest.ini @@ -0,0 +1,81 @@ +[DEFAULT] +prefs = + dom.animations-api.compositing.enabled=true + dom.animations-api.core.enabled=true + dom.animations-api.getAnimations.enabled=true + dom.animations-api.implicit-keyframes.enabled=true + dom.animations-api.timelines.enabled=true + gfx.omta.background-color=true + layout.css.motion-path.enabled=true + layout.css.individual-transform.enabled=true + layout.css.scroll-driven-animations.enabled=true + gfx.font_loader.delay=0 +# 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 + mozilla/xhr_doc.html + mozilla/file_deferred_start.html + mozilla/file_disable_animations_api_autoremove.html + mozilla/file_disable_animations_api_compositing.html + mozilla/file_disable_animations_api_get_animations.html + mozilla/file_disable_animations_api_implicit_keyframes.html + mozilla/file_disable_animations_api_timelines.html + mozilla/file_discrete_animations.html + mozilla/file_transition_finish_on_compositor.html + ../../../layout/style/test/property_database.js + testcommon.js + !/dom/events/test/event_leak_utils.js + +[document-timeline/test_document-timeline.html] +[document-timeline/test_request_animation_frame.html] +[mozilla/test_cascade.html] +[mozilla/test_cubic_bezier_limits.html] +[mozilla/test_deferred_start.html] +skip-if = (os == 'win' && bits == 64) # Bug 1363957 +[mozilla/test_disable_animations_api_autoremove.html] +[mozilla/test_disable_animations_api_compositing.html] +[mozilla/test_disable_animations_api_get_animations.html] +[mozilla/test_disable_animations_api_implicit_keyframes.html] +[mozilla/test_disable_animations_api_timelines.html] +[mozilla/test_disabled_properties.html] +[mozilla/test_discrete_animations.html] +[mozilla/test_distance_of_basic_shape.html] +[mozilla/test_distance_of_filter.html] +[mozilla/test_distance_of_path_function.html] +[mozilla/test_distance_of_transform.html] +[mozilla/test_document_timeline_origin_time_range.html] +[mozilla/test_get_animations_on_scroll_animations.html] +[mozilla/test_hide_and_show.html] +[mozilla/test_mainthread_synchronization_pref.html] +[mozilla/test_moz_prefixed_properties.html] +[mozilla/test_pending_animation_tracker.html] +[mozilla/test_restyles.html] +support-files = + mozilla/file_restyles.html + mozilla/empty.html +skip-if = + (os == 'android' && debug) #Bug 1784931 + (os == 'linux' && tsan) #Bug 1784931 + http3 +[mozilla/test_restyling_xhr_doc.html] +[mozilla/test_set_easing.html] +[mozilla/test_style_after_finished_on_compositor.html] +[mozilla/test_transform_limits.html] +[mozilla/test_transition_finish_on_compositor.html] +skip-if = toolkit == 'android' +[mozilla/test_underlying_discrete_value.html] +[mozilla/test_unstyled.html] +[mozilla/test_event_listener_leaks.html] +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[style/test_animation-seeking-with-current-time.html] +[style/test_animation-seeking-with-start-time.html] +[style/test_animation-setting-effect.html] +[style/test_composite.html] +skip-if = xorigin +[style/test_interpolation-from-interpolatematrix-to-none.html] +[style/test_missing-keyframe.html] +[style/test_missing-keyframe-on-compositor.html] +skip-if = + fission && xorigin # Bug 1716403 - New fission platform triage +[style/test_transform-non-normalizable-rotate3d.html] diff --git a/dom/animation/test/mozilla/empty.html b/dom/animation/test/mozilla/empty.html new file mode 100644 index 0000000000..739422cbfa --- /dev/null +++ b/dom/animation/test/mozilla/empty.html @@ -0,0 +1,2 @@ + + 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 0000000000..863fc80fec --- /dev/null +++ b/dom/animation/test/mozilla/file_deferred_start.html @@ -0,0 +1,179 @@ + + + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html new file mode 100644 index 0000000000..79cb508467 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html @@ -0,0 +1,69 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_compositing.html b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html new file mode 100644 index 0000000000..6d9ba35dc0 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_compositing.html @@ -0,0 +1,137 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html new file mode 100644 index 0000000000..3d484444a7 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_get_animations.html @@ -0,0 +1,20 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html new file mode 100644 index 0000000000..9cd05e7d40 --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_implicit_keyframes.html @@ -0,0 +1,48 @@ + + + + + + diff --git a/dom/animation/test/mozilla/file_disable_animations_api_timelines.html b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html new file mode 100644 index 0000000000..39fedb299a --- /dev/null +++ b/dom/animation/test/mozilla/file_disable_animations_api_timelines.html @@ -0,0 +1,30 @@ + + + + + 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 0000000000..e0de609bc5 --- /dev/null +++ b/dom/animation/test/mozilla/file_discrete_animations.html @@ -0,0 +1,122 @@ + + + +Test Mozilla-specific discrete animatable properties + + + + + diff --git a/dom/animation/test/mozilla/file_restyles.html b/dom/animation/test/mozilla/file_restyles.html new file mode 100644 index 0000000000..8d72cb6c44 --- /dev/null +++ b/dom/animation/test/mozilla/file_restyles.html @@ -0,0 +1,2275 @@ + + + + +Tests restyles caused by animations + + + + + + + + + + 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 0000000000..4912d05dd1 --- /dev/null +++ b/dom/animation/test/mozilla/file_transition_finish_on_compositor.html @@ -0,0 +1,67 @@ + + + + + + + + diff --git a/dom/animation/test/mozilla/test_cascade.html b/dom/animation/test/mozilla/test_cascade.html new file mode 100644 index 0000000000..4bdb07530e --- /dev/null +++ b/dom/animation/test/mozilla/test_cascade.html @@ -0,0 +1,37 @@ + + + + + + + +
+ + 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 0000000000..bdbc78654f --- /dev/null +++ b/dom/animation/test/mozilla/test_cubic_bezier_limits.html @@ -0,0 +1,168 @@ + + + + + + + +
+ + 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 0000000000..7d0a15b1f7 --- /dev/null +++ b/dom/animation/test/mozilla/test_deferred_start.html @@ -0,0 +1,21 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html new file mode 100644 index 0000000000..56e6362273 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html @@ -0,0 +1,15 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_compositing.html b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html new file mode 100644 index 0000000000..94216ea62d --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_compositing.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html new file mode 100644 index 0000000000..a7253439b7 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_get_animations.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html new file mode 100644 index 0000000000..aaebf1f00a --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_implicit_keyframes.html @@ -0,0 +1,14 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_disable_animations_api_timelines.html b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html new file mode 100644 index 0000000000..a20adf4ea2 --- /dev/null +++ b/dom/animation/test/mozilla/test_disable_animations_api_timelines.html @@ -0,0 +1,16 @@ + + + + +
+ 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 0000000000..2244143ceb --- /dev/null +++ b/dom/animation/test/mozilla/test_disabled_properties.html @@ -0,0 +1,73 @@ + + + + + + +
+ + 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 0000000000..d4826a74bd --- /dev/null +++ b/dom/animation/test/mozilla/test_discrete_animations.html @@ -0,0 +1,16 @@ + + + + +
+ diff --git a/dom/animation/test/mozilla/test_distance_of_basic_shape.html b/dom/animation/test/mozilla/test_distance_of_basic_shape.html new file mode 100644 index 0000000000..65e403bf06 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_basic_shape.html @@ -0,0 +1,91 @@ + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_distance_of_filter.html b/dom/animation/test/mozilla/test_distance_of_filter.html new file mode 100644 index 0000000000..33f772d983 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_filter.html @@ -0,0 +1,248 @@ + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_distance_of_path_function.html b/dom/animation/test/mozilla/test_distance_of_path_function.html new file mode 100644 index 0000000000..af6592c892 --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_path_function.html @@ -0,0 +1,140 @@ + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_distance_of_transform.html b/dom/animation/test/mozilla/test_distance_of_transform.html new file mode 100644 index 0000000000..96ff1eb66d --- /dev/null +++ b/dom/animation/test/mozilla/test_distance_of_transform.html @@ -0,0 +1,404 @@ + + + + + +
+ + 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 0000000000..b2aeef8a77 --- /dev/null +++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html @@ -0,0 +1,32 @@ + + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_event_listener_leaks.html b/dom/animation/test/mozilla/test_event_listener_leaks.html new file mode 100644 index 0000000000..bcfadaf9e9 --- /dev/null +++ b/dom/animation/test/mozilla/test_event_listener_leaks.html @@ -0,0 +1,43 @@ + + + + + Bug 1450271 - Test Animation event listener leak conditions + + + + + + + + + diff --git a/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html new file mode 100644 index 0000000000..7d20e5b70b --- /dev/null +++ b/dom/animation/test/mozilla/test_get_animations_on_scroll_animations.html @@ -0,0 +1,74 @@ + + + +Test getAnimations() which doesn't return scroll animations + + + + + + +
+ + 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 0000000000..f36543bb1e --- /dev/null +++ b/dom/animation/test/mozilla/test_hide_and_show.html @@ -0,0 +1,198 @@ + + + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html new file mode 100644 index 0000000000..3653fd9536 --- /dev/null +++ b/dom/animation/test/mozilla/test_mainthread_synchronization_pref.html @@ -0,0 +1,42 @@ + + + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_moz_prefixed_properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html new file mode 100644 index 0000000000..f65d05134d --- /dev/null +++ b/dom/animation/test/mozilla/test_moz_prefixed_properties.html @@ -0,0 +1,93 @@ + + + + Test animations of all properties that have -moz prefix + + + + + + +
+ + + + diff --git a/dom/animation/test/mozilla/test_pending_animation_tracker.html b/dom/animation/test/mozilla/test_pending_animation_tracker.html new file mode 100644 index 0000000000..022efa7bcf --- /dev/null +++ b/dom/animation/test/mozilla/test_pending_animation_tracker.html @@ -0,0 +1,134 @@ + + + +Test animations in PendingAnimationTracker + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html new file mode 100644 index 0000000000..bc1ab70c74 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyles.html @@ -0,0 +1,22 @@ + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html new file mode 100644 index 0000000000..67b6ac8845 --- /dev/null +++ b/dom/animation/test/mozilla/test_restyling_xhr_doc.html @@ -0,0 +1,106 @@ + + + + + +
+ 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 0000000000..55c77f0e8f --- /dev/null +++ b/dom/animation/test/mozilla/test_set_easing.html @@ -0,0 +1,36 @@ + + + +Test setting easing in sandbox + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html new file mode 100644 index 0000000000..bccae9e0d5 --- /dev/null +++ b/dom/animation/test/mozilla/test_style_after_finished_on_compositor.html @@ -0,0 +1,138 @@ + + + +Test for styles after finished on the compositor + + + + + + +
+ + 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 0000000000..92d1b7e1ec --- /dev/null +++ b/dom/animation/test/mozilla/test_transform_limits.html @@ -0,0 +1,56 @@ + + + + + + +
+ + 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 0000000000..46a154b9af --- /dev/null +++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html @@ -0,0 +1,22 @@ + + + + +
+ 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 0000000000..3961305df3 --- /dev/null +++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html @@ -0,0 +1,188 @@ + + + + + + +
+ + diff --git a/dom/animation/test/mozilla/test_unstyled.html b/dom/animation/test/mozilla/test_unstyled.html new file mode 100644 index 0000000000..4724979c11 --- /dev/null +++ b/dom/animation/test/mozilla/test_unstyled.html @@ -0,0 +1,54 @@ + + + + + + + +
+ + diff --git a/dom/animation/test/mozilla/xhr_doc.html b/dom/animation/test/mozilla/xhr_doc.html new file mode 100644 index 0000000000..b9fa57e3f5 --- /dev/null +++ b/dom/animation/test/mozilla/xhr_doc.html @@ -0,0 +1,2 @@ + +
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 0000000000..265de8f0f5 --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-current-time.html @@ -0,0 +1,123 @@ + + + + + Tests for seeking using Animation.currentTime + + + + + + +
+ + + 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 0000000000..e56db5f23d --- /dev/null +++ b/dom/animation/test/style/test_animation-seeking-with-start-time.html @@ -0,0 +1,123 @@ + + + + + Tests for seeking using Animation.startTime + + + + + + +
+ + + 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 0000000000..8712072a51 --- /dev/null +++ b/dom/animation/test/style/test_animation-setting-effect.html @@ -0,0 +1,127 @@ + + + + + Tests for setting effects by using Animation.effect + + + + + +
+ + + diff --git a/dom/animation/test/style/test_composite.html b/dom/animation/test/style/test_composite.html new file mode 100644 index 0000000000..1383b1b1e6 --- /dev/null +++ b/dom/animation/test/style/test_composite.html @@ -0,0 +1,142 @@ + + + + + + + + +
+ + diff --git a/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html new file mode 100644 index 0000000000..1da95392eb --- /dev/null +++ b/dom/animation/test/style/test_interpolation-from-interpolatematrix-to-none.html @@ -0,0 +1,43 @@ + + + + + +
+ + diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html new file mode 100644 index 0000000000..8b92a89168 --- /dev/null +++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.html @@ -0,0 +1,577 @@ + + + + + + + + +
+ + diff --git a/dom/animation/test/style/test_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe.html new file mode 100644 index 0000000000..4047e62408 --- /dev/null +++ b/dom/animation/test/style/test_missing-keyframe.html @@ -0,0 +1,110 @@ + + + + + + +
+ + diff --git a/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html new file mode 100644 index 0000000000..ad2584ac40 --- /dev/null +++ b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html @@ -0,0 +1,28 @@ + + + + + +
+ + diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js new file mode 100644 index 0000000000..e1645d4a19 --- /dev/null +++ b/dom/animation/test/testcommon.js @@ -0,0 +1,527 @@ +/* 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://drafts.csswg.org/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 * 2, description); +} + +/* + * Compare a time value based on its precision requirements with a fixed value. + */ +function assert_time_equals_literal(actual, expected, description) { + assert_approx_equals(actual, expected, TIME_PRECISION, description); +} + +/* + * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'. + * This function allows error, 0.01, because on Android when we are scaling down + * the document, it results in some errors. + */ +function assert_matrix_equals(actual, expected, description) { + var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/; + assert_regexp_match(actual, matrixRegExp, "Actual value should be a matrix"); + assert_regexp_match( + expected, + matrixRegExp, + "Expected value should be a matrix" + ); + + var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number); + var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number); + + assert_equals( + actualMatrixArray.length, + expectedMatrixArray.length, + "Array lengths should be equal (got '" + + expected + + "' and '" + + actual + + "'): " + + description + ); + for (var i = 0; i < actualMatrixArray.length; i++) { + assert_approx_equals( + actualMatrixArray[i], + expectedMatrixArray[i], + 0.01, + "Matrix array should be equal (got '" + + expected + + "' and '" + + actual + + "'): " + + description + ); + } +} + +/** + * Compare given values which are same format of + * KeyframeEffectReadonly::GetProperties. + */ +function assert_properties_equal(actual, expected) { + assert_equals(actual.length, expected.length); + + const compareProperties = (a, b) => + a.property == b.property ? 0 : a.property < b.property ? -1 : 1; + + const sortedActual = actual.sort(compareProperties); + const sortedExpected = expected.sort(compareProperties); + + const serializeValues = values => + values + .map( + value => + "{ " + + ["offset", "value", "easing", "composite"] + .map(member => `${member}: ${value[member]}`) + .join(", ") + + " }" + ) + .join(", "); + + for (let i = 0; i < sortedActual.length; i++) { + assert_equals( + sortedActual[i].property, + sortedExpected[i].property, + "CSS property name should match" + ); + assert_equals( + serializeValues(sortedActual[i].values), + serializeValues(sortedExpected[i].values), + `Values arrays do not match for ` + `${sortedActual[i].property} property` + ); + } +} + +/** + * Construct a object which is same to a value of + * KeyframeEffectReadonly::GetProperties(). + * The method returns undefined as a value in case of missing keyframe. + * Therefor, we can use undefined for |value| and |easing| parameter. + * @param offset - keyframe offset. e.g. 0.1 + * @param value - any keyframe value. e.g. undefined '1px', 'center', 0.5 + * @param composite - 'replace', 'add', 'accumulate' + * @param easing - e.g. undefined, 'linear', 'ease' and so on + * @return Object - + * e.g. { offset: 0.1, value: '1px', composite: 'replace', easing: 'ease'} + */ +function valueFormat(offset, value, composite, easing) { + return { offset, value, easing, composite }; +} + +/** + * 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.remove(); + } + }); + } + 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(); + }); + } +} + +/** + * Takes a CSS property (e.g. margin-left) and returns the equivalent IDL + * name (e.g. marginLeft). + */ +function propertyToIDL(property) { + var prefixMatch = property.match(/^-(\w+)-/); + if (prefixMatch) { + var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1]; + property = prefix + property.substring(prefixMatch[0].length - 1); + } + // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + return property.replace(/-([a-z])/gi, function (str, group) { + return group.toUpperCase(); + }); +} + +/** + * Promise wrapper for requestAnimationFrame. + */ +function waitForFrame() { + return new Promise(function (resolve, reject) { + window.requestAnimationFrame(resolve); + }); +} + +/** + * Waits for a requestAnimationFrame callback in the next refresh driver tick. + * Note that the 'dom.animations-api.core.enabled' and + * 'dom.animations-api.timelines.enabled' prefs should be true to use this + * function. + */ +function waitForNextFrame(aWindow = window) { + const timeAtStart = aWindow.document.timeline.currentTime; + return new Promise(resolve => { + aWindow.requestAnimationFrame(() => { + if (timeAtStart === aWindow.document.timeline.currentTime) { + aWindow.requestAnimationFrame(resolve); + } else { + resolve(); + } + }); + }); +} + +/** + * Returns a Promise that is resolved after the given number of consecutive + * animation frames have occured (using requestAnimationFrame callbacks). + * + * @param aFrameCount The number of animation frames. + * @param aOnFrame An optional function to be processed in each animation frame. + * @param aWindow An optional window object to be used for requestAnimationFrame. + */ +function waitForAnimationFrames(aFrameCount, aOnFrame, aWindow = window) { + const timeAtStart = aWindow.document.timeline.currentTime; + return new Promise(function (resolve, reject) { + function handleFrame() { + if (aOnFrame && typeof aOnFrame === "function") { + aOnFrame(); + } + if ( + timeAtStart != aWindow.document.timeline.currentTime && + --aFrameCount <= 0 + ) { + resolve(); + } else { + aWindow.requestAnimationFrame(handleFrame); // wait another frame + } + } + aWindow.requestAnimationFrame(handleFrame); + }); +} + +/** + * Promise wrapper for requestIdleCallback. + */ +function waitForIdle() { + return new Promise(resolve => { + requestIdleCallback(resolve); + }); +} + +/** + * 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 = 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", + "assert_regexp_match", + "promise_test", + "test", + ]) { + if (opener[funcName]) { + window[funcName] = opener[funcName].bind(opener); + } + } + + window.EventWatcher = opener.EventWatcher; + + function done() { + opener.add_completion_callback(function () { + self.close(); + }); + opener.done(); + } +} + +/* + * Returns a promise that is resolved when the document has finished loading. + */ +function waitForDocumentLoad() { + return new Promise(function (resolve, reject) { + if (document.readyState === "complete") { + resolve(); + } else { + window.addEventListener("load", resolve); + } + }); +} + +/* + * Enters test refresh mode, and restores the mode when |t| finishes. + */ +function useTestRefreshMode(t) { + function ensureNoSuppressedPaints() { + return new Promise(resolve => { + function checkSuppressedPaints() { + if (!SpecialPowers.DOMWindowUtils.paintingSuppressed) { + resolve(); + } else { + window.requestAnimationFrame(checkSuppressedPaints); + } + } + checkSuppressedPaints(); + }); + } + + return ensureNoSuppressedPaints().then(() => { + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); + t.add_cleanup(() => { + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + }); + }); +} + +/** + * Returns true if off-main-thread animations. + */ +function isOMTAEnabled() { + const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations"; + return ( + SpecialPowers.DOMWindowUtils.layerManagerRemote && + SpecialPowers.getBoolPref(OMTAPrefKey) + ); +} + +/** + * Append an SVG element to the target element. + * + * @param target The element which want to append. + * @param attrs A array object with attribute name and values to set on + * the SVG element. + * @return An SVG outer element. + */ +function addSVGElement(target, tag, attrs) { + if (!target) { + return null; + } + var element = document.createElementNS("http://www.w3.org/2000/svg", tag); + if (attrs) { + for (var attrName in attrs) { + element.setAttributeNS(null, attrName, attrs[attrName]); + } + } + target.appendChild(element); + return element; +} + +/* + * Get Animation distance between two specified values for a specific property. + * + * @param target The target element. + * @param prop The CSS property. + * @param v1 The first property value. + * @param v2 The Second property value. + * + * @return The distance between |v1| and |v2| for |prop| on |target|. + */ +function getDistance(target, prop, v1, v2) { + if (!target) { + return 0.0; + } + return SpecialPowers.DOMWindowUtils.computeAnimationDistance( + target, + prop, + v1, + v2 + ); +} + +/* + * A promise wrapper for waiting MozAfterPaint. + */ +function waitForPaints() { + // FIXME: Bug 1415065. Instead waiting for two requestAnimationFrames, we + // should wait for MozAfterPaint once after MozAfterPaint is fired properly + // (bug 1341294). + return waitForAnimationFrames(2); +} + +// Returns true if |aAnimation| begins at the current timeline time. We +// sometimes need to detect this case because if we started an animation +// asynchronously (e.g. using play()) and then ended up running the next frame +// at precisely the time the animation started (due to aligning with vsync +// refresh rate) then we won't end up restyling in that frame. +function animationStartsRightNow(aAnimation) { + return ( + aAnimation.startTime === aAnimation.timeline.currentTime && + aAnimation.currentTime === 0 + ); +} + +// Waits for a given animation being ready to restyle. +async function waitForAnimationReadyToRestyle(aAnimation) { + await aAnimation.ready; + // If |aAnimation| begins at the current timeline time, we will not process + // restyling in the initial frame because of aligning with the refresh driver, + // the animation frame in which the ready promise is resolved happens to + // coincide perfectly with the start time of the animation. In this case no + // restyling is needed in the frame so we have to wait one more frame. + if (animationStartsRightNow(aAnimation)) { + await waitForNextFrame(aAnimation.ownerGlobal); + } +} + +function getDocShellForObservingRestylesForWindow(aWindow) { + const docShell = SpecialPowers.wrap(aWindow).docShell; + + docShell.recordProfileTimelineMarkers = true; + docShell.popProfileTimelineMarkers(); + + return docShell; +} + +// Returns the animation restyle markers observed during |frameCount| refresh +// driver ticks in this `window`. This function is typically used to count the +// number of restyles that take place as part of the style update that happens +// on each refresh driver tick, as opposed to synchronous restyles triggered by +// script. +// +// For the latter observeAnimSyncStyling (below) should be used. +function observeStyling(frameCount, onFrame) { + return observeStylingInTargetWindow(window, frameCount, onFrame); +} + +// As with observeStyling but applied to target window |aWindow|. +function observeStylingInTargetWindow(aWindow, aFrameCount, aOnFrame) { + const docShell = getDocShellForObservingRestylesForWindow(aWindow); + + return new Promise(resolve => { + return waitForAnimationFrames(aFrameCount, aOnFrame, aWindow).then(() => { + const markers = docShell.popProfileTimelineMarkers(); + docShell.recordProfileTimelineMarkers = false; + const stylingMarkers = Array.prototype.filter.call( + markers, + (marker, index) => { + return marker.name == "Styles" && marker.isAnimationOnly; + } + ); + resolve(stylingMarkers); + }); + }); +} -- cgit v1.2.3