зеркало из https://github.com/mozilla/gecko-dev.git
366 строки
13 KiB
C++
366 строки
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() {
|
|
Animation::Tick();
|
|
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;
|
|
iterationStartTime = computedTiming.mDuration.MultDouble(
|
|
(iterationBoundary - computedTiming.mIterationStart));
|
|
}
|
|
|
|
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);
|
|
}
|
|
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::MaybeFlushUnanimatedStyle() const {
|
|
if (!GetOwningCSSAnimation()) {
|
|
return;
|
|
}
|
|
|
|
if (dom::Document* doc = GetRenderedDocument()) {
|
|
doc->FlushPendingNotifications(
|
|
ChangesToFlush(FlushType::Style, false /* flush animations */));
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla::dom
|