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 "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/AnimatedPropertyID.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 "nsCSSPropertyID.h" // for eCSSProperty_offset_path, etc
28 #include "nsDisplayList.h" // for nsDisplayTransform, etc
30 namespace mozilla::layers
{
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).
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.
45 const bool isHorizontal
=
46 aOptions
.axis() == layers::ScrollDirection::eHorizontal
;
48 isHorizontal
? aScrollMeta
->mRange
.width
: aScrollMeta
->mRange
.height
;
51 "We don't expect to get a zero or negative range on the compositor");
53 // The offset may be negative if the writing mode is from right to left.
54 // Use std::abs() here to avoid getting a negative progress.
56 std::abs(isHorizontal
? aScrollMeta
->mOffset
.x
: aScrollMeta
->mOffset
.y
);
57 double progress
= position
/ range
;
58 // Just in case to avoid getting a progress more than 100%, for overscrolling.
59 progress
= std::min(progress
, 1.0);
60 auto timelineTime
= TimeDuration(aEndTime
.MultDouble(progress
));
61 return dom::Animation::CurrentTimeFromTimelineTime(timelineTime
, aStartTime
,
65 static dom::Nullable
<TimeDuration
> CalculateElapsedTime(
66 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
67 const MutexAutoLock
& aProofOfMapLock
, const PropertyAnimation
& aAnimation
,
68 const TimeStamp aPreviousFrameTime
, const TimeStamp aCurrentFrameTime
,
69 const AnimatedValue
* aPreviousValue
) {
70 // -------------------------------------
71 // Case 1: scroll-timeline animations.
72 // -------------------------------------
73 if (aAnimation
.mScrollTimelineOptions
) {
76 "We don't send scroll animations to the compositor if APZ is disabled");
78 return CalculateElapsedTimeForScrollTimeline(
79 aAPZSampler
->GetCurrentScrollOffsetAndRange(
80 aLayersId
, aAnimation
.mScrollTimelineOptions
.value().source(),
82 aAnimation
.mScrollTimelineOptions
.value(), aAnimation
.mTiming
.EndTime(),
83 aAnimation
.mStartTime
.refOr(aAnimation
.mHoldTime
),
84 aAnimation
.mPlaybackRate
);
87 // -------------------------------------
88 // Case 2: document-timeline animations.
89 // -------------------------------------
91 (!aAnimation
.mOriginTime
.IsNull() && aAnimation
.mStartTime
.isSome()) ||
92 aAnimation
.mIsNotPlaying
,
93 "If we are playing, we should have an origin time and a start time");
95 // Determine if the animation was play-pending and used a ready time later
96 // than the previous frame time.
98 // To determine this, _all_ of the following conditions need to hold:
100 // * There was no previous animation value (i.e. this is the first frame for
101 // the animation since it was sent to the compositor), and
102 // * The animation is playing, and
103 // * There is a previous frame time, and
104 // * The ready time of the animation is ahead of the previous frame time.
106 bool hasFutureReadyTime
= false;
107 if (!aPreviousValue
&& !aAnimation
.mIsNotPlaying
&&
108 !aPreviousFrameTime
.IsNull()) {
109 // This is the inverse of the calculation performed in
110 // AnimationInfo::StartPendingAnimations to calculate the start time of
111 // play-pending animations.
112 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
113 // underflow in the middle of the calulation.
114 const TimeStamp readyTime
=
115 aAnimation
.mOriginTime
+
116 (aAnimation
.mStartTime
.ref() +
117 aAnimation
.mHoldTime
.MultDouble(1.0 / aAnimation
.mPlaybackRate
));
118 hasFutureReadyTime
= !readyTime
.IsNull() && readyTime
> aPreviousFrameTime
;
120 // Use the previous vsync time to make main thread animations and compositor
121 // more closely aligned.
123 // On the first frame where we have animations the previous timestamp will
124 // not be set so we simply use the current timestamp. As a result we will
125 // end up painting the first frame twice. That doesn't appear to be
126 // noticeable, however.
128 // Likewise, if the animation is play-pending, it may have a ready time that
129 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
130 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
131 // jumping backwards into the range prior to when the animation starts.
132 const TimeStamp
& timeStamp
= aPreviousFrameTime
.IsNull() || hasFutureReadyTime
134 : aPreviousFrameTime
;
136 // If the animation is not currently playing, e.g. paused or
137 // finished, then use the hold time to stay at the same position.
138 TimeDuration elapsedDuration
=
139 aAnimation
.mIsNotPlaying
|| aAnimation
.mStartTime
.isNothing()
140 ? aAnimation
.mHoldTime
141 : (timeStamp
- aAnimation
.mOriginTime
- aAnimation
.mStartTime
.ref())
142 .MultDouble(aAnimation
.mPlaybackRate
);
143 return elapsedDuration
;
146 enum class CanSkipCompose
{
150 // This function samples the animation for a specific property. We may have
151 // multiple animations for a single property, and the later animations override
152 // the eariler ones. This function returns the sampled animation value,
153 // |aAnimationValue| for a single CSS property.
154 static AnimationHelper::SampleResult
SampleAnimationForProperty(
155 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
156 const MutexAutoLock
& aProofOfMapLock
, TimeStamp aPreviousFrameTime
,
157 TimeStamp aCurrentFrameTime
, const AnimatedValue
* aPreviousValue
,
158 CanSkipCompose aCanSkipCompose
,
159 nsTArray
<PropertyAnimation
>& aPropertyAnimations
,
160 RefPtr
<StyleAnimationValue
>& aAnimationValue
) {
161 MOZ_ASSERT(!aPropertyAnimations
.IsEmpty(), "Should have animations");
163 auto reason
= AnimationHelper::SampleResult::Reason::None
;
164 bool hasInEffectAnimations
= false;
166 // In cases where this function returns a SampleResult::Skipped, we actually
167 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
168 // call site that the value that would have been computed matches the stored
169 // value that we end up using. This flag is used to ensure we populate
170 // aAnimationValue in this scenario.
171 bool shouldBeSkipped
= false;
173 // Process in order, since later animations override earlier ones.
174 for (PropertyAnimation
& animation
: aPropertyAnimations
) {
175 dom::Nullable
<TimeDuration
> elapsedDuration
= CalculateElapsedTime(
176 aAPZSampler
, aLayersId
, aProofOfMapLock
, animation
, aPreviousFrameTime
,
177 aCurrentFrameTime
, aPreviousValue
);
179 const auto progressTimelinePosition
=
180 animation
.mScrollTimelineOptions
181 ? dom::Animation::AtProgressTimelineBoundary(
182 TimeDuration::FromMilliseconds(
183 PROGRESS_TIMELINE_DURATION_MILLISEC
),
184 elapsedDuration
, animation
.mStartTime
.refOr(TimeDuration()),
185 animation
.mPlaybackRate
)
186 : dom::Animation::ProgressTimelinePosition::NotBoundary
;
188 ComputedTiming computedTiming
= dom::AnimationEffect::GetComputedTimingAt(
189 elapsedDuration
, animation
.mTiming
, animation
.mPlaybackRate
,
190 progressTimelinePosition
);
192 if (computedTiming
.mProgress
.IsNull()) {
193 // For the scroll-driven animations, it's possible to let it go between
194 // the active phase and the before/after phase, and so its progress
195 // becomes null. In this case, we shouldn't just skip this animation.
196 // Instead, we have to reset the previous sampled result. Basically, we
197 // use |mProgressOnLastCompose| to check if it goes from the active phase.
198 // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
199 // caller know we need to use the base style for this property.
201 // If there are any other animations which need to be sampled together
202 // (in the same property animation group), this |reason| will be ignored.
203 if (animation
.mScrollTimelineOptions
&&
204 !animation
.mProgressOnLastCompose
.IsNull() &&
205 (computedTiming
.mPhase
== ComputedTiming::AnimationPhase::Before
||
206 computedTiming
.mPhase
== ComputedTiming::AnimationPhase::After
)) {
207 // Appearally, we go back to delay, so need to reset the last
208 // composition meta data. This is necessary because
209 // 1. this animation is in delay so it shouldn't have any composition
211 // 2. we will not go into this condition multiple times during delay
212 // phase because we rely on |mProgressOnLastCompose|.
213 animation
.ResetLastCompositionValues();
214 reason
= AnimationHelper::SampleResult::Reason::ScrollToDelayPhase
;
219 dom::IterationCompositeOperation iterCompositeOperation
=
220 animation
.mIterationComposite
;
222 // Skip calculation if the progress hasn't changed since the last
224 // Note that we don't skip calculate this animation if there is another
225 // animation since the other animation might be 'accumulate' or 'add', or
226 // might have a missing keyframe (i.e. this animation value will be used in
227 // the missing keyframe).
228 // FIXME Bug 1455476: We should do this optimizations for the case where
229 // the layer has multiple animations and multiple properties.
230 if (aCanSkipCompose
== CanSkipCompose::IfPossible
&&
231 !dom::KeyframeEffect::HasComputedTimingChanged(
232 computedTiming
, iterCompositeOperation
,
233 animation
.mProgressOnLastCompose
,
234 animation
.mCurrentIterationOnLastCompose
)) {
236 shouldBeSkipped
= true;
238 return AnimationHelper::SampleResult::Skipped();
242 uint32_t segmentIndex
= 0;
243 size_t segmentSize
= animation
.mSegments
.Length();
244 PropertyAnimation::SegmentData
* segment
= animation
.mSegments
.Elements();
245 while (segment
->mEndPortion
< computedTiming
.mProgress
.Value() &&
246 segmentIndex
< segmentSize
- 1) {
251 double positionInSegment
=
252 (computedTiming
.mProgress
.Value() - segment
->mStartPortion
) /
253 (segment
->mEndPortion
- segment
->mStartPortion
);
255 double portion
= StyleComputedTimingFunction::GetPortion(
256 segment
->mFunction
, positionInSegment
, computedTiming
.mBeforeFlag
);
258 // Like above optimization, skip calculation if the target segment isn't
259 // changed and if the portion in the segment isn't changed.
260 // This optimization is needed for CSS animations/transitions with step
261 // timing functions (e.g. the throbber animation on tabs or frame based
263 // FIXME Bug 1455476: Like the above optimization, we should apply this
264 // optimizations for multiple animation cases and multiple properties as
266 if (aCanSkipCompose
== CanSkipCompose::IfPossible
&&
267 animation
.mSegmentIndexOnLastCompose
== segmentIndex
&&
268 !animation
.mPortionInSegmentOnLastCompose
.IsNull() &&
269 animation
.mPortionInSegmentOnLastCompose
.Value() == portion
) {
271 shouldBeSkipped
= true;
273 return AnimationHelper::SampleResult::Skipped();
277 AnimationPropertySegment animSegment
;
278 animSegment
.mFromKey
= 0.0;
279 animSegment
.mToKey
= 1.0;
280 animSegment
.mFromValue
= AnimationValue(segment
->mStartValue
);
281 animSegment
.mToValue
= AnimationValue(segment
->mEndValue
);
282 animSegment
.mFromComposite
= segment
->mStartComposite
;
283 animSegment
.mToComposite
= segment
->mEndComposite
;
285 // interpolate the property
287 Servo_ComposeAnimationSegment(
288 &animSegment
, aAnimationValue
,
289 animation
.mSegments
.LastElement().mEndValue
, iterCompositeOperation
,
290 portion
, computedTiming
.mCurrentIteration
)
294 if (shouldBeSkipped
) {
295 return AnimationHelper::SampleResult::Skipped();
299 hasInEffectAnimations
= true;
300 animation
.mProgressOnLastCompose
= computedTiming
.mProgress
;
301 animation
.mCurrentIterationOnLastCompose
= computedTiming
.mCurrentIteration
;
302 animation
.mSegmentIndexOnLastCompose
= segmentIndex
;
303 animation
.mPortionInSegmentOnLastCompose
.SetValue(portion
);
306 auto rv
= hasInEffectAnimations
? AnimationHelper::SampleResult::Sampled()
307 : AnimationHelper::SampleResult();
312 // This function samples the animations for a group of CSS properties. We may
313 // have multiple CSS properties in a group (e.g. transform-like properties).
314 // So the returned animation array, |aAnimationValues|, include all the
315 // animation values of these CSS properties.
316 AnimationHelper::SampleResult
AnimationHelper::SampleAnimationForEachNode(
317 const APZSampler
* aAPZSampler
, const LayersId
& aLayersId
,
318 const MutexAutoLock
& aProofOfMapLock
, TimeStamp aPreviousFrameTime
,
319 TimeStamp aCurrentFrameTime
, const AnimatedValue
* aPreviousValue
,
320 nsTArray
<PropertyAnimationGroup
>& aPropertyAnimationGroups
,
321 SampledAnimationArray
& aAnimationValues
/* output */) {
322 MOZ_ASSERT(!aPropertyAnimationGroups
.IsEmpty(),
323 "Should be called with animation data");
324 MOZ_ASSERT(aAnimationValues
.IsEmpty(),
325 "Should be called with empty aAnimationValues");
327 nsTArray
<RefPtr
<StyleAnimationValue
>> baseStyleOfDelayAnimations
;
328 nsTArray
<RefPtr
<StyleAnimationValue
>> nonAnimatingValues
;
329 for (PropertyAnimationGroup
& group
: aPropertyAnimationGroups
) {
330 // Initialize animation value with base style.
331 RefPtr
<StyleAnimationValue
> currValue
= group
.mBaseStyle
;
333 CanSkipCompose canSkipCompose
=
334 aPreviousValue
&& aPropertyAnimationGroups
.Length() == 1 &&
335 group
.mAnimations
.Length() == 1
336 ? CanSkipCompose::IfPossible
337 : CanSkipCompose::No
;
340 !group
.mAnimations
.IsEmpty() ||
341 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
343 "Only transform-like properties can have empty PropertyAnimation list");
345 // For properties which are not animating (i.e. their values are always the
346 // same), we store them in a different array, and then merge them into the
347 // final result (a.k.a. aAnimationValues) because we shouldn't take them
348 // into account for SampleResult. (In other words, these properties
349 // shouldn't affect the optimization.)
350 if (group
.mAnimations
.IsEmpty()) {
351 nonAnimatingValues
.AppendElement(std::move(currValue
));
355 SampleResult result
= SampleAnimationForProperty(
356 aAPZSampler
, aLayersId
, aProofOfMapLock
, aPreviousFrameTime
,
357 aCurrentFrameTime
, aPreviousValue
, canSkipCompose
, group
.mAnimations
,
360 // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
361 // the result is skipped only if the property count == 1.
362 if (result
.IsSkipped()) {
364 aAnimationValues
.AppendElement(std::move(currValue
));
369 if (!result
.IsSampled()) {
370 if (result
.mReason
== SampleResult::Reason::ScrollToDelayPhase
) {
371 MOZ_ASSERT(currValue
&& currValue
== group
.mBaseStyle
);
372 baseStyleOfDelayAnimations
.AppendElement(std::move(currValue
));
377 // Insert the interpolation result into the output array.
378 MOZ_ASSERT(currValue
);
379 aAnimationValues
.AppendElement(std::move(currValue
));
383 aAnimationValues
.IsEmpty() ? SampleResult() : SampleResult::Sampled();
385 // If there is no other sampled result, we may store these base styles
386 // (together with the non-animating values) to the webrenderer before it gets
387 // sync with the main thread.
388 if (rv
.IsNone() && !baseStyleOfDelayAnimations
.IsEmpty()) {
389 aAnimationValues
.AppendElements(std::move(baseStyleOfDelayAnimations
));
390 rv
.mReason
= SampleResult::Reason::ScrollToDelayPhase
;
393 if (!aAnimationValues
.IsEmpty()) {
394 aAnimationValues
.AppendElements(std::move(nonAnimatingValues
));
399 static dom::FillMode
GetAdjustedFillMode(const Animation
& aAnimation
) {
400 // Adjust fill mode so that if the main thread is delayed in clearing
401 // this animation we don't introduce flicker by jumping back to the old
403 auto fillMode
= static_cast<dom::FillMode
>(aAnimation
.fillMode());
404 float playbackRate
= aAnimation
.playbackRate();
406 case dom::FillMode::None
:
407 if (playbackRate
> 0) {
408 fillMode
= dom::FillMode::Forwards
;
409 } else if (playbackRate
< 0) {
410 fillMode
= dom::FillMode::Backwards
;
413 case dom::FillMode::Backwards
:
414 if (playbackRate
> 0) {
415 fillMode
= dom::FillMode::Both
;
418 case dom::FillMode::Forwards
:
419 if (playbackRate
< 0) {
420 fillMode
= dom::FillMode::Both
;
430 static bool HasTransformLikeAnimations(const AnimationArray
& aAnimations
) {
431 nsCSSPropertyIDSet transformSet
=
432 nsCSSPropertyIDSet::TransformLikeProperties();
434 for (const Animation
& animation
: aAnimations
) {
435 if (animation
.isNotAnimating()) {
439 if (transformSet
.HasProperty(animation
.property())) {
448 AnimationStorageData
AnimationHelper::ExtractAnimations(
449 const LayersId
& aLayersId
, const AnimationArray
& aAnimations
,
450 const CompositorAnimationStorage
* aStorage
,
451 const TimeStamp
& aPreviousSampleTime
) {
452 AnimationStorageData storageData
;
453 storageData
.mLayersId
= aLayersId
;
455 nsCSSPropertyID prevID
= eCSSProperty_UNKNOWN
;
456 PropertyAnimationGroup
* currData
= nullptr;
457 DebugOnly
<const layers::Animatable
*> currBaseStyle
= nullptr;
459 for (const Animation
& animation
: aAnimations
) {
460 // Animations with same property are grouped together, so we can just
461 // check if the current property is the same as the previous one for
462 // knowing this is a new group.
463 if (prevID
!= animation
.property()) {
464 // Got a different group, we should create a different array.
465 currData
= storageData
.mAnimation
.AppendElement();
466 currData
->mProperty
= animation
.property();
467 if (animation
.transformData()) {
468 MOZ_ASSERT(!storageData
.mTransformData
,
469 "Only one entry has TransformData");
470 storageData
.mTransformData
= animation
.transformData();
473 prevID
= animation
.property();
475 // Reset the debug pointer.
476 currBaseStyle
= nullptr;
479 MOZ_ASSERT(currData
);
480 if (animation
.baseStyle().type() != Animatable::Tnull_t
) {
481 MOZ_ASSERT(!currBaseStyle
|| *currBaseStyle
== animation
.baseStyle(),
482 "Should be the same base style");
484 currData
->mBaseStyle
= AnimationValue::FromAnimatable(
485 animation
.property(), animation
.baseStyle());
486 currBaseStyle
= &animation
.baseStyle();
489 // If this layers::Animation sets isNotAnimating to true, it only has
490 // base style and doesn't have any animation information, so we can skip
491 // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
493 if (animation
.isNotAnimating()) {
494 MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
495 animation
.property()),
496 "Only transform-like properties could set this true");
498 if (animation
.property() == eCSSProperty_offset_path
) {
499 MOZ_ASSERT(currData
->mBaseStyle
,
500 "Fixed offset-path should have base style");
501 MOZ_ASSERT(HasTransformLikeAnimations(aAnimations
));
503 const StyleOffsetPath
& offsetPath
=
504 animation
.baseStyle().get_StyleOffsetPath();
505 // FIXME: Bug 1837042. Cache all basic shapes.
506 if (offsetPath
.IsPath()) {
507 MOZ_ASSERT(!storageData
.mCachedMotionPath
,
508 "Only one offset-path: path() is set");
510 RefPtr
<gfx::PathBuilder
> builder
=
511 MotionPathUtils::GetCompositorPathBuilder();
512 storageData
.mCachedMotionPath
= MotionPathUtils::BuildSVGPath(
513 offsetPath
.AsSVGPathData(), builder
);
520 PropertyAnimation
* propertyAnimation
=
521 currData
->mAnimations
.AppendElement();
523 propertyAnimation
->mOriginTime
= animation
.originTime();
524 propertyAnimation
->mStartTime
= animation
.startTime();
525 propertyAnimation
->mHoldTime
= animation
.holdTime();
526 propertyAnimation
->mPlaybackRate
= animation
.playbackRate();
527 propertyAnimation
->mIterationComposite
=
528 static_cast<dom::IterationCompositeOperation
>(
529 animation
.iterationComposite());
530 propertyAnimation
->mIsNotPlaying
= animation
.isNotPlaying();
531 propertyAnimation
->mTiming
=
532 TimingParams
{animation
.duration(),
534 animation
.endDelay(),
535 animation
.iterations(),
536 animation
.iterationStart(),
537 static_cast<dom::PlaybackDirection
>(animation
.direction()),
538 GetAdjustedFillMode(animation
),
539 animation
.easingFunction()};
540 propertyAnimation
->mScrollTimelineOptions
=
541 animation
.scrollTimelineOptions();
543 RefPtr
<StyleAnimationValue
> startValue
;
544 if (animation
.replacedTransitionId()) {
545 if (const auto* animatedValue
=
546 aStorage
->GetAnimatedValue(*animation
.replacedTransitionId())) {
547 startValue
= animatedValue
->AsAnimationValue(animation
.property());
548 // Basically, the timeline time is increasing monotonically, so it may
549 // not make sense to have a negative start time (i.e. the case when
550 // aPreviousSampleTime is behind the origin time). Therefore, if the
551 // previous sample time is less than the origin time, we skip the
552 // replacement of the start time.
553 if (!aPreviousSampleTime
.IsNull() &&
554 (aPreviousSampleTime
>= animation
.originTime())) {
555 propertyAnimation
->mStartTime
=
556 Some(aPreviousSampleTime
- animation
.originTime());
559 MOZ_ASSERT(animation
.segments().Length() == 1,
560 "The CSS Transition only has one segement");
564 nsTArray
<PropertyAnimation::SegmentData
>& segmentData
=
565 propertyAnimation
->mSegments
;
566 for (const AnimationSegment
& segment
: animation
.segments()) {
567 segmentData
.AppendElement(PropertyAnimation::SegmentData
{
568 // Note that even though we re-compute the start value on the main
569 // thread, we still replace it with the last sampled value, to avoid
571 startValue
? startValue
572 : AnimationValue::FromAnimatable(animation
.property(),
573 segment
.startState()),
574 AnimationValue::FromAnimatable(animation
.property(),
576 segment
.sampleFn(), segment
.startPortion(), segment
.endPortion(),
577 static_cast<dom::CompositeOperation
>(segment
.startComposite()),
578 static_cast<dom::CompositeOperation
>(segment
.endComposite())});
583 // Sanity check that the grouped animation data is correct by looking at the
585 if (!storageData
.mAnimation
.IsEmpty()) {
586 nsCSSPropertyIDSet seenProperties
;
587 for (const auto& group
: storageData
.mAnimation
) {
588 nsCSSPropertyID id
= group
.mProperty
;
590 MOZ_ASSERT(!seenProperties
.HasProperty(id
), "Should be a new property");
591 seenProperties
.AddProperty(id
);
595 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
596 DisplayItemType::TYPE_TRANSFORM
)) ||
597 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
598 DisplayItemType::TYPE_OPACITY
)) ||
599 seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
600 DisplayItemType::TYPE_BACKGROUND_COLOR
)),
601 "The property set of output should be the subset of transform-like "
602 "properties, opacity, or background_color.");
604 if (seenProperties
.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
605 DisplayItemType::TYPE_TRANSFORM
))) {
606 MOZ_ASSERT(storageData
.mTransformData
, "Should have TransformData");
609 if (seenProperties
.HasProperty(eCSSProperty_offset_path
)) {
610 MOZ_ASSERT(storageData
.mTransformData
, "Should have TransformData");
611 MOZ_ASSERT(storageData
.mTransformData
->motionPathData(),
612 "Should have MotionPathData");
620 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
621 static uint32_t sNextId
= 0;
624 uint32_t procId
= static_cast<uint32_t>(base::GetCurrentProcId());
625 uint64_t nextId
= procId
;
626 nextId
= nextId
<< 32 | sNextId
;
630 gfx::Matrix4x4
AnimationHelper::ServoAnimationValueToMatrix4x4(
631 const SampledAnimationArray
& aValues
, const TransformData
& aTransformData
,
632 gfx::Path
* aCachedMotionPath
) {
633 using nsStyleTransformMatrix::TransformReferenceBox
;
635 // This is a bit silly just to avoid the transform list copy from the
636 // animation transform list.
637 auto noneTranslate
= StyleTranslate::None();
638 auto noneRotate
= StyleRotate::None();
639 auto noneScale
= StyleScale::None();
640 const StyleTransform noneTransform
;
642 const StyleTranslate
* translate
= nullptr;
643 const StyleRotate
* rotate
= nullptr;
644 const StyleScale
* scale
= nullptr;
645 const StyleTransform
* transform
= nullptr;
646 Maybe
<StyleOffsetPath
> path
;
647 const StyleLengthPercentage
* distance
= nullptr;
648 const StyleOffsetRotate
* offsetRotate
= nullptr;
649 const StylePositionOrAuto
* anchor
= nullptr;
650 const StyleOffsetPosition
* position
= nullptr;
652 for (const auto& value
: aValues
) {
654 AnimatedPropertyID
property(eCSSProperty_UNKNOWN
);
655 Servo_AnimationValue_GetPropertyId(value
, &property
);
656 switch (property
.mID
) {
657 case eCSSProperty_transform
:
658 MOZ_ASSERT(!transform
);
659 transform
= Servo_AnimationValue_GetTransform(value
);
661 case eCSSProperty_translate
:
662 MOZ_ASSERT(!translate
);
663 translate
= Servo_AnimationValue_GetTranslate(value
);
665 case eCSSProperty_rotate
:
667 rotate
= Servo_AnimationValue_GetRotate(value
);
669 case eCSSProperty_scale
:
671 scale
= Servo_AnimationValue_GetScale(value
);
673 case eCSSProperty_offset_path
:
675 path
.emplace(StyleOffsetPath::None());
676 Servo_AnimationValue_GetOffsetPath(value
, path
.ptr());
678 case eCSSProperty_offset_distance
:
679 MOZ_ASSERT(!distance
);
680 distance
= Servo_AnimationValue_GetOffsetDistance(value
);
682 case eCSSProperty_offset_rotate
:
683 MOZ_ASSERT(!offsetRotate
);
684 offsetRotate
= Servo_AnimationValue_GetOffsetRotate(value
);
686 case eCSSProperty_offset_anchor
:
688 anchor
= Servo_AnimationValue_GetOffsetAnchor(value
);
690 case eCSSProperty_offset_position
:
691 MOZ_ASSERT(!position
);
692 position
= Servo_AnimationValue_GetOffsetPosition(value
);
695 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
699 TransformReferenceBox
refBox(nullptr, aTransformData
.bounds());
700 Maybe
<ResolvedMotionPathData
> motion
= MotionPathUtils::ResolveMotionPath(
701 path
.ptrOr(nullptr), distance
, offsetRotate
, anchor
, position
,
702 aTransformData
.motionPathData(), refBox
, aCachedMotionPath
);
704 // We expect all our transform data to arrive in device pixels
705 gfx::Point3D transformOrigin
= aTransformData
.transformOrigin();
706 nsDisplayTransform::FrameTransformProperties
props(
707 translate
? *translate
: noneTranslate
, rotate
? *rotate
: noneRotate
,
708 scale
? *scale
: noneScale
, transform
? *transform
: noneTransform
,
709 motion
, transformOrigin
);
711 return nsDisplayTransform::GetResultingTransformMatrix(
712 props
, refBox
, aTransformData
.appUnitsPerDevPixel());
715 static uint8_t CollectOverflowedSideLines(const gfxQuad
& aPrerenderedQuad
,
716 SideBits aOverflowSides
,
717 gfxLineSegment sideLines
[4]) {
720 if (aOverflowSides
& SideBits::eTop
) {
721 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[0],
722 aPrerenderedQuad
.mPoints
[1]);
725 if (aOverflowSides
& SideBits::eRight
) {
726 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[1],
727 aPrerenderedQuad
.mPoints
[2]);
730 if (aOverflowSides
& SideBits::eBottom
) {
731 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[2],
732 aPrerenderedQuad
.mPoints
[3]);
735 if (aOverflowSides
& SideBits::eLeft
) {
736 sideLines
[count
] = gfxLineSegment(aPrerenderedQuad
.mPoints
[3],
737 aPrerenderedQuad
.mPoints
[0]);
744 enum RegionBits
: uint8_t {
752 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits
);
754 static RegionBits
GetRegionBitsForPoint(double aX
, double aY
,
755 const gfxRect
& aClip
) {
756 RegionBits result
= RegionBits::Inside
;
757 if (aX
< aClip
.X()) {
758 result
|= RegionBits::Left
;
759 } else if (aX
> aClip
.XMost()) {
760 result
|= RegionBits::Right
;
763 if (aY
< aClip
.Y()) {
764 result
|= RegionBits::Bottom
;
765 } else if (aY
> aClip
.YMost()) {
766 result
|= RegionBits::Top
;
771 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
772 static bool LineSegmentIntersectsClip(double aX0
, double aY0
, double aX1
,
773 double aY1
, const gfxRect
& aClip
) {
774 RegionBits b0
= GetRegionBitsForPoint(aX0
, aY0
, aClip
);
775 RegionBits b1
= GetRegionBitsForPoint(aX1
, aY1
, aClip
);
779 // Completely inside.
784 // Completely outside.
789 // Choose an outside point.
790 RegionBits outsidePointBits
= b1
> b0
? b1
: b0
;
791 if (outsidePointBits
& RegionBits::Top
) {
792 x
= aX0
+ (aX1
- aX0
) * (aClip
.YMost() - aY0
) / (aY1
- aY0
);
794 } else if (outsidePointBits
& RegionBits::Bottom
) {
795 x
= aX0
+ (aX1
- aX0
) * (aClip
.Y() - aY0
) / (aY1
- aY0
);
797 } else if (outsidePointBits
& RegionBits::Right
) {
798 y
= aY0
+ (aY1
- aY0
) * (aClip
.XMost() - aX0
) / (aX1
- aX0
);
800 } else if (outsidePointBits
& RegionBits::Left
) {
801 y
= aY0
+ (aY1
- aY0
) * (aClip
.X() - aX0
) / (aX1
- aX0
);
805 if (outsidePointBits
== b0
) {
808 b0
= GetRegionBitsForPoint(aX0
, aY0
, aClip
);
812 b1
= GetRegionBitsForPoint(aX1
, aY1
, aClip
);
815 MOZ_ASSERT_UNREACHABLE();
820 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect
& aPrerenderedRect
,
821 SideBits aOverflowSides
,
822 const gfx::Matrix4x4
& aTransform
,
823 const ParentLayerRect
& aClipRect
) {
824 if (aClipRect
.IsEmpty()) {
828 gfxQuad prerenderedQuad
= gfxUtils::TransformToQuad(
829 ThebesRect(aPrerenderedRect
.ToUnknownRect()), aTransform
);
831 gfxLineSegment sideLines
[4];
832 uint8_t overflowSideCount
=
833 CollectOverflowedSideLines(prerenderedQuad
, aOverflowSides
, sideLines
);
835 gfxRect clipRect
= ThebesRect(aClipRect
.ToUnknownRect());
836 for (uint8_t j
= 0; j
< overflowSideCount
; j
++) {
837 if (LineSegmentIntersectsClip(sideLines
[j
].mStart
.x
, sideLines
[j
].mStart
.y
,
838 sideLines
[j
].mEnd
.x
, sideLines
[j
].mEnd
.y
,
844 // With step timing functions there are cases the transform jumps to a
845 // position where the partial pre-render area is totally outside of the clip
846 // rect without any intersection of the partial pre-render area and the clip
847 // rect happened in previous compositions but there remains visible area of
848 // the entire transformed area.
850 // So now all four points of the transformed partial pre-render rect are
851 // outside of the clip rect, if all these four points are in either side of
852 // the clip rect, we consider it's jank so that on the main-thread we will
853 // either a) rebuild the up-to-date display item if there remains visible area
854 // or b) no longer rebuild the display item if it's totally outside of the
857 // Note that RegionBits::Left and Right are mutually exclusive,
858 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
859 // any bits, it means all four points are in the same side.
860 return GetRegionBitsForPoint(prerenderedQuad
.mPoints
[0].x
,
861 prerenderedQuad
.mPoints
[0].y
, clipRect
) &
862 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[1].x
,
863 prerenderedQuad
.mPoints
[1].y
, clipRect
) &
864 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[2].x
,
865 prerenderedQuad
.mPoints
[2].y
, clipRect
) &
866 GetRegionBitsForPoint(prerenderedQuad
.mPoints
[3].x
,
867 prerenderedQuad
.mPoints
[3].y
, clipRect
);
870 } // namespace mozilla::layers