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 "AnimationHelper.h"
8 #include "base/process_util.h"
9 #include "gfx2DGlue.h" // for ThebesRect
10 #include "gfxLineSegment.h" // for gfxLineSegment
11 #include "gfxPoint.h" // for gfxPoint
12 #include "gfxQuad.h" // for gfxQuad
13 #include "gfxRect.h" // for gfxRect
14 #include "gfxUtils.h" // for gfxUtils::TransformToQuad
15 #include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
16 #include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode
17 #include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
18 #include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly
19 #include "mozilla/dom/Nullable.h" // for dom::Nullable
20 #include "mozilla/layers/APZSampler.h" // for APZSampler
21 #include "mozilla/AnimatedPropertyID.h"
22 #include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
23 #include "mozilla/Maybe.h" // for Maybe<>
24 #include "mozilla/MotionPathUtils.h" // for ResolveMotionPath()
25 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
26 #include "nsCSSPropertyID.h" // for eCSSProperty_offset_path, etc
27 #include "nsDisplayList.h" // for nsDisplayTransform, etc
29 namespace mozilla::layers
{
31 static dom::Nullable
<TimeDuration
> CalculateElapsedTimeForScrollTimeline(
32 const Maybe
<APZSampler::ScrollOffsetAndRange
> aScrollMeta
,
33 const ScrollTimelineOptions
& aOptions
, const StickyTimeDuration
& aEndTime
,
34 const TimeDuration
& aStartTime
, float aPlaybackRate
) {
35 // We return Nothing If the associated APZ controller is not available
36 // (because it may be destroyed but this animation is still alive).
38 // This may happen after we reload a page. There may be a race condition
39 // because the animation is still alive but the APZ is destroyed. In this
40 // case, this animation is invalid, so we return nullptr.
44 const bool isHorizontal
=
45 aOptions
.axis() == layers::ScrollDirection::eHorizontal
;
47 isHorizontal
? aScrollMeta
->mRange
.width
: aScrollMeta
->mRange
.height
;
50 "We don't expect to get a zero or negative range on the compositor");
52 // The offset may be negative if the writing mode is from right to left.
53 // Use std::abs() here to avoid getting a negative progress.
55 std::abs(isHorizontal
? aScrollMeta
->mOffset
.x
: aScrollMeta
->mOffset
.y
);
56 double progress
= position
/ range
;
57 // Just in case to avoid getting a progress more than 100%, for overscrolling.
58 progress
= std::min(progress
, 1.0);
59 auto timelineTime
= TimeDuration(aEndTime
.MultDouble(progress
));
60 return dom::Animation::CurrentTimeFromTimelineTime(timelineTime
, aStartTime
,
64 static dom::Nullable
<TimeDuration
> CalculateElapsedTime(
65 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
66 const MutexAutoLock
& aProofOfMapLock
, const PropertyAnimation
& aAnimation
,
67 const TimeStamp aPreviousFrameTime
, const TimeStamp aCurrentFrameTime
,
68 const AnimatedValue
* aPreviousValue
) {
69 // -------------------------------------
70 // Case 1: scroll-timeline animations.
71 // -------------------------------------
72 if (aAnimation
.mScrollTimelineOptions
) {
75 "We don't send scroll animations to the compositor if APZ is disabled");
77 return CalculateElapsedTimeForScrollTimeline(
78 aAPZSampler
->GetCurrentScrollOffsetAndRange(
79 aLayersId
, aAnimation
.mScrollTimelineOptions
.value().source(),
81 aAnimation
.mScrollTimelineOptions
.value(), aAnimation
.mTiming
.EndTime(),
82 aAnimation
.mStartTime
.refOr(aAnimation
.mHoldTime
),
83 aAnimation
.mPlaybackRate
);
86 // -------------------------------------
87 // Case 2: document-timeline animations.
88 // -------------------------------------
90 (!aAnimation
.mOriginTime
.IsNull() && aAnimation
.mStartTime
.isSome()) ||
91 aAnimation
.mIsNotPlaying
,
92 "If we are playing, we should have an origin time and a start time");
94 // Determine if the animation was play-pending and used a ready time later
95 // than the previous frame time.
97 // To determine this, _all_ of the following conditions need to hold:
99 // * There was no previous animation value (i.e. this is the first frame for
100 // the animation since it was sent to the compositor), and
101 // * The animation is playing, and
102 // * There is a previous frame time, and
103 // * The ready time of the animation is ahead of the previous frame time.
105 bool hasFutureReadyTime
= false;
106 if (!aPreviousValue
&& !aAnimation
.mIsNotPlaying
&&
107 !aPreviousFrameTime
.IsNull()) {
108 // This is the inverse of the calculation performed in
109 // AnimationInfo::StartPendingAnimations to calculate the start time of
110 // play-pending animations.
111 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
112 // underflow in the middle of the calulation.
113 const TimeStamp readyTime
=
114 aAnimation
.mOriginTime
+
115 (aAnimation
.mStartTime
.ref() +
116 aAnimation
.mHoldTime
.MultDouble(1.0 / aAnimation
.mPlaybackRate
));
117 hasFutureReadyTime
= !readyTime
.IsNull() && readyTime
> aPreviousFrameTime
;
119 // Use the previous vsync time to make main thread animations and compositor
120 // more closely aligned.
122 // On the first frame where we have animations the previous timestamp will
123 // not be set so we simply use the current timestamp. As a result we will
124 // end up painting the first frame twice. That doesn't appear to be
125 // noticeable, however.
127 // Likewise, if the animation is play-pending, it may have a ready time that
128 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
129 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
130 // jumping backwards into the range prior to when the animation starts.
131 const TimeStamp
& timeStamp
= aPreviousFrameTime
.IsNull() || hasFutureReadyTime
133 : aPreviousFrameTime
;
135 // If the animation is not currently playing, e.g. paused or
136 // finished, then use the hold time to stay at the same position.
137 TimeDuration elapsedDuration
=
138 aAnimation
.mIsNotPlaying
|| aAnimation
.mStartTime
.isNothing()
139 ? aAnimation
.mHoldTime
140 : (timeStamp
- aAnimation
.mOriginTime
- aAnimation
.mStartTime
.ref())
141 .MultDouble(aAnimation
.mPlaybackRate
);
142 return elapsedDuration
;
145 enum class CanSkipCompose
{
149 // This function samples the animation for a specific property. We may have
150 // multiple animations for a single property, and the later animations override
151 // the eariler ones. This function returns the sampled animation value,
152 // |aAnimationValue| for a single CSS property.
153 static AnimationHelper::SampleResult
SampleAnimationForProperty(
154 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
155 const MutexAutoLock
& aProofOfMapLock
, TimeStamp aPreviousFrameTime
,
156 TimeStamp aCurrentFrameTime
, const AnimatedValue
* aPreviousValue
,
157 CanSkipCompose aCanSkipCompose
,
158 nsTArray
<PropertyAnimation
>& aPropertyAnimations
,
159 RefPtr
<StyleAnimationValue
>& aAnimationValue
) {
160 MOZ_ASSERT(!aPropertyAnimations
.IsEmpty(), "Should have animations");
162 auto reason
= AnimationHelper::SampleResult::Reason::None
;
163 bool hasInEffectAnimations
= false;
165 // In cases where this function returns a SampleResult::Skipped, we actually
166 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
167 // call site that the value that would have been computed matches the stored
168 // value that we end up using. This flag is used to ensure we populate
169 // aAnimationValue in this scenario.
170 bool shouldBeSkipped
= false;
172 // Process in order, since later animations override earlier ones.
173 for (PropertyAnimation
& animation
: aPropertyAnimations
) {
174 dom::Nullable
<TimeDuration
> elapsedDuration
= CalculateElapsedTime(
175 aAPZSampler
, aLayersId
, aProofOfMapLock
, animation
, aPreviousFrameTime
,
176 aCurrentFrameTime
, aPreviousValue
);
178 const auto progressTimelinePosition
=
179 animation
.mScrollTimelineOptions
180 ? dom::Animation::AtProgressTimelineBoundary(
181 TimeDuration::FromMilliseconds(
182 PROGRESS_TIMELINE_DURATION_MILLISEC
),
183 elapsedDuration
, animation
.mStartTime
.refOr(TimeDuration()),
184 animation
.mPlaybackRate
)
185 : dom::Animation::ProgressTimelinePosition::NotBoundary
;
187 ComputedTiming computedTiming
= dom::AnimationEffect::GetComputedTimingAt(
188 elapsedDuration
, animation
.mTiming
, animation
.mPlaybackRate
,
189 progressTimelinePosition
);
191 if (computedTiming
.mProgress
.IsNull()) {
192 // For the scroll-driven animations, it's possible to let it go between
193 // the active phase and the before/after phase, and so its progress
194 // becomes null. In this case, we shouldn't just skip this animation.
195 // Instead, we have to reset the previous sampled result. Basically, we
196 // use |mProgressOnLastCompose| to check if it goes from the active phase.
197 // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
198 // caller know we need to use the base style for this property.
200 // If there are any other animations which need to be sampled together
201 // (in the same property animation group), this |reason| will be ignored.
202 if (animation
.mScrollTimelineOptions
&&
203 !animation
.mProgressOnLastCompose
.IsNull() &&
204 (computedTiming
.mPhase
== ComputedTiming::AnimationPhase::Before
||
205 computedTiming
.mPhase
== ComputedTiming::AnimationPhase::After
)) {
206 // Appearally, we go back to delay, so need to reset the last
207 // composition meta data. This is necessary because
208 // 1. this animation is in delay so it shouldn't have any composition
210 // 2. we will not go into this condition multiple times during delay
211 // phase because we rely on |mProgressOnLastCompose|.
212 animation
.ResetLastCompositionValues();
213 reason
= AnimationHelper::SampleResult::Reason::ScrollToDelayPhase
;
218 dom::IterationCompositeOperation iterCompositeOperation
=
219 animation
.mIterationComposite
;
221 // Skip calculation if the progress hasn't changed since the last
223 // Note that we don't skip calculate this animation if there is another
224 // animation since the other animation might be 'accumulate' or 'add', or
225 // might have a missing keyframe (i.e. this animation value will be used in
226 // the missing keyframe).
227 // FIXME Bug 1455476: We should do this optimizations for the case where
228 // the layer has multiple animations and multiple properties.
229 if (aCanSkipCompose
== CanSkipCompose::IfPossible
&&
230 !dom::KeyframeEffect::HasComputedTimingChanged(
231 computedTiming
, iterCompositeOperation
,
232 animation
.mProgressOnLastCompose
,
233 animation
.mCurrentIterationOnLastCompose
)) {
235 shouldBeSkipped
= true;
237 return AnimationHelper::SampleResult::Skipped();
241 uint32_t segmentIndex
= 0;
242 size_t segmentSize
= animation
.mSegments
.Length();
243 PropertyAnimation::SegmentData
* segment
= animation
.mSegments
.Elements();
244 while (segment
->mEndPortion
< computedTiming
.mProgress
.Value() &&
245 segmentIndex
< segmentSize
- 1) {
250 double positionInSegment
=
251 (computedTiming
.mProgress
.Value() - segment
->mStartPortion
) /
252 (segment
->mEndPortion
- segment
->mStartPortion
);
254 double portion
= StyleComputedTimingFunction::GetPortion(
255 segment
->mFunction
, positionInSegment
, computedTiming
.mBeforeFlag
);
257 // Like above optimization, skip calculation if the target segment isn't
258 // changed and if the portion in the segment isn't changed.
259 // This optimization is needed for CSS animations/transitions with step
260 // timing functions (e.g. the throbber animation on tabs or frame based
262 // FIXME Bug 1455476: Like the above optimization, we should apply this
263 // optimizations for multiple animation cases and multiple properties as
265 if (aCanSkipCompose
== CanSkipCompose::IfPossible
&&
266 animation
.mSegmentIndexOnLastCompose
== segmentIndex
&&
267 !animation
.mPortionInSegmentOnLastCompose
.IsNull() &&
268 animation
.mPortionInSegmentOnLastCompose
.Value() == portion
) {
270 shouldBeSkipped
= true;
272 return AnimationHelper::SampleResult::Skipped();
276 AnimationPropertySegment animSegment
;
277 animSegment
.mFromKey
= 0.0;
278 animSegment
.mToKey
= 1.0;
279 animSegment
.mFromValue
= AnimationValue(segment
->mStartValue
);
280 animSegment
.mToValue
= AnimationValue(segment
->mEndValue
);
281 animSegment
.mFromComposite
= segment
->mStartComposite
;
282 animSegment
.mToComposite
= segment
->mEndComposite
;
284 // interpolate the property
286 Servo_ComposeAnimationSegment(
287 &animSegment
, aAnimationValue
,
288 animation
.mSegments
.LastElement().mEndValue
, iterCompositeOperation
,
289 portion
, computedTiming
.mCurrentIteration
)
293 if (shouldBeSkipped
) {
294 return AnimationHelper::SampleResult::Skipped();
298 hasInEffectAnimations
= true;
299 animation
.mProgressOnLastCompose
= computedTiming
.mProgress
;
300 animation
.mCurrentIterationOnLastCompose
= computedTiming
.mCurrentIteration
;
301 animation
.mSegmentIndexOnLastCompose
= segmentIndex
;
302 animation
.mPortionInSegmentOnLastCompose
.SetValue(portion
);
305 auto rv
= hasInEffectAnimations
? AnimationHelper::SampleResult::Sampled()
306 : AnimationHelper::SampleResult();
311 // This function samples the animations for a group of CSS properties. We may
312 // have multiple CSS properties in a group (e.g. transform-like properties).
313 // So the returned animation array, |aAnimationValues|, include all the
314 // animation values of these CSS properties.
315 AnimationHelper::SampleResult
AnimationHelper::SampleAnimationForEachNode(
316 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
317 const MutexAutoLock
& aProofOfMapLock
, TimeStamp aPreviousFrameTime
,
318 TimeStamp aCurrentFrameTime
, const AnimatedValue
* aPreviousValue
,
319 nsTArray
<PropertyAnimationGroup
>& aPropertyAnimationGroups
,
320 nsTArray
<RefPtr
<StyleAnimationValue
>>& aAnimationValues
/* out */) {
321 MOZ_ASSERT(!aPropertyAnimationGroups
.IsEmpty(),
322 "Should be called with animation data");
323 MOZ_ASSERT(aAnimationValues
.IsEmpty(),
324 "Should be called with empty aAnimationValues");
326 nsTArray
<RefPtr
<StyleAnimationValue
>> baseStyleOfDelayAnimations
;
327 nsTArray
<RefPtr
<StyleAnimationValue
>> nonAnimatingValues
;
328 for (PropertyAnimationGroup
& group
: aPropertyAnimationGroups
) {
329 // Initialize animation value with base style.
330 RefPtr
<StyleAnimationValue
> currValue
= group
.mBaseStyle
;
332 CanSkipCompose canSkipCompose
=
333 aPreviousValue
&& aPropertyAnimationGroups
.Length() == 1 &&
334 group
.mAnimations
.Length() == 1
335 ? CanSkipCompose::IfPossible
336 : CanSkipCompose::No
;
339 !group
.mAnimations
.IsEmpty() ||
340 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
342 "Only transform-like properties can have empty PropertyAnimation list");
344 // For properties which are not animating (i.e. their values are always the
345 // same), we store them in a different array, and then merge them into the
346 // final result (a.k.a. aAnimationValues) because we shouldn't take them
347 // into account for SampleResult. (In other words, these properties
348 // shouldn't affect the optimization.)
349 if (group
.mAnimations
.IsEmpty()) {
350 nonAnimatingValues
.AppendElement(std::move(currValue
));
354 SampleResult result
= SampleAnimationForProperty(
355 aAPZSampler
, aLayersId
, aProofOfMapLock
, aPreviousFrameTime
,
356 aCurrentFrameTime
, aPreviousValue
, canSkipCompose
, group
.mAnimations
,
359 // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
360 // the result is skipped only if the property count == 1.
361 if (result
.IsSkipped()) {
363 aAnimationValues
.AppendElement(std::move(currValue
));
368 if (!result
.IsSampled()) {
369 if (result
.mReason
== SampleResult::Reason::ScrollToDelayPhase
) {
370 MOZ_ASSERT(currValue
&& currValue
== group
.mBaseStyle
);
371 baseStyleOfDelayAnimations
.AppendElement(std::move(currValue
));
376 // Insert the interpolation result into the output array.
377 MOZ_ASSERT(currValue
);
378 aAnimationValues
.AppendElement(std::move(currValue
));
382 aAnimationValues
.IsEmpty() ? SampleResult() : SampleResult::Sampled();
384 // If there is no other sampled result, we may store these base styles
385 // (together with the non-animating values) to the webrenderer before it gets
386 // sync with the main thread.
387 if (rv
.IsNone() && !baseStyleOfDelayAnimations
.IsEmpty()) {
388 aAnimationValues
.AppendElements(std::move(baseStyleOfDelayAnimations
));
389 rv
.mReason
= SampleResult::Reason::ScrollToDelayPhase
;
392 if (!aAnimationValues
.IsEmpty()) {
393 aAnimationValues
.AppendElements(std::move(nonAnimatingValues
));
398 static dom::FillMode
GetAdjustedFillMode(const Animation
& aAnimation
) {
399 // Adjust fill mode so that if the main thread is delayed in clearing
400 // this animation we don't introduce flicker by jumping back to the old
402 auto fillMode
= static_cast<dom::FillMode
>(aAnimation
.fillMode());
403 float playbackRate
= aAnimation
.playbackRate();
405 case dom::FillMode::None
:
406 if (playbackRate
> 0) {
407 fillMode
= dom::FillMode::Forwards
;
408 } else if (playbackRate
< 0) {
409 fillMode
= dom::FillMode::Backwards
;
412 case dom::FillMode::Backwards
:
413 if (playbackRate
> 0) {
414 fillMode
= dom::FillMode::Both
;
417 case dom::FillMode::Forwards
:
418 if (playbackRate
< 0) {
419 fillMode
= dom::FillMode::Both
;
429 static bool HasTransformLikeAnimations(const AnimationArray
& aAnimations
) {
430 nsCSSPropertyIDSet transformSet
=
431 nsCSSPropertyIDSet::TransformLikeProperties();
433 for (const Animation
& animation
: aAnimations
) {
434 if (animation
.isNotAnimating()) {
438 if (transformSet
.HasProperty(animation
.property())) {
447 AnimationStorageData
AnimationHelper::ExtractAnimations(
448 const LayersId
& aLayersId
, const AnimationArray
& aAnimations
) {
449 AnimationStorageData storageData
;
450 storageData
.mLayersId
= aLayersId
;
452 nsCSSPropertyID prevID
= eCSSProperty_UNKNOWN
;
453 PropertyAnimationGroup
* currData
= nullptr;
454 DebugOnly
<const layers::Animatable
*> currBaseStyle
= nullptr;
456 for (const Animation
& animation
: aAnimations
) {
457 // Animations with same property are grouped together, so we can just
458 // check if the current property is the same as the previous one for
459 // knowing this is a new group.
460 if (prevID
!= animation
.property()) {
461 // Got a different group, we should create a different array.
462 currData
= storageData
.mAnimation
.AppendElement();
463 currData
->mProperty
= animation
.property();
464 if (animation
.transformData()) {
465 MOZ_ASSERT(!storageData
.mTransformData
,
466 "Only one entry has TransformData");
467 storageData
.mTransformData
= animation
.transformData();
470 prevID
= animation
.property();
472 // Reset the debug pointer.
473 currBaseStyle
= nullptr;
476 MOZ_ASSERT(currData
);
477 if (animation
.baseStyle().type() != Animatable::Tnull_t
) {
478 MOZ_ASSERT(!currBaseStyle
|| *currBaseStyle
== animation
.baseStyle(),
479 "Should be the same base style");
481 currData
->mBaseStyle
= AnimationValue::FromAnimatable(
482 animation
.property(), animation
.baseStyle());
483 currBaseStyle
= &animation
.baseStyle();
486 // If this layers::Animation sets isNotAnimating to true, it only has
487 // base style and doesn't have any animation information, so we can skip
488 // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
490 if (animation
.isNotAnimating()) {
491 MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
492 animation
.property()),
493 "Only transform-like properties could set this true");
495 if (animation
.property() == eCSSProperty_offset_path
) {
496 MOZ_ASSERT(currData
->mBaseStyle
,
497 "Fixed offset-path should have base style");
498 MOZ_ASSERT(HasTransformLikeAnimations(aAnimations
));
500 const StyleOffsetPath
& offsetPath
=
501 animation
.baseStyle().get_StyleOffsetPath();
502 // FIXME: Bug 1837042. Cache all basic shapes.
503 if (offsetPath
.IsPath()) {
504 MOZ_ASSERT(!storageData
.mCachedMotionPath
,
505 "Only one offset-path: path() is set");
507 RefPtr
<gfx::PathBuilder
> builder
=
508 MotionPathUtils::GetCompositorPathBuilder();
509 storageData
.mCachedMotionPath
= MotionPathUtils::BuildSVGPath(
510 offsetPath
.AsSVGPathData(), builder
);
517 PropertyAnimation
* propertyAnimation
=
518 currData
->mAnimations
.AppendElement();
520 propertyAnimation
->mOriginTime
= animation
.originTime();
521 propertyAnimation
->mStartTime
= animation
.startTime();
522 propertyAnimation
->mHoldTime
= animation
.holdTime();
523 propertyAnimation
->mPlaybackRate
= animation
.playbackRate();
524 propertyAnimation
->mIterationComposite
=
525 static_cast<dom::IterationCompositeOperation
>(
526 animation
.iterationComposite());
527 propertyAnimation
->mIsNotPlaying
= animation
.isNotPlaying();
528 propertyAnimation
->mTiming
=
529 TimingParams
{animation
.duration(),
531 animation
.endDelay(),
532 animation
.iterations(),
533 animation
.iterationStart(),
534 static_cast<dom::PlaybackDirection
>(animation
.direction()),
535 GetAdjustedFillMode(animation
),
536 animation
.easingFunction()};
537 propertyAnimation
->mScrollTimelineOptions
=
538 animation
.scrollTimelineOptions();
540 nsTArray
<PropertyAnimation::SegmentData
>& segmentData
=
541 propertyAnimation
->mSegments
;
542 for (const AnimationSegment
& segment
: animation
.segments()) {
543 segmentData
.AppendElement(PropertyAnimation::SegmentData
{
544 AnimationValue::FromAnimatable(animation
.property(),
545 segment
.startState()),
546 AnimationValue::FromAnimatable(animation
.property(),
548 segment
.sampleFn(), segment
.startPortion(), segment
.endPortion(),
549 static_cast<dom::CompositeOperation
>(segment
.startComposite()),
550 static_cast<dom::CompositeOperation
>(segment
.endComposite())});
555 // Sanity check that the grouped animation data is correct by looking at the
557 if (!storageData
.mAnimation
.IsEmpty()) {
558 nsCSSPropertyIDSet seenProperties
;
559 for (const auto& group
: storageData
.mAnimation
) {
560 nsCSSPropertyID id
= group
.mProperty
;
562 MOZ_ASSERT(!seenProperties
.HasProperty(id
), "Should be a new property");
563 seenProperties
.AddProperty(id
);
567 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
568 DisplayItemType::TYPE_TRANSFORM
)) ||
569 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
570 DisplayItemType::TYPE_OPACITY
)) ||
571 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
572 DisplayItemType::TYPE_BACKGROUND_COLOR
)),
573 "The property set of output should be the subset of transform-like "
574 "properties, opacity, or background_color.");
576 if (seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
577 DisplayItemType::TYPE_TRANSFORM
))) {
578 MOZ_ASSERT(storageData
.mTransformData
, "Should have TransformData");
581 if (seenProperties
.HasProperty(eCSSProperty_offset_path
)) {
582 MOZ_ASSERT(storageData
.mTransformData
, "Should have TransformData");
583 MOZ_ASSERT(storageData
.mTransformData
->motionPathData(),
584 "Should have MotionPathData");
592 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
593 static uint32_t sNextId
= 0;
596 uint32_t procId
= static_cast<uint32_t>(base::GetCurrentProcId());
597 uint64_t nextId
= procId
;
598 nextId
= nextId
<< 32 | sNextId
;
602 gfx::Matrix4x4
AnimationHelper::ServoAnimationValueToMatrix4x4(
603 const nsTArray
<RefPtr
<StyleAnimationValue
>>& aValues
,
604 const TransformData
& aTransformData
, gfx::Path
* aCachedMotionPath
) {
605 using nsStyleTransformMatrix::TransformReferenceBox
;
607 // This is a bit silly just to avoid the transform list copy from the
608 // animation transform list.
609 auto noneTranslate
= StyleTranslate::None();
610 auto noneRotate
= StyleRotate::None();
611 auto noneScale
= StyleScale::None();
612 const StyleTransform noneTransform
;
614 const StyleTranslate
* translate
= nullptr;
615 const StyleRotate
* rotate
= nullptr;
616 const StyleScale
* scale
= nullptr;
617 const StyleTransform
* transform
= nullptr;
618 Maybe
<StyleOffsetPath
> path
;
619 const StyleLengthPercentage
* distance
= nullptr;
620 const StyleOffsetRotate
* offsetRotate
= nullptr;
621 const StylePositionOrAuto
* anchor
= nullptr;
622 const StyleOffsetPosition
* position
= nullptr;
624 for (const auto& value
: aValues
) {
626 AnimatedPropertyID
property(eCSSProperty_UNKNOWN
);
627 Servo_AnimationValue_GetPropertyId(value
, &property
);
628 switch (property
.mID
) {
629 case eCSSProperty_transform
:
630 MOZ_ASSERT(!transform
);
631 transform
= Servo_AnimationValue_GetTransform(value
);
633 case eCSSProperty_translate
:
634 MOZ_ASSERT(!translate
);
635 translate
= Servo_AnimationValue_GetTranslate(value
);
637 case eCSSProperty_rotate
:
639 rotate
= Servo_AnimationValue_GetRotate(value
);
641 case eCSSProperty_scale
:
643 scale
= Servo_AnimationValue_GetScale(value
);
645 case eCSSProperty_offset_path
:
647 path
.emplace(StyleOffsetPath::None());
648 Servo_AnimationValue_GetOffsetPath(value
, path
.ptr());
650 case eCSSProperty_offset_distance
:
651 MOZ_ASSERT(!distance
);
652 distance
= Servo_AnimationValue_GetOffsetDistance(value
);
654 case eCSSProperty_offset_rotate
:
655 MOZ_ASSERT(!offsetRotate
);
656 offsetRotate
= Servo_AnimationValue_GetOffsetRotate(value
);
658 case eCSSProperty_offset_anchor
:
660 anchor
= Servo_AnimationValue_GetOffsetAnchor(value
);
662 case eCSSProperty_offset_position
:
663 MOZ_ASSERT(!position
);
664 position
= Servo_AnimationValue_GetOffsetPosition(value
);
667 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
671 TransformReferenceBox
refBox(nullptr, aTransformData
.bounds());
672 Maybe
<ResolvedMotionPathData
> motion
= MotionPathUtils::ResolveMotionPath(
673 path
.ptrOr(nullptr), distance
, offsetRotate
, anchor
, position
,
674 aTransformData
.motionPathData(), refBox
, aCachedMotionPath
);
676 // We expect all our transform data to arrive in device pixels
677 gfx::Point3D transformOrigin
= aTransformData
.transformOrigin();
678 nsDisplayTransform::FrameTransformProperties
props(
679 translate
? *translate
: noneTranslate
, rotate
? *rotate
: noneRotate
,
680 scale
? *scale
: noneScale
, transform
? *transform
: noneTransform
,
681 motion
, transformOrigin
);
683 return nsDisplayTransform::GetResultingTransformMatrix(
684 props
, refBox
, aTransformData
.appUnitsPerDevPixel());
687 static uint8_t CollectOverflowedSideLines(const gfxQuad
& aPrerenderedQuad
,
688 SideBits aOverflowSides
,
689 gfxLineSegment sideLines
[4]) {
692 if (aOverflowSides
& SideBits::eTop
) {
693 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[0],
694 aPrerenderedQuad
.mPoints
[1]);
697 if (aOverflowSides
& SideBits::eRight
) {
698 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[1],
699 aPrerenderedQuad
.mPoints
[2]);
702 if (aOverflowSides
& SideBits::eBottom
) {
703 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[2],
704 aPrerenderedQuad
.mPoints
[3]);
707 if (aOverflowSides
& SideBits::eLeft
) {
708 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[3],
709 aPrerenderedQuad
.mPoints
[0]);
716 enum RegionBits
: uint8_t {
724 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits
);
726 static RegionBits
GetRegionBitsForPoint(double aX
, double aY
,
727 const gfxRect
& aClip
) {
728 RegionBits result
= RegionBits::Inside
;
729 if (aX
< aClip
.X()) {
730 result
|= RegionBits::Left
;
731 } else if (aX
> aClip
.XMost()) {
732 result
|= RegionBits::Right
;
735 if (aY
< aClip
.Y()) {
736 result
|= RegionBits::Bottom
;
737 } else if (aY
> aClip
.YMost()) {
738 result
|= RegionBits::Top
;
743 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
744 static bool LineSegmentIntersectsClip(double aX0
, double aY0
, double aX1
,
745 double aY1
, const gfxRect
& aClip
) {
746 RegionBits b0
= GetRegionBitsForPoint(aX0
, aY0
, aClip
);
747 RegionBits b1
= GetRegionBitsForPoint(aX1
, aY1
, aClip
);
751 // Completely inside.
756 // Completely outside.
761 // Choose an outside point.
762 RegionBits outsidePointBits
= b1
> b0
? b1
: b0
;
763 if (outsidePointBits
& RegionBits::Top
) {
764 x
= aX0
+ (aX1
- aX0
) * (aClip
.YMost() - aY0
) / (aY1
- aY0
);
766 } else if (outsidePointBits
& RegionBits::Bottom
) {
767 x
= aX0
+ (aX1
- aX0
) * (aClip
.Y() - aY0
) / (aY1
- aY0
);
769 } else if (outsidePointBits
& RegionBits::Right
) {
770 y
= aY0
+ (aY1
- aY0
) * (aClip
.XMost() - aX0
) / (aX1
- aX0
);
772 } else if (outsidePointBits
& RegionBits::Left
) {
773 y
= aY0
+ (aY1
- aY0
) * (aClip
.X() - aX0
) / (aX1
- aX0
);
777 if (outsidePointBits
== b0
) {
780 b0
= GetRegionBitsForPoint(aX0
, aY0
, aClip
);
784 b1
= GetRegionBitsForPoint(aX1
, aY1
, aClip
);
787 MOZ_ASSERT_UNREACHABLE();
792 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect
& aPrerenderedRect
,
793 SideBits aOverflowSides
,
794 const gfx::Matrix4x4
& aTransform
,
795 const ParentLayerRect
& aClipRect
) {
796 if (aClipRect
.IsEmpty()) {
800 gfxQuad prerenderedQuad
= gfxUtils::TransformToQuad(
801 ThebesRect(aPrerenderedRect
.ToUnknownRect()), aTransform
);
803 gfxLineSegment sideLines
[4];
804 uint8_t overflowSideCount
=
805 CollectOverflowedSideLines(prerenderedQuad
, aOverflowSides
, sideLines
);
807 gfxRect clipRect
= ThebesRect(aClipRect
.ToUnknownRect());
808 for (uint8_t j
= 0; j
< overflowSideCount
; j
++) {
809 if (LineSegmentIntersectsClip(sideLines
[j
].mStart
.x
, sideLines
[j
].mStart
.y
,
810 sideLines
[j
].mEnd
.x
, sideLines
[j
].mEnd
.y
,
816 // With step timing functions there are cases the transform jumps to a
817 // position where the partial pre-render area is totally outside of the clip
818 // rect without any intersection of the partial pre-render area and the clip
819 // rect happened in previous compositions but there remains visible area of
820 // the entire transformed area.
822 // So now all four points of the transformed partial pre-render rect are
823 // outside of the clip rect, if all these four points are in either side of
824 // the clip rect, we consider it's jank so that on the main-thread we will
825 // either a) rebuild the up-to-date display item if there remains visible area
826 // or b) no longer rebuild the display item if it's totally outside of the
829 // Note that RegionBits::Left and Right are mutually exclusive,
830 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
831 // any bits, it means all four points are in the same side.
832 return GetRegionBitsForPoint(prerenderedQuad
.mPoints
[0].x
,
833 prerenderedQuad
.mPoints
[0].y
, clipRect
) &
834 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[1].x
,
835 prerenderedQuad
.mPoints
[1].y
, clipRect
) &
836 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[2].x
,
837 prerenderedQuad
.mPoints
[2].y
, clipRect
) &
838 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[3].x
,
839 prerenderedQuad
.mPoints
[3].y
, clipRect
);
842 } // namespace mozilla::layers