summaryrefslogtreecommitdiffstats
path: root/gfx/layers/AnimationHelper.cpp
blob: ece69cd40380a94e489cd39f750adb042681dbaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "AnimationHelper.h"
#include "base/process_util.h"
#include "gfx2DGlue.h"                 // for ThebesRect
#include "gfxLineSegment.h"            // for gfxLineSegment
#include "gfxPoint.h"                  // for gfxPoint
#include "gfxQuad.h"                   // for gfxQuad
#include "gfxRect.h"                   // for gfxRect
#include "gfxUtils.h"                  // for gfxUtils::TransformToQuad
#include "mozilla/ServoStyleConsts.h"  // for StyleComputedTimingFunction
#include "mozilla/dom/AnimationEffectBinding.h"  // for dom::FillMode
#include "mozilla/dom/KeyframeEffectBinding.h"   // for dom::IterationComposite
#include "mozilla/dom/KeyframeEffect.h"  // for dom::KeyFrameEffectReadOnly
#include "mozilla/dom/Nullable.h"        // for dom::Nullable
#include "mozilla/layers/APZSampler.h"   // for APZSampler
#include "mozilla/AnimatedPropertyID.h"
#include "mozilla/LayerAnimationInfo.h"   // for GetCSSPropertiesFor()
#include "mozilla/Maybe.h"                // for Maybe<>
#include "mozilla/MotionPathUtils.h"      // for ResolveMotionPath()
#include "mozilla/StyleAnimationValue.h"  // for StyleAnimationValue, etc
#include "nsCSSPropertyID.h"              // for eCSSProperty_offset_path, etc
#include "nsDisplayList.h"                // for nsDisplayTransform, etc

namespace mozilla::layers {

static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
    const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
    const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime,
    const TimeDuration& aStartTime, float aPlaybackRate) {
  // We return Nothing If the associated APZ controller is not available
  // (because it may be destroyed but this animation is still alive).
  if (!aScrollMeta) {
    // This may happen after we reload a page. There may be a race condition
    // because the animation is still alive but the APZ is destroyed. In this
    // case, this animation is invalid, so we return nullptr.
    return nullptr;
  }

  const bool isHorizontal =
      aOptions.axis() == layers::ScrollDirection::eHorizontal;
  double range =
      isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height;
  MOZ_ASSERT(
      range > 0,
      "We don't expect to get a zero or negative range on the compositor");

  // The offset may be negative if the writing mode is from right to left.
  // Use std::abs() here to avoid getting a negative progress.
  double position =
      std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y);
  double progress = position / range;
  // Just in case to avoid getting a progress more than 100%, for overscrolling.
  progress = std::min(progress, 1.0);
  auto timelineTime = TimeDuration(aEndTime.MultDouble(progress));
  return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
                                                     aPlaybackRate);
}

