CSSTransition.cpp (15005B)
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/. */ 6 7 #include "CSSTransition.h" 8 9 #include "mozilla/AnimationEventDispatcher.h" 10 #include "mozilla/TimeStamp.h" 11 #include "mozilla/dom/CSSTransitionBinding.h" 12 #include "mozilla/dom/KeyframeEffect.h" 13 #include "mozilla/dom/KeyframeEffectBinding.h" 14 #include "nsPresContext.h" 15 16 namespace mozilla::dom { 17 18 JSObject* CSSTransition::WrapObject(JSContext* aCx, 19 JS::Handle<JSObject*> aGivenProto) { 20 return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto); 21 } 22 23 void CSSTransition::GetTransitionProperty(nsString& aRetVal) const { 24 MOZ_ASSERT(mTransitionProperty.IsValid(), 25 "Transition Property should be initialized"); 26 mTransitionProperty.ToString(aRetVal); 27 } 28 29 AnimationPlayState CSSTransition::PlayStateFromJS() const { 30 FlushUnanimatedStyle(); 31 return Animation::PlayStateFromJS(); 32 } 33 34 bool CSSTransition::PendingFromJS() const { 35 // Transitions don't become pending again after they start running but, if 36 // while the transition is still pending, style is updated in such a way 37 // that the transition will be canceled, we need to report false here. 38 // Hence we need to flush, but only when we're pending. 39 if (Pending()) { 40 FlushUnanimatedStyle(); 41 } 42 return Animation::PendingFromJS(); 43 } 44 45 void CSSTransition::PlayFromJS(ErrorResult& aRv) { 46 FlushUnanimatedStyle(); 47 Animation::PlayFromJS(aRv); 48 } 49 50 void CSSTransition::UpdateTiming(SeekFlag aSeekFlag, 51 SyncNotifyFlag aSyncNotifyFlag) { 52 if (mNeedsNewAnimationIndexWhenRun && 53 PlayState() != AnimationPlayState::Idle) { 54 mAnimationIndex = sNextAnimationIndex++; 55 mNeedsNewAnimationIndexWhenRun = false; 56 } 57 58 Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); 59 } 60 61 void CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) { 62 if (!mOwningElement.ShouldFireEvents()) { 63 return; 64 } 65 66 nsPresContext* presContext = mOwningElement.GetPresContext(); 67 if (!presContext) { 68 return; 69 } 70 71 static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); 72 73 TransitionPhase currentPhase; 74 StickyTimeDuration intervalStartTime; 75 StickyTimeDuration intervalEndTime; 76 77 if (!mEffect) { 78 currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this); 79 } else { 80 ComputedTiming computedTiming = mEffect->GetComputedTiming(); 81 82 currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); 83 intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); 84 intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); 85 } 86 87 if (mPendingState != PendingState::NotPending && 88 (mPreviousTransitionPhase == TransitionPhase::Idle || 89 mPreviousTransitionPhase == TransitionPhase::Pending)) { 90 currentPhase = TransitionPhase::Pending; 91 } 92 93 if (currentPhase == mPreviousTransitionPhase) { 94 return; 95 } 96 97 // TimeStamps to use for ordering the events when they are dispatched. We 98 // use a TimeStamp so we can compare events produced by different elements, 99 // perhaps even with different timelines. 100 // The zero timestamp is for transitionrun events where we ignore the delay 101 // for the purpose of ordering events. 102 TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); 103 TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); 104 TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); 105 106 AutoTArray<AnimationEventInfo, 3> events; 107 108 auto appendTransitionEvent = [&](EventMessage aMessage, 109 const StickyTimeDuration& aElapsedTime, 110 const TimeStamp& aScheduledEventTimeStamp) { 111 double elapsedTime = aElapsedTime.ToSeconds(); 112 if (aMessage == eTransitionCancel) { 113 // 0 is an inappropriate value for this callsite. What we need to do is 114 // use a single random value for all increasing times reportable. 115 // That is to say, whenever elapsedTime goes negative (because an 116 // animation restarts, something rewinds the animation, or otherwise) 117 // a new random value for the mix-in must be generated. 118 elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( 119 elapsedTime, 0, mRTPCallerType); 120 } 121 events.AppendElement(AnimationEventInfo( 122 TransitionProperty(), mOwningElement.Target(), aMessage, elapsedTime, 123 mAnimationIndex, aScheduledEventTimeStamp, this)); 124 }; 125 126 // Handle cancel events first 127 if ((mPreviousTransitionPhase != TransitionPhase::Idle && 128 mPreviousTransitionPhase != TransitionPhase::After) && 129 currentPhase == TransitionPhase::Idle) { 130 appendTransitionEvent(eTransitionCancel, aActiveTime, 131 GetTimelineCurrentTimeAsTimeStamp()); 132 } 133 134 // All other events 135 switch (mPreviousTransitionPhase) { 136 case TransitionPhase::Idle: 137 if (currentPhase == TransitionPhase::Pending || 138 currentPhase == TransitionPhase::Before) { 139 // When we are replacing a transition and flushing the style in the 140 // meantime, after a timeout, we may tick this transition without a 141 // proper |mPendingReadyTime| because the refresh driver is not in 142 // refresh, i.e. mInRefresh is false. So in the current tick we queue 143 // this event but the transition would be triggered in the next tick. 144 // 145 // In general, we use Animation::EnsurePaintIsScheduled() to assign a 146 // valid time to |mPendingReadyTime| of this transition, and then we 147 // could trigger this transition if this value is set. When triggering, 148 // we set a proper |mStartTime|, which could be used to calculate the 149 // animation time, i.e. |zeroTimeStamp|. 150 // 151 // However, due to this race condition (i.e. the transition hasn't been 152 // triggered yet but we are enqueuing this event), it's posssible to 153 // have a null |zeroTimeStamp|, which breaks the sorting of transition 154 // events. So we use the current time as a fallback way to make sure we 155 // have a reasonable schedule time for sorting. 156 appendTransitionEvent(eTransitionRun, intervalStartTime, 157 zeroTimeStamp.IsNull() 158 ? GetTimelineCurrentTimeAsTimeStamp() 159 : zeroTimeStamp); 160 } else if (currentPhase == TransitionPhase::Active) { 161 appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); 162 appendTransitionEvent(eTransitionStart, intervalStartTime, 163 startTimeStamp); 164 } else if (currentPhase == TransitionPhase::After) { 165 appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); 166 appendTransitionEvent(eTransitionStart, intervalStartTime, 167 startTimeStamp); 168 appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); 169 } 170 break; 171 172 case TransitionPhase::Pending: 173 case TransitionPhase::Before: 174 if (currentPhase == TransitionPhase::Active) { 175 appendTransitionEvent(eTransitionStart, intervalStartTime, 176 startTimeStamp); 177 } else if (currentPhase == TransitionPhase::After) { 178 appendTransitionEvent(eTransitionStart, intervalStartTime, 179 startTimeStamp); 180 appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); 181 } 182 break; 183 184 case TransitionPhase::Active: 185 if (currentPhase == TransitionPhase::After) { 186 appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); 187 } else if (currentPhase == TransitionPhase::Before) { 188 appendTransitionEvent(eTransitionEnd, intervalStartTime, 189 startTimeStamp); 190 } 191 break; 192 193 case TransitionPhase::After: 194 if (currentPhase == TransitionPhase::Active) { 195 appendTransitionEvent(eTransitionStart, intervalEndTime, 196 startTimeStamp); 197 } else if (currentPhase == TransitionPhase::Before) { 198 appendTransitionEvent(eTransitionStart, intervalEndTime, 199 startTimeStamp); 200 appendTransitionEvent(eTransitionEnd, intervalStartTime, endTimeStamp); 201 } 202 break; 203 } 204 mPreviousTransitionPhase = currentPhase; 205 206 if (!events.IsEmpty()) { 207 presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); 208 } 209 } 210 211 void CSSTransition::Tick(TickState& aState) { 212 Animation::Tick(aState); 213 QueueEvents(); 214 } 215 216 const CSSPropertyId& CSSTransition::TransitionProperty() const { 217 MOZ_ASSERT(mTransitionProperty.IsValid(), 218 "Transition property should be initialized"); 219 return mTransitionProperty; 220 } 221 222 AnimationValue CSSTransition::ToValue() const { 223 MOZ_ASSERT(!mTransitionToValue.IsNull(), 224 "Transition ToValue should be initialized"); 225 return mTransitionToValue; 226 } 227 228 int32_t CSSTransition::CompareCompositeOrder( 229 const Maybe<EventContext>& aContext, const CSSTransition& aOther, 230 const Maybe<EventContext>& aOtherContext, 231 nsContentUtils::NodeIndexCache& aCache) const { 232 MOZ_ASSERT((IsTiedToMarkup() || aContext) && 233 (aOther.IsTiedToMarkup() || aOtherContext), 234 "Should only be called for CSS transitions that are sorted " 235 "as CSS transitions (i.e. tied to CSS markup) or with overridden " 236 "target and animation index"); 237 238 // 0. Object-equality case 239 if (&aOther == this) { 240 return 0; 241 } 242 243 // 1. Sort by document order 244 const OwningElementRef& owningElement1 = 245 IsTiedToMarkup() ? mOwningElement : OwningElementRef(aContext->mTarget); 246 const OwningElementRef& owningElement2 = 247 aOther.IsTiedToMarkup() ? aOther.mOwningElement 248 : OwningElementRef(aOtherContext->mTarget); 249 if (!owningElement1.Equals(owningElement2)) { 250 return owningElement1.Compare(owningElement2, aCache); 251 } 252 253 // 2. (Same element and pseudo): Sort by transition generation 254 const uint64_t index1 = IsTiedToMarkup() ? mAnimationIndex : aContext->mIndex; 255 const uint64_t index2 = 256 aOther.IsTiedToMarkup() ? aOther.mAnimationIndex : aOtherContext->mIndex; 257 if (index1 != index2) { 258 return index1 < index2 ? -1 : 1; 259 } 260 261 // 3. (Same transition generation): Sort by transition property 262 if (mTransitionProperty == aOther.mTransitionProperty) { 263 return 0; 264 } 265 nsAutoString name, otherName; 266 GetTransitionProperty(name); 267 aOther.GetTransitionProperty(otherName); 268 return name < otherName ? -1 : 1; 269 } 270 271 /* static */ 272 Nullable<TimeDuration> CSSTransition::GetCurrentTimeAt( 273 const AnimationTimeline& aTimeline, const TimeStamp& aBaseTime, 274 const TimeDuration& aStartTime, double aPlaybackRate) { 275 Nullable<TimeDuration> result; 276 277 Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime); 278 if (!timelineTime.IsNull()) { 279 result.SetValue( 280 (timelineTime.Value() - aStartTime).MultDouble(aPlaybackRate)); 281 } 282 283 return result; 284 } 285 286 double CSSTransition::CurrentValuePortion() const { 287 if (!GetEffect()) { 288 return 0.0; 289 } 290 291 // Transitions use a fill mode of 'backwards' so GetComputedTiming will 292 // never return a null time progress due to being *before* the animation 293 // interval. However, it might be possible that we're behind on flushing 294 // causing us to get called *after* the animation interval. So, just in 295 // case, we override the fill mode to 'both' to ensure the progress 296 // is never null. 297 TimingParams timingToUse = GetEffect()->SpecifiedTiming(); 298 timingToUse.SetFill(dom::FillMode::Both); 299 ComputedTiming computedTiming = GetEffect()->GetComputedTiming(&timingToUse); 300 301 if (computedTiming.mProgress.IsNull()) { 302 return 0.0; 303 } 304 305 // 'transition-timing-function' corresponds to the effect timing while 306 // the transition keyframes have a linear timing function so we can ignore 307 // them for the purposes of calculating the value portion. 308 return computedTiming.mProgress.Value(); 309 } 310 311 bool CSSTransition::UpdateStartValueFromReplacedTransition() { 312 MOZ_ASSERT(mEffect && mEffect->AsKeyframeEffect() && 313 mEffect->AsKeyframeEffect()->HasAnimationOfPropertySet( 314 nsCSSPropertyIDSet::CompositorAnimatables()), 315 "Should be called for compositor-runnable transitions"); 316 317 if (!mReplacedTransition) { 318 return false; 319 } 320 321 // We don't set |mReplacedTransition| if the timeline of this transition is 322 // different from the document timeline. The timeline of Animation may be 323 // null via script, so if it's null, it must be different from the document 324 // timeline (because document timeline is readonly so we cannot change it by 325 // script). Therefore, we check this assertion if mReplacedTransition is 326 // valid. 327 MOZ_ASSERT(mTimeline, 328 "Should have a timeline if we are replacing transition start " 329 "values"); 330 331 if (Maybe<double> valuePosition = 332 ComputeTransformedProgress(*mTimeline, *mReplacedTransition)) { 333 // FIXME: Bug 1634945. We may have to use the last value on the compositor 334 // to replace the start value. 335 const AnimationValue& replacedFrom = mReplacedTransition->mFromValue; 336 const AnimationValue& replacedTo = mReplacedTransition->mToValue; 337 AnimationValue startValue; 338 startValue.mServo = 339 Servo_AnimationValues_Interpolate(replacedFrom.mServo, 340 replacedTo.mServo, *valuePosition) 341 .Consume(); 342 343 mEffect->AsKeyframeEffect()->ReplaceTransitionStartValue( 344 std::move(startValue)); 345 } 346 347 mReplacedTransition.reset(); 348 349 return true; 350 } 351 352 /* static*/ 353 Maybe<double> CSSTransition::ComputeTransformedProgress( 354 const AnimationTimeline& aTimeline, 355 const ReplacedTransitionProperties& aProperties) { 356 ComputedTiming computedTiming = AnimationEffect::GetComputedTimingAt( 357 CSSTransition::GetCurrentTimeAt(aTimeline, TimeStamp::Now(), 358 aProperties.mStartTime, 359 aProperties.mPlaybackRate), 360 aProperties.mTiming, aProperties.mPlaybackRate, 361 Animation::ProgressTimelinePosition::NotBoundary); 362 if (computedTiming.mProgress.IsNull()) { 363 return Nothing(); 364 } 365 366 return Some(StyleComputedTimingFunction::GetPortion( 367 aProperties.mTimingFunction, computedTiming.mProgress.Value(), 368 computedTiming.mBeforeFlag)); 369 } 370 371 void CSSTransition::SetEffectFromStyle(KeyframeEffect* aEffect) { 372 MOZ_ASSERT(aEffect->IsValidTransition()); 373 374 Animation::SetEffectNoUpdate(aEffect); 375 mTransitionProperty = aEffect->Properties()[0].mProperty; 376 mTransitionToValue = aEffect->Properties()[0].mSegments[0].mToValue; 377 } 378 379 } // namespace mozilla::dom