diff options
Diffstat (limited to 'dom/animation/CSSTransition.cpp')
-rw-r--r-- | dom/animation/CSSTransition.cpp | 333 |
1 files changed, 333 insertions, 0 deletions
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<JSObject*> 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<TransitionPhase>(*this); + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast<TransitionPhase>(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<AnimationEventInfo, 3> 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<CSSTransition*>(this)->CachedChildIndexRef(), + aOther.mOwningElement, + const_cast<CSSTransition*>(&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<TimeDuration> CSSTransition::GetCurrentTimeAt( + const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, + const TimeDuration& aStartTime, double aPlaybackRate) { + Nullable<TimeDuration> result; + + Nullable<TimeDuration> 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 |