static dom::Nullable<TimeDuration> CalculateElapsedTime(
    const APZSampler* aAPZSampler, const LayersId& aLayersId,
    const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation,
    const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime,
    const AnimatedValue* aPreviousValue) {
  // -------------------------------------
  // Case 1: scroll-timeline animations.
  // -------------------------------------
  if (aAnimation.mScrollTimelineOptions) {
    MOZ_ASSERT(
        aAPZSampler,
        "We don't send scroll animations to the compositor if APZ is disabled");

    return CalculateElapsedTimeForScrollTimeline(
        aAPZSampler->GetCurrentScrollOffsetAndRange(
            aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
            aProofOfMapLock),
        aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(),
        aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
        aAnimation.mPlaybackRate);
  }

  // -------------------------------------
  // Case 2: document-timeline animations.
  // -------------------------------------
  MOZ_ASSERT(
      (!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) ||
          aAnimation.mIsNotPlaying,
      "If we are playing, we should have an origin time and a start time");

  // Determine if the animation was play-pending and used a ready time later
  // than the previous frame time.
  //
  // To determine this, _all_ of the following conditions need to hold:
  //
  // * There was no previous animation value (i.e. this is the first frame for
  //   the animation since it was sent to the compositor), and
  // * The animation is playing, and
  // * There is a previous frame time, and
  // * The ready time of the animation is ahead of the previous frame time.
  //
  bool hasFutureReadyTime = false;
  if (!aPreviousValue && !aAnimation.mIsNotPlaying &&
      !aPreviousFrameTime.IsNull()) {
    // This is the inverse of the calculation performed in
    // AnimationInfo::StartPendingAnimations to calculate the start time of
    // play-pending animations.
    // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
    // underflow in the middle of the calulation.
    const TimeStamp readyTime =
        aAnimation.mOriginTime +
        (aAnimation.mStartTime.ref() +
         aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate));
    hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime;
  }
  // Use the previous vsync time to make main thread animations and compositor
  // more closely aligned.
  //
  // On the first frame where we have animations the previous timestamp will
  // not be set so we simply use the current timestamp.  As a result we will
  // end up painting the first frame twice.  That doesn't appear to be
  // noticeable, however.
  //
  // Likewise, if the animation is play-pending, it may have a ready time that
  // is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
  // To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
  // jumping backwards into the range prior to when the animation starts.
  const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime
                                   ? aCurrentFrameTime
                                   : aPreviousFrameTime;

  // If the animation is not currently playing, e.g. paused or
  // finished, then use the hold time to stay at the same position.
  TimeDuration elapsedDuration =
      aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing()
          ? aAnimation.mHoldTime
          : (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref())
                .MultDouble(aAnimation.mPlaybackRate);
  return elapsedDuration;
}

