AnimationHelper.cpp (36331B)
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 "AnimationHelper.h" 8 #include "CompositorAnimationStorage.h" 9 #include "base/process_util.h" 10 #include "gfx2DGlue.h" // for ThebesRect 11 #include "gfxLineSegment.h" // for gfxLineSegment 12 #include "gfxPoint.h" // for gfxPoint 13 #include "gfxQuad.h" // for gfxQuad 14 #include "gfxRect.h" // for gfxRect 15 #include "gfxUtils.h" // for gfxUtils::TransformToQuad 16 #include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction 17 #include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode 18 #include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite 19 #include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly 20 #include "mozilla/dom/Nullable.h" // for dom::Nullable 21 #include "mozilla/layers/APZSampler.h" // for APZSampler 22 #include "mozilla/CSSPropertyId.h" 23 #include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor() 24 #include "mozilla/Maybe.h" // for Maybe<> 25 #include "mozilla/MotionPathUtils.h" // for ResolveMotionPath() 26 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc 27 #include "NonCustomCSSPropertyId.h" // for eCSSProperty_offset_path, etc 28 #include "nsDisplayList.h" // for nsDisplayTransform, etc 29 30 namespace mozilla::layers { 31 32 static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline( 33 const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta, 34 const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime, 35 const TimeDuration& aStartTime, float aPlaybackRate) { 36 // We return Nothing If the associated APZ controller is not available 37 // (because it may be destroyed but this animation is still alive). 38 if (!aScrollMeta) { 39 // This may happen after we reload a page. There may be a race condition 40 // because the animation is still alive but the APZ is destroyed. In this 41 // case, this animation is invalid, so we return nullptr. 42 return nullptr; 43 } 44 45 const bool isHorizontal = 46 aOptions.axis() == layers::ScrollDirection::eHorizontal; 47 double range = 48 isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height; 49 // The APZ sampler may give us a zero range (e.g. if the user resizes the 50 // element). 51 if (range == 0.0) { 52 // If the range is zero, we cannot calculate the progress, so just return 53 // nullptr. 54 return nullptr; 55 } 56 MOZ_ASSERT( 57 range > 0.0, 58 "We don't expect to get a zero or negative range on the compositor"); 59 60 // The offset may be negative if the writing mode is from right to left. 61 // Use std::abs() here to avoid getting a negative progress. 62 double position = 63 std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y); 64 double progress = position / range; 65 // Just in case to avoid getting a progress more than 100%, for overscrolling. 66 progress = std::min(progress, 1.0); 67 auto timelineTime = TimeDuration(aEndTime.MultDouble(progress)); 68 return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime, 69 aPlaybackRate); 70 } 71 72 static dom::Nullable<TimeDuration> CalculateElapsedTime( 73 const APZSampler* aAPZSampler, const LayersId& aLayersId, 74 const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation, 75 const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime, 76 const AnimatedValue* aPreviousValue) { 77 // ------------------------------------- 78 // Case 1: scroll-timeline animations. 79 // ------------------------------------- 80 if (aAnimation.mScrollTimelineOptions) { 81 MOZ_ASSERT( 82 aAPZSampler, 83 "We don't send scroll animations to the compositor if APZ is disabled"); 84 85 return CalculateElapsedTimeForScrollTimeline( 86 aAPZSampler->GetCurrentScrollOffsetAndRange( 87 aLayersId, aAnimation.mScrollTimelineOptions.value().source(), 88 aProofOfMapLock), 89 aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(), 90 aAnimation.mStartTime.refOr(aAnimation.mHoldTime), 91 aAnimation.mPlaybackRate); 92 } 93 94 // ------------------------------------- 95 // Case 2: document-timeline animations. 96 // ------------------------------------- 97 MOZ_ASSERT( 98 (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) || 99 aAnimation.mIsNotPlaying, 100 "If we are playing, we should have an origin time and a start time"); 101 102 // Determine if the animation was play-pending and used a ready time later 103 // than the previous frame time. 104 // 105 // To determine this, _all_ of the following conditions need to hold: 106 // 107 // * There was no previous animation value (i.e. this is the first frame for 108 // the animation since it was sent to the compositor), and 109 // * The animation is playing, and 110 // * There is a previous frame time, and 111 // * The ready time of the animation is ahead of the previous frame time. 112 // 113 bool hasFutureReadyTime = false; 114 if (!aPreviousValue && !aAnimation.mIsNotPlaying && 115 !aPreviousFrameTime.IsNull()) { 116 // This is the inverse of the calculation performed in 117 // AnimationInfo::StartPendingAnimations to calculate the start time of 118 // play-pending animations. 119 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid 120 // underflow in the middle of the calulation. 121 const TimeStamp readyTime = 122 aAnimation.mOriginTime + 123 (aAnimation.mStartTime.ref() + 124 aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate)); 125 hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime; 126 } 127 // Use the previous vsync time to make main thread animations and compositor 128 // more closely aligned. 129 // 130 // On the first frame where we have animations the previous timestamp will 131 // not be set so we simply use the current timestamp. As a result we will 132 // end up painting the first frame twice. That doesn't appear to be 133 // noticeable, however. 134 // 135 // Likewise, if the animation is play-pending, it may have a ready time that 136 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|). 137 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily 138 // jumping backwards into the range prior to when the animation starts. 139 const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime 140 ? aCurrentFrameTime 141 : aPreviousFrameTime; 142 143 // If the animation is not currently playing, e.g. paused or 144 // finished, then use the hold time to stay at the same position. 145 TimeDuration elapsedDuration = 146 aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing() 147 ? aAnimation.mHoldTime 148 : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref()) 149 .MultDouble(aAnimation.mPlaybackRate); 150 return elapsedDuration; 151 } 152 153 enum class CanSkipCompose { 154 IfPossible, 155 No, 156 }; 157 // This function samples the animation for a specific property. We may have 158 // multiple animations for a single property, and the later animations override 159 // the eariler ones. This function returns the sampled animation value, 160 // |aAnimationValue| for a single CSS property. 161 static AnimationHelper::SampleResult SampleAnimationForProperty( 162 const APZSampler* aAPZSampler, const LayersId& aLayersId, 163 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime, 164 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue, 165 CanSkipCompose aCanSkipCompose, 166 nsTArray<PropertyAnimation>& aPropertyAnimations, 167 RefPtr<StyleAnimationValue>& aAnimationValue) { 168 MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations"); 169 170 auto reason = AnimationHelper::SampleResult::Reason::None; 171 bool hasInEffectAnimations = false; 172 #ifdef DEBUG 173 // In cases where this function returns a SampleResult::Skipped, we actually 174 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the 175 // call site that the value that would have been computed matches the stored 176 // value that we end up using. This flag is used to ensure we populate 177 // aAnimationValue in this scenario. 178 bool shouldBeSkipped = false; 179 #endif 180 // Process in order, since later animations override earlier ones. 181 for (PropertyAnimation& animation : aPropertyAnimations) { 182 dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime( 183 aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime, 184 aCurrentFrameTime, aPreviousValue); 185 186 const auto progressTimelinePosition = 187 animation.mScrollTimelineOptions 188 ? dom::Animation::AtProgressTimelineBoundary( 189 TimeDuration::FromMilliseconds( 190 PROGRESS_TIMELINE_DURATION_MILLISEC), 191 elapsedDuration, animation.mStartTime.refOr(TimeDuration()), 192 animation.mPlaybackRate) 193 : dom::Animation::ProgressTimelinePosition::NotBoundary; 194 195 ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt( 196 elapsedDuration, animation.mTiming, animation.mPlaybackRate, 197 progressTimelinePosition); 198 199 if (computedTiming.mProgress.IsNull()) { 200 // For the scroll-driven animations, it's possible to let it go between 201 // the active phase and the before/after phase, and so its progress 202 // becomes null. In this case, we shouldn't just skip this animation. 203 // Instead, we have to reset the previous sampled result. Basically, we 204 // use |mProgressOnLastCompose| to check if it goes from the active phase. 205 // If so, we set the returned |mReason| to ScrollToDelayPhase to let the 206 // caller know we need to use the base style for this property. 207 // 208 // If there are any other animations which need to be sampled together 209 // (in the same property animation group), this |reason| will be ignored. 210 if (animation.mScrollTimelineOptions && 211 !animation.mProgressOnLastCompose.IsNull() && 212 (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before || 213 computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) { 214 // Appearally, we go back to delay, so need to reset the last 215 // composition meta data. This is necessary because 216 // 1. this animation is in delay so it shouldn't have any composition 217 // meta data, and 218 // 2. we will not go into this condition multiple times during delay 219 // phase because we rely on |mProgressOnLastCompose|. 220 animation.ResetLastCompositionValues(); 221 reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase; 222 } 223 continue; 224 } 225 226 dom::IterationCompositeOperation iterCompositeOperation = 227 animation.mIterationComposite; 228 229 // Skip calculation if the progress hasn't changed since the last 230 // calculation. 231 // Note that we don't skip calculate this animation if there is another 232 // animation since the other animation might be 'accumulate' or 'add', or 233 // might have a missing keyframe (i.e. this animation value will be used in 234 // the missing keyframe). 235 // FIXME Bug 1455476: We should do this optimizations for the case where 236 // the layer has multiple animations and multiple properties. 237 if (aCanSkipCompose == CanSkipCompose::IfPossible && 238 !dom::KeyframeEffect::HasComputedTimingChanged( 239 computedTiming, iterCompositeOperation, 240 animation.mProgressOnLastCompose, 241 animation.mCurrentIterationOnLastCompose)) { 242 #ifdef DEBUG 243 shouldBeSkipped = true; 244 #else 245 return AnimationHelper::SampleResult::Skipped(); 246 #endif 247 } 248 249 uint32_t segmentIndex = 0; 250 size_t segmentSize = animation.mSegments.Length(); 251 PropertyAnimation::SegmentData* segment = animation.mSegments.Elements(); 252 while (segment->mEndPortion < computedTiming.mProgress.Value() && 253 segmentIndex < segmentSize - 1) { 254 ++segment; 255 ++segmentIndex; 256 } 257 258 double positionInSegment = 259 (computedTiming.mProgress.Value() - segment->mStartPortion) / 260 (segment->mEndPortion - segment->mStartPortion); 261 262 double portion = StyleComputedTimingFunction::GetPortion( 263 segment->mFunction, positionInSegment, computedTiming.mBeforeFlag); 264 265 // Like above optimization, skip calculation if the target segment isn't 266 // changed and if the portion in the segment isn't changed. 267 // This optimization is needed for CSS animations/transitions with step 268 // timing functions (e.g. the throbber animation on tabs or frame based 269 // animations). 270 // FIXME Bug 1455476: Like the above optimization, we should apply this 271 // optimizations for multiple animation cases and multiple properties as 272 // well. 273 if (aCanSkipCompose == CanSkipCompose::IfPossible && 274 animation.mSegmentIndexOnLastCompose == segmentIndex && 275 !animation.mPortionInSegmentOnLastCompose.IsNull() && 276 animation.mPortionInSegmentOnLastCompose.Value() == portion) { 277 #ifdef DEBUG 278 shouldBeSkipped = true; 279 #else 280 return AnimationHelper::SampleResult::Skipped(); 281 #endif 282 } 283 284 AnimationPropertySegment animSegment; 285 animSegment.mFromKey = 0.0; 286 animSegment.mToKey = 1.0; 287 animSegment.mFromValue = AnimationValue(segment->mStartValue); 288 animSegment.mToValue = AnimationValue(segment->mEndValue); 289 animSegment.mFromComposite = segment->mStartComposite; 290 animSegment.mToComposite = segment->mEndComposite; 291 292 // interpolate the property 293 aAnimationValue = 294 Servo_ComposeAnimationSegment( 295 &animSegment, aAnimationValue, 296 animation.mSegments.LastElement().mEndValue, iterCompositeOperation, 297 portion, computedTiming.mCurrentIteration) 298 .Consume(); 299 300 #ifdef DEBUG 301 if (shouldBeSkipped) { 302 return AnimationHelper::SampleResult::Skipped(); 303 } 304 #endif 305 306 hasInEffectAnimations = true; 307 animation.mProgressOnLastCompose = computedTiming.mProgress; 308 animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration; 309 animation.mSegmentIndexOnLastCompose = segmentIndex; 310 animation.mPortionInSegmentOnLastCompose.SetValue(portion); 311 } 312 313 auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled() 314 : AnimationHelper::SampleResult(); 315 rv.mReason = reason; 316 return rv; 317 } 318 319 // This function samples the animations for a group of CSS properties. We may 320 // have multiple CSS properties in a group (e.g. transform-like properties). 321 // So the returned animation array, |aAnimationValues|, include all the 322 // animation values of these CSS properties. 323 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode( 324 const APZSampler* aAPZSampler, const LayersId& aLayersId, 325 const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime, 326 TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue, 327 nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups, 328 SampledAnimationArray& aAnimationValues /* output */) { 329 MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(), 330 "Should be called with animation data"); 331 MOZ_ASSERT(aAnimationValues.IsEmpty(), 332 "Should be called with empty aAnimationValues"); 333 334 nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations; 335 nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues; 336 for (PropertyAnimationGroup& group : aPropertyAnimationGroups) { 337 // Initialize animation value with base style. 338 RefPtr<StyleAnimationValue> currValue = group.mBaseStyle; 339 340 CanSkipCompose canSkipCompose = 341 aPreviousValue && aPropertyAnimationGroups.Length() == 1 && 342 group.mAnimations.Length() == 1 343 ? CanSkipCompose::IfPossible 344 : CanSkipCompose::No; 345 346 MOZ_ASSERT( 347 !group.mAnimations.IsEmpty() || 348 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty( 349 group.mProperty), 350 "Only transform-like properties can have empty PropertyAnimation list"); 351 352 // For properties which are not animating (i.e. their values are always the 353 // same), we store them in a different array, and then merge them into the 354 // final result (a.k.a. aAnimationValues) because we shouldn't take them 355 // into account for SampleResult. (In other words, these properties 356 // shouldn't affect the optimization.) 357 if (group.mAnimations.IsEmpty()) { 358 nonAnimatingValues.AppendElement(std::move(currValue)); 359 continue; 360 } 361 362 SampleResult result = SampleAnimationForProperty( 363 aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime, 364 aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations, 365 currValue); 366 367 // FIXME: Bug 1455476: Do optimization for multiple properties. For now, 368 // the result is skipped only if the property count == 1. 369 if (result.IsSkipped()) { 370 #ifdef DEBUG 371 aAnimationValues.AppendElement(std::move(currValue)); 372 #endif 373 return result; 374 } 375 376 if (!result.IsSampled()) { 377 if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) { 378 MOZ_ASSERT(currValue && currValue == group.mBaseStyle); 379 baseStyleOfDelayAnimations.AppendElement(std::move(currValue)); 380 } 381 continue; 382 } 383 384 // Insert the interpolation result into the output array. 385 MOZ_ASSERT(currValue); 386 aAnimationValues.AppendElement(std::move(currValue)); 387 } 388 389 SampleResult rv = 390 aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled(); 391 392 // If there is no other sampled result, we may store these base styles 393 // (together with the non-animating values) to the webrenderer before it gets 394 // sync with the main thread. 395 if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) { 396 aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations)); 397 rv.mReason = SampleResult::Reason::ScrollToDelayPhase; 398 } 399 400 if (!aAnimationValues.IsEmpty()) { 401 aAnimationValues.AppendElements(std::move(nonAnimatingValues)); 402 } 403 return rv; 404 } 405 406 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) { 407 // Adjust fill mode so that if the main thread is delayed in clearing 408 // this animation we don't introduce flicker by jumping back to the old 409 // underlying value. 410 auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode()); 411 float playbackRate = aAnimation.playbackRate(); 412 switch (fillMode) { 413 case dom::FillMode::None: 414 if (playbackRate > 0) { 415 fillMode = dom::FillMode::Forwards; 416 } else if (playbackRate < 0) { 417 fillMode = dom::FillMode::Backwards; 418 } 419 break; 420 case dom::FillMode::Backwards: 421 if (playbackRate > 0) { 422 fillMode = dom::FillMode::Both; 423 } 424 break; 425 case dom::FillMode::Forwards: 426 if (playbackRate < 0) { 427 fillMode = dom::FillMode::Both; 428 } 429 break; 430 default: 431 break; 432 } 433 return fillMode; 434 } 435 436 #ifdef DEBUG 437 static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) { 438 nsCSSPropertyIDSet transformSet = 439 nsCSSPropertyIDSet::TransformLikeProperties(); 440 441 for (const Animation& animation : aAnimations) { 442 if (animation.isNotAnimating()) { 443 continue; 444 } 445 446 if (transformSet.HasProperty(animation.property())) { 447 return true; 448 } 449 } 450 451 return false; 452 } 453 #endif 454 455 AnimationStorageData AnimationHelper::ExtractAnimations( 456 const LayersId& aLayersId, const AnimationArray& aAnimations, 457 const CompositorAnimationStorage* aStorage, 458 const TimeStamp& aPreviousSampleTime) { 459 AnimationStorageData storageData; 460 storageData.mLayersId = aLayersId; 461 462 NonCustomCSSPropertyId prevId = eCSSProperty_UNKNOWN; 463 PropertyAnimationGroup* currData = nullptr; 464 DebugOnly<const layers::Animatable*> currBaseStyle = nullptr; 465 466 for (const Animation& animation : aAnimations) { 467 // Animations with same property are grouped together, so we can just 468 // check if the current property is the same as the previous one for 469 // knowing this is a new group. 470 if (prevId != animation.property()) { 471 // Got a different group, we should create a different array. 472 currData = storageData.mAnimation.AppendElement(); 473 currData->mProperty = animation.property(); 474 if (animation.transformData()) { 475 MOZ_ASSERT(!storageData.mTransformData, 476 "Only one entry has TransformData"); 477 storageData.mTransformData = animation.transformData(); 478 } 479 480 prevId = animation.property(); 481 482 // Reset the debug pointer. 483 currBaseStyle = nullptr; 484 } 485 486 MOZ_ASSERT(currData); 487 if (animation.baseStyle().type() != Animatable::Tnull_t) { 488 MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(), 489 "Should be the same base style"); 490 491 currData->mBaseStyle = AnimationValue::FromAnimatable( 492 animation.property(), animation.baseStyle()); 493 currBaseStyle = &animation.baseStyle(); 494 } 495 496 // If this layers::Animation sets isNotAnimating to true, it only has 497 // base style and doesn't have any animation information, so we can skip 498 // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be 499 // an empty array.) 500 if (animation.isNotAnimating()) { 501 MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty( 502 animation.property()), 503 "Only transform-like properties could set this true"); 504 505 if (animation.property() == eCSSProperty_offset_path) { 506 MOZ_ASSERT(currData->mBaseStyle, 507 "Fixed offset-path should have base style"); 508 MOZ_ASSERT(HasTransformLikeAnimations(aAnimations)); 509 510 const StyleOffsetPath& offsetPath = 511 animation.baseStyle().get_StyleOffsetPath(); 512 // FIXME: Bug 1837042. Cache all basic shapes. 513 if (offsetPath.IsPath()) { 514 MOZ_ASSERT(!storageData.mCachedMotionPath, 515 "Only one offset-path: path() is set"); 516 517 RefPtr<gfx::PathBuilder> builder = 518 MotionPathUtils::GetCompositorPathBuilder(); 519 storageData.mCachedMotionPath = MotionPathUtils::BuildSVGPath( 520 offsetPath.AsSVGPathData(), builder); 521 } 522 } 523 524 continue; 525 } 526 527 PropertyAnimation* propertyAnimation = 528 currData->mAnimations.AppendElement(); 529 530 propertyAnimation->mOriginTime = animation.originTime(); 531 propertyAnimation->mStartTime = animation.startTime(); 532 propertyAnimation->mHoldTime = animation.holdTime(); 533 propertyAnimation->mPlaybackRate = animation.playbackRate(); 534 propertyAnimation->mIterationComposite = 535 static_cast<dom::IterationCompositeOperation>( 536 animation.iterationComposite()); 537 propertyAnimation->mIsNotPlaying = animation.isNotPlaying(); 538 propertyAnimation->mTiming = 539 TimingParams{animation.duration(), 540 animation.delay(), 541 animation.endDelay(), 542 animation.iterations(), 543 animation.iterationStart(), 544 static_cast<dom::PlaybackDirection>(animation.direction()), 545 GetAdjustedFillMode(animation), 546 animation.easingFunction()}; 547 propertyAnimation->mScrollTimelineOptions = 548 animation.scrollTimelineOptions(); 549 550 RefPtr<StyleAnimationValue> startValue; 551 if (animation.replacedTransitionId()) { 552 if (const auto* animatedValue = 553 aStorage->GetAnimatedValue(*animation.replacedTransitionId())) { 554 startValue = animatedValue->AsAnimationValue(animation.property()); 555 // Basically, the timeline time is increasing monotonically, so it may 556 // not make sense to have a negative start time (i.e. the case when 557 // aPreviousSampleTime is behind the origin time). Therefore, if the 558 // previous sample time is less than the origin time, we skip the 559 // replacement of the start time. 560 if (!aPreviousSampleTime.IsNull() && 561 (aPreviousSampleTime >= animation.originTime())) { 562 propertyAnimation->mStartTime = 563 Some(aPreviousSampleTime - animation.originTime()); 564 } 565 566 MOZ_ASSERT(animation.segments().Length() == 1, 567 "The CSS Transition only has one segement"); 568 } 569 } 570 571 nsTArray<PropertyAnimation::SegmentData>& segmentData = 572 propertyAnimation->mSegments; 573 for (const AnimationSegment& segment : animation.segments()) { 574 segmentData.AppendElement(PropertyAnimation::SegmentData{ 575 // Note that even though we re-compute the start value on the main 576 // thread, we still replace it with the last sampled value, to avoid 577 // any possible lag. 578 startValue ? startValue 579 : AnimationValue::FromAnimatable(animation.property(), 580 segment.startState()), 581 AnimationValue::FromAnimatable(animation.property(), 582 segment.endState()), 583 segment.sampleFn(), segment.startPortion(), segment.endPortion(), 584 static_cast<dom::CompositeOperation>(segment.startComposite()), 585 static_cast<dom::CompositeOperation>(segment.endComposite())}); 586 } 587 } 588 589 #ifdef DEBUG 590 // Sanity check that the grouped animation data is correct by looking at the 591 // property set. 592 if (!storageData.mAnimation.IsEmpty()) { 593 nsCSSPropertyIDSet seenProperties; 594 for (const auto& group : storageData.mAnimation) { 595 NonCustomCSSPropertyId id = group.mProperty; 596 597 MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property"); 598 seenProperties.AddProperty(id); 599 } 600 601 MOZ_ASSERT( 602 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( 603 DisplayItemType::TYPE_TRANSFORM)) || 604 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( 605 DisplayItemType::TYPE_OPACITY)) || 606 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( 607 DisplayItemType::TYPE_BACKGROUND_COLOR)), 608 "The property set of output should be the subset of transform-like " 609 "properties, opacity, or background_color."); 610 611 if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor( 612 DisplayItemType::TYPE_TRANSFORM))) { 613 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData"); 614 } 615 616 if (seenProperties.HasProperty(eCSSProperty_offset_path)) { 617 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData"); 618 MOZ_ASSERT(storageData.mTransformData->motionPathData(), 619 "Should have MotionPathData"); 620 } 621 } 622 #endif 623 624 return storageData; 625 } 626 627 uint64_t AnimationHelper::GetNextCompositorAnimationsId() { 628 static uint32_t sNextId = 0; 629 ++sNextId; 630 631 uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId()); 632 uint64_t nextId = procId; 633 nextId = nextId << 32 | sNextId; 634 return nextId; 635 } 636 637 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4( 638 const SampledAnimationArray& aValues, const TransformData& aTransformData, 639 gfx::Path* aCachedMotionPath) { 640 using nsStyleTransformMatrix::TransformReferenceBox; 641 642 // This is a bit silly just to avoid the transform list copy from the 643 // animation transform list. 644 auto noneTranslate = StyleTranslate::None(); 645 auto noneRotate = StyleRotate::None(); 646 auto noneScale = StyleScale::None(); 647 const StyleTransform noneTransform; 648 649 const StyleTranslate* translate = nullptr; 650 const StyleRotate* rotate = nullptr; 651 const StyleScale* scale = nullptr; 652 const StyleTransform* transform = nullptr; 653 Maybe<StyleOffsetPath> path; 654 const StyleLengthPercentage* distance = nullptr; 655 const StyleOffsetRotate* offsetRotate = nullptr; 656 const StylePositionOrAuto* anchor = nullptr; 657 const StyleOffsetPosition* position = nullptr; 658 659 for (const auto& value : aValues) { 660 MOZ_ASSERT(value); 661 CSSPropertyId property(eCSSProperty_UNKNOWN); 662 Servo_AnimationValue_GetPropertyId(value, &property); 663 switch (property.mId) { 664 case eCSSProperty_transform: 665 MOZ_ASSERT(!transform); 666 transform = Servo_AnimationValue_GetTransform(value); 667 break; 668 case eCSSProperty_translate: 669 MOZ_ASSERT(!translate); 670 translate = Servo_AnimationValue_GetTranslate(value); 671 break; 672 case eCSSProperty_rotate: 673 MOZ_ASSERT(!rotate); 674 rotate = Servo_AnimationValue_GetRotate(value); 675 break; 676 case eCSSProperty_scale: 677 MOZ_ASSERT(!scale); 678 scale = Servo_AnimationValue_GetScale(value); 679 break; 680 case eCSSProperty_offset_path: 681 MOZ_ASSERT(!path); 682 path.emplace(StyleOffsetPath::None()); 683 Servo_AnimationValue_GetOffsetPath(value, path.ptr()); 684 break; 685 case eCSSProperty_offset_distance: 686 MOZ_ASSERT(!distance); 687 distance = Servo_AnimationValue_GetOffsetDistance(value); 688 break; 689 case eCSSProperty_offset_rotate: 690 MOZ_ASSERT(!offsetRotate); 691 offsetRotate = Servo_AnimationValue_GetOffsetRotate(value); 692 break; 693 case eCSSProperty_offset_anchor: 694 MOZ_ASSERT(!anchor); 695 anchor = Servo_AnimationValue_GetOffsetAnchor(value); 696 break; 697 case eCSSProperty_offset_position: 698 MOZ_ASSERT(!position); 699 position = Servo_AnimationValue_GetOffsetPosition(value); 700 break; 701 default: 702 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property"); 703 } 704 } 705 706 TransformReferenceBox refBox(nullptr, aTransformData.bounds()); 707 Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath( 708 path.ptrOr(nullptr), distance, offsetRotate, anchor, position, 709 aTransformData.motionPathData(), refBox, aCachedMotionPath); 710 711 // We expect all our transform data to arrive in device pixels 712 gfx::Point3D transformOrigin = aTransformData.transformOrigin(); 713 nsDisplayTransform::FrameTransformProperties props( 714 translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate, 715 scale ? *scale : noneScale, transform ? *transform : noneTransform, 716 motion, transformOrigin); 717 718 return nsDisplayTransform::GetResultingTransformMatrix( 719 props, refBox, aTransformData.appUnitsPerDevPixel()); 720 } 721 722 static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad, 723 SideBits aOverflowSides, 724 gfxLineSegment sideLines[4]) { 725 uint8_t count = 0; 726 727 if (aOverflowSides & SideBits::eTop) { 728 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0], 729 aPrerenderedQuad.mPoints[1]); 730 count++; 731 } 732 if (aOverflowSides & SideBits::eRight) { 733 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1], 734 aPrerenderedQuad.mPoints[2]); 735 count++; 736 } 737 if (aOverflowSides & SideBits::eBottom) { 738 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2], 739 aPrerenderedQuad.mPoints[3]); 740 count++; 741 } 742 if (aOverflowSides & SideBits::eLeft) { 743 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3], 744 aPrerenderedQuad.mPoints[0]); 745 count++; 746 } 747 748 return count; 749 } 750 751 enum RegionBits : uint8_t { 752 Inside = 0, 753 Left = (1 << 0), 754 Right = (1 << 1), 755 Bottom = (1 << 2), 756 Top = (1 << 3), 757 }; 758 759 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits); 760 761 static RegionBits GetRegionBitsForPoint(double aX, double aY, 762 const gfxRect& aClip) { 763 RegionBits result = RegionBits::Inside; 764 if (aX < aClip.X()) { 765 result |= RegionBits::Left; 766 } else if (aX > aClip.XMost()) { 767 result |= RegionBits::Right; 768 } 769 770 if (aY < aClip.Y()) { 771 result |= RegionBits::Bottom; 772 } else if (aY > aClip.YMost()) { 773 result |= RegionBits::Top; 774 } 775 return result; 776 }; 777 778 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm 779 static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1, 780 double aY1, const gfxRect& aClip) { 781 RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip); 782 RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip); 783 784 while (true) { 785 if (!(b0 | b1)) { 786 // Completely inside. 787 return true; 788 } 789 790 if (b0 & b1) { 791 // Completely outside. 792 return false; 793 } 794 795 double x, y; 796 // Choose an outside point. 797 RegionBits outsidePointBits = b1 > b0 ? b1 : b0; 798 if (outsidePointBits & RegionBits::Top) { 799 x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0); 800 y = aClip.YMost(); 801 } else if (outsidePointBits & RegionBits::Bottom) { 802 x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0); 803 y = aClip.Y(); 804 } else if (outsidePointBits & RegionBits::Right) { 805 y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0); 806 x = aClip.XMost(); 807 } else if (outsidePointBits & RegionBits::Left) { 808 y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0); 809 x = aClip.X(); 810 } 811 812 if (outsidePointBits == b0) { 813 aX0 = x; 814 aY0 = y; 815 b0 = GetRegionBitsForPoint(aX0, aY0, aClip); 816 } else { 817 aX1 = x; 818 aY1 = y; 819 b1 = GetRegionBitsForPoint(aX1, aY1, aClip); 820 } 821 } 822 MOZ_ASSERT_UNREACHABLE(); 823 return false; 824 } 825 826 // static 827 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect, 828 SideBits aOverflowSides, 829 const gfx::Matrix4x4& aTransform, 830 const ParentLayerRect& aClipRect) { 831 if (aClipRect.IsEmpty()) { 832 return false; 833 } 834 835 gfxQuad prerenderedQuad = gfxUtils::TransformToQuad( 836 ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform); 837 838 gfxLineSegment sideLines[4]; 839 uint8_t overflowSideCount = 840 CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines); 841 842 gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect()); 843 for (uint8_t j = 0; j < overflowSideCount; j++) { 844 if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y, 845 sideLines[j].mEnd.x, sideLines[j].mEnd.y, 846 clipRect)) { 847 return true; 848 } 849 } 850 851 // With step timing functions there are cases the transform jumps to a 852 // position where the partial pre-render area is totally outside of the clip 853 // rect without any intersection of the partial pre-render area and the clip 854 // rect happened in previous compositions but there remains visible area of 855 // the entire transformed area. 856 // 857 // So now all four points of the transformed partial pre-render rect are 858 // outside of the clip rect, if all these four points are in either side of 859 // the clip rect, we consider it's jank so that on the main-thread we will 860 // either a) rebuild the up-to-date display item if there remains visible area 861 // or b) no longer rebuild the display item if it's totally outside of the 862 // clip rect. 863 // 864 // Note that RegionBits::Left and Right are mutually exclusive, 865 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains 866 // any bits, it means all four points are in the same side. 867 return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x, 868 prerenderedQuad.mPoints[0].y, clipRect) & 869 GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x, 870 prerenderedQuad.mPoints[1].y, clipRect) & 871 GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x, 872 prerenderedQuad.mPoints[2].y, clipRect) & 873 GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x, 874 prerenderedQuad.mPoints[3].y, clipRect); 875 } 876 877 } // namespace mozilla::layers