summaryrefslogtreecommitdiffstats
path: root/dom/animation/CSSAnimation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/animation/CSSAnimation.cpp')
-rw-r--r--dom/animation/CSSAnimation.cpp376
1 files changed, 376 insertions, 0 deletions
diff --git a/dom/animation/CSSAnimation.cpp b/dom/animation/CSSAnimation.cpp
new file mode 100644
index 0000000000..89bc9d71f6
--- /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<JSObject*> 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<double>& 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(TickState& aState) {
+ Animation::Tick(aState);
+ 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<CSSAnimation*>(this)->CachedChildIndexRef(),
+ aOther.mOwningElement,
+ const_cast<CSSAnimation*>(&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<ComputedTiming::AnimationPhase>(*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<AnimationEventInfo, 2> 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<JSObject*> 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