enum class CanSkipCompose {
  IfPossible,
  No,
};
// This function samples the animation for a specific property. We may have
// multiple animations for a single property, and the later animations override
// the eariler ones. This function returns the sampled animation value,
// |aAnimationValue| for a single CSS property.
static AnimationHelper::SampleResult SampleAnimationForProperty(
    const APZSampler* aAPZSampler, const LayersId& aLayersId,
    const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
    TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
    CanSkipCompose aCanSkipCompose,
    nsTArray<PropertyAnimation>& aPropertyAnimations,
    RefPtr<StyleAnimationValue>& aAnimationValue) {
  MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");

  auto reason = AnimationHelper::SampleResult::Reason::None;
  bool hasInEffectAnimations = false;
#ifdef DEBUG
  // In cases where this function returns a SampleResult::Skipped, we actually
  // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
  // call site that the value that would have been computed matches the stored
  // value that we end up using. This flag is used to ensure we populate
  // aAnimationValue in this scenario.
  bool shouldBeSkipped = false;
#endif
  // Process in order, since later animations override earlier ones.
  for (PropertyAnimation& animation : aPropertyAnimations) {
    dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime(
        aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
        aCurrentFrameTime, aPreviousValue);

    const auto progressTimelinePosition =
        animation.mScrollTimelineOptions
            ? dom::Animation::AtProgressTimelineBoundary(
                  TimeDuration::FromMilliseconds(
                      PROGRESS_TIMELINE_DURATION_MILLISEC),
                  elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
                  animation.mPlaybackRate)
            : dom::Animation::ProgressTimelinePosition::NotBoundary;

    ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
        elapsedDuration, animation.mTiming, animation.mPlaybackRate,
        progressTimelinePosition);

    if (computedTiming.mProgress.IsNull()) {
      // For the scroll-driven animations, it's possible to let it go between
      // the active phase and the before/after phase, and so its progress
      // becomes null. In this case, we shouldn't just skip this animation.
      // Instead, we have to reset the previous sampled result. Basically, we
      // use |mProgressOnLastCompose| to check if it goes from the active phase.
      // If so, we set the returned |mReason| to ScrollToDelayPhase to let the
      // caller know we need to use the base style for this property.
      //
      // If there are any other animations which need to be sampled together
      // (in the same property animation group), this |reason| will be ignored.
      if (animation.mScrollTimelineOptions &&
          !animation.mProgressOnLastCompose.IsNull() &&
          (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
           computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) {
        // Appearally, we go back to delay, so need to reset the last
        // composition meta data. This is necessary because
        // 1. this animation is in delay so it shouldn't have any composition
        //    meta data, and
        // 2. we will not go into this condition multiple times during delay
        //    phase because we rely on |mProgressOnLastCompose|.
        animation.ResetLastCompositionValues();
        reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase;
      }
      continue;
    }

    dom::IterationCompositeOperation iterCompositeOperation =
        animation.mIterationComposite;

    // Skip calculation if the progress hasn't changed since the last
    // calculation.
    // Note that we don't skip calculate this animation if there is another
    // animation since the other animation might be 'accumulate' or 'add', or
    // might have a missing keyframe (i.e. this animation value will be used in
    // the missing keyframe).
    // FIXME Bug 1455476: We should do this optimizations for the case where
    // the layer has multiple animations and multiple properties.
    if (aCanSkipCompose == CanSkipCompose::IfPossible &&
        !dom::KeyframeEffect::HasComputedTimingChanged(
            computedTiming, iterCompositeOperation,
            animation.mProgressOnLastCompose,
            animation.mCurrentIterationOnLastCompose)) {
#ifdef DEBUG
      shouldBeSkipped = true;
#else
      return AnimationHelper::SampleResult::Skipped();
#endif
    }

    uint32_t segmentIndex = 0;
    size_t segmentSize = animation.mSegments.Length();
    PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
    while (segment->mEndPortion < computedTiming.mProgress.Value() &&
           segmentIndex < segmentSize - 1) {
      ++segment;
      ++segmentIndex;
    }

    double positionInSegment =
        (computedTiming.mProgress.Value() - segment->mStartPortion) /
        (segment->mEndPortion - segment->mStartPortion);

    double portion = StyleComputedTimingFunction::GetPortion(
        segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);

    // Like above optimization, skip calculation if the target segment isn't
    // changed and if the portion in the segment isn't changed.
    // This optimization is needed for CSS animations/transitions with step
    // timing functions (e.g. the throbber animation on tabs or frame based
    // animations).
    // FIXME Bug 1455476: Like the above optimization, we should apply this
    // optimizations for multiple animation cases and multiple properties as
    // well.
    if (aCanSkipCompose == CanSkipCompose::IfPossible &&
        animation.mSegmentIndexOnLastCompose == segmentIndex &&
        !animation.mPortionInSegmentOnLastCompose.IsNull() &&
        animation.mPortionInSegmentOnLastCompose.Value() == portion) {
#ifdef DEBUG
      shouldBeSkipped = true;
#else
      return AnimationHelper::SampleResult::Skipped();
#endif
    }

    AnimationPropertySegment animSegment;
    animSegment.mFromKey = 0.0;
    animSegment.mToKey = 1.0;
    animSegment.mFromValue = AnimationValue(segment->mStartValue);
    animSegment.mToValue = AnimationValue(segment->mEndValue);
    animSegment.mFromComposite = segment->mStartComposite;
    animSegment.mToComposite = segment->mEndComposite;

    // interpolate the property
    aAnimationValue =
        Servo_ComposeAnimationSegment(
            &animSegment, aAnimationValue,
            animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
            portion, computedTiming.mCurrentIteration)
            .Consume();

#ifdef DEBUG
    if (shouldBeSkipped) {
      return AnimationHelper::SampleResult::Skipped();
    }
#endif

    hasInEffectAnimations = true;
    animation.mProgressOnLastCompose = computedTiming.mProgress;
    animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
    animation.mSegmentIndexOnLastCompose = segmentIndex;
    animation.mPortionInSegmentOnLastCompose.SetValue(portion);
  }

  auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled()
                                  : AnimationHelper::SampleResult();
  rv.mReason = reason;
  return rv;
}

