mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-24 19:37:15 +00:00

Instead of starting transitions and animations as a result of a paint, use the refresh driver tick to do this. This sets the transition-ready time to the current time during the next refresh driver tick that it was started on (see mSawTickWhilePending). This is similar to what's described in the bugs comments, and seems to work nicely in practice. We could easily change that (current time) by a paint-based time if needed (when available), which would be more similar to what we were doing. But I'd rather do the simple thing for now, and land this shortly after the soft freeze is over so that we have time to watch out for regressions. There's one regression on a test that birtles wrote (using an XHR doc and switching the timeline to a rendered doc's timeline). We use the timeline's document rather than the target document to determine whether to trigger animations now. That's one of the cases where we'd keep vsync perma-running without this patch, and Chrome also fails that test. Maybe the test should be removed / the spec should be tweaked to allow this behavior? This causes some progression in some CSS transitions tests too, and I added an extra test for the vsync behavior. Over-all this is much simpler to reason about and I think we should try to do this. Differential Revision: https://phabricator.services.mozilla.com/D193583
377 lines
13 KiB
C++
377 lines
13 KiB
C++
/* -*- 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 "CSSAnimation.h"
|
|
|
|
#include "mozilla/AnimationEventDispatcher.h"
|
|
#include "mozilla/dom/CSSAnimationBinding.h"
|
|
#include "mozilla/dom/KeyframeEffectBinding.h"
|
|
#include "mozilla/TimeStamp.h"
|
|
#include "nsPresContext.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
using AnimationPhase = ComputedTiming::AnimationPhase;
|
|
|
|
JSObject* CSSAnimation::WrapObject(JSContext* aCx,
|
|
JS::Handle<JSObject*> aGivenProto) {
|
|
return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
|
|
Animation::SetEffect(aEffect);
|
|
|
|
AddOverriddenProperties(CSSAnimationProperties::Effect);
|
|
}
|
|
|
|
void CSSAnimation::SetStartTimeAsDouble(const Nullable<double>& aStartTime) {
|
|
// Note that we always compare with the paused state since for the purposes
|
|
// of determining if play control is being overridden or not, we want to
|
|
// treat the finished state as running.
|
|
bool wasPaused = PlayState() == AnimationPlayState::Paused;
|
|
|
|
Animation::SetStartTimeAsDouble(aStartTime);
|
|
|
|
bool isPaused = PlayState() == AnimationPlayState::Paused;
|
|
|
|
if (wasPaused != isPaused) {
|
|
AddOverriddenProperties(CSSAnimationProperties::PlayState);
|
|
}
|
|
}
|
|
|
|
mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
|
|
FlushUnanimatedStyle();
|
|
return Animation::GetReady(aRv);
|
|
}
|
|
|
|
void CSSAnimation::Reverse(ErrorResult& aRv) {
|
|
// As with CSSAnimation::SetStartTimeAsDouble, we're really only interested in
|
|
// the paused state.
|
|
bool wasPaused = PlayState() == AnimationPlayState::Paused;
|
|
|
|
Animation::Reverse(aRv);
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
bool isPaused = PlayState() == AnimationPlayState::Paused;
|
|
|
|
if (wasPaused != isPaused) {
|
|
AddOverriddenProperties(CSSAnimationProperties::PlayState);
|
|
}
|
|
}
|
|
|
|
AnimationPlayState CSSAnimation::PlayStateFromJS() const {
|
|
// Flush style to ensure that any properties controlling animation state
|
|
// (e.g. animation-play-state) are fully updated.
|
|
FlushUnanimatedStyle();
|
|
return Animation::PlayStateFromJS();
|
|
}
|
|
|
|
bool CSSAnimation::PendingFromJS() const {
|
|
// Flush style since, for example, if the animation-play-state was just
|
|
// changed its possible we should now be pending.
|
|
FlushUnanimatedStyle();
|
|
return Animation::PendingFromJS();
|
|
}
|
|
|
|
void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
|
|
// Note that flushing style below might trigger calls to
|
|
// PlayFromStyle()/PauseFromStyle() on this object.
|
|
FlushUnanimatedStyle();
|
|
Animation::PlayFromJS(aRv);
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
AddOverriddenProperties(CSSAnimationProperties::PlayState);
|
|
}
|
|
|
|
void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
|
|
Animation::PauseFromJS(aRv);
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
AddOverriddenProperties(CSSAnimationProperties::PlayState);
|
|
}
|
|
|
|
void CSSAnimation::PlayFromStyle() {
|
|
ErrorResult rv;
|
|
Animation::Play(rv, Animation::LimitBehavior::Continue);
|
|
// play() should not throw when LimitBehavior is Continue
|
|
MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
|
|
}
|
|
|
|
void CSSAnimation::PauseFromStyle() {
|
|
ErrorResult rv;
|
|
Animation::Pause(rv);
|
|
// pause() should only throw when *all* of the following conditions are true:
|
|
// - we are in the idle state, and
|
|
// - we have a negative playback rate, and
|
|
// - we have an infinitely repeating animation
|
|
// The first two conditions will never happen under regular style processing
|
|
// but could happen if an author made modifications to the Animation object
|
|
// and then updated animation-play-state. It's an unusual case and there's
|
|
// no obvious way to pass on the exception information so we just silently
|
|
// fail for now.
|
|
if (rv.Failed()) {
|
|
NS_WARNING("Unexpected exception pausing animation - silently failing");
|
|
}
|
|
}
|
|
|
|
void CSSAnimation::Tick(TickState& aState) {
|
|
Animation::Tick(aState);
|
|
QueueEvents();
|
|
}
|
|
|
|
bool CSSAnimation::HasLowerCompositeOrderThan(
|
|
const CSSAnimation& aOther) const {
|
|
MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
|
|
"Should only be called for CSS animations that are sorted "
|
|
"as CSS animations (i.e. tied to CSS markup)");
|
|
|
|
// 0. Object-equality case
|
|
if (&aOther == this) {
|
|
return false;
|
|
}
|
|
|
|
// 1. Sort by document order
|
|
if (!mOwningElement.Equals(aOther.mOwningElement)) {
|
|
return mOwningElement.LessThan(
|
|
const_cast<CSSAnimation*>(this)->CachedChildIndexRef(),
|
|
aOther.mOwningElement,
|
|
const_cast<CSSAnimation*>(&aOther)->CachedChildIndexRef());
|
|
}
|
|
|
|
// 2. (Same element and pseudo): Sort by position in animation-name
|
|
return mAnimationIndex < aOther.mAnimationIndex;
|
|
}
|
|
|
|
void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
|
|
// If the animation is pending, we ignore animation events until we finish
|
|
// pending.
|
|
if (mPendingState != PendingState::NotPending) {
|
|
return;
|
|
}
|
|
|
|
// CSS animations dispatch events at their owning element. This allows
|
|
// script to repurpose a CSS animation to target a different element,
|
|
// to use a group effect (which has no obvious "target element"), or
|
|
// to remove the animation effect altogether whilst still getting
|
|
// animation events.
|
|
//
|
|
// It does mean, however, that for a CSS animation that has no owning
|
|
// element (e.g. it was created using the CSSAnimation constructor or
|
|
// disassociated from CSS) no events are fired. If it becomes desirable
|
|
// for these animations to still fire events we should spec the concept
|
|
// of the "original owning element" or "event target" and allow script
|
|
// to set it when creating a CSSAnimation object.
|
|
if (!mOwningElement.IsSet()) {
|
|
return;
|
|
}
|
|
|
|
nsPresContext* presContext = mOwningElement.GetPresContext();
|
|
if (!presContext) {
|
|
return;
|
|
}
|
|
|
|
uint64_t currentIteration = 0;
|
|
ComputedTiming::AnimationPhase currentPhase;
|
|
StickyTimeDuration intervalStartTime;
|
|
StickyTimeDuration intervalEndTime;
|
|
StickyTimeDuration iterationStartTime;
|
|
|
|
if (!mEffect) {
|
|
currentPhase =
|
|
GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
|
|
if (currentPhase == mPreviousPhase) {
|
|
return;
|
|
}
|
|
} else {
|
|
ComputedTiming computedTiming = mEffect->GetComputedTiming();
|
|
currentPhase = computedTiming.mPhase;
|
|
currentIteration = computedTiming.mCurrentIteration;
|
|
if (currentPhase == mPreviousPhase &&
|
|
currentIteration == mPreviousIteration) {
|
|
return;
|
|
}
|
|
intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
|
|
intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
|
|
|
|
uint64_t iterationBoundary = mPreviousIteration > currentIteration
|
|
? currentIteration + 1
|
|
: currentIteration;
|
|
double multiplier = iterationBoundary - computedTiming.mIterationStart;
|
|
if (multiplier != 0.0) {
|
|
iterationStartTime = computedTiming.mDuration.MultDouble(multiplier);
|
|
}
|
|
}
|
|
|
|
TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
|
|
TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
|
|
TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
|
|
|
|
AutoTArray<AnimationEventInfo, 2> events;
|
|
|
|
auto appendAnimationEvent = [&](EventMessage aMessage,
|
|
const StickyTimeDuration& aElapsedTime,
|
|
const TimeStamp& aScheduledEventTimeStamp) {
|
|
double elapsedTime = aElapsedTime.ToSeconds();
|
|
if (aMessage == eAnimationCancel) {
|
|
// 0 is an inappropriate value for this callsite. What we need to do is
|
|
// use a single random value for all increasing times reportable.
|
|
// That is to say, whenever elapsedTime goes negative (because an
|
|
// animation restarts, something rewinds the animation, or otherwise)
|
|
// a new random value for the mix-in must be generated.
|
|
elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
|
|
elapsedTime, 0, mRTPCallerType);
|
|
}
|
|
events.AppendElement(
|
|
AnimationEventInfo(mAnimationName, mOwningElement.Target(), aMessage,
|
|
elapsedTime, aScheduledEventTimeStamp, this));
|
|
};
|
|
|
|
// Handle cancel event first
|
|
if ((mPreviousPhase != AnimationPhase::Idle &&
|
|
mPreviousPhase != AnimationPhase::After) &&
|
|
currentPhase == AnimationPhase::Idle) {
|
|
appendAnimationEvent(eAnimationCancel, aActiveTime,
|
|
GetTimelineCurrentTimeAsTimeStamp());
|
|
}
|
|
|
|
switch (mPreviousPhase) {
|
|
case AnimationPhase::Idle:
|
|
case AnimationPhase::Before:
|
|
if (currentPhase == AnimationPhase::Active) {
|
|
appendAnimationEvent(eAnimationStart, intervalStartTime,
|
|
startTimeStamp);
|
|
} else if (currentPhase == AnimationPhase::After) {
|
|
appendAnimationEvent(eAnimationStart, intervalStartTime,
|
|
startTimeStamp);
|
|
appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
|
|
}
|
|
break;
|
|
case AnimationPhase::Active:
|
|
if (currentPhase == AnimationPhase::Before) {
|
|
appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
|
|
} else if (currentPhase == AnimationPhase::Active) {
|
|
// The currentIteration must have changed or element we would have
|
|
// returned early above.
|
|
MOZ_ASSERT(currentIteration != mPreviousIteration);
|
|
appendAnimationEvent(eAnimationIteration, iterationStartTime,
|
|
iterationTimeStamp);
|
|
} else if (currentPhase == AnimationPhase::After) {
|
|
appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
|
|
}
|
|
break;
|
|
case AnimationPhase::After:
|
|
if (currentPhase == AnimationPhase::Before) {
|
|
appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
|
|
appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
|
|
} else if (currentPhase == AnimationPhase::Active) {
|
|
appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
|
|
}
|
|
break;
|
|
}
|
|
mPreviousPhase = currentPhase;
|
|
mPreviousIteration = currentIteration;
|
|
|
|
if (!events.IsEmpty()) {
|
|
presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
|
|
}
|
|
}
|
|
|
|
void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
|
|
SyncNotifyFlag aSyncNotifyFlag) {
|
|
if (mNeedsNewAnimationIndexWhenRun &&
|
|
PlayState() != AnimationPlayState::Idle) {
|
|
mAnimationIndex = sNextAnimationIndex++;
|
|
mNeedsNewAnimationIndexWhenRun = false;
|
|
}
|
|
|
|
Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
|
|
}
|
|
|
|
/////////////////////// CSSAnimationKeyframeEffect ////////////////////////
|
|
|
|
void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
|
|
MaybeFlushUnanimatedStyle();
|
|
KeyframeEffect::GetTiming(aRetVal);
|
|
}
|
|
|
|
void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
|
|
ComputedEffectTiming& aRetVal) const {
|
|
MaybeFlushUnanimatedStyle();
|
|
KeyframeEffect::GetComputedTimingAsDict(aRetVal);
|
|
}
|
|
|
|
void CSSAnimationKeyframeEffect::UpdateTiming(
|
|
const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
|
|
KeyframeEffect::UpdateTiming(aTiming, aRv);
|
|
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
|
|
CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
|
|
if (aTiming.mDuration.WasPassed()) {
|
|
updatedProperties |= CSSAnimationProperties::Duration;
|
|
}
|
|
if (aTiming.mIterations.WasPassed()) {
|
|
updatedProperties |= CSSAnimationProperties::IterationCount;
|
|
}
|
|
if (aTiming.mDirection.WasPassed()) {
|
|
updatedProperties |= CSSAnimationProperties::Direction;
|
|
}
|
|
if (aTiming.mDelay.WasPassed()) {
|
|
updatedProperties |= CSSAnimationProperties::Delay;
|
|
}
|
|
if (aTiming.mFill.WasPassed()) {
|
|
updatedProperties |= CSSAnimationProperties::FillMode;
|
|
}
|
|
|
|
cssAnimation->AddOverriddenProperties(updatedProperties);
|
|
}
|
|
}
|
|
|
|
void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
|
|
JS::Handle<JSObject*> aKeyframes,
|
|
ErrorResult& aRv) {
|
|
KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv);
|
|
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
|
|
cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Keyframes);
|
|
}
|
|
}
|
|
|
|
void CSSAnimationKeyframeEffect::SetComposite(
|
|
const CompositeOperation& aComposite) {
|
|
KeyframeEffect::SetComposite(aComposite);
|
|
|
|
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
|
|
cssAnimation->AddOverriddenProperties(CSSAnimationProperties::Composition);
|
|
}
|
|
}
|
|
|
|
void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
|
|
if (!GetOwningCSSAnimation()) {
|
|
return;
|
|
}
|
|
|
|
if (dom::Document* doc = GetRenderedDocument()) {
|
|
doc->FlushPendingNotifications(
|
|
ChangesToFlush(FlushType::Style, false /* flush animations */));
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla::dom
|