tor-browser

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

CSSAnimation.cpp (13300B)


      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 "CSSAnimation.h"
      8 
      9 #include "mozilla/AnimationEventDispatcher.h"
     10 #include "mozilla/TimeStamp.h"
     11 #include "mozilla/dom/CSSAnimationBinding.h"
     12 #include "mozilla/dom/KeyframeEffectBinding.h"
     13 #include "nsPresContext.h"
     14 
     15 namespace mozilla::dom {
     16 
     17 using AnimationPhase = ComputedTiming::AnimationPhase;
     18 
     19 JSObject* CSSAnimation::WrapObject(JSContext* aCx,
     20                                   JS::Handle<JSObject*> aGivenProto) {
     21  return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto);
     22 }
     23 
     24 void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
     25  Animation::SetEffect(aEffect);
     26 
     27  AddOverriddenProperties(CSSAnimationProperties::Effect);
     28 }
     29 
     30 void CSSAnimation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) {
     31  // Note that we always compare with the paused state since for the purposes
     32  // of determining if play control is being overridden or not, we want to
     33  // treat the finished state as running.
     34  bool wasPaused = PlayState() == AnimationPlayState::Paused;
     35 
     36  Animation::SetStartTimeAsDouble(aStartTime);
     37 
     38  bool isPaused = PlayState() == AnimationPlayState::Paused;
     39 
     40  if (wasPaused != isPaused) {
     41    AddOverriddenProperties(CSSAnimationProperties::PlayState);
     42  }
     43 }
     44 
     45 mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
     46  FlushUnanimatedStyle();
     47  return Animation::GetReady(aRv);
     48 }
     49 
     50 void CSSAnimation::Reverse(ErrorResult& aRv) {
     51  // As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in
     52  // the paused state.
     53  bool wasPaused = PlayState() == AnimationPlayState::Paused;
     54 
     55  Animation::Reverse(aRv);
     56  if (aRv.Failed()) {
     57    return;
     58  }
     59 
     60  bool isPaused = PlayState() == AnimationPlayState::Paused;
     61 
     62  if (wasPaused != isPaused) {
     63    AddOverriddenProperties(CSSAnimationProperties::PlayState);
     64  }
     65 }
     66 
     67 AnimationPlayState CSSAnimation::PlayStateFromJS() const {
     68  // Flush style to ensure that any properties controlling animation state
     69  // (e.g. animation-play-state) are fully updated.
     70  FlushUnanimatedStyle();
     71  return Animation::PlayStateFromJS();
     72 }
     73 
     74 bool CSSAnimation::PendingFromJS() const {
     75  // Flush style since, for example, if the animation-play-state was just
     76  // changed its possible we should now be pending.
     77  FlushUnanimatedStyle();
     78  return Animation::PendingFromJS();
     79 }
     80 
     81 void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
     82  // Note that flushing style below might trigger calls to
     83  // PlayFromStyle()/PauseFromStyle() on this object.
     84  FlushUnanimatedStyle();
     85  Animation::PlayFromJS(aRv);
     86  if (aRv.Failed()) {
     87    return;
     88  }
     89 
     90  AddOverriddenProperties(CSSAnimationProperties::PlayState);
     91 }
     92 
     93 void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
     94  Animation::PauseFromJS(aRv);
     95  if (aRv.Failed()) {
     96    return;
     97  }
     98 
     99  AddOverriddenProperties(CSSAnimationProperties::PlayState);
    100 }
    101 
    102 void CSSAnimation::PlayFromStyle() {
    103  ErrorResult rv;
    104  Animation::Play(rv, Animation::LimitBehavior::Continue);
    105  // play() should not throw when LimitBehavior is Continue
    106  MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
    107 }
    108 
    109 void CSSAnimation::PauseFromStyle() {
    110  ErrorResult rv;
    111  Animation::Pause(rv);
    112  // pause() should only throw when *all* of the following conditions are true:
    113  // - we are in the idle state, and
    114  // - we have a negative playback rate, and
    115  // - we have an infinitely repeating animation
    116  // The first two conditions will never happen under regular style processing
    117  // but could happen if an author made modifications to the Animation object
    118  // and then updated animation-play-state. It's an unusual case and there's
    119  // no obvious way to pass on the exception information so we just silently
    120  // fail for now.
    121  if (rv.Failed()) {
    122    NS_WARNING("Unexpected exception pausing animation - silently failing");
    123  }
    124 }
    125 
    126 void CSSAnimation::Tick(TickState& aState) {
    127  Animation::Tick(aState);
    128  QueueEvents();
    129 }
    130 
    131 int32_t CSSAnimation::CompareCompositeOrder(
    132    const CSSAnimation& aOther, nsContentUtils::NodeIndexCache& aCache) const {
    133  MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
    134             "Should only be called for CSS animations that are sorted "
    135             "as CSS animations (i.e. tied to CSS markup)");
    136 
    137  // 0. Object-equality case
    138  if (&aOther == this) {
    139    return 0;
    140  }
    141 
    142  // 1. Sort by document order
    143  if (!mOwningElement.Equals(aOther.mOwningElement)) {
    144    return mOwningElement.Compare(aOther.mOwningElement, aCache);
    145  }
    146 
    147  // 2. (Same element and pseudo): Sort by position in animation-name
    148  MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex);
    149  return mAnimationIndex < aOther.mAnimationIndex ? -1 : 1;
    150 }
    151 
    152 void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
    153  // If the animation is pending, we ignore animation events until we finish
    154  // pending.
    155  if (mPendingState != PendingState::NotPending) {
    156    return;
    157  }
    158 
    159  // CSS animations dispatch events at their owning element. This allows
    160  // script to repurpose a CSS animation to target a different element,
    161  // to use a group effect (which has no obvious "target element"), or
    162  // to remove the animation effect altogether whilst still getting
    163  // animation events.
    164  //
    165  // It does mean, however, that for a CSS animation that has no owning
    166  // element (e.g. it was created using the CSSAnimation constructor or
    167  // disassociated from CSS) no events are fired. If it becomes desirable
    168  // for these animations to still fire events we should spec the concept
    169  // of the "original owning element" or "event target" and allow script
    170  // to set it when creating a CSSAnimation object.
    171  if (!mOwningElement.ShouldFireEvents()) {
    172    return;
    173  }
    174 
    175  nsPresContext* presContext = mOwningElement.GetPresContext();
    176  if (!presContext) {
    177    return;
    178  }
    179 
    180  uint64_t currentIteration = 0;
    181  ComputedTiming::AnimationPhase currentPhase;
    182  StickyTimeDuration intervalStartTime;
    183  StickyTimeDuration intervalEndTime;
    184  StickyTimeDuration iterationStartTime;
    185 
    186  if (!mEffect) {
    187    currentPhase =
    188        GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
    189    if (currentPhase == mPreviousPhase) {
    190      return;
    191    }
    192  } else {
    193    ComputedTiming computedTiming = mEffect->GetComputedTiming();
    194    currentPhase = computedTiming.mPhase;
    195    currentIteration = computedTiming.mCurrentIteration;
    196    if (currentPhase == mPreviousPhase &&
    197        currentIteration == mPreviousIteration) {
    198      return;
    199    }
    200    intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
    201    intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
    202 
    203    uint64_t iterationBoundary = mPreviousIteration > currentIteration
    204                                     ? currentIteration + 1
    205                                     : currentIteration;
    206    double multiplier = iterationBoundary - computedTiming.mIterationStart;
    207    if (multiplier != 0.0) {
    208      iterationStartTime = computedTiming.mDuration.MultDouble(multiplier);
    209    }
    210  }
    211 
    212  TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
    213  TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
    214  TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
    215 
    216  AutoTArray<AnimationEventInfo, 2> events;
    217 
    218  auto appendAnimationEvent = [&](EventMessage aMessage,
    219                                  const StickyTimeDuration& aElapsedTime,
    220                                  const TimeStamp& aScheduledEventTimeStamp) {
    221    double elapsedTime = aElapsedTime.ToSeconds();
    222    if (aMessage == eAnimationCancel) {
    223      // 0 is an inappropriate value for this callsite. What we need to do is
    224      // use a single random value for all increasing times reportable.
    225      // That is to say, whenever elapsedTime goes negative (because an
    226      // animation restarts, something rewinds the animation, or otherwise)
    227      // a new random value for the mix-in must be generated.
    228      elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
    229          elapsedTime, 0, mRTPCallerType);
    230    }
    231    events.AppendElement(AnimationEventInfo(
    232        mAnimationName, mOwningElement.Target(), aMessage, elapsedTime,
    233        mAnimationIndex, aScheduledEventTimeStamp, this));
    234  };
    235 
    236  // Handle cancel event first
    237  if ((mPreviousPhase != AnimationPhase::Idle &&
    238       mPreviousPhase != AnimationPhase::After) &&
    239      currentPhase == AnimationPhase::Idle) {
    240    appendAnimationEvent(eAnimationCancel, aActiveTime,
    241                         GetTimelineCurrentTimeAsTimeStamp());
    242  }
    243 
    244  switch (mPreviousPhase) {
    245    case AnimationPhase::Idle:
    246    case AnimationPhase::Before:
    247      if (currentPhase == AnimationPhase::Active) {
    248        appendAnimationEvent(eAnimationStart, intervalStartTime,
    249                             startTimeStamp);
    250      } else if (currentPhase == AnimationPhase::After) {
    251        appendAnimationEvent(eAnimationStart, intervalStartTime,
    252                             startTimeStamp);
    253        appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
    254      }
    255      break;
    256    case AnimationPhase::Active:
    257      if (currentPhase == AnimationPhase::Before) {
    258        appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
    259      } else if (currentPhase == AnimationPhase::Active) {
    260        // The currentIteration must have changed or element we would have
    261        // returned early above.
    262        MOZ_ASSERT(currentIteration != mPreviousIteration);
    263        appendAnimationEvent(eAnimationIteration, iterationStartTime,
    264                             iterationTimeStamp);
    265      } else if (currentPhase == AnimationPhase::After) {
    266        appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
    267      }
    268      break;
    269    case AnimationPhase::After:
    270      if (currentPhase == AnimationPhase::Before) {
    271        appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
    272        appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
    273      } else if (currentPhase == AnimationPhase::Active) {
    274        appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
    275      }
    276      break;
    277  }
    278  mPreviousPhase = currentPhase;
    279  mPreviousIteration = currentIteration;
    280 
    281  if (!events.IsEmpty()) {
    282    presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
    283  }
    284 }
    285 
    286 void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
    287                                SyncNotifyFlag aSyncNotifyFlag) {
    288  if (mNeedsNewAnimationIndexWhenRun &&
    289      PlayState() != AnimationPlayState::Idle) {
    290    mAnimationIndex = sNextAnimationIndex++;
    291    mNeedsNewAnimationIndexWhenRun = false;
    292  }
    293 
    294  Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
    295 }
    296 
    297 /////////////////////// CSSAnimationKeyframeEffect ////////////////////////
    298 
    299 void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
    300  MaybeFlushUnanimatedStyle();
    301  KeyframeEffect::GetTiming(aRetVal);
    302 }
    303 
    304 void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
    305    ComputedEffectTiming& aRetVal) const {
    306  MaybeFlushUnanimatedStyle();
    307  KeyframeEffect::GetComputedTimingAsDict(aRetVal);
    308 }
    309 
    310 void CSSAnimationKeyframeEffect::UpdateTiming(
    311    const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
    312  KeyframeEffect::UpdateTiming(aTiming, aRv);
    313 
    314  if (aRv.Failed()) {
    315    return;
    316  }
    317 
    318  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    319    CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
    320    if (aTiming.mDuration.WasPassed()) {
    321      updatedProperties |= CSSAnimationProperties::Duration;
    322    }
    323    if (aTiming.mIterations.WasPassed()) {
    324      updatedProperties |= CSSAnimationProperties::IterationCount;
    325    }
    326    if (aTiming.mDirection.WasPassed()) {
    327      updatedProperties |= CSSAnimationProperties::Direction;
    328    }
    329    if (aTiming.mDelay.WasPassed()) {
    330      updatedProperties |= CSSAnimationProperties::Delay;
    331    }
    332    if (aTiming.mFill.WasPassed()) {
    333      updatedProperties |= CSSAnimationProperties::FillMode;
    334    }
    335 
    336    cssAnimation->AddOverriddenProperties(updatedProperties);
    337  }
    338 }
    339 
    340 void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
    341                                              JS::Handle<JSObject*> aKeyframes,
    342                                              ErrorResult& aRv) {
    343  KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv);
    344 
    345  if (aRv.Failed()) {
    346    return;
    347  }
    348 
    349  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    350    cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes);
    351  }
    352 }
    353 
    354 void CSSAnimationKeyframeEffect::SetComposite(
    355    const CompositeOperation& aComposite) {
    356  KeyframeEffect::SetComposite(aComposite);
    357 
    358  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    359    cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Composition);
    360  }
    361 }
    362 
    363 void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
    364  if (!GetOwningCSSAnimation()) {
    365    return;
    366  }
    367 
    368  if (dom::Document* doc = GetRenderedDocument()) {
    369    doc->FlushPendingNotifications(
    370        ChangesToFlush(FlushType::Style, false /* flush animations */, false));
    371  }
    372 }
    373 
    374 }  // namespace mozilla::dom