// This function samples the animations for a group of CSS properties. We may
// have multiple CSS properties in a group (e.g. transform-like properties).
// So the returned animation array, |aAnimationValues|, include all the
// animation values of these CSS properties.
AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
    const APZSampler* aAPZSampler, const LayersId& aLayersId,
    const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
    TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
    nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
    nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues /* out */) {
  MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
             "Should be called with animation data");
  MOZ_ASSERT(aAnimationValues.IsEmpty(),
             "Should be called with empty aAnimationValues");

  nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations;
  nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues;
  for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
    // Initialize animation value with base style.
    RefPtr<StyleAnimationValue> currValue = group.mBaseStyle;

    CanSkipCompose canSkipCompose =
        aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
                group.mAnimations.Length() == 1
            ? CanSkipCompose::IfPossible
            : CanSkipCompose::No;

    MOZ_ASSERT(
        !group.mAnimations.IsEmpty() ||
            nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
                group.mProperty),
        "Only transform-like properties can have empty PropertyAnimation list");

    // For properties which are not animating (i.e. their values are always the
    // same), we store them in a different array, and then merge them into the
    // final result (a.k.a. aAnimationValues) because we shouldn't take them
    // into account for SampleResult. (In other words, these properties
    // shouldn't affect the optimization.)
    if (group.mAnimations.IsEmpty()) {
      nonAnimatingValues.AppendElement(std::move(currValue));
      continue;
    }

    SampleResult result = SampleAnimationForProperty(
        aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime,
        aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations,
        currValue);

    // FIXME: Bug 1455476: Do optimization for multiple properties. For now,
    // the result is skipped only if the property count == 1.
    if (result.IsSkipped()) {
#ifdef DEBUG
      aAnimationValues.AppendElement(std::move(currValue));
#endif
      return result;
    }

    if (!result.IsSampled()) {
      if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) {
        MOZ_ASSERT(currValue && currValue == group.mBaseStyle);
        baseStyleOfDelayAnimations.AppendElement(std::move(currValue));
      }
      continue;
    }

    // Insert the interpolation result into the output array.
    MOZ_ASSERT(currValue);
    aAnimationValues.AppendElement(std::move(currValue));
  }

  SampleResult rv =
      aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled();

  // If there is no other sampled result, we may store these base styles
  // (together with the non-animating values) to the webrenderer before it gets
  // sync with the main thread.
  if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) {
    aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations));
    rv.mReason = SampleResult::Reason::ScrollToDelayPhase;
  }

  if (!aAnimationValues.IsEmpty()) {
    aAnimationValues.AppendElements(std::move(nonAnimatingValues));
  }
  return rv;
}

static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
  // Adjust fill mode so that if the main thread is delayed in clearing
  // this animation we don't introduce flicker by jumping back to the old
  // underlying value.
  auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
  float playbackRate = aAnimation.playbackRate();
  switch (fillMode) {
    case dom::FillMode::None:
      if (playbackRate > 0) {
        fillMode = dom::FillMode::Forwards;
      } else if (playbackRate < 0) {
        fillMode = dom::FillMode::Backwards;
      }
      break;
    case dom::FillMode::Backwards:
      if (playbackRate > 0) {
        fillMode = dom::FillMode::Both;
      }
      break;
    case dom::FillMode::Forwards:
      if (playbackRate < 0) {
        fillMode = dom::FillMode::Both;
      }
      break;
    default:
      break;
  }
  return fillMode;
}

#ifdef DEBUG
static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
  nsCSSPropertyIDSet transformSet =
      nsCSSPropertyIDSet::TransformLikeProperties();

  for (const Animation& animation : aAnimations) {
    if (animation.isNotAnimating()) {
      continue;
    }

    if (transformSet.HasProperty(animation.property())) {
      return true;
    }
  }

  return false;
}
#endif

AnimationStorageData AnimationHelper::ExtractAnimations(
    const LayersId& aLayersId, const AnimationArray& aAnimations) {
  AnimationStorageData storageData;
  storageData.mLayersId = aLayersId;

  nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
  PropertyAnimationGroup* currData = nullptr;
  DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;

  for (const Animation& animation : aAnimations) {
    // Animations with same property are grouped together, so we can just
    // check if the current property is the same as the previous one for
    // knowing this is a new group.
    if (prevID != animation.property()) {
      // Got a different group, we should create a different array.
      currData = storageData.mAnimation.AppendElement();
      currData->mProperty = animation.property();
      if (animation.transformData()) {
        MOZ_ASSERT(!storageData.mTransformData,
                   "Only one entry has TransformData");
        storageData.mTransformData = animation.transformData();
      }

      prevID = animation.property();

      // Reset the debug pointer.
      currBaseStyle = nullptr;
    }

    MOZ_ASSERT(currData);
    if (animation.baseStyle().type() != Animatable::Tnull_t) {
      MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
                 "Should be the same base style");

      currData->mBaseStyle = AnimationValue::FromAnimatable(
          animation.property(), animation.baseStyle());
      currBaseStyle = &animation.baseStyle();
    }

    // If this layers::Animation sets isNotAnimating to true, it only has
    // base style and doesn't have any animation information, so we can skip
    // the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
    // an empty array.)
    if (animation.isNotAnimating()) {
      MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
                     animation.property()),
                 "Only transform-like properties could set this true");

      if (animation.property() == eCSSProperty_offset_path) {
        MOZ_ASSERT(currData->mBaseStyle,
                   "Fixed offset-path should have base style");
        MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));

        const StyleOffsetPath& offsetPath =
            animation.baseStyle().get_StyleOffsetPath();
        // FIXME: Bug 1837042. Cache all basic shapes.
        if (offsetPath.IsPath()) {
          MOZ_ASSERT(!storageData.mCachedMotionPath,
                     "Only one offset-path: path() is set");

          RefPtr<gfx::PathBuilder> builder =
              MotionPathUtils::GetCompositorPathBuilder();
          storageData.mCachedMotionPath = MotionPathUtils::BuildSVGPath(
              offsetPath.AsSVGPathData(), builder);
        }
      }

      continue;
    }

    PropertyAnimation* propertyAnimation =
        currData->mAnimations.AppendElement();

    propertyAnimation->mOriginTime = animation.originTime();
    propertyAnimation->mStartTime = animation.startTime();
    propertyAnimation->mHoldTime = animation.holdTime();
    propertyAnimation->mPlaybackRate = animation.playbackRate();
    propertyAnimation->mIterationComposite =
        static_cast<dom::IterationCompositeOperation>(
            animation.iterationComposite());
    propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
    propertyAnimation->mTiming =
        TimingParams{animation.duration(),
                     animation.delay(),
                     animation.endDelay(),
                     animation.iterations(),
                     animation.iterationStart(),
                     static_cast<dom::PlaybackDirection>(animation.direction()),
                     GetAdjustedFillMode(animation),
                     animation.easingFunction()};
    propertyAnimation->mScrollTimelineOptions =
        animation.scrollTimelineOptions();

    nsTArray<PropertyAnimation::SegmentData>& segmentData =
        propertyAnimation->mSegments;
    for (const AnimationSegment& segment : animation.segments()) {
      segmentData.AppendElement(PropertyAnimation::SegmentData{
          AnimationValue::FromAnimatable(animation.property(),
                                         segment.startState()),
          AnimationValue::FromAnimatable(animation.property(),
                                         segment.endState()),
          segment.sampleFn(), segment.startPortion(), segment.endPortion(),
          static_cast<dom::CompositeOperation>(segment.startComposite()),
          static_cast<dom::CompositeOperation>(segment.endComposite())});
    }
  }

#ifdef DEBUG
  // Sanity check that the grouped animation data is correct by looking at the
  // property set.
  if (!storageData.mAnimation.IsEmpty()) {
    nsCSSPropertyIDSet seenProperties;
    for (const auto& group : storageData.mAnimation) {
      nsCSSPropertyID id = group.mProperty;

      MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
      seenProperties.AddProperty(id);
    }

    MOZ_ASSERT(
        seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
            DisplayItemType::TYPE_TRANSFORM)) ||
            seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
                DisplayItemType::TYPE_OPACITY)) ||
            seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
                DisplayItemType::TYPE_BACKGROUND_COLOR)),
        "The property set of output should be the subset of transform-like "
        "properties, opacity, or background_color.");

    if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
            DisplayItemType::TYPE_TRANSFORM))) {
      MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
    }

    if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
      MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
      MOZ_ASSERT(storageData.mTransformData->motionPathData(),
                 "Should have MotionPathData");
    }
  }
#endif

  return storageData;
}

uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
  static uint32_t sNextId = 0;
  ++sNextId;

  uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
  uint64_t nextId = procId;
  nextId = nextId << 32 | sNextId;
  return nextId;
}

gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
    const nsTArray<RefPtr<StyleAnimationValue>>& aValues,
    const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
  using nsStyleTransformMatrix::TransformReferenceBox;

  // This is a bit silly just to avoid the transform list copy from the
  // animation transform list.
  auto noneTranslate = StyleTranslate::None();
  auto noneRotate = StyleRotate::None();
  auto noneScale = StyleScale::None();
  const StyleTransform noneTransform;

  const StyleTranslate* translate = nullptr;
  const StyleRotate* rotate = nullptr;
  const StyleScale* scale = nullptr;
  const StyleTransform* transform = nullptr;
  Maybe<StyleOffsetPath> path;
  const StyleLengthPercentage* distance = nullptr;
  const StyleOffsetRotate* offsetRotate = nullptr;
  const StylePositionOrAuto* anchor = nullptr;
  const StyleOffsetPosition* position = nullptr;

  for (const auto& value : aValues) {
    MOZ_ASSERT(value);
    AnimatedPropertyID property(eCSSProperty_UNKNOWN);
    Servo_AnimationValue_GetPropertyId(value, &property);
    switch (property.mID) {
      case eCSSProperty_transform:
        MOZ_ASSERT(!transform);
        transform = Servo_AnimationValue_GetTransform(value);
        break;
      case eCSSProperty_translate:
        MOZ_ASSERT(!translate);
        translate = Servo_AnimationValue_GetTranslate(value);
        break;
      case eCSSProperty_rotate:
        MOZ_ASSERT(!rotate);
        rotate = Servo_AnimationValue_GetRotate(value);
        break;
      case eCSSProperty_scale:
        MOZ_ASSERT(!scale);
        scale = Servo_AnimationValue_GetScale(value);
        break;
      case eCSSProperty_offset_path:
        MOZ_ASSERT(!path);
        path.emplace(StyleOffsetPath::None());
        Servo_AnimationValue_GetOffsetPath(value, path.ptr());
        break;
      case eCSSProperty_offset_distance:
        MOZ_ASSERT(!distance);
        distance = Servo_AnimationValue_GetOffsetDistance(value);
        break;
      case eCSSProperty_offset_rotate:
        MOZ_ASSERT(!offsetRotate);
        offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
        break;
      case eCSSProperty_offset_anchor:
        MOZ_ASSERT(!anchor);
        anchor = Servo_AnimationValue_GetOffsetAnchor(value);
        break;
      case eCSSProperty_offset_position:
        MOZ_ASSERT(!position);
        position = Servo_AnimationValue_GetOffsetPosition(value);
        break;
      default:
        MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
    }
  }

  TransformReferenceBox refBox(nullptr, aTransformData.bounds());
  Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
      path.ptrOr(nullptr), distance, offsetRotate, anchor, position,
      aTransformData.motionPathData(), refBox, aCachedMotionPath);

  // We expect all our transform data to arrive in device pixels
  gfx::Point3D transformOrigin = aTransformData.transformOrigin();
  nsDisplayTransform::FrameTransformProperties props(
      translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
      scale ? *scale : noneScale, transform ? *transform : noneTransform,
      motion, transformOrigin);

  return nsDisplayTransform::GetResultingTransformMatrix(
      props, refBox, aTransformData.appUnitsPerDevPixel());
}

static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
                                          SideBits aOverflowSides,
                                          gfxLineSegment sideLines[4]) {
  uint8_t count = 0;

  if (aOverflowSides & SideBits::eTop) {
    sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
                                      aPrerenderedQuad.mPoints[1]);
    count++;
  }
  if (aOverflowSides & SideBits::eRight) {
    sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
                                      aPrerenderedQuad.mPoints[2]);
    count++;
  }
  if (aOverflowSides & SideBits::eBottom) {
    sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
                                      aPrerenderedQuad.mPoints[3]);
    count++;
  }
  if (aOverflowSides & SideBits::eLeft) {
    sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
                                      aPrerenderedQuad.mPoints[0]);
    count++;
  }

  return count;
}

enum RegionBits : uint8_t {
  Inside = 0,
  Left = (1 << 0),
  Right = (1 << 1),
  Bottom = (1 << 2),
  Top = (1 << 3),
};

MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);

static RegionBits GetRegionBitsForPoint(double aX, double aY,
                                        const gfxRect& aClip) {
  RegionBits result = RegionBits::Inside;
  if (aX < aClip.X()) {
    result |= RegionBits::Left;
  } else if (aX > aClip.XMost()) {
    result |= RegionBits::Right;
  }

  if (aY < aClip.Y()) {
    result |= RegionBits::Bottom;
  } else if (aY > aClip.YMost()) {
    result |= RegionBits::Top;
  }
  return result;
};

// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
                                      double aY1, const gfxRect& aClip) {
  RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
  RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);

  while (true) {
    if (!(b0 | b1)) {
      // Completely inside.
      return true;
    }

    if (b0 & b1) {
      // Completely outside.
      return false;
    }

    double x, y;
    // Choose an outside point.
    RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
    if (outsidePointBits & RegionBits::Top) {
      x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
      y = aClip.YMost();
    } else if (outsidePointBits & RegionBits::Bottom) {
      x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
      y = aClip.Y();
    } else if (outsidePointBits & RegionBits::Right) {
      y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
      x = aClip.XMost();
    } else if (outsidePointBits & RegionBits::Left) {
      y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
      x = aClip.X();
    }

    if (outsidePointBits == b0) {
      aX0 = x;
      aY0 = y;
      b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
    } else {
      aX1 = x;
      aY1 = y;
      b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
    }
  }
  MOZ_ASSERT_UNREACHABLE();
  return false;
}

// static
bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
                                   SideBits aOverflowSides,
                                   const gfx::Matrix4x4& aTransform,
                                   const ParentLayerRect& aClipRect) {
  if (aClipRect.IsEmpty()) {
    return false;
  }

  gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
      ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);

  gfxLineSegment sideLines[4];
  uint8_t overflowSideCount =
      CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);

  gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
  for (uint8_t j = 0; j < overflowSideCount; j++) {
    if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
                                  sideLines[j].mEnd.x, sideLines[j].mEnd.y,
                                  clipRect)) {
      return true;
    }
  }

  // With step timing functions there are cases the transform jumps to a
  // position where the partial pre-render area is totally outside of the clip
  // rect without any intersection of the partial pre-render area and the clip
  // rect happened in previous compositions but there remains visible area of
  // the entire transformed area.
  //
  // So now all four points of the transformed partial pre-render rect are
  // outside of the clip rect, if all these four points are in either side of
  // the clip rect, we consider it's jank so that on the main-thread we will
  // either a) rebuild the up-to-date display item if there remains visible area
  // or b) no longer rebuild the display item if it's totally outside of the
  // clip rect.
  //
  // Note that RegionBits::Left and Right are mutually exclusive,
  // RegionBits::Top and Bottom are also mutually exclusive, so if there remains
  // any bits, it means all four points are in the same side.
  return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
                               prerenderedQuad.mPoints[0].y, clipRect) &
         GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
                               prerenderedQuad.mPoints[1].y, clipRect) &
         GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
                               prerenderedQuad.mPoints[2].y, clipRect) &
         GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
                               prerenderedQuad.mPoints[3].y, clipRect);
}

}  // namespace mozilla::layers