/* 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/KeyframeUtils.h" #include "mozilla/TimeStamp.h" #include "mozilla/dom/CSSAnimationBinding.h" #include "mozilla/dom/KeyframeEffectBinding.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); PropertiesWillSetFromJS(CSSAnimationProperties::Effect); } void CSSAnimation::SetStartTime(const Nullable& aStartTime, ErrorResult& aRv) { // 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::SetStartTime(aStartTime, aRv); if (aRv.Failed()) { return; } bool isPaused = PlayState() == AnimationPlayState::Paused; if (wasPaused != isPaused) { PropertiesWillSetFromJS(CSSAnimationProperties::PlayState); } } mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) { FlushUnanimatedStyle(); return Animation::GetReady(aRv); } void CSSAnimation::Reverse(ErrorResult& aRv) { // As with CSSAnimation::SetStartTime, 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) { PropertiesWillSetFromJS(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; } PropertiesWillSetFromJS(CSSAnimationProperties::PlayState); } void CSSAnimation::PauseFromJS(ErrorResult& aRv) { Animation::PauseFromJS(aRv); if (aRv.Failed()) { return; } PropertiesWillSetFromJS(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(); } int32_t CSSAnimation::CompareCompositeOrder( const CSSAnimation& aOther, nsContentUtils::NodeIndexCache& aCache) 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 0; } // 1. Sort by document order if (!mOwningElement.Equals(aOther.mOwningElement)) { return mOwningElement.Compare(aOther.mOwningElement, aCache); } // 2. (Same element and pseudo): Sort by position in animation-name MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex); return mAnimationIndex < aOther.mAnimationIndex ? -1 : 1; } 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.ShouldFireEvents()) { 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, mAnimationIndex, 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->PropertiesWillSetFromJS(updatedProperties); } } void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext, JS::Handle aKeyframes, ErrorResult& aRv) { nsTArray keyframes = KeyframeUtils::GetKeyframesFromObject( aContext, mDocument, aKeyframes, "KeyframeEffect.setKeyframes", aRv); if (aRv.Failed()) { return; } // SetKeyframes below will trigger the keyframe generation (for missing 0% and // 100%), so we set this flag to avoid generating the keyframes if they come // from JS. // Note that this is different from CSSAnimationProperties::Keyframes below // because we have to avoid generating keyframes even if we don't have // animation, i.e. this is a keyframe-effect-specific flag for keyframes // generation. mIgnoreKeyframesGeneration = true; // This is from the JS API, so we use the associated animation's timeline and // range if any. RefPtr style = GetTargetComputedStyle(Flush::None); KeyframeEffect::SetKeyframes( std::move(keyframes), style, mAnimation ? mAnimation->GetTimeline() : nullptr, mAnimation ? &mAnimation->GetTimelineRange() : nullptr); if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Keyframes); } } void CSSAnimationKeyframeEffect::SetComposite( const CompositeOperation& aComposite) { KeyframeEffect::SetComposite(aComposite); if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Composition); } } class ComputedOffsetComparator { public: static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) { // Keep the specified order if any of the keyframes have no computed offset. return !std::isnan(aLhs.mComputedOffset) && !std::isnan(aRhs.mComputedOffset) && aLhs.mComputedOffset < aRhs.mComputedOffset; } }; enum class TargetKeyframe : uint8_t { Initial, Final, }; static Keyframe* GetOrCreateInitialOrFinalComputedKeyframe( nsTArray& aPercentageKeyframes, nsTArray& aRangeKeyframes, const TargetKeyframe aTargetKeyframe, const StyleComputedTimingFunction& aDefaultTimingFunction, const CompositeOperationOrAuto aDefaultComposite) { const double targetOffset = aTargetKeyframe == TargetKeyframe::Initial ? 0.0 : 1.0; auto IsKeyframeMatched = [&](const Keyframe& aKeyframe) { return aKeyframe.mComputedOffset == targetOffset && (aKeyframe.mComposite == CompositeOperationOrAuto::Auto || aKeyframe.mComposite == aDefaultComposite) && (aKeyframe.mTimingFunction ? *aKeyframe.mTimingFunction == aDefaultTimingFunction : aDefaultTimingFunction.IsLinearKeyword()); }; // 1. Search the offset in |aRangeKeyframes| first, in the reverse order. We // search |aRangeKeyframes| first because we perfer the later keyframe in // the used order, which breaks the tie with the computed order. And range // offsets are always put after percentage offsets in the computed order. // // Note: We intentionally search in the reverse order because we prefer the // later keyframe if mulitple keyframes have 0% or 100% (with the matched // easing and composite). It should happen only in keyframes with // offsets because we don't group them when converting // them from Servo |KeyframesStep| into Gecko |Keyframe|, in // Servo_StyleSet_GetKeyframesForName(), and intentionally keep those // keyframes unsorted (i.e. the specified order). // e.g. // cover 0% {...} // entry 0% {...} // Both of them have computed offset 0%. Since we keep |mKeyframes| with // offsets unsorted, so we can simply choose the last // one when breaking a tie. // // This is spec'ed especially for 100%. For 0%, the spec doesnt' mention that // so we use the same rule for now. // https://github.com/w3c/csswg-drafts/issues/14281 for (auto& keyframe : Reversed(aRangeKeyframes)) { MOZ_ASSERT(keyframe.mOffset); // Skip the keyframe with unresolved computed offset. if (std::isnan(keyframe.mComputedOffset)) { continue; } if (IsKeyframeMatched(keyframe)) { return &keyframe; } // |aRangeKeyframes| is sorted by |keyframe.mComputedOffset|, so we can // break early. if (keyframe.mComputedOffset < targetOffset) { break; } } // 2. Search the offset in |aPercentageKeyframes|. We group the keyframes with // offsets already so we shouldn't have mulitple matches. // // Note: |insertPosition| is used to find the last 0% or 100% keyframe. We // would like to insert the missing keyframe after it. // e.g. // 0% { animation-timing-function: linear; } // 50% { width: 100px; } // 100% { animation-composition: add; } // We have to generate the keyframes like this: // [0] { offset: 0.0, easing: 'linear', composite: 'auto' } // [1] { offset: 0.0, easing: 'ease', composite: 'replace' } // generated 0% // [2] { offset: 0.5, easing: 'ease', composite: 'auto' } // [3] { offset: 1.0, easing: 'ease', composite: 'add' } // [4] { offset: 1.0, easing: 'ease', composite: 'replace' } // generated 100% // So |insertPosition| for 0% is 1, and for 100% is 4. size_t insertPosition = 0; for (auto& keyframe : aPercentageKeyframes) { MOZ_ASSERT(keyframe.mOffset); MOZ_ASSERT(!std::isnan(keyframe.mComputedOffset)); if (IsKeyframeMatched(keyframe)) { return &keyframe; } if (keyframe.mComputedOffset > targetOffset) { break; } ++insertPosition; } // Now this is a missing 0%/100% keyframe. Let's generate it. Keyframe* newKeyframe = aPercentageKeyframes.InsertElementAt(insertPosition); newKeyframe->mOffset.emplace( Keyframe::OffsetType::PercentageOffset(targetOffset)); newKeyframe->mComputedOffset = targetOffset; if (!aDefaultTimingFunction.IsLinearKeyword()) { newKeyframe->mTimingFunction.emplace(aDefaultTimingFunction); } newKeyframe->mComposite = aDefaultComposite; return newKeyframe; } // The step 3 (Generate Initial and Final Frames) in // https://drafts.csswg.org/css-animations-2/#keyframe-processing // // Note: we use "computed keyframes" here because we expect those keyframe // offsets have been resolved (so we have resolved computed offsets here for the // active timeline). The only difference from spec is that we don't compute the // property values here since the caller will do that. Perhaps we could merge // them together in the future. // https://drafts.csswg.org/web-animations-1/#computed-keyframes bool CSSAnimationKeyframeEffect::GetComputedKeyframes( nsTArray& aKeyframes) const { // Check if we do need to do this. This flag is set if the keyframes come from // the JS API. if (mIgnoreKeyframesGeneration) { return false; } // FIXME: Perhaps we could cache |allProperties| for keyframes with // offsets. (The properties of keyframes with // offsets need to be checked everytime // unfortunately.) AnimatedPropertyIDSet allProperties; AnimatedPropertyIDSet fromProperties; AnimatedPropertyIDSet toProperties; // Add 2 more elements for the insertion of 0% and 100% if any. nsTArray percentageKeyframes(mKeyframes.Length() + 2); nsTArray rangeKeyframes(mKeyframes.Length()); // Iterate keyframes and collect properties first. We have to append the // propeties to the 0% or 100% keyframes if needed. We merged keyframes and // converted the properties into physical longhands for the specified // keyframes already, so we can just collect the propertis information here. for (const Keyframe& keyframe : mKeyframes) { MOZ_ASSERT(keyframe.mOffset); if (keyframe.mOffset->IsPercentageOffset()) { percentageKeyframes.AppendElement(keyframe); } else { rangeKeyframes.AppendElement(keyframe); } if (std::isnan(keyframe.mComputedOffset)) { continue; } AnimatedPropertyIDSet currentProperties; for (const auto& pair : keyframe.mPropertyValues) { MOZ_ASSERT( !pair.mProperty.IsShorthand(), "The shorthands should be expanded already for CSS Animations"); MOZ_ASSERT(KeyframeUtils::IsAnimatableProperty(pair.mProperty), "It should be animatable for CSS Animations"); currentProperties.AddProperty(pair.mProperty); } allProperties.AddProperties(currentProperties); // Note that the automatic from (0%) and to (100%) keyframes are only // generated for properties that don’t have keyframes at or earlier than 0% // or at or after 100% (respectively). // https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes // // So we have to collect the properties with offsets <= 0 and offsets >= 1, // no matter what keyframe-specific timing function and keyframe-specific // composite are used. if (keyframe.mComputedOffset <= 0.0) { fromProperties.AddProperties(currentProperties); } else if (keyframe.mComputedOffset >= 1.0) { toProperties.AddProperties(currentProperties); } } // We keep the specified order if all of them are unresolved. Otherwise, sort // them with computed offsets to get the used order. We do this for finding // and generating the initial and final keyframes (since we need the used // order to search the keyframes below). // // Also, in getKeyframes(), other browsers show the keyframes with // offsets in the order of the computed offsets. This // is vague in the spec after we introduced , so we // just match the behavior of other browsers here. rangeKeyframes.StableSort(ComputedOffsetComparator()); // The default composite used to search to 0% and 100% keyframes below. const dom::CompositeOperationOrAuto defaultComposite = [&]() { switch (mDefaultComposite) { case CompositeOperation::Replace: return dom::CompositeOperationOrAuto::Replace; case CompositeOperation::Add: return dom::CompositeOperationOrAuto::Add; case CompositeOperation::Accumulate: return dom::CompositeOperationOrAuto::Accumulate; } // The initial value is replace. return dom::CompositeOperationOrAuto::Replace; }(); // Generate Initial and Final Frames: // 1. Find or create the initial keyframe, a keyframe with a keyframe offset // of 0%, default timing function as its keyframe timing function, and // default composite as its keyframe composite. // 2. For any property in animated properties that is not otherwise present // in a keyframe with an offset of 0% or one that would be positioned // earlier in the used keyframe order, add the computed value of that // property on element to initial keyframe’s keyframe values. // 3. If initial keyframe’s keyframe values is not empty, prepend initial // keyframe to keyframes. (Note: We add it to keyframes after the last // keyframe with offset 0%.) const bool isEmptyKeyframes = percentageKeyframes.IsEmpty() && rangeKeyframes.IsEmpty(); auto appendProperties = [&](Keyframe& aKeyframe, const AnimatedPropertyIDSet& aCurrentProperties) { auto& propertyValues = aKeyframe.mPropertyValues; for (const auto& property : allProperties) { if (aCurrentProperties.HasProperty(property)) { continue; } // The nullptr of |mServoDeclarationBlock| means we use the base values, // i.e. the value generated for that property by finding the computed // value for that property in the absence of animations. propertyValues.AppendElement(PropertyValuePair{property, nullptr}); } }; // Check the equality of two property sets. Note that |fromProperties| must be // a subset of |allProperties|. if (!allProperties.IsSubsetOf(fromProperties) && !isEmptyKeyframes) { Keyframe* fromKeyframe = GetOrCreateInitialOrFinalComputedKeyframe( percentageKeyframes, rangeKeyframes, TargetKeyframe::Initial, mDefaultTimingFunction, defaultComposite); MOZ_ASSERT(fromKeyframe); appendProperties(*fromKeyframe, fromProperties); } // 4. Repeat for final keyframe, using an offset of 100%, considering // keyframes positioned later in the used keyframe order, and appending to // keyframes. Note that |toProperties| must be a subset of |allProperties|. if (!allProperties.IsSubsetOf(toProperties) && !isEmptyKeyframes) { Keyframe* toKeyframe = GetOrCreateInitialOrFinalComputedKeyframe( percentageKeyframes, rangeKeyframes, TargetKeyframe::Final, mDefaultTimingFunction, defaultComposite); MOZ_ASSERT(toKeyframe); appendProperties(*toKeyframe, toProperties); } aKeyframes = std::move(percentageKeyframes); if (!rangeKeyframes.IsEmpty()) { aKeyframes.AppendElements(std::move(rangeKeyframes)); } return true; } void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const { if (!GetOwningCSSAnimation()) { return; } if (dom::Document* doc = GetRenderedDocument()) { doc->FlushPendingNotifications( ChangesToFlush(FlushType::Style, false /* flush animations */, false)); } } } // namespace mozilla::dom