1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "CSSTransition.h"
9 #include "mozilla/AnimationEventDispatcher.h"
10 #include "mozilla/dom/CSSTransitionBinding.h"
11 #include "mozilla/dom/KeyframeEffectBinding.h"
12 #include "mozilla/dom/KeyframeEffect.h"
13 #include "mozilla/TimeStamp.h"
14 #include "nsPresContext.h"
16 namespace mozilla::dom
{
18 JSObject
* CSSTransition::WrapObject(JSContext
* aCx
,
19 JS::Handle
<JSObject
*> aGivenProto
) {
20 return dom::CSSTransition_Binding::Wrap(aCx
, this, aGivenProto
);
23 void CSSTransition::GetTransitionProperty(nsString
& aRetVal
) const {
24 MOZ_ASSERT(eCSSProperty_UNKNOWN
!= mTransitionProperty
,
25 "Transition Property should be initialized");
27 NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty
));
30 AnimationPlayState
CSSTransition::PlayStateFromJS() const {
31 FlushUnanimatedStyle();
32 return Animation::PlayStateFromJS();
35 bool CSSTransition::PendingFromJS() const {
36 // Transitions don't become pending again after they start running but, if
37 // while the transition is still pending, style is updated in such a way
38 // that the transition will be canceled, we need to report false here.
39 // Hence we need to flush, but only when we're pending.
41 FlushUnanimatedStyle();
43 return Animation::PendingFromJS();
46 void CSSTransition::PlayFromJS(ErrorResult
& aRv
) {
47 FlushUnanimatedStyle();
48 Animation::PlayFromJS(aRv
);
51 void CSSTransition::UpdateTiming(SeekFlag aSeekFlag
,
52 SyncNotifyFlag aSyncNotifyFlag
) {
53 if (mNeedsNewAnimationIndexWhenRun
&&
54 PlayState() != AnimationPlayState::Idle
) {
55 mAnimationIndex
= sNextAnimationIndex
++;
56 mNeedsNewAnimationIndexWhenRun
= false;
59 Animation::UpdateTiming(aSeekFlag
, aSyncNotifyFlag
);
62 void CSSTransition::QueueEvents(const StickyTimeDuration
& aActiveTime
) {
63 if (!mOwningElement
.IsSet()) {
67 nsPresContext
* presContext
= mOwningElement
.GetPresContext();
72 static constexpr StickyTimeDuration zeroDuration
= StickyTimeDuration();
74 TransitionPhase currentPhase
;
75 StickyTimeDuration intervalStartTime
;
76 StickyTimeDuration intervalEndTime
;
79 currentPhase
= GetAnimationPhaseWithoutEffect
<TransitionPhase
>(*this);
81 ComputedTiming computedTiming
= mEffect
->GetComputedTiming();
83 currentPhase
= static_cast<TransitionPhase
>(computedTiming
.mPhase
);
84 intervalStartTime
= IntervalStartTime(computedTiming
.mActiveDuration
);
85 intervalEndTime
= IntervalEndTime(computedTiming
.mActiveDuration
);
88 if (mPendingState
!= PendingState::NotPending
&&
89 (mPreviousTransitionPhase
== TransitionPhase::Idle
||
90 mPreviousTransitionPhase
== TransitionPhase::Pending
)) {
91 currentPhase
= TransitionPhase::Pending
;
94 if (currentPhase
== mPreviousTransitionPhase
) {
98 // TimeStamps to use for ordering the events when they are dispatched. We
99 // use a TimeStamp so we can compare events produced by different elements,
100 // perhaps even with different timelines.
101 // The zero timestamp is for transitionrun events where we ignore the delay
102 // for the purpose of ordering events.
103 TimeStamp zeroTimeStamp
= AnimationTimeToTimeStamp(zeroDuration
);
104 TimeStamp startTimeStamp
= ElapsedTimeToTimeStamp(intervalStartTime
);
105 TimeStamp endTimeStamp
= ElapsedTimeToTimeStamp(intervalEndTime
);
107 AutoTArray
<AnimationEventInfo
, 3> events
;
109 auto appendTransitionEvent
= [&](EventMessage aMessage
,
110 const StickyTimeDuration
& aElapsedTime
,
111 const TimeStamp
& aScheduledEventTimeStamp
) {
112 double elapsedTime
= aElapsedTime
.ToSeconds();
113 if (aMessage
== eTransitionCancel
) {
114 // 0 is an inappropriate value for this callsite. What we need to do is
115 // use a single random value for all increasing times reportable.
116 // That is to say, whenever elapsedTime goes negative (because an
117 // animation restarts, something rewinds the animation, or otherwise)
118 // a new random value for the mix-in must be generated.
119 elapsedTime
= nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
120 elapsedTime
, 0, mRTPCallerType
);
122 events
.AppendElement(AnimationEventInfo(
123 TransitionProperty(), mOwningElement
.Target(), aMessage
, elapsedTime
,
124 aScheduledEventTimeStamp
, this));
127 // Handle cancel events first
128 if ((mPreviousTransitionPhase
!= TransitionPhase::Idle
&&
129 mPreviousTransitionPhase
!= TransitionPhase::After
) &&
130 currentPhase
== TransitionPhase::Idle
) {
131 appendTransitionEvent(eTransitionCancel
, aActiveTime
,
132 GetTimelineCurrentTimeAsTimeStamp());
136 switch (mPreviousTransitionPhase
) {
137 case TransitionPhase::Idle
:
138 if (currentPhase
== TransitionPhase::Pending
||
139 currentPhase
== TransitionPhase::Before
) {
140 appendTransitionEvent(eTransitionRun
, intervalStartTime
, zeroTimeStamp
);
141 } else if (currentPhase
== TransitionPhase::Active
) {
142 appendTransitionEvent(eTransitionRun
, intervalStartTime
, zeroTimeStamp
);
143 appendTransitionEvent(eTransitionStart
, intervalStartTime
,
145 } else if (currentPhase
== TransitionPhase::After
) {
146 appendTransitionEvent(eTransitionRun
, intervalStartTime
, zeroTimeStamp
);
147 appendTransitionEvent(eTransitionStart
, intervalStartTime
,
149 appendTransitionEvent(eTransitionEnd
, intervalEndTime
, endTimeStamp
);
153 case TransitionPhase::Pending
:
154 case TransitionPhase::Before
:
155 if (currentPhase
== TransitionPhase::Active
) {
156 appendTransitionEvent(eTransitionStart
, intervalStartTime
,
158 } else if (currentPhase
== TransitionPhase::After
) {
159 appendTransitionEvent(eTransitionStart
, intervalStartTime
,
161 appendTransitionEvent(eTransitionEnd
, intervalEndTime
, endTimeStamp
);
165 case TransitionPhase::Active
:
166 if (currentPhase
== TransitionPhase::After
) {
167 appendTransitionEvent(eTransitionEnd
, intervalEndTime
, endTimeStamp
);
168 } else if (currentPhase
== TransitionPhase::Before
) {
169 appendTransitionEvent(eTransitionEnd
, intervalStartTime
,
174 case TransitionPhase::After
:
175 if (currentPhase
== TransitionPhase::Active
) {
176 appendTransitionEvent(eTransitionStart
, intervalEndTime
,
178 } else if (currentPhase
== TransitionPhase::Before
) {
179 appendTransitionEvent(eTransitionStart
, intervalEndTime
,
181 appendTransitionEvent(eTransitionEnd
, intervalStartTime
, endTimeStamp
);
185 mPreviousTransitionPhase
= currentPhase
;
187 if (!events
.IsEmpty()) {
188 presContext
->AnimationEventDispatcher()->QueueEvents(std::move(events
));
192 void CSSTransition::Tick() {
197 nsCSSPropertyID
CSSTransition::TransitionProperty() const {
198 MOZ_ASSERT(eCSSProperty_UNKNOWN
!= mTransitionProperty
,
199 "Transition property should be initialized");
200 return mTransitionProperty
;
203 AnimationValue
CSSTransition::ToValue() const {
204 MOZ_ASSERT(!mTransitionToValue
.IsNull(),
205 "Transition ToValue should be initialized");
206 return mTransitionToValue
;
209 bool CSSTransition::HasLowerCompositeOrderThan(
210 const CSSTransition
& aOther
) const {
211 MOZ_ASSERT(IsTiedToMarkup() && aOther
.IsTiedToMarkup(),
212 "Should only be called for CSS transitions that are sorted "
213 "as CSS transitions (i.e. tied to CSS markup)");
215 // 0. Object-equality case
216 if (&aOther
== this) {
220 // 1. Sort by document order
221 if (!mOwningElement
.Equals(aOther
.mOwningElement
)) {
222 return mOwningElement
.LessThan(
223 const_cast<CSSTransition
*>(this)->CachedChildIndexRef(),
224 aOther
.mOwningElement
,
225 const_cast<CSSTransition
*>(&aOther
)->CachedChildIndexRef());
228 // 2. (Same element and pseudo): Sort by transition generation
229 if (mAnimationIndex
!= aOther
.mAnimationIndex
) {
230 return mAnimationIndex
< aOther
.mAnimationIndex
;
233 // 3. (Same transition generation): Sort by transition property
234 return nsCSSProps::GetStringValue(TransitionProperty()) <
235 nsCSSProps::GetStringValue(aOther
.TransitionProperty());
239 Nullable
<TimeDuration
> CSSTransition::GetCurrentTimeAt(
240 const AnimationTimeline
& aTimeline
, const TimeStamp
& aBaseTime
,
241 const TimeDuration
& aStartTime
, double aPlaybackRate
) {
242 Nullable
<TimeDuration
> result
;
244 Nullable
<TimeDuration
> timelineTime
= aTimeline
.ToTimelineTime(aBaseTime
);
245 if (!timelineTime
.IsNull()) {
247 (timelineTime
.Value() - aStartTime
).MultDouble(aPlaybackRate
));
253 double CSSTransition::CurrentValuePortion() const {
258 // Transitions use a fill mode of 'backwards' so GetComputedTiming will
259 // never return a null time progress due to being *before* the animation
260 // interval. However, it might be possible that we're behind on flushing
261 // causing us to get called *after* the animation interval. So, just in
262 // case, we override the fill mode to 'both' to ensure the progress
264 TimingParams timingToUse
= GetEffect()->SpecifiedTiming();
265 timingToUse
.SetFill(dom::FillMode::Both
);
266 ComputedTiming computedTiming
= GetEffect()->GetComputedTiming(&timingToUse
);
268 if (computedTiming
.mProgress
.IsNull()) {
272 // 'transition-timing-function' corresponds to the effect timing while
273 // the transition keyframes have a linear timing function so we can ignore
274 // them for the purposes of calculating the value portion.
275 return computedTiming
.mProgress
.Value();
278 void CSSTransition::UpdateStartValueFromReplacedTransition() {
279 MOZ_ASSERT(mEffect
&& mEffect
->AsKeyframeEffect() &&
280 mEffect
->AsKeyframeEffect()->HasAnimationOfPropertySet(
281 nsCSSPropertyIDSet::CompositorAnimatables()),
282 "Should be called for compositor-runnable transitions");
284 if (!mReplacedTransition
) {
288 // We don't set |mReplacedTransition| if the timeline of this transition is
289 // different from the document timeline. The timeline of Animation may be
290 // null via script, so if it's null, it must be different from the document
291 // timeline (because document timeline is readonly so we cannot change it by
292 // script). Therefore, we check this assertion if mReplacedTransition is
294 MOZ_ASSERT(mTimeline
,
295 "Should have a timeline if we are replacing transition start "
298 ComputedTiming computedTiming
= AnimationEffect::GetComputedTimingAt(
299 CSSTransition::GetCurrentTimeAt(*mTimeline
, TimeStamp::Now(),
300 mReplacedTransition
->mStartTime
,
301 mReplacedTransition
->mPlaybackRate
),
302 mReplacedTransition
->mTiming
, mReplacedTransition
->mPlaybackRate
,
303 Animation::ProgressTimelinePosition::NotBoundary
);
305 if (!computedTiming
.mProgress
.IsNull()) {
306 double valuePosition
= StyleComputedTimingFunction::GetPortion(
307 mReplacedTransition
->mTimingFunction
, computedTiming
.mProgress
.Value(),
308 computedTiming
.mBeforeFlag
);
310 const AnimationValue
& replacedFrom
= mReplacedTransition
->mFromValue
;
311 const AnimationValue
& replacedTo
= mReplacedTransition
->mToValue
;
312 AnimationValue startValue
;
314 Servo_AnimationValues_Interpolate(replacedFrom
.mServo
,
315 replacedTo
.mServo
, valuePosition
)
318 mEffect
->AsKeyframeEffect()->ReplaceTransitionStartValue(
319 std::move(startValue
));
322 mReplacedTransition
.reset();
325 void CSSTransition::SetEffectFromStyle(KeyframeEffect
* aEffect
) {
326 MOZ_ASSERT(aEffect
->IsValidTransition());
328 Animation::SetEffectNoUpdate(aEffect
);
329 mTransitionProperty
= aEffect
->Properties()[0].mProperty
;
330 mTransitionToValue
= aEffect
->Properties()[0].mSegments
[0].mToValue
;
333 } // namespace mozilla::dom