CSSAnimation.cpp (13300B)
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 "CSSAnimation.h" 8 9 #include "mozilla/AnimationEventDispatcher.h" 10 #include "mozilla/TimeStamp.h" 11 #include "mozilla/dom/CSSAnimationBinding.h" 12 #include "mozilla/dom/KeyframeEffectBinding.h" 13 #include "nsPresContext.h" 14 15 namespace mozilla::dom { 16 17 using AnimationPhase = ComputedTiming::AnimationPhase; 18 19 JSObject* CSSAnimation::WrapObject(JSContext* aCx, 20 JS::Handle<JSObject*> aGivenProto) { 21 return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto); 22 } 23 24 void CSSAnimation::SetEffect(AnimationEffect* aEffect) { 25 Animation::SetEffect(aEffect); 26 27 AddOverriddenProperties(CSSAnimationProperties::Effect); 28 } 29 30 void CSSAnimation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) { 31 // Note that we always compare with the paused state since for the purposes 32 // of determining if play control is being overridden or not, we want to 33 // treat the finished state as running. 34 bool wasPaused = PlayState() == AnimationPlayState::Paused; 35 36 Animation::SetStartTimeAsDouble(aStartTime); 37 38 bool isPaused = PlayState() == AnimationPlayState::Paused; 39 40 if (wasPaused != isPaused) { 41 AddOverriddenProperties(CSSAnimationProperties::PlayState); 42 } 43 } 44 45 mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) { 46 FlushUnanimatedStyle(); 47 return Animation::GetReady(aRv); 48 } 49 50 void CSSAnimation::Reverse(ErrorResult& aRv) { 51 // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in 52 // the paused state. 53 bool wasPaused = PlayState() == AnimationPlayState::Paused; 54 55 Animation::Reverse(aRv); 56 if (aRv.Failed()) { 57 return; 58 } 59 60 bool isPaused = PlayState() == AnimationPlayState::Paused; 61 62 if (wasPaused != isPaused) { 63 AddOverriddenProperties(CSSAnimationProperties::PlayState); 64 } 65 } 66 67 AnimationPlayState CSSAnimation::PlayStateFromJS() const { 68 // Flush style to ensure that any properties controlling animation state 69 // (e.g. animation-play-state) are fully updated. 70 FlushUnanimatedStyle(); 71 return Animation::PlayStateFromJS(); 72 } 73 74 bool CSSAnimation::PendingFromJS() const { 75 // Flush style since, for example, if the animation-play-state was just 76 // changed its possible we should now be pending. 77 FlushUnanimatedStyle(); 78 return Animation::PendingFromJS(); 79 } 80 81 void CSSAnimation::PlayFromJS(ErrorResult& aRv) { 82 // Note that flushing style below might trigger calls to 83 // PlayFromStyle()/PauseFromStyle() on this object. 84 FlushUnanimatedStyle(); 85 Animation::PlayFromJS(aRv); 86 if (aRv.Failed()) { 87 return; 88 } 89 90 AddOverriddenProperties(CSSAnimationProperties::PlayState); 91 } 92 93 void CSSAnimation::PauseFromJS(ErrorResult& aRv) { 94 Animation::PauseFromJS(aRv); 95 if (aRv.Failed()) { 96 return; 97 } 98 99 AddOverriddenProperties(CSSAnimationProperties::PlayState); 100 } 101 102 void CSSAnimation::PlayFromStyle() { 103 ErrorResult rv; 104 Animation::Play(rv, Animation::LimitBehavior::Continue); 105 // play() should not throw when LimitBehavior is Continue 106 MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation"); 107 } 108 109 void CSSAnimation::PauseFromStyle() { 110 ErrorResult rv; 111 Animation::Pause(rv); 112 // pause() should only throw when *all* of the following conditions are true: 113 // - we are in the idle state, and 114 // - we have a negative playback rate, and 115 // - we have an infinitely repeating animation 116 // The first two conditions will never happen under regular style processing 117 // but could happen if an author made modifications to the Animation object 118 // and then updated animation-play-state. It's an unusual case and there's 119 // no obvious way to pass on the exception information so we just silently 120 // fail for now. 121 if (rv.Failed()) { 122 NS_WARNING("Unexpected exception pausing animation - silently failing"); 123 } 124 } 125 126 void CSSAnimation::Tick(TickState& aState) { 127 Animation::Tick(aState); 128 QueueEvents(); 129 } 130 131 int32_t CSSAnimation::CompareCompositeOrder( 132 const CSSAnimation& aOther, nsContentUtils::NodeIndexCache& aCache) const { 133 MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), 134 "Should only be called for CSS animations that are sorted " 135 "as CSS animations (i.e. tied to CSS markup)"); 136 137 // 0. Object-equality case 138 if (&aOther == this) { 139 return 0; 140 } 141 142 // 1. Sort by document order 143 if (!mOwningElement.Equals(aOther.mOwningElement)) { 144 return mOwningElement.Compare(aOther.mOwningElement, aCache); 145 } 146 147 // 2. (Same element and pseudo): Sort by position in animation-name 148 MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex); 149 return mAnimationIndex < aOther.mAnimationIndex ? -1 : 1; 150 } 151 152 void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) { 153 // If the animation is pending, we ignore animation events until we finish 154 // pending. 155 if (mPendingState != PendingState::NotPending) { 156 return; 157 } 158 159 // CSS animations dispatch events at their owning element. This allows 160 // script to repurpose a CSS animation to target a different element, 161 // to use a group effect (which has no obvious "target element"), or 162 // to remove the animation effect altogether whilst still getting 163 // animation events. 164 // 165 // It does mean, however, that for a CSS animation that has no owning 166 // element (e.g. it was created using the CSSAnimation constructor or 167 // disassociated from CSS) no events are fired. If it becomes desirable 168 // for these animations to still fire events we should spec the concept 169 // of the "original owning element" or "event target" and allow script 170 // to set it when creating a CSSAnimation object. 171 if (!mOwningElement.ShouldFireEvents()) { 172 return; 173 } 174 175 nsPresContext* presContext = mOwningElement.GetPresContext(); 176 if (!presContext) { 177 return; 178 } 179 180 uint64_t currentIteration = 0; 181 ComputedTiming::AnimationPhase currentPhase; 182 StickyTimeDuration intervalStartTime; 183 StickyTimeDuration intervalEndTime; 184 StickyTimeDuration iterationStartTime; 185 186 if (!mEffect) { 187 currentPhase = 188 GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this); 189 if (currentPhase == mPreviousPhase) { 190 return; 191 } 192 } else { 193 ComputedTiming computedTiming = mEffect->GetComputedTiming(); 194 currentPhase = computedTiming.mPhase; 195 currentIteration = computedTiming.mCurrentIteration; 196 if (currentPhase == mPreviousPhase && 197 currentIteration == mPreviousIteration) { 198 return; 199 } 200 intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); 201 intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); 202 203 uint64_t iterationBoundary = mPreviousIteration > currentIteration 204 ? currentIteration + 1 205 : currentIteration; 206 double multiplier = iterationBoundary - computedTiming.mIterationStart; 207 if (multiplier != 0.0) { 208 iterationStartTime = computedTiming.mDuration.MultDouble(multiplier); 209 } 210 } 211 212 TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); 213 TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); 214 TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); 215 216 AutoTArray<AnimationEventInfo, 2> events; 217 218 auto appendAnimationEvent = [&](EventMessage aMessage, 219 const StickyTimeDuration& aElapsedTime, 220 const TimeStamp& aScheduledEventTimeStamp) { 221 double elapsedTime = aElapsedTime.ToSeconds(); 222 if (aMessage == eAnimationCancel) { 223 // 0 is an inappropriate value for this callsite. What we need to do is 224 // use a single random value for all increasing times reportable. 225 // That is to say, whenever elapsedTime goes negative (because an 226 // animation restarts, something rewinds the animation, or otherwise) 227 // a new random value for the mix-in must be generated. 228 elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly( 229 elapsedTime, 0, mRTPCallerType); 230 } 231 events.AppendElement(AnimationEventInfo( 232 mAnimationName, mOwningElement.Target(), aMessage, elapsedTime, 233 mAnimationIndex, aScheduledEventTimeStamp, this)); 234 }; 235 236 // Handle cancel event first 237 if ((mPreviousPhase != AnimationPhase::Idle && 238 mPreviousPhase != AnimationPhase::After) && 239 currentPhase == AnimationPhase::Idle) { 240 appendAnimationEvent(eAnimationCancel, aActiveTime, 241 GetTimelineCurrentTimeAsTimeStamp()); 242 } 243 244 switch (mPreviousPhase) { 245 case AnimationPhase::Idle: 246 case AnimationPhase::Before: 247 if (currentPhase == AnimationPhase::Active) { 248 appendAnimationEvent(eAnimationStart, intervalStartTime, 249 startTimeStamp); 250 } else if (currentPhase == AnimationPhase::After) { 251 appendAnimationEvent(eAnimationStart, intervalStartTime, 252 startTimeStamp); 253 appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp); 254 } 255 break; 256 case AnimationPhase::Active: 257 if (currentPhase == AnimationPhase::Before) { 258 appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp); 259 } else if (currentPhase == AnimationPhase::Active) { 260 // The currentIteration must have changed or element we would have 261 // returned early above. 262 MOZ_ASSERT(currentIteration != mPreviousIteration); 263 appendAnimationEvent(eAnimationIteration, iterationStartTime, 264 iterationTimeStamp); 265 } else if (currentPhase == AnimationPhase::After) { 266 appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp); 267 } 268 break; 269 case AnimationPhase::After: 270 if (currentPhase == AnimationPhase::Before) { 271 appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp); 272 appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp); 273 } else if (currentPhase == AnimationPhase::Active) { 274 appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp); 275 } 276 break; 277 } 278 mPreviousPhase = currentPhase; 279 mPreviousIteration = currentIteration; 280 281 if (!events.IsEmpty()) { 282 presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); 283 } 284 } 285 286 void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag, 287 SyncNotifyFlag aSyncNotifyFlag) { 288 if (mNeedsNewAnimationIndexWhenRun && 289 PlayState() != AnimationPlayState::Idle) { 290 mAnimationIndex = sNextAnimationIndex++; 291 mNeedsNewAnimationIndexWhenRun = false; 292 } 293 294 Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); 295 } 296 297 /////////////////////// CSSAnimationKeyframeEffect //////////////////////// 298 299 void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const { 300 MaybeFlushUnanimatedStyle(); 301 KeyframeEffect::GetTiming(aRetVal); 302 } 303 304 void CSSAnimationKeyframeEffect::GetComputedTimingAsDict( 305 ComputedEffectTiming& aRetVal) const { 306 MaybeFlushUnanimatedStyle(); 307 KeyframeEffect::GetComputedTimingAsDict(aRetVal); 308 } 309 310 void CSSAnimationKeyframeEffect::UpdateTiming( 311 const OptionalEffectTiming& aTiming, ErrorResult& aRv) { 312 KeyframeEffect::UpdateTiming(aTiming, aRv); 313 314 if (aRv.Failed()) { 315 return; 316 } 317 318 if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { 319 CSSAnimationProperties updatedProperties = CSSAnimationProperties::None; 320 if (aTiming.mDuration.WasPassed()) { 321 updatedProperties |= CSSAnimationProperties::Duration; 322 } 323 if (aTiming.mIterations.WasPassed()) { 324 updatedProperties |= CSSAnimationProperties::IterationCount; 325 } 326 if (aTiming.mDirection.WasPassed()) { 327 updatedProperties |= CSSAnimationProperties::Direction; 328 } 329 if (aTiming.mDelay.WasPassed()) { 330 updatedProperties |= CSSAnimationProperties::Delay; 331 } 332 if (aTiming.mFill.WasPassed()) { 333 updatedProperties |= CSSAnimationProperties::FillMode; 334 } 335 336 cssAnimation->AddOverriddenProperties(updatedProperties); 337 } 338 } 339 340 void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext, 341 JS::Handle<JSObject*> aKeyframes, 342 ErrorResult& aRv) { 343 KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv); 344 345 if (aRv.Failed()) { 346 return; 347 } 348 349 if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { 350 cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes); 351 } 352 } 353 354 void CSSAnimationKeyframeEffect::SetComposite( 355 const CompositeOperation& aComposite) { 356 KeyframeEffect::SetComposite(aComposite); 357 358 if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) { 359 cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Composition); 360 } 361 } 362 363 void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const { 364 if (!GetOwningCSSAnimation()) { 365 return; 366 } 367 368 if (dom::Document* doc = GetRenderedDocument()) { 369 doc->FlushPendingNotifications( 370 ChangesToFlush(FlushType::Style, false /* flush animations */, false)); 371 } 372 } 373 374 } // namespace mozilla::dom