diff options
Diffstat (limited to '')
-rw-r--r-- | dom/animation/AnimationEffect.cpp | 370 |
1 files changed, 370 insertions, 0 deletions
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<nsAutoAnimationMutationBatch> 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<TimeDuration>& 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<uint64_t>(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<TimeDuration> 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<double>() + : static_cast<double>(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<TimeDuration> AnimationEffect::GetLocalTime() const { + // Since the *animation* start time is currently always zero, the local + // time is equal to the parent time. + Nullable<TimeDuration> result; + if (mAnimation) { + result = mAnimation->GetCurrentTimeAsDuration(); + } + return result; +} + +} // namespace mozilla::dom |