tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AnimationHelper.cpp (36331B)


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