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 "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/ComputedTimingFunction.h" // for ComputedTimingFunction
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/CompositorThread.h" // for CompositorThreadHolder
21 #include "mozilla/layers/CompositorAnimationStorage.h" // for CompositorAnimationStorage
22 #include "mozilla/layers/LayerAnimationUtils.h" // for TimingFunctionToComputedTimingFunction
23 #include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
24 #include "mozilla/MotionPathUtils.h" // for ResolveMotionPath()
25 #include "mozilla/ServoBindings.h" // for Servo_ComposeAnimationSegment, etc
26 #include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
27 #include "nsDeviceContext.h" // for AppUnitsPerCSSPixel
28 #include "nsDisplayList.h" // for nsDisplayTransform, etc
29
30 namespace mozilla {
31 namespace layers {
32
33 enum class CanSkipCompose {
34 IfPossible,
35 No,
36 };
SampleAnimationForProperty(TimeStamp aPreviousFrameTime,TimeStamp aCurrentFrameTime,const AnimatedValue * aPreviousValue,CanSkipCompose aCanSkipCompose,nsTArray<PropertyAnimation> & aPropertyAnimations,RefPtr<RawServoAnimationValue> & aAnimationValue)37 static AnimationHelper::SampleResult SampleAnimationForProperty(
38 TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
39 const AnimatedValue* aPreviousValue, CanSkipCompose aCanSkipCompose,
40 nsTArray<PropertyAnimation>& aPropertyAnimations,
41 RefPtr<RawServoAnimationValue>& aAnimationValue) {
42 MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
43 bool hasInEffectAnimations = false;
44 #ifdef DEBUG
45 // In cases where this function returns a SampleResult::Skipped, we actually
46 // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
47 // call site that the value that would have been computed matches the stored
48 // value that we end up using. This flag is used to ensure we populate
49 // aAnimationValue in this scenario.
50 bool shouldBeSkipped = false;
51 #endif
52 // Process in order, since later animations override earlier ones.
53 for (PropertyAnimation& animation : aPropertyAnimations) {
54 MOZ_ASSERT(
55 (!animation.mOriginTime.IsNull() && animation.mStartTime.isSome()) ||
56 animation.mIsNotPlaying,
57 "If we are playing, we should have an origin time and a start time");
58
59 // Determine if the animation was play-pending and used a ready time later
60 // than the previous frame time.
61 //
62 // To determine this, _all_ of the following conditions need to hold:
63 //
64 // * There was no previous animation value (i.e. this is the first frame for
65 // the animation since it was sent to the compositor), and
66 // * The animation is playing, and
67 // * There is a previous frame time, and
68 // * The ready time of the animation is ahead of the previous frame time.
69 //
70 bool hasFutureReadyTime = false;
71 if (!aPreviousValue && !animation.mIsNotPlaying &&
72 !aPreviousFrameTime.IsNull()) {
73 // This is the inverse of the calculation performed in
74 // AnimationInfo::StartPendingAnimations to calculate the start time of
75 // play-pending animations.
76 // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
77 // underflow in the middle of the calulation.
78 const TimeStamp readyTime =
79 animation.mOriginTime +
80 (animation.mStartTime.ref() +
81 animation.mHoldTime.MultDouble(1.0 / animation.mPlaybackRate));
82 hasFutureReadyTime =
83 !readyTime.IsNull() && readyTime > aPreviousFrameTime;
84 }
85 // Use the previous vsync time to make main thread animations and compositor
86 // more closely aligned.
87 //
88 // On the first frame where we have animations the previous timestamp will
89 // not be set so we simply use the current timestamp. As a result we will
90 // end up painting the first frame twice. That doesn't appear to be
91 // noticeable, however.
92 //
93 // Likewise, if the animation is play-pending, it may have a ready time that
94 // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
95 // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
96 // jumping backwards into the range prior to when the animation starts.
97 const TimeStamp& timeStamp =
98 aPreviousFrameTime.IsNull() || hasFutureReadyTime ? aCurrentFrameTime
99 : aPreviousFrameTime;
100
101 // If the animation is not currently playing, e.g. paused or
102 // finished, then use the hold time to stay at the same position.
103 TimeDuration elapsedDuration =
104 animation.mIsNotPlaying || animation.mStartTime.isNothing()
105 ? animation.mHoldTime
106 : (timeStamp - animation.mOriginTime - animation.mStartTime.ref())
107 .MultDouble(animation.mPlaybackRate);
108
109 ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
110 dom::Nullable<TimeDuration>(elapsedDuration), animation.mTiming,
111 animation.mPlaybackRate);
112
113 if (computedTiming.mProgress.IsNull()) {
114 continue;
115 }
116
117 dom::IterationCompositeOperation iterCompositeOperation =
118 animation.mIterationComposite;
119
120 // Skip calculation if the progress hasn't changed since the last
121 // calculation.
122 // Note that we don't skip calculate this animation if there is another
123 // animation since the other animation might be 'accumulate' or 'add', or
124 // might have a missing keyframe (i.e. this animation value will be used in
125 // the missing keyframe).
126 // FIXME Bug 1455476: We should do this optimizations for the case where
127 // the layer has multiple animations and multiple properties.
128 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
129 !dom::KeyframeEffect::HasComputedTimingChanged(
130 computedTiming, iterCompositeOperation,
131 animation.mProgressOnLastCompose,
132 animation.mCurrentIterationOnLastCompose)) {
133 #ifdef DEBUG
134 shouldBeSkipped = true;
135 #else
136 return AnimationHelper::SampleResult::Skipped;
137 #endif
138 }
139
140 uint32_t segmentIndex = 0;
141 size_t segmentSize = animation.mSegments.Length();
142 PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
143 while (segment->mEndPortion < computedTiming.mProgress.Value() &&
144 segmentIndex < segmentSize - 1) {
145 ++segment;
146 ++segmentIndex;
147 }
148
149 double positionInSegment =
150 (computedTiming.mProgress.Value() - segment->mStartPortion) /
151 (segment->mEndPortion - segment->mStartPortion);
152
153 double portion = ComputedTimingFunction::GetPortion(
154 segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
155
156 // Like above optimization, skip calculation if the target segment isn't
157 // changed and if the portion in the segment isn't changed.
158 // This optimization is needed for CSS animations/transitions with step
159 // timing functions (e.g. the throbber animation on tabs or frame based
160 // animations).
161 // FIXME Bug 1455476: Like the above optimization, we should apply this
162 // optimizations for multiple animation cases and multiple properties as
163 // well.
164 if (aCanSkipCompose == CanSkipCompose::IfPossible &&
165 animation.mSegmentIndexOnLastCompose == segmentIndex &&
166 !animation.mPortionInSegmentOnLastCompose.IsNull() &&
167 animation.mPortionInSegmentOnLastCompose.Value() == portion) {
168 #ifdef DEBUG
169 shouldBeSkipped = true;
170 #else
171 return AnimationHelper::SampleResult::Skipped;
172 #endif
173 }
174
175 AnimationPropertySegment animSegment;
176 animSegment.mFromKey = 0.0;
177 animSegment.mToKey = 1.0;
178 animSegment.mFromValue = AnimationValue(segment->mStartValue);
179 animSegment.mToValue = AnimationValue(segment->mEndValue);
180 animSegment.mFromComposite = segment->mStartComposite;
181 animSegment.mToComposite = segment->mEndComposite;
182
183 // interpolate the property
184 aAnimationValue =
185 Servo_ComposeAnimationSegment(
186 &animSegment, aAnimationValue,
187 animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
188 portion, computedTiming.mCurrentIteration)
189 .Consume();
190
191 #ifdef DEBUG
192 if (shouldBeSkipped) {
193 return AnimationHelper::SampleResult::Skipped;
194 }
195 #endif
196
197 hasInEffectAnimations = true;
198 animation.mProgressOnLastCompose = computedTiming.mProgress;
199 animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
200 animation.mSegmentIndexOnLastCompose = segmentIndex;
201 animation.mPortionInSegmentOnLastCompose.SetValue(portion);
202 }
203
204 return hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled
205 : AnimationHelper::SampleResult::None;
206 }
207
SampleAnimationForEachNode(TimeStamp aPreviousFrameTime,TimeStamp aCurrentFrameTime,const AnimatedValue * aPreviousValue,nsTArray<PropertyAnimationGroup> & aPropertyAnimationGroups,nsTArray<RefPtr<RawServoAnimationValue>> & aAnimationValues)208 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
209 TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
210 const AnimatedValue* aPreviousValue,
211 nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
212 nsTArray<RefPtr<RawServoAnimationValue>>& aAnimationValues /* out */) {
213 MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
214 "Should be called with animation data");
215 MOZ_ASSERT(aAnimationValues.IsEmpty(),
216 "Should be called with empty aAnimationValues");
217
218 nsTArray<RefPtr<RawServoAnimationValue>> nonAnimatingValues;
219 for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
220 // Initialize animation value with base style.
221 RefPtr<RawServoAnimationValue> currValue = group.mBaseStyle;
222
223 CanSkipCompose canSkipCompose =
224 aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
225 group.mAnimations.Length() == 1
226 ? CanSkipCompose::IfPossible
227 : CanSkipCompose::No;
228
229 MOZ_ASSERT(
230 !group.mAnimations.IsEmpty() ||
231 nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
232 group.mProperty),
233 "Only transform-like properties can have empty PropertyAnimation list");
234
235 // For properties which are not animating (i.e. their values are always the
236 // same), we store them in a different array, and then merge them into the
237 // final result (a.k.a. aAnimationValues) because we shouldn't take them
238 // into account for SampleResult. (In other words, these properties
239 // shouldn't affect the optimization.)
240 if (group.mAnimations.IsEmpty()) {
241 nonAnimatingValues.AppendElement(std::move(currValue));
242 continue;
243 }
244
245 SampleResult result = SampleAnimationForProperty(
246 aPreviousFrameTime, aCurrentFrameTime, aPreviousValue, canSkipCompose,
247 group.mAnimations, currValue);
248
249 // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
250 // the result is skipped only if the property count == 1.
251 if (result == SampleResult::Skipped) {
252 #ifdef DEBUG
253 aAnimationValues.AppendElement(std::move(currValue));
254 #endif
255 return SampleResult::Skipped;
256 }
257
258 if (result != SampleResult::Sampled) {
259 continue;
260 }
261
262 // Insert the interpolation result into the output array.
263 MOZ_ASSERT(currValue);
264 aAnimationValues.AppendElement(std::move(currValue));
265 }
266
267 SampleResult rv =
268 aAnimationValues.IsEmpty() ? SampleResult::None : SampleResult::Sampled;
269 if (rv == SampleResult::Sampled) {
270 aAnimationValues.AppendElements(std::move(nonAnimatingValues));
271 }
272 return rv;
273 }
274
GetAdjustedFillMode(const Animation & aAnimation)275 static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
276 // Adjust fill mode so that if the main thread is delayed in clearing
277 // this animation we don't introduce flicker by jumping back to the old
278 // underlying value.
279 auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
280 float playbackRate = aAnimation.playbackRate();
281 switch (fillMode) {
282 case dom::FillMode::None:
283 if (playbackRate > 0) {
284 fillMode = dom::FillMode::Forwards;
285 } else if (playbackRate < 0) {
286 fillMode = dom::FillMode::Backwards;
287 }
288 break;
289 case dom::FillMode::Backwards:
290 if (playbackRate > 0) {
291 fillMode = dom::FillMode::Both;
292 }
293 break;
294 case dom::FillMode::Forwards:
295 if (playbackRate < 0) {
296 fillMode = dom::FillMode::Both;
297 }
298 break;
299 default:
300 break;
301 }
302 return fillMode;
303 }
304
305 #ifdef DEBUG
HasTransformLikeAnimations(const AnimationArray & aAnimations)306 static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
307 nsCSSPropertyIDSet transformSet =
308 nsCSSPropertyIDSet::TransformLikeProperties();
309
310 for (const Animation& animation : aAnimations) {
311 if (animation.isNotAnimating()) {
312 continue;
313 }
314
315 if (transformSet.HasProperty(animation.property())) {
316 return true;
317 }
318 }
319
320 return false;
321 }
322 #endif
323
ExtractAnimations(const LayersId & aLayersId,const AnimationArray & aAnimations)324 AnimationStorageData AnimationHelper::ExtractAnimations(
325 const LayersId& aLayersId, const AnimationArray& aAnimations) {
326 AnimationStorageData storageData;
327 storageData.mLayersId = aLayersId;
328
329 nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
330 PropertyAnimationGroup* currData = nullptr;
331 DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
332
333 for (const Animation& animation : aAnimations) {
334 // Animations with same property are grouped together, so we can just
335 // check if the current property is the same as the previous one for
336 // knowing this is a new group.
337 if (prevID != animation.property()) {
338 // Got a different group, we should create a different array.
339 currData = storageData.mAnimation.AppendElement();
340 currData->mProperty = animation.property();
341 if (animation.transformData()) {
342 MOZ_ASSERT(!storageData.mTransformData,
343 "Only one entry has TransformData");
344 storageData.mTransformData = animation.transformData();
345 }
346
347 prevID = animation.property();
348
349 // Reset the debug pointer.
350 currBaseStyle = nullptr;
351 }
352
353 MOZ_ASSERT(currData);
354 if (animation.baseStyle().type() != Animatable::Tnull_t) {
355 MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
356 "Should be the same base style");
357
358 currData->mBaseStyle = AnimationValue::FromAnimatable(
359 animation.property(), animation.baseStyle());
360 currBaseStyle = &animation.baseStyle();
361 }
362
363 // If this layers::Animation sets isNotAnimating to true, it only has
364 // base style and doesn't have any animation information, so we can skip
365 // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
366 // an empty array.)
367 if (animation.isNotAnimating()) {
368 MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
369 animation.property()),
370 "Only transform-like properties could set this true");
371
372 if (animation.property() == eCSSProperty_offset_path) {
373 MOZ_ASSERT(currData->mBaseStyle,
374 "Fixed offset-path should have base style");
375 MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));
376
377 AnimationValue value{currData->mBaseStyle};
378 const StyleOffsetPath& offsetPath = value.GetOffsetPathProperty();
379 if (offsetPath.IsPath()) {
380 MOZ_ASSERT(!storageData.mCachedMotionPath,
381 "Only one offset-path: path() is set");
382
383 RefPtr<gfx::PathBuilder> builder =
384 MotionPathUtils::GetCompositorPathBuilder();
385 storageData.mCachedMotionPath =
386 MotionPathUtils::BuildPath(offsetPath.AsPath(), builder);
387 }
388 }
389
390 continue;
391 }
392
393 PropertyAnimation* propertyAnimation =
394 currData->mAnimations.AppendElement();
395
396 propertyAnimation->mOriginTime = animation.originTime();
397 propertyAnimation->mStartTime = animation.startTime();
398 propertyAnimation->mHoldTime = animation.holdTime();
399 propertyAnimation->mPlaybackRate = animation.playbackRate();
400 propertyAnimation->mIterationComposite =
401 static_cast<dom::IterationCompositeOperation>(
402 animation.iterationComposite());
403 propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
404 propertyAnimation->mTiming =
405 TimingParams{animation.duration(),
406 animation.delay(),
407 animation.endDelay(),
408 animation.iterations(),
409 animation.iterationStart(),
410 static_cast<dom::PlaybackDirection>(animation.direction()),
411 GetAdjustedFillMode(animation),
412 AnimationUtils::TimingFunctionToComputedTimingFunction(
413 animation.easingFunction())};
414
415 nsTArray<PropertyAnimation::SegmentData>& segmentData =
416 propertyAnimation->mSegments;
417 for (const AnimationSegment& segment : animation.segments()) {
418 segmentData.AppendElement(PropertyAnimation::SegmentData{
419 AnimationValue::FromAnimatable(animation.property(),
420 segment.startState()),
421 AnimationValue::FromAnimatable(animation.property(),
422 segment.endState()),
423 AnimationUtils::TimingFunctionToComputedTimingFunction(
424 segment.sampleFn()),
425 segment.startPortion(), segment.endPortion(),
426 static_cast<dom::CompositeOperation>(segment.startComposite()),
427 static_cast<dom::CompositeOperation>(segment.endComposite())});
428 }
429 }
430
431 #ifdef DEBUG
432 // Sanity check that the grouped animation data is correct by looking at the
433 // property set.
434 if (!storageData.mAnimation.IsEmpty()) {
435 nsCSSPropertyIDSet seenProperties;
436 for (const auto& group : storageData.mAnimation) {
437 nsCSSPropertyID id = group.mProperty;
438
439 MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
440 seenProperties.AddProperty(id);
441 }
442
443 MOZ_ASSERT(
444 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
445 DisplayItemType::TYPE_TRANSFORM)) ||
446 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
447 DisplayItemType::TYPE_OPACITY)) ||
448 seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
449 DisplayItemType::TYPE_BACKGROUND_COLOR)),
450 "The property set of output should be the subset of transform-like "
451 "properties, opacity, or background_color.");
452
453 if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
454 DisplayItemType::TYPE_TRANSFORM))) {
455 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
456 }
457
458 if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
459 MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
460 MOZ_ASSERT(storageData.mTransformData->motionPathData(),
461 "Should have MotionPathData");
462 }
463 }
464 #endif
465
466 return storageData;
467 }
468
GetNextCompositorAnimationsId()469 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
470 static uint32_t sNextId = 0;
471 ++sNextId;
472
473 uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
474 uint64_t nextId = procId;
475 nextId = nextId << 32 | sNextId;
476 return nextId;
477 }
478
ServoAnimationValueToMatrix4x4(const nsTArray<RefPtr<RawServoAnimationValue>> & aValues,const TransformData & aTransformData,gfx::Path * aCachedMotionPath)479 gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
480 const nsTArray<RefPtr<RawServoAnimationValue>>& aValues,
481 const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
482 using nsStyleTransformMatrix::TransformReferenceBox;
483
484 // This is a bit silly just to avoid the transform list copy from the
485 // animation transform list.
486 auto noneTranslate = StyleTranslate::None();
487 auto noneRotate = StyleRotate::None();
488 auto noneScale = StyleScale::None();
489 const StyleTransform noneTransform;
490
491 const StyleTranslate* translate = nullptr;
492 const StyleRotate* rotate = nullptr;
493 const StyleScale* scale = nullptr;
494 const StyleTransform* transform = nullptr;
495 const StyleOffsetPath* path = nullptr;
496 const StyleLengthPercentage* distance = nullptr;
497 const StyleOffsetRotate* offsetRotate = nullptr;
498 const StylePositionOrAuto* anchor = nullptr;
499
500 for (const auto& value : aValues) {
501 MOZ_ASSERT(value);
502 nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value);
503 switch (id) {
504 case eCSSProperty_transform:
505 MOZ_ASSERT(!transform);
506 transform = Servo_AnimationValue_GetTransform(value);
507 break;
508 case eCSSProperty_translate:
509 MOZ_ASSERT(!translate);
510 translate = Servo_AnimationValue_GetTranslate(value);
511 break;
512 case eCSSProperty_rotate:
513 MOZ_ASSERT(!rotate);
514 rotate = Servo_AnimationValue_GetRotate(value);
515 break;
516 case eCSSProperty_scale:
517 MOZ_ASSERT(!scale);
518 scale = Servo_AnimationValue_GetScale(value);
519 break;
520 case eCSSProperty_offset_path:
521 MOZ_ASSERT(!path);
522 path = Servo_AnimationValue_GetOffsetPath(value);
523 break;
524 case eCSSProperty_offset_distance:
525 MOZ_ASSERT(!distance);
526 distance = Servo_AnimationValue_GetOffsetDistance(value);
527 break;
528 case eCSSProperty_offset_rotate:
529 MOZ_ASSERT(!offsetRotate);
530 offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
531 break;
532 case eCSSProperty_offset_anchor:
533 MOZ_ASSERT(!anchor);
534 anchor = Servo_AnimationValue_GetOffsetAnchor(value);
535 break;
536 default:
537 MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
538 }
539 }
540
541 TransformReferenceBox refBox(nullptr, aTransformData.bounds());
542 Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
543 path, distance, offsetRotate, anchor, aTransformData.motionPathData(),
544 refBox, aCachedMotionPath);
545
546 // We expect all our transform data to arrive in device pixels
547 gfx::Point3D transformOrigin = aTransformData.transformOrigin();
548 nsDisplayTransform::FrameTransformProperties props(
549 translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
550 scale ? *scale : noneScale, transform ? *transform : noneTransform,
551 motion, transformOrigin);
552
553 return nsDisplayTransform::GetResultingTransformMatrix(
554 props, refBox, aTransformData.appUnitsPerDevPixel());
555 }
556
CollectOverflowedSideLines(const gfxQuad & aPrerenderedQuad,SideBits aOverflowSides,gfxLineSegment sideLines[4])557 static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
558 SideBits aOverflowSides,
559 gfxLineSegment sideLines[4]) {
560 uint8_t count = 0;
561
562 if (aOverflowSides & SideBits::eTop) {
563 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
564 aPrerenderedQuad.mPoints[1]);
565 count++;
566 }
567 if (aOverflowSides & SideBits::eRight) {
568 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
569 aPrerenderedQuad.mPoints[2]);
570 count++;
571 }
572 if (aOverflowSides & SideBits::eBottom) {
573 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
574 aPrerenderedQuad.mPoints[3]);
575 count++;
576 }
577 if (aOverflowSides & SideBits::eLeft) {
578 sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
579 aPrerenderedQuad.mPoints[0]);
580 count++;
581 }
582
583 return count;
584 }
585
586 enum RegionBits : uint8_t {
587 Inside = 0,
588 Left = (1 << 0),
589 Right = (1 << 1),
590 Bottom = (1 << 2),
591 Top = (1 << 3),
592 };
593
594 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
595
GetRegionBitsForPoint(double aX,double aY,const gfxRect & aClip)596 static RegionBits GetRegionBitsForPoint(double aX, double aY,
597 const gfxRect& aClip) {
598 RegionBits result = RegionBits::Inside;
599 if (aX < aClip.X()) {
600 result |= RegionBits::Left;
601 } else if (aX > aClip.XMost()) {
602 result |= RegionBits::Right;
603 }
604
605 if (aY < aClip.Y()) {
606 result |= RegionBits::Bottom;
607 } else if (aY > aClip.YMost()) {
608 result |= RegionBits::Top;
609 }
610 return result;
611 };
612
613 // https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
LineSegmentIntersectsClip(double aX0,double aY0,double aX1,double aY1,const gfxRect & aClip)614 static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
615 double aY1, const gfxRect& aClip) {
616 RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
617 RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
618
619 while (true) {
620 if (!(b0 | b1)) {
621 // Completely inside.
622 return true;
623 }
624
625 if (b0 & b1) {
626 // Completely outside.
627 return false;
628 }
629
630 double x, y;
631 // Choose an outside point.
632 RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
633 if (outsidePointBits & RegionBits::Top) {
634 x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
635 y = aClip.YMost();
636 } else if (outsidePointBits & RegionBits::Bottom) {
637 x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
638 y = aClip.Y();
639 } else if (outsidePointBits & RegionBits::Right) {
640 y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
641 x = aClip.XMost();
642 } else if (outsidePointBits & RegionBits::Left) {
643 y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
644 x = aClip.X();
645 }
646
647 if (outsidePointBits == b0) {
648 aX0 = x;
649 aY0 = y;
650 b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
651 } else {
652 aX1 = x;
653 aY1 = y;
654 b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
655 }
656 }
657 MOZ_ASSERT_UNREACHABLE();
658 return false;
659 }
660
661 // static
ShouldBeJank(const LayoutDeviceRect & aPrerenderedRect,SideBits aOverflowSides,const gfx::Matrix4x4 & aTransform,const ParentLayerRect & aClipRect)662 bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
663 SideBits aOverflowSides,
664 const gfx::Matrix4x4& aTransform,
665 const ParentLayerRect& aClipRect) {
666 if (aClipRect.IsEmpty()) {
667 return false;
668 }
669
670 gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
671 ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
672
673 gfxLineSegment sideLines[4];
674 uint8_t overflowSideCount =
675 CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
676
677 gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
678 for (uint8_t j = 0; j < overflowSideCount; j++) {
679 if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
680 sideLines[j].mEnd.x, sideLines[j].mEnd.y,
681 clipRect)) {
682 return true;
683 }
684 }
685
686 // With step timing functions there are cases the transform jumps to a
687 // position where the partial pre-render area is totally outside of the clip
688 // rect without any intersection of the partial pre-render area and the clip
689 // rect happened in previous compositions but there remains visible area of
690 // the entire transformed area.
691 //
692 // So now all four points of the transformed partial pre-render rect are
693 // outside of the clip rect, if all these four points are in either side of
694 // the clip rect, we consider it's jank so that on the main-thread we will
695 // either a) rebuild the up-to-date display item if there remains visible area
696 // or b) no longer rebuild the display item if it's totally outside of the
697 // clip rect.
698 //
699 // Note that RegionBits::Left and Right are mutually exclusive,
700 // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
701 // any bits, it means all four points are in the same side.
702 return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
703 prerenderedQuad.mPoints[0].y, clipRect) &
704 GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
705 prerenderedQuad.mPoints[1].y, clipRect) &
706 GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
707 prerenderedQuad.mPoints[2].y, clipRect) &
708 GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
709 prerenderedQuad.mPoints[3].y, clipRect);
710 }
711
712 } // namespace layers
713 } // namespace mozilla
714