import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,110 @@
/* -*- 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 "AnimValuesStyleRule.h"
#include "nsRuleData.h"
#include "nsStyleContext.h"
namespace mozilla {
NS_IMPL_ISUPPORTS(AnimValuesStyleRule, nsIStyleRule)
void
AnimValuesStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
{
nsStyleContext *contextParent = aRuleData->mStyleContext->GetParent();
if (contextParent && contextParent->HasPseudoElementData()) {
// Don't apply transitions or animations to things inside of
// pseudo-elements.
// FIXME (Bug 522599): Add tests for this.
// Prevent structs from being cached on the rule node since we're inside
// a pseudo-element, as we could determine cacheability differently
// when walking the rule tree for a style context that is not inside
// a pseudo-element. Note that nsRuleNode::GetStyle##name_ and GetStyleData
// will never look at cached structs when we're animating things inside
// a pseduo-element, so that we don't incorrectly return a struct that
// is only appropriate for non-pseudo-elements.
aRuleData->mConditions.SetUncacheable();
return;
}
for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) {
nsCSSPropertyID property = static_cast<nsCSSPropertyID>(iter.Key());
if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
nsCSSProps::kSIDTable[property])) {
nsCSSValue *prop = aRuleData->ValueFor(property);
if (prop->GetUnit() == eCSSUnit_Null) {
DebugOnly<bool> ok =
StyleAnimationValue::UncomputeValue(property, iter.Data(),
*prop);
MOZ_ASSERT(ok, "could not store computed value");
}
}
}
}
bool
AnimValuesStyleRule::MightMapInheritedStyleData()
{
return mStyleBits & NS_STYLE_INHERITED_STRUCT_MASK;
}
bool
AnimValuesStyleRule::GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty,
nsCSSValue* aValue)
{
MOZ_ASSERT(false, "GetDiscretelyAnimatedCSSValue is not implemented yet");
return false;
}
void
AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty,
const StyleAnimationValue &aValue)
{
MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN,
"Unexpected css property");
mAnimationValues.Put(aProperty, aValue);
mStyleBits |=
nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]);
}
void
AnimValuesStyleRule::AddValue(nsCSSPropertyID aProperty,
StyleAnimationValue&& aValue)
{
MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN,
"Unexpected css property");
mAnimationValues.Put(aProperty, Move(aValue));
mStyleBits |=
nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]);
}
#ifdef DEBUG
void
AnimValuesStyleRule::List(FILE* out, int32_t aIndent) const
{
nsAutoCString str;
for (int32_t index = aIndent; --index >= 0; ) {
str.AppendLiteral(" ");
}
str.AppendLiteral("[anim values] { ");
for (auto iter = mAnimationValues.ConstIter(); !iter.Done(); iter.Next()) {
nsCSSPropertyID property = static_cast<nsCSSPropertyID>(iter.Key());
str.Append(nsCSSProps::GetStringValue(property));
str.AppendLiteral(": ");
nsAutoString value;
Unused <<
StyleAnimationValue::UncomputeValue(property, iter.Data(), value);
AppendUTF16toUTF8(value, str);
str.AppendLiteral("; ");
}
str.AppendLiteral("}\n");
fprintf_stderr(out, "%s", str.get());
}
#endif
} // namespace mozilla

View file

@ -0,0 +1,58 @@
/* -*- 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/. */
#ifndef mozilla_AnimValuesStyleRule_h
#define mozilla_AnimValuesStyleRule_h
#include "mozilla/StyleAnimationValue.h"
#include "nsCSSPropertyID.h"
#include "nsCSSPropertyIDSet.h"
#include "nsDataHashtable.h"
#include "nsHashKeys.h" // For nsUint32HashKey
#include "nsIStyleRule.h"
#include "nsISupportsImpl.h" // For NS_DECL_ISUPPORTS
#include "nsRuleNode.h" // For nsCachedStyleData
#include "nsTArray.h" // For nsTArray
namespace mozilla {
/**
* A style rule that maps property-StyleAnimationValue pairs.
*/
class AnimValuesStyleRule final : public nsIStyleRule
{
public:
AnimValuesStyleRule()
: mStyleBits(0) {}
// nsISupports implementation
NS_DECL_ISUPPORTS
// nsIStyleRule implementation
void MapRuleInfoInto(nsRuleData* aRuleData) override;
bool MightMapInheritedStyleData() override;
bool GetDiscretelyAnimatedCSSValue(nsCSSPropertyID aProperty,
nsCSSValue* aValue) override;
#ifdef DEBUG
void List(FILE* out = stdout, int32_t aIndent = 0) const override;
#endif
// For the following functions, it there is already a value for |aProperty| it
// will be replaced with |aValue|.
void AddValue(nsCSSPropertyID aProperty, const StyleAnimationValue &aValue);
void AddValue(nsCSSPropertyID aProperty, StyleAnimationValue&& aValue);
private:
~AnimValuesStyleRule() {}
nsDataHashtable<nsUint32HashKey, StyleAnimationValue> mAnimationValues;
uint32_t mStyleBits;
};
} // namespace mozilla
#endif // mozilla_AnimValuesStyleRule_h

1426
dom/animation/Animation.cpp Normal file

File diff suppressed because it is too large Load diff

453
dom/animation/Animation.h Normal file
View file

@ -0,0 +1,453 @@
/* -*- 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/. */
#ifndef mozilla_dom_Animation_h
#define mozilla_dom_Animation_h
#include "nsWrapperCache.h"
#include "nsCycleCollectionParticipant.h"
#include "mozilla/Attributes.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/EffectCompositor.h" // For EffectCompositor::CascadeLevel
#include "mozilla/LinkedList.h"
#include "mozilla/TimeStamp.h" // for TimeStamp, TimeDuration
#include "mozilla/dom/AnimationBinding.h" // for AnimationPlayState
#include "mozilla/dom/AnimationEffectReadOnly.h"
#include "mozilla/dom/AnimationTimeline.h"
#include "mozilla/dom/Promise.h"
#include "nsCSSPropertyID.h"
#include "nsIGlobalObject.h"
// X11 has a #define for CurrentTime.
#ifdef CurrentTime
#undef CurrentTime
#endif
// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
// GetTickCount().
#ifdef GetCurrentTime
#undef GetCurrentTime
#endif
struct JSContext;
class nsCSSPropertyIDSet;
class nsIDocument;
class nsPresContext;
namespace mozilla {
class AnimValuesStyleRule;
namespace dom {
class CSSAnimation;
class CSSTransition;
class Animation
: public DOMEventTargetHelper
, public LinkedListElement<Animation>
{
protected:
virtual ~Animation() {}
public:
explicit Animation(nsIGlobalObject* aGlobal)
: DOMEventTargetHelper(aGlobal)
, mPlaybackRate(1.0)
, mPendingState(PendingState::NotPending)
, mAnimationIndex(sNextAnimationIndex++)
, mFinishedAtLastComposeStyle(false)
, mIsRelevant(false)
, mFinishedIsResolved(false)
{
}
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation,
DOMEventTargetHelper)
nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); }
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
virtual CSSAnimation* AsCSSAnimation() { return nullptr; }
virtual const CSSAnimation* AsCSSAnimation() const { return nullptr; }
virtual CSSTransition* AsCSSTransition() { return nullptr; }
virtual const CSSTransition* AsCSSTransition() const { return nullptr; }
/**
* Flag to pass to Play to indicate whether or not it should automatically
* rewind the current time to the start point if the animation is finished.
* For regular calls to play() from script we should do this, but when a CSS
* animation's animation-play-state changes we shouldn't rewind the animation.
*/
enum class LimitBehavior {
AutoRewind,
Continue
};
// Animation interface methods
static already_AddRefed<Animation>
Constructor(const GlobalObject& aGlobal,
AnimationEffectReadOnly* aEffect,
const Optional<AnimationTimeline*>& aTimeline,
ErrorResult& aRv);
void GetId(nsAString& aResult) const { aResult = mId; }
void SetId(const nsAString& aId);
AnimationEffectReadOnly* GetEffect() const { return mEffect; }
void SetEffect(AnimationEffectReadOnly* aEffect);
AnimationTimeline* GetTimeline() const { return mTimeline; }
void SetTimeline(AnimationTimeline* aTimeline);
Nullable<TimeDuration> GetStartTime() const { return mStartTime; }
void SetStartTime(const Nullable<TimeDuration>& aNewStartTime);
Nullable<TimeDuration> GetCurrentTime() const;
void SetCurrentTime(const TimeDuration& aNewCurrentTime);
double PlaybackRate() const { return mPlaybackRate; }
void SetPlaybackRate(double aPlaybackRate);
AnimationPlayState PlayState() const;
virtual Promise* GetReady(ErrorResult& aRv);
virtual Promise* GetFinished(ErrorResult& aRv);
void Cancel();
virtual void Finish(ErrorResult& aRv);
virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior);
virtual void Pause(ErrorResult& aRv);
virtual void Reverse(ErrorResult& aRv);
bool IsRunningOnCompositor() const;
IMPL_EVENT_HANDLER(finish);
IMPL_EVENT_HANDLER(cancel);
// Wrapper functions for Animation DOM methods when called
// from script.
//
// We often use the same methods internally and from script but when called
// from script we (or one of our subclasses) perform extra steps such as
// flushing style or converting the return type.
Nullable<double> GetStartTimeAsDouble() const;
void SetStartTimeAsDouble(const Nullable<double>& aStartTime);
Nullable<double> GetCurrentTimeAsDouble() const;
void SetCurrentTimeAsDouble(const Nullable<double>& aCurrentTime,
ErrorResult& aRv);
virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); }
virtual void PlayFromJS(ErrorResult& aRv)
{
Play(aRv, LimitBehavior::AutoRewind);
}
/**
* PauseFromJS is currently only here for symmetry with PlayFromJS but
* in future we will likely have to flush style in
* CSSAnimation::PauseFromJS so we leave it for now.
*/
void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
// Wrapper functions for Animation DOM methods when called from style.
virtual void CancelFromStyle() { CancelNoUpdate(); }
void SetTimelineNoUpdate(AnimationTimeline* aTimeline);
void SetEffectNoUpdate(AnimationEffectReadOnly* aEffect);
virtual void Tick();
bool NeedsTicks() const
{
AnimationPlayState playState = PlayState();
return playState == AnimationPlayState::Running ||
playState == AnimationPlayState::Pending;
}
/**
* Set the time to use for starting or pausing a pending animation.
*
* Typically, when an animation is played, it does not start immediately but
* is added to a table of pending animations on the document of its effect.
* In the meantime it sets its hold time to the time from which playback
* should begin.
*
* When the document finishes painting, any pending animations in its table
* are marked as being ready to start by calling StartOnNextTick.
* The moment when the paint completed is also recorded, converted to a
* timeline time, and passed to StartOnTick. This is so that when these
* animations do start, they can be timed from the point when painting
* completed.
*
* After calling TriggerOnNextTick, animations remain in the pending state
* until the next refresh driver tick. At that time they transition out of
* the pending state using the time passed to TriggerOnNextTick as the
* effective time at which they resumed.
*
* This approach means that any setup time required for performing the
* initial paint of an animation such as layerization is not deducted from
* the running time of the animation. Without this we can easily drop the
* first few frames of an animation, or, on slower devices, the whole
* animation.
*
* Furthermore:
*
* - Starting the animation immediately when painting finishes is problematic
* because the start time of the animation will be ahead of its timeline
* (since the timeline time is based on the refresh driver time).
* That's a problem because the animation is playing but its timing
* suggests it starts in the future. We could update the timeline to match
* the start time of the animation but then we'd also have to update the
* timing and style of all animations connected to that timeline or else be
* stuck in an inconsistent state until the next refresh driver tick.
*
* - If we simply use the refresh driver time on its next tick, the lag
* between triggering an animation and its effective start is unacceptably
* long.
*
* For pausing, we apply the same asynchronous approach. This is so that we
* synchronize with animations that are running on the compositor. Otherwise
* if the main thread lags behind the compositor there will be a noticeable
* jump backwards when the main thread takes over. Even though main thread
* animations could be paused immediately, we do it asynchronously for
* consistency and so that animations paused together end up in step.
*
* Note that the caller of this method is responsible for removing the
* animation from any PendingAnimationTracker it may have been added to.
*/
void TriggerOnNextTick(const Nullable<TimeDuration>& aReadyTime);
/**
* Testing only: Start or pause a pending animation using the current
* timeline time. This is used to support existing tests that expect
* animations to begin immediately. Ideally we would rewrite the those tests
* and get rid of this method, but there are a lot of them.
*
* As with TriggerOnNextTick, the caller of this method is responsible for
* removing the animation from any PendingAnimationTracker it may have been
* added to.
*/
void TriggerNow();
/**
* When StartOnNextTick is called, we store the ready time but we don't apply
* it until the next tick. In the meantime, GetStartTime() will return null.
*
* However, if we build layer animations again before the next tick, we
* should initialize them with the start time that GetStartTime() will return
* on the next tick.
*
* If we were to simply set the start time of layer animations to null, their
* start time would be updated to the current wallclock time when rendering
* finishes, thus making them out of sync with the start time stored here.
* This, in turn, will make the animation jump backwards when we build
* animations on the next tick and apply the start time stored here.
*
* This method returns the start time, if resolved. Otherwise, if we have
* a pending ready time, it returns the corresponding start time. If neither
* of those are available, it returns null.
*/
Nullable<TimeDuration> GetCurrentOrPendingStartTime() const;
/**
* Calculates the corresponding start time to use for an animation that is
* currently pending with current time |mHoldTime| but should behave
* as if it began or resumed playback at timeline time |aReadyTime|.
*/
TimeDuration StartTimeFromReadyTime(const TimeDuration& aReadyTime) const;
/**
* Converts a time in the timescale of this Animation's currentTime, to a
* TimeStamp. Returns a null TimeStamp if the conversion cannot be performed
* because of the current state of this Animation (e.g. it has no timeline, a
* zero playbackRate, an unresolved start time etc.) or the value of the time
* passed-in (e.g. an infinite time).
*/
TimeStamp AnimationTimeToTimeStamp(const StickyTimeDuration& aTime) const;
// Converts an AnimationEvent's elapsedTime value to an equivalent TimeStamp
// that can be used to sort events by when they occurred.
TimeStamp ElapsedTimeToTimeStamp(const StickyTimeDuration& aElapsedTime) const;
bool IsPausedOrPausing() const
{
return PlayState() == AnimationPlayState::Paused ||
mPendingState == PendingState::PausePending;
}
bool HasCurrentEffect() const
{
return GetEffect() && GetEffect()->IsCurrent();
}
bool IsInEffect() const
{
return GetEffect() && GetEffect()->IsInEffect();
}
/**
* Returns true if this animation's playback state makes it a candidate for
* running on the compositor.
* We send animations to the compositor when their target effect is 'current'
* (a definition that is roughly equivalent to when they are in their before
* or active phase). However, we don't send animations to the compositor when
* they are paused/pausing (including being effectively paused due to
* having a zero playback rate), have a zero-duration active interval, or have
* no target effect at all.
*/
bool IsPlayableOnCompositor() const
{
return HasCurrentEffect() &&
mPlaybackRate != 0.0 &&
(PlayState() == AnimationPlayState::Running ||
mPendingState == PendingState::PlayPending) &&
!GetEffect()->IsActiveDurationZero();
}
bool IsRelevant() const { return mIsRelevant; }
void UpdateRelevance();
/**
* Returns true if this Animation has a lower composite order than aOther.
*/
bool HasLowerCompositeOrderThan(const Animation& aOther) const;
/**
* Returns the level at which the effect(s) associated with this Animation
* are applied to the CSS cascade.
*/
virtual EffectCompositor::CascadeLevel CascadeLevel() const
{
return EffectCompositor::CascadeLevel::Animations;
}
/**
* Returns true if this animation does not currently need to update
* style on the main thread (e.g. because it is empty, or is
* running on the compositor).
*/
bool CanThrottle() const;
/**
* Updates |aStyleRule| with the animation values of this animation's effect,
* if any.
* Any properties contained in |aPropertiesToSkip| will not be added or
* updated in |aStyleRule|.
*/
void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
const nsCSSPropertyIDSet& aPropertiesToSkip);
void NotifyEffectTimingUpdated();
protected:
void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
void SilentlySetPlaybackRate(double aPlaybackRate);
void CancelNoUpdate();
void PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior);
void PauseNoUpdate(ErrorResult& aRv);
void ResumeAt(const TimeDuration& aReadyTime);
void PauseAt(const TimeDuration& aReadyTime);
void FinishPendingAt(const TimeDuration& aReadyTime)
{
if (mPendingState == PendingState::PlayPending) {
ResumeAt(aReadyTime);
} else if (mPendingState == PendingState::PausePending) {
PauseAt(aReadyTime);
} else {
NS_NOTREACHED("Can't finish pending if we're not in a pending state");
}
}
/**
* Finishing behavior depends on if changes to timing occurred due
* to a seek or regular playback.
*/
enum class SeekFlag {
NoSeek,
DidSeek
};
enum class SyncNotifyFlag {
Sync,
Async
};
virtual void UpdateTiming(SeekFlag aSeekFlag,
SyncNotifyFlag aSyncNotifyFlag);
void UpdateFinishedState(SeekFlag aSeekFlag,
SyncNotifyFlag aSyncNotifyFlag);
void UpdateEffect();
void FlushStyle() const;
void PostUpdate();
void ResetFinishedPromise();
void MaybeResolveFinishedPromise();
void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag);
void DoFinishNotificationImmediately();
void DispatchPlaybackEvent(const nsAString& aName);
/**
* Remove this animation from the pending animation tracker and reset
* mPendingState as necessary. The caller is responsible for resolving or
* aborting the mReady promise as necessary.
*/
void CancelPendingTasks();
/**
* Performs the same steps as CancelPendingTasks and also rejects and
* recreates the ready promise if the animation was pending.
*/
void ResetPendingTasks();
bool IsPossiblyOrphanedPendingAnimation() const;
StickyTimeDuration EffectEnd() const;
nsIDocument* GetRenderedDocument() const;
RefPtr<AnimationTimeline> mTimeline;
RefPtr<AnimationEffectReadOnly> mEffect;
// The beginning of the delay period.
Nullable<TimeDuration> mStartTime; // Timeline timescale
Nullable<TimeDuration> mHoldTime; // Animation timescale
Nullable<TimeDuration> mPendingReadyTime; // Timeline timescale
Nullable<TimeDuration> mPreviousCurrentTime; // Animation timescale
double mPlaybackRate;
// A Promise that is replaced on each call to Play()
// and fulfilled when Play() is successfully completed.
// This object is lazily created by GetReady.
// See http://w3c.github.io/web-animations/#current-ready-promise
RefPtr<Promise> mReady;
// A Promise that is resolved when we reach the end of the effect, or
// 0 when playing backwards. The Promise is replaced if the animation is
// finished but then a state change makes it not finished.
// This object is lazily created by GetFinished.
// See http://w3c.github.io/web-animations/#current-finished-promise
RefPtr<Promise> mFinished;
// Indicates if the animation is in the pending state (and what state it is
// waiting to enter when it finished pending). We use this rather than
// checking if this animation is tracked by a PendingAnimationTracker because
// the animation will continue to be pending even after it has been removed
// from the PendingAnimationTracker while it is waiting for the next tick
// (see TriggerOnNextTick for details).
enum class PendingState { NotPending, PlayPending, PausePending };
PendingState mPendingState;
static uint64_t sNextAnimationIndex;
// The relative position of this animation within the global animation list.
// This is kNoIndex while the animation is in the idle state and is updated
// each time the animation transitions out of the idle state.
//
// Note that subclasses such as CSSTransition and CSSAnimation may repurpose
// this member to implement their own brand of sorting. As a result, it is
// possible for two different objects to have the same index.
uint64_t mAnimationIndex;
bool mFinishedAtLastComposeStyle;
// Indicates that the animation should be exposed in an element's
// getAnimations() list.
bool mIsRelevant;
nsRevocableEventPtr<nsRunnableMethod<Animation>> mFinishNotificationTask;
// True if mFinished is resolved or would be resolved if mFinished has
// yet to be created. This is not set when mFinished is rejected since
// in that case mFinished is immediately reset to represent a new current
// finished promise.
bool mFinishedIsResolved;
nsString mId;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_Animation_h

View file

@ -0,0 +1,32 @@
/* -*- 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/. */
#ifndef mozilla_AnimationComparator_h
#define mozilla_AnimationComparator_h
namespace mozilla {
// Although this file is called AnimationComparator, we don't actually
// implement AnimationComparator (to compare const Animation& parameters)
// since it's not actually needed (yet).
template<typename AnimationPtrType>
class AnimationPtrComparator {
public:
bool Equals(const AnimationPtrType& a, const AnimationPtrType& b) const
{
return a == b;
}
bool LessThan(const AnimationPtrType& a, const AnimationPtrType& b) const
{
return a->HasLowerCompositeOrderThan(*b);
}
};
} // namespace mozilla
#endif // mozilla_AnimationComparator_h

View file

@ -0,0 +1,343 @@
/* -*- 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 "mozilla/dom/AnimationEffectReadOnly.h"
#include "mozilla/dom/AnimationEffectReadOnlyBinding.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/FloatingPoint.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationEffectReadOnly)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationEffectReadOnly)
if (tmp->mTiming) {
tmp->mTiming->Unlink();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument, mTiming, mAnimation)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEffectReadOnly)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument, mTiming, mAnimation)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationEffectReadOnly)
NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationEffectReadOnly)
NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationEffectReadOnly)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationEffectReadOnly)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
AnimationEffectReadOnly::AnimationEffectReadOnly(
nsIDocument* aDocument, AnimationEffectTimingReadOnly* aTiming)
: mDocument(aDocument)
, mTiming(aTiming)
{
MOZ_ASSERT(aTiming);
}
// https://w3c.github.io/web-animations/#current
bool
AnimationEffectReadOnly::IsCurrent() const
{
if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
return false;
}
ComputedTiming computedTiming = GetComputedTiming();
return computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
computedTiming.mPhase == ComputedTiming::AnimationPhase::Active;
}
// https://w3c.github.io/web-animations/#in-effect
bool
AnimationEffectReadOnly::IsInEffect() const
{
ComputedTiming computedTiming = GetComputedTiming();
return !computedTiming.mProgress.IsNull();
}
already_AddRefed<AnimationEffectTimingReadOnly>
AnimationEffectReadOnly::Timing()
{
RefPtr<AnimationEffectTimingReadOnly> temp(mTiming);
return temp.forget();
}
void
AnimationEffectReadOnly::SetSpecifiedTiming(const TimingParams& aTiming)
{
if (mTiming->AsTimingParams() == aTiming) {
return;
}
mTiming->SetTimingParams(aTiming);
if (mAnimation) {
mAnimation->NotifyEffectTimingUpdated();
}
// For keyframe effects, NotifyEffectTimingUpdated above will eventually cause
// KeyframeEffectReadOnly::NotifyAnimationTimingUpdated to be called so it can
// update its registration with the target element as necessary.
}
ComputedTiming
AnimationEffectReadOnly::GetComputedTimingAt(
const Nullable<TimeDuration>& aLocalTime,
const TimingParams& aTiming,
double aPlaybackRate)
{
const StickyTimeDuration zeroDuration;
// Always return the same object to benefit from return-value optimization.
ComputedTiming result;
if (aTiming.mDuration) {
MOZ_ASSERT(aTiming.mDuration.ref() >= zeroDuration,
"Iteration duration should be positive");
result.mDuration = aTiming.mDuration.ref();
}
MOZ_ASSERT(aTiming.mIterations >= 0.0 && !IsNaN(aTiming.mIterations),
"mIterations should be nonnegative & finite, as ensured by "
"ValidateIterations or CSSParser");
result.mIterations = aTiming.mIterations;
MOZ_ASSERT(aTiming.mIterationStart >= 0.0,
"mIterationStart should be nonnegative, as ensured by "
"ValidateIterationStart");
result.mIterationStart = aTiming.mIterationStart;
result.mActiveDuration = aTiming.ActiveDuration();
result.mEndTime = aTiming.EndTime();
result.mFill = aTiming.mFill == dom::FillMode::Auto ?
dom::FillMode::None :
aTiming.mFill;
// The default constructor for ComputedTiming sets all other members to
// values consistent with an animation that has not been sampled.
if (aLocalTime.IsNull()) {
return result;
}
const TimeDuration& localTime = aLocalTime.Value();
// Calculate the time within the active interval.
// https://w3c.github.io/web-animations/#active-time
StickyTimeDuration activeTime;
StickyTimeDuration beforeActiveBoundary =
std::max(std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime),
zeroDuration);
StickyTimeDuration activeAfterBoundary =
std::max(std::min(StickyTimeDuration(aTiming.mDelay +
result.mActiveDuration),
result.mEndTime),
zeroDuration);
if (localTime > activeAfterBoundary ||
(aPlaybackRate >= 0 && localTime == activeAfterBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::After;
if (!result.FillsForwards()) {
// The animation isn't active or filling at this time.
return result;
}
activeTime =
std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay),
result.mActiveDuration),
zeroDuration);
} else if (localTime < beforeActiveBoundary ||
(aPlaybackRate < 0 && localTime == beforeActiveBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::Before;
if (!result.FillsBackwards()) {
// The animation isn't active or filling at this time.
return result;
}
activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay),
zeroDuration);
} else {
MOZ_ASSERT(result.mActiveDuration != zeroDuration,
"How can we be in the middle of a zero-duration interval?");
result.mPhase = ComputedTiming::AnimationPhase::Active;
activeTime = localTime - aTiming.mDelay;
}
// Convert active time to a multiple of iterations.
// https://w3c.github.io/web-animations/#overall-progress
double overallProgress;
if (result.mDuration == zeroDuration) {
overallProgress = result.mPhase == ComputedTiming::AnimationPhase::Before
? 0.0
: result.mIterations;
} else {
overallProgress = activeTime / result.mDuration;
}
// Factor in iteration start offset.
if (IsFinite(overallProgress)) {
overallProgress += result.mIterationStart;
}
// Determine the 0-based index of the current iteration.
// https://w3c.github.io/web-animations/#current-iteration
result.mCurrentIteration =
IsInfinite(result.mIterations) &&
result.mPhase == ComputedTiming::AnimationPhase::After
? UINT64_MAX // In GetComputedTimingDictionary(),
// we will convert this into Infinity
: static_cast<uint64_t>(overallProgress);
// Convert the overall progress to a fraction of a single iteration--the
// simply iteration progress.
// https://w3c.github.io/web-animations/#simple-iteration-progress
double progress = IsFinite(overallProgress)
? fmod(overallProgress, 1.0)
: fmod(result.mIterationStart, 1.0);
// When we finish exactly at the end of an iteration we need to report
// the end of the final iteration and not the start of the next iteration.
// We *don't* want to do this when we have a zero-iteration animation or
// when the animation has been effectively made into a zero-duration animation
// using a negative end-delay, however.
if (result.mPhase == ComputedTiming::AnimationPhase::After &&
progress == 0.0 &&
result.mIterations != 0.0 &&
(activeTime != zeroDuration || result.mDuration == zeroDuration)) {
// The only way we can be in the after phase with a progress of zero and
// a current iteration of zero, is if we have a zero iteration count or
// were clipped using a negative end delay--both of which we should have
// detected above.
MOZ_ASSERT(result.mCurrentIteration != 0,
"Should not have zero current iteration");
progress = 1.0;
if (result.mCurrentIteration != UINT64_MAX) {
result.mCurrentIteration--;
}
}
// Factor in the direction.
bool thisIterationReverse = false;
switch (aTiming.mDirection) {
case PlaybackDirection::Normal:
thisIterationReverse = false;
break;
case PlaybackDirection::Reverse:
thisIterationReverse = true;
break;
case PlaybackDirection::Alternate:
thisIterationReverse = (result.mCurrentIteration & 1) == 1;
break;
case PlaybackDirection::Alternate_reverse:
thisIterationReverse = (result.mCurrentIteration & 1) == 0;
break;
default:
MOZ_ASSERT(true, "Unknown PlaybackDirection type");
}
if (thisIterationReverse) {
progress = 1.0 - progress;
}
// Calculate the 'before flag' which we use when applying step timing
// functions.
if ((result.mPhase == ComputedTiming::AnimationPhase::After &&
thisIterationReverse) ||
(result.mPhase == ComputedTiming::AnimationPhase::Before &&
!thisIterationReverse)) {
result.mBeforeFlag = ComputedTimingFunction::BeforeFlag::Set;
}
// Apply the easing.
if (aTiming.mFunction) {
progress = aTiming.mFunction->GetValue(progress, result.mBeforeFlag);
}
MOZ_ASSERT(IsFinite(progress), "Progress value should be finite");
result.mProgress.SetValue(progress);
return result;
}
ComputedTiming
AnimationEffectReadOnly::GetComputedTiming(const TimingParams* aTiming) const
{
double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
return GetComputedTimingAt(GetLocalTime(),
aTiming ? *aTiming : SpecifiedTiming(),
playbackRate);
}
// Helper functions for generating a ComputedTimingProperties dictionary
static void
GetComputedTimingDictionary(const ComputedTiming& aComputedTiming,
const Nullable<TimeDuration>& aLocalTime,
const TimingParams& aTiming,
ComputedTimingProperties& aRetVal)
{
// AnimationEffectTimingProperties
aRetVal.mDelay = aTiming.mDelay.ToMilliseconds();
aRetVal.mEndDelay = aTiming.mEndDelay.ToMilliseconds();
aRetVal.mFill = aComputedTiming.mFill;
aRetVal.mIterations = aComputedTiming.mIterations;
aRetVal.mIterationStart = aComputedTiming.mIterationStart;
aRetVal.mDuration.SetAsUnrestrictedDouble() =
aComputedTiming.mDuration.ToMilliseconds();
aRetVal.mDirection = aTiming.mDirection;
// ComputedTimingProperties
aRetVal.mActiveDuration = aComputedTiming.mActiveDuration.ToMilliseconds();
aRetVal.mEndTime = aComputedTiming.mEndTime.ToMilliseconds();
aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(aLocalTime);
aRetVal.mProgress = aComputedTiming.mProgress;
if (!aRetVal.mProgress.IsNull()) {
// Convert the returned currentIteration into Infinity if we set
// (uint64_t) aComputedTiming.mCurrentIteration to UINT64_MAX
double iteration = aComputedTiming.mCurrentIteration == UINT64_MAX
? PositiveInfinity<double>()
: static_cast<double>(aComputedTiming.mCurrentIteration);
aRetVal.mCurrentIteration.SetValue(iteration);
}
}
void
AnimationEffectReadOnly::GetComputedTimingAsDict(
ComputedTimingProperties& aRetVal) const
{
double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
const Nullable<TimeDuration> currentTime = GetLocalTime();
GetComputedTimingDictionary(GetComputedTimingAt(currentTime,
SpecifiedTiming(),
playbackRate),
currentTime,
SpecifiedTiming(),
aRetVal);
}
AnimationEffectReadOnly::~AnimationEffectReadOnly()
{
// mTiming is cycle collected, so we have to do null check first even though
// mTiming shouldn't be null during the lifetime of KeyframeEffect.
if (mTiming) {
mTiming->Unlink();
}
}
Nullable<TimeDuration>
AnimationEffectReadOnly::GetLocalTime() const
{
// Since the *animation* start time is currently always zero, the local
// time is equal to the parent time.
Nullable<TimeDuration> result;
if (mAnimation) {
result = mAnimation->GetCurrentTime();
}
return result;
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,102 @@
/* -*- 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/. */
#ifndef mozilla_dom_AnimationEffectReadOnly_h
#define mozilla_dom_AnimationEffectReadOnly_h
#include "mozilla/ComputedTiming.h"
#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/Maybe.h"
#include "mozilla/StickyTimeDuration.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/TimingParams.h"
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
namespace mozilla {
struct ElementPropertyTransition;
namespace dom {
class Animation;
class AnimationEffectTimingReadOnly;
class KeyframeEffectReadOnly;
struct ComputedTimingProperties;
class AnimationEffectReadOnly : public nsISupports,
public nsWrapperCache
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationEffectReadOnly)
AnimationEffectReadOnly(nsIDocument* aDocument,
AnimationEffectTimingReadOnly* aTiming);
virtual KeyframeEffectReadOnly* AsKeyframeEffect() { return nullptr; }
virtual ElementPropertyTransition* AsTransition() { return nullptr; }
virtual const ElementPropertyTransition* AsTransition() const
{
return nullptr;
}
nsISupports* GetParentObject() const { return mDocument; }
bool IsCurrent() const;
bool IsInEffect() const;
bool IsActiveDurationZero() const
{
return SpecifiedTiming().ActiveDuration() == StickyTimeDuration();
}
already_AddRefed<AnimationEffectTimingReadOnly> Timing();
const TimingParams& SpecifiedTiming() const
{
return mTiming->AsTimingParams();
}
void SetSpecifiedTiming(const TimingParams& aTiming);
// This function takes as input the timing parameters of an animation and
// returns the computed timing at the specified local time.
//
// The local time may be null in which case only static parameters such as the
// active duration are calculated. All other members of the returned object
// are given a null/initial value.
//
// This function returns a null mProgress member of the return value
// if the animation should not be run
// (because it is not currently active and is not filling at this time).
static ComputedTiming
GetComputedTimingAt(const Nullable<TimeDuration>& aLocalTime,
const TimingParams& aTiming,
double aPlaybackRate);
// Shortcut that gets the computed timing using the current local time as
// calculated from the timeline time.
ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;
void GetComputedTimingAsDict(ComputedTimingProperties& aRetVal) const;
virtual void SetAnimation(Animation* aAnimation) = 0;
Animation* GetAnimation() const { return mAnimation; };
protected:
virtual ~AnimationEffectReadOnly();
Nullable<TimeDuration> GetLocalTime() const;
protected:
RefPtr<nsIDocument> mDocument;
RefPtr<AnimationEffectTimingReadOnly> mTiming;
RefPtr<Animation> mAnimation;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_AnimationEffectReadOnly_h

View file

@ -0,0 +1,152 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "mozilla/dom/AnimationEffectTiming.h"
#include "mozilla/dom/AnimatableBinding.h"
#include "mozilla/dom/AnimationEffectTimingBinding.h"
#include "mozilla/dom/KeyframeEffect.h"
#include "mozilla/TimingParams.h"
#include "nsAString.h"
namespace mozilla {
namespace dom {
JSObject*
AnimationEffectTiming::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return AnimationEffectTimingBinding::Wrap(aCx, this, aGivenProto);
}
static inline void
PostSpecifiedTimingUpdated(KeyframeEffect* aEffect)
{
if (aEffect) {
aEffect->NotifySpecifiedTimingUpdated();
}
}
void
AnimationEffectTiming::SetDelay(double aDelay)
{
TimeDuration delay = TimeDuration::FromMilliseconds(aDelay);
if (mTiming.mDelay == delay) {
return;
}
mTiming.mDelay = delay;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetEndDelay(double aEndDelay)
{
TimeDuration endDelay = TimeDuration::FromMilliseconds(aEndDelay);
if (mTiming.mEndDelay == endDelay) {
return;
}
mTiming.mEndDelay = endDelay;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetFill(const FillMode& aFill)
{
if (mTiming.mFill == aFill) {
return;
}
mTiming.mFill = aFill;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetIterationStart(double aIterationStart,
ErrorResult& aRv)
{
if (mTiming.mIterationStart == aIterationStart) {
return;
}
TimingParams::ValidateIterationStart(aIterationStart, aRv);
if (aRv.Failed()) {
return;
}
mTiming.mIterationStart = aIterationStart;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetIterations(double aIterations, ErrorResult& aRv)
{
if (mTiming.mIterations == aIterations) {
return;
}
TimingParams::ValidateIterations(aIterations, aRv);
if (aRv.Failed()) {
return;
}
mTiming.mIterations = aIterations;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetDuration(const UnrestrictedDoubleOrString& aDuration,
ErrorResult& aRv)
{
Maybe<StickyTimeDuration> newDuration =
TimingParams::ParseDuration(aDuration, aRv);
if (aRv.Failed()) {
return;
}
if (mTiming.mDuration == newDuration) {
return;
}
mTiming.mDuration = newDuration;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetDirection(const PlaybackDirection& aDirection)
{
if (mTiming.mDirection == aDirection) {
return;
}
mTiming.mDirection = aDirection;
PostSpecifiedTimingUpdated(mEffect);
}
void
AnimationEffectTiming::SetEasing(const nsAString& aEasing, ErrorResult& aRv)
{
Maybe<ComputedTimingFunction> newFunction =
TimingParams::ParseEasing(aEasing, mDocument, aRv);
if (aRv.Failed()) {
return;
}
if (mTiming.mFunction == newFunction) {
return;
}
mTiming.mFunction = newFunction;
PostSpecifiedTimingUpdated(mEffect);
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,49 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef mozilla_dom_AnimationEffectTiming_h
#define mozilla_dom_AnimationEffectTiming_h
#include "mozilla/dom/AnimationEffectTimingReadOnly.h"
#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
#include "nsStringFwd.h"
namespace mozilla {
namespace dom {
class KeyframeEffect;
class AnimationEffectTiming : public AnimationEffectTimingReadOnly
{
public:
AnimationEffectTiming(nsIDocument* aDocument,
const TimingParams& aTiming,
KeyframeEffect* aEffect)
: AnimationEffectTimingReadOnly(aDocument, aTiming)
, mEffect(aEffect) { }
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
void Unlink() override { mEffect = nullptr; }
void SetDelay(double aDelay);
void SetEndDelay(double aEndDelay);
void SetFill(const FillMode& aFill);
void SetIterationStart(double aIterationStart, ErrorResult& aRv);
void SetIterations(double aIterations, ErrorResult& aRv);
void SetDuration(const UnrestrictedDoubleOrString& aDuration,
ErrorResult& aRv);
void SetDirection(const PlaybackDirection& aDirection);
void SetEasing(const nsAString& aEasing, ErrorResult& aRv);
private:
KeyframeEffect* MOZ_NON_OWNING_REF mEffect;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_AnimationEffectTiming_h

View file

@ -0,0 +1,51 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "mozilla/dom/AnimationEffectTimingReadOnly.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/dom/AnimatableBinding.h"
#include "mozilla/dom/AnimationEffectTimingReadOnlyBinding.h"
#include "mozilla/dom/CSSPseudoElement.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationEffectTimingReadOnly, mDocument)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AnimationEffectTimingReadOnly, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AnimationEffectTimingReadOnly, Release)
JSObject*
AnimationEffectTimingReadOnly::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return AnimationEffectTimingReadOnlyBinding::Wrap(aCx, this, aGivenProto);
}
void
AnimationEffectTimingReadOnly::GetDuration(
OwningUnrestrictedDoubleOrString& aRetVal) const
{
if (mTiming.mDuration) {
aRetVal.SetAsUnrestrictedDouble() = mTiming.mDuration->ToMilliseconds();
} else {
aRetVal.SetAsString().AssignLiteral("auto");
}
}
void
AnimationEffectTimingReadOnly::GetEasing(nsString& aRetVal) const
{
if (mTiming.mFunction) {
mTiming.mFunction->AppendToString(aRetVal);
} else {
aRetVal.AssignLiteral("linear");
}
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,63 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef mozilla_dom_AnimationEffectTimingReadOnly_h
#define mozilla_dom_AnimationEffectTimingReadOnly_h
#include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/TimingParams.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/UnionTypes.h"
#include "nsCycleCollectionParticipant.h"
#include "nsWrapperCache.h"
namespace mozilla {
namespace dom {
class AnimationEffectTimingReadOnly : public nsWrapperCache
{
public:
AnimationEffectTimingReadOnly() = default;
AnimationEffectTimingReadOnly(nsIDocument* aDocument,
const TimingParams& aTiming)
: mDocument(aDocument)
, mTiming(aTiming) { }
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationEffectTimingReadOnly)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AnimationEffectTimingReadOnly)
protected:
virtual ~AnimationEffectTimingReadOnly() = default;
public:
nsISupports* GetParentObject() const { return mDocument; }
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
double Delay() const { return mTiming.mDelay.ToMilliseconds(); }
double EndDelay() const { return mTiming.mEndDelay.ToMilliseconds(); }
FillMode Fill() const { return mTiming.mFill; }
double IterationStart() const { return mTiming.mIterationStart; }
double Iterations() const { return mTiming.mIterations; }
void GetDuration(OwningUnrestrictedDoubleOrString& aRetVal) const;
PlaybackDirection Direction() const { return mTiming.mDirection; }
void GetEasing(nsString& aRetVal) const;
const TimingParams& AsTimingParams() const { return mTiming; }
void SetTimingParams(const TimingParams& aTiming) { mTiming = aTiming; }
virtual void Unlink() { }
protected:
RefPtr<nsIDocument> mDocument;
TimingParams mTiming;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_AnimationEffectTimingReadOnly_h

View file

@ -0,0 +1,79 @@
/* -*- 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 "AnimationPerformanceWarning.h"
#include "nsContentUtils.h"
namespace mozilla {
template<uint32_t N> nsresult
AnimationPerformanceWarning::ToLocalizedStringWithIntParams(
const char* aKey, nsXPIDLString& aLocalizedString) const
{
nsAutoString strings[N];
const char16_t* charParams[N];
for (size_t i = 0, n = mParams->Length(); i < n; i++) {
strings[i].AppendInt((*mParams)[i]);
charParams[i] = strings[i].get();
}
return nsContentUtils::FormatLocalizedString(
nsContentUtils::eLAYOUT_PROPERTIES, aKey, charParams, aLocalizedString);
}
bool
AnimationPerformanceWarning::ToLocalizedString(
nsXPIDLString& aLocalizedString) const
{
const char* key = nullptr;
switch (mType) {
case Type::ContentTooSmall:
MOZ_ASSERT(mParams && mParams->Length() == 2,
"Parameter's length should be 2 for ContentTooSmall");
return NS_SUCCEEDED(
ToLocalizedStringWithIntParams<2>(
"CompositorAnimationWarningContentTooSmall", aLocalizedString));
case Type::ContentTooLarge:
MOZ_ASSERT(mParams && mParams->Length() == 7,
"Parameter's length should be 7 for ContentTooLarge");
return NS_SUCCEEDED(
ToLocalizedStringWithIntParams<7>(
"CompositorAnimationWarningContentTooLarge", aLocalizedString));
case Type::TransformBackfaceVisibilityHidden:
key = "CompositorAnimationWarningTransformBackfaceVisibilityHidden";
break;
case Type::TransformPreserve3D:
key = "CompositorAnimationWarningTransformPreserve3D";
break;
case Type::TransformSVG:
key = "CompositorAnimationWarningTransformSVG";
break;
case Type::TransformWithGeometricProperties:
key = "CompositorAnimationWarningTransformWithGeometricProperties";
break;
case Type::TransformFrameInactive:
key = "CompositorAnimationWarningTransformFrameInactive";
break;
case Type::OpacityFrameInactive:
key = "CompositorAnimationWarningOpacityFrameInactive";
break;
case Type::HasRenderingObserver:
key = "CompositorAnimationWarningHasRenderingObserver";
break;
}
nsresult rv =
nsContentUtils::GetLocalizedString(nsContentUtils::eLAYOUT_PROPERTIES,
key, aLocalizedString);
return NS_SUCCEEDED(rv);
}
} // namespace mozilla

View file

@ -0,0 +1,79 @@
/* -*- 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/. */
#ifndef mozilla_dom_AnimationPerformanceWarning_h
#define mozilla_dom_AnimationPerformanceWarning_h
#include <initializer_list>
class nsXPIDLString;
namespace mozilla {
// Represents the reason why we can't run the CSS property on the compositor.
struct AnimationPerformanceWarning
{
enum class Type : uint8_t {
ContentTooSmall,
ContentTooLarge,
TransformBackfaceVisibilityHidden,
TransformPreserve3D,
TransformSVG,
TransformWithGeometricProperties,
TransformFrameInactive,
OpacityFrameInactive,
HasRenderingObserver,
};
explicit AnimationPerformanceWarning(Type aType)
: mType(aType) { }
AnimationPerformanceWarning(Type aType,
std::initializer_list<int32_t> aParams)
: mType(aType)
{
// FIXME: Once std::initializer_list::size() become a constexpr function,
// we should use static_assert here.
MOZ_ASSERT(aParams.size() <= kMaxParamsForLocalization,
"The length of parameters should be less than "
"kMaxParamsForLocalization");
mParams.emplace(aParams);
}
// Maximum number of parameters passed to
// nsContentUtils::FormatLocalizedString to localize warning messages.
//
// NOTE: This constexpr can't be forward declared, so if you want to use
// this variable, please include this header file directly.
// This value is the same as the limit of nsStringBundle::FormatString.
// See the implementation of nsStringBundle::FormatString.
static constexpr uint8_t kMaxParamsForLocalization = 10;
// Indicates why this property could not be animated on the compositor.
Type mType;
// Optional parameters that may be used for localization.
Maybe<nsTArray<int32_t>> mParams;
bool ToLocalizedString(nsXPIDLString& aLocalizedString) const;
template<uint32_t N>
nsresult ToLocalizedStringWithIntParams(
const char* aKey, nsXPIDLString& aLocalizedString) const;
bool operator==(const AnimationPerformanceWarning& aOther) const
{
return mType == aOther.mType &&
mParams == aOther.mParams;
}
bool operator!=(const AnimationPerformanceWarning& aOther) const
{
return !(*this == aOther);
}
};
} // namespace mozilla
#endif // mozilla_dom_AnimationPerformanceWarning_h

View file

@ -0,0 +1,78 @@
/* -*- 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/. */
#ifndef mozilla_AnimationTarget_h
#define mozilla_AnimationTarget_h
#include "mozilla/Attributes.h" // For MOZ_NON_OWNING_REF
#include "mozilla/Maybe.h"
#include "mozilla/RefPtr.h"
#include "nsCSSPseudoElements.h"
namespace mozilla {
namespace dom {
class Element;
} // namespace dom
struct OwningAnimationTarget
{
OwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType)
: mElement(aElement), mPseudoType(aType) { }
explicit OwningAnimationTarget(dom::Element* aElement)
: mElement(aElement) { }
bool operator==(const OwningAnimationTarget& aOther) const
{
return mElement == aOther.mElement &&
mPseudoType == aOther.mPseudoType;
}
// mElement represents the parent element of a pseudo-element, not the
// generated content element.
RefPtr<dom::Element> mElement;
CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo;
};
struct NonOwningAnimationTarget
{
NonOwningAnimationTarget(dom::Element* aElement, CSSPseudoElementType aType)
: mElement(aElement), mPseudoType(aType) { }
explicit NonOwningAnimationTarget(const OwningAnimationTarget& aOther)
: mElement(aOther.mElement), mPseudoType(aOther.mPseudoType) { }
// mElement represents the parent element of a pseudo-element, not the
// generated content element.
dom::Element* MOZ_NON_OWNING_REF mElement = nullptr;
CSSPseudoElementType mPseudoType = CSSPseudoElementType::NotPseudo;
};
// Helper functions for cycle-collecting Maybe<OwningAnimationTarget>
inline void
ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
Maybe<OwningAnimationTarget>& aTarget,
const char* aName,
uint32_t aFlags = 0)
{
if (aTarget) {
ImplCycleCollectionTraverse(aCallback, aTarget->mElement, aName, aFlags);
}
}
inline void
ImplCycleCollectionUnlink(Maybe<OwningAnimationTarget>& aTarget)
{
if (aTarget) {
ImplCycleCollectionUnlink(aTarget->mElement);
}
}
} // namespace mozilla
#endif // mozilla_AnimationTarget_h

View file

@ -0,0 +1,63 @@
/* -*- 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 "AnimationTimeline.h"
#include "mozilla/AnimationComparator.h"
#include "mozilla/dom/Animation.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AnimationTimeline)
tmp->mAnimationOrder.clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mAnimations)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mAnimations)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AnimationTimeline)
NS_IMPL_CYCLE_COLLECTING_ADDREF(AnimationTimeline)
NS_IMPL_CYCLE_COLLECTING_RELEASE(AnimationTimeline)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AnimationTimeline)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
void
AnimationTimeline::NotifyAnimationUpdated(Animation& aAnimation)
{
if (mAnimations.Contains(&aAnimation)) {
return;
}
if (aAnimation.GetTimeline() && aAnimation.GetTimeline() != this) {
aAnimation.GetTimeline()->RemoveAnimation(&aAnimation);
}
mAnimations.PutEntry(&aAnimation);
mAnimationOrder.insertBack(&aAnimation);
}
void
AnimationTimeline::RemoveAnimation(Animation* aAnimation)
{
MOZ_ASSERT(!aAnimation->GetTimeline() || aAnimation->GetTimeline() == this);
if (aAnimation->isInList()) {
aAnimation->remove();
}
mAnimations.RemoveEntry(aAnimation);
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,125 @@
/* -*- 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/. */
#ifndef mozilla_dom_AnimationTimeline_h
#define mozilla_dom_AnimationTimeline_h
#include "nsISupports.h"
#include "nsWrapperCache.h"
#include "nsCycleCollectionParticipant.h"
#include "js/TypeDecls.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/Attributes.h"
#include "nsHashKeys.h"
#include "nsIGlobalObject.h"
#include "nsTHashtable.h"
// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
// GetTickCount().
#ifdef GetCurrentTime
#undef GetCurrentTime
#endif
namespace mozilla {
namespace dom {
class Animation;
class AnimationTimeline
: public nsISupports
, public nsWrapperCache
{
public:
explicit AnimationTimeline(nsIGlobalObject* aWindow)
: mWindow(aWindow)
{
MOZ_ASSERT(mWindow);
}
protected:
virtual ~AnimationTimeline()
{
mAnimationOrder.clear();
}
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AnimationTimeline)
nsIGlobalObject* GetParentObject() const { return mWindow; }
// AnimationTimeline methods
virtual Nullable<TimeDuration> GetCurrentTime() const = 0;
// Wrapper functions for AnimationTimeline DOM methods when called from
// script.
Nullable<double> GetCurrentTimeAsDouble() const {
return AnimationUtils::TimeDurationToDouble(GetCurrentTime());
}
/**
* Returns true if the times returned by GetCurrentTime() are convertible
* to and from wallclock-based TimeStamp (e.g. from TimeStamp::Now()) values
* using ToTimelineTime() and ToTimeStamp().
*
* Typically this is true, but it will be false in the case when this
* timeline has no refresh driver or is tied to a refresh driver under test
* control.
*/
virtual bool TracksWallclockTime() const = 0;
/**
* Converts a TimeStamp to the equivalent value in timeline time.
* Note that when TracksWallclockTime() is false, there is no correspondence
* between timeline time and wallclock time. In such a case, passing a
* timestamp from TimeStamp::Now() to this method will not return a
* meaningful result.
*/
virtual Nullable<TimeDuration> ToTimelineTime(const TimeStamp&
aTimeStamp) const = 0;
virtual TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const = 0;
/**
* Inform this timeline that |aAnimation| which is or was observing the
* timeline, has been updated. This serves as both the means to associate
* AND disassociate animations with a timeline. The timeline itself will
* determine if it needs to begin, continue or stop tracking this animation.
*/
virtual void NotifyAnimationUpdated(Animation& aAnimation);
/**
* Returns true if any CSS animations, CSS transitions or Web animations are
* currently associated with this timeline. As soon as an animation is
* applied to an element it is associated with the timeline even if it has a
* delayed start, so this includes animations that may not be active for some
* time.
*/
bool HasAnimations() const {
return !mAnimations.IsEmpty();
}
virtual void RemoveAnimation(Animation* aAnimation);
protected:
nsCOMPtr<nsIGlobalObject> mWindow;
// Animations observing this timeline
//
// We store them in (a) a hashset for quick lookup, and (b) an array
// to maintain a fixed sampling order.
//
// The hashset keeps a strong reference to each animation since
// dealing with addref/release with LinkedList is difficult.
typedef nsTHashtable<nsRefPtrHashKey<dom::Animation>> AnimationSet;
AnimationSet mAnimations;
LinkedList<dom::Animation> mAnimationOrder;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_AnimationTimeline_h

View file

@ -0,0 +1,81 @@
/* -*- 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 "AnimationUtils.h"
#include "nsContentUtils.h" // For nsContentUtils::IsCallerChrome
#include "nsDebug.h"
#include "nsIAtom.h"
#include "nsIContent.h"
#include "nsIDocument.h"
#include "nsGlobalWindow.h"
#include "nsString.h"
#include "xpcpublic.h" // For xpc::NativeGlobal
#include "mozilla/Preferences.h"
namespace mozilla {
/* static */ void
AnimationUtils::LogAsyncAnimationFailure(nsCString& aMessage,
const nsIContent* aContent)
{
if (aContent) {
aMessage.AppendLiteral(" [");
aMessage.Append(nsAtomCString(aContent->NodeInfo()->NameAtom()));
nsIAtom* id = aContent->GetID();
if (id) {
aMessage.AppendLiteral(" with id '");
aMessage.Append(nsAtomCString(aContent->GetID()));
aMessage.Append('\'');
}
aMessage.Append(']');
}
aMessage.Append('\n');
printf_stderr("%s", aMessage.get());
}
/* static */ nsIDocument*
AnimationUtils::GetCurrentRealmDocument(JSContext* aCx)
{
nsGlobalWindow* win = xpc::CurrentWindowOrNull(aCx);
if (!win) {
return nullptr;
}
return win->GetDoc();
}
/* static */ bool
AnimationUtils::IsOffscreenThrottlingEnabled()
{
static bool sOffscreenThrottlingEnabled;
static bool sPrefCached = false;
if (!sPrefCached) {
sPrefCached = true;
Preferences::AddBoolVarCache(&sOffscreenThrottlingEnabled,
"dom.animations.offscreen-throttling");
}
return sOffscreenThrottlingEnabled;
}
/* static */ bool
AnimationUtils::IsCoreAPIEnabledForCaller()
{
static bool sCoreAPIEnabled;
static bool sPrefCached = false;
if (!sPrefCached) {
sPrefCached = true;
Preferences::AddBoolVarCache(&sCoreAPIEnabled,
"dom.animations-api.core.enabled");
}
return sCoreAPIEnabled || nsContentUtils::IsCallerChrome();
}
} // namespace mozilla

View file

@ -0,0 +1,74 @@
/* -*- 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/. */
#ifndef mozilla_dom_AnimationUtils_h
#define mozilla_dom_AnimationUtils_h
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/Nullable.h"
#include "nsStringFwd.h"
class nsIContent;
class nsIDocument;
struct JSContext;
namespace mozilla {
class ComputedTimingFunction;
class AnimationUtils
{
public:
static dom::Nullable<double>
TimeDurationToDouble(const dom::Nullable<TimeDuration>& aTime)
{
dom::Nullable<double> result;
if (!aTime.IsNull()) {
result.SetValue(aTime.Value().ToMilliseconds());
}
return result;
}
static dom::Nullable<TimeDuration>
DoubleToTimeDuration(const dom::Nullable<double>& aTime)
{
dom::Nullable<TimeDuration> result;
if (!aTime.IsNull()) {
result.SetValue(TimeDuration::FromMilliseconds(aTime.Value()));
}
return result;
}
static void LogAsyncAnimationFailure(nsCString& aMessage,
const nsIContent* aContent = nullptr);
/**
* Get the document from the JS context to use when parsing CSS properties.
*/
static nsIDocument*
GetCurrentRealmDocument(JSContext* aCx);
/**
* Checks if offscreen animation throttling is enabled.
*/
static bool
IsOffscreenThrottlingEnabled();
/**
* Returns true if the preference to enable the core Web Animations API is
* true or the caller is chrome.
*/
static bool
IsCoreAPIEnabledForCaller();
};
} // namespace mozilla
#endif

View file

@ -0,0 +1,123 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "mozilla/dom/CSSPseudoElement.h"
#include "mozilla/dom/CSSPseudoElementBinding.h"
#include "mozilla/dom/Element.h"
#include "mozilla/AnimationComparator.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CSSPseudoElement, mParentElement)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CSSPseudoElement, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CSSPseudoElement, Release)
CSSPseudoElement::CSSPseudoElement(Element* aElement,
CSSPseudoElementType aType)
: mParentElement(aElement)
, mPseudoType(aType)
{
MOZ_ASSERT(aElement);
MOZ_ASSERT(aType == CSSPseudoElementType::after ||
aType == CSSPseudoElementType::before,
"Unexpected Pseudo Type");
}
CSSPseudoElement::~CSSPseudoElement()
{
// Element might have been unlinked already, so we have to do null check.
if (mParentElement) {
mParentElement->DeleteProperty(
GetCSSPseudoElementPropertyAtom(mPseudoType));
}
}
ParentObject
CSSPseudoElement::GetParentObject() const
{
return mParentElement->GetParentObject();
}
JSObject*
CSSPseudoElement::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return CSSPseudoElementBinding::Wrap(aCx, this, aGivenProto);
}
void
CSSPseudoElement::GetAnimations(const AnimationFilter& filter,
nsTArray<RefPtr<Animation>>& aRetVal)
{
nsIDocument* doc = mParentElement->GetComposedDoc();
if (doc) {
doc->FlushPendingNotifications(Flush_Style);
}
Element::GetAnimationsUnsorted(mParentElement, mPseudoType, aRetVal);
aRetVal.Sort(AnimationPtrComparator<RefPtr<Animation>>());
}
already_AddRefed<Animation>
CSSPseudoElement::Animate(
JSContext* aContext,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
ErrorResult& aError)
{
Nullable<ElementOrCSSPseudoElement> target;
target.SetValue().SetAsCSSPseudoElement() = this;
return Element::Animate(target, aContext, aKeyframes, aOptions, aError);
}
/* static */ already_AddRefed<CSSPseudoElement>
CSSPseudoElement::GetCSSPseudoElement(Element* aElement,
CSSPseudoElementType aType)
{
if (!aElement) {
return nullptr;
}
nsIAtom* propName = CSSPseudoElement::GetCSSPseudoElementPropertyAtom(aType);
RefPtr<CSSPseudoElement> pseudo =
static_cast<CSSPseudoElement*>(aElement->GetProperty(propName));
if (pseudo) {
return pseudo.forget();
}
// CSSPseudoElement is a purely external interface created on-demand, and
// when all references from script to the pseudo are dropped, we can drop the
// CSSPseudoElement object, so use a non-owning reference from Element to
// CSSPseudoElement.
pseudo = new CSSPseudoElement(aElement, aType);
nsresult rv = aElement->SetProperty(propName, pseudo, nullptr, true);
if (NS_FAILED(rv)) {
NS_WARNING("SetProperty failed");
return nullptr;
}
return pseudo.forget();
}
/* static */ nsIAtom*
CSSPseudoElement::GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType)
{
switch (aType) {
case CSSPseudoElementType::before:
return nsGkAtoms::cssPseudoElementBeforeProperty;
case CSSPseudoElementType::after:
return nsGkAtoms::cssPseudoElementAfterProperty;
default:
NS_NOTREACHED("Should not try to get CSSPseudoElement "
"other than ::before or ::after");
return nullptr;
}
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,91 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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/. */
#ifndef mozilla_dom_CSSPseudoElement_h
#define mozilla_dom_CSSPseudoElement_h
#include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Element.h"
#include "mozilla/RefPtr.h"
#include "nsCSSPseudoElements.h"
#include "nsWrapperCache.h"
namespace mozilla {
namespace dom {
class Animation;
class Element;
class UnrestrictedDoubleOrKeyframeAnimationOptions;
class CSSPseudoElement final : public nsWrapperCache
{
public:
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CSSPseudoElement)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CSSPseudoElement)
protected:
virtual ~CSSPseudoElement();
public:
ParentObject GetParentObject() const;
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
CSSPseudoElementType GetType() const { return mPseudoType; }
void GetType(nsString& aRetVal) const
{
MOZ_ASSERT(nsCSSPseudoElements::GetPseudoAtom(mPseudoType),
"All pseudo-types allowed by this class should have a"
" corresponding atom");
// Our atoms use one colon and we would like to return two colons syntax
// for the returned pseudo type string, so serialize this to the
// non-deprecated two colon syntax.
aRetVal.Assign(char16_t(':'));
aRetVal.Append(
nsDependentAtomString(nsCSSPseudoElements::GetPseudoAtom(mPseudoType)));
}
already_AddRefed<Element> ParentElement() const
{
RefPtr<Element> retVal(mParentElement);
return retVal.forget();
}
void GetAnimations(const AnimationFilter& filter,
nsTArray<RefPtr<Animation>>& aRetVal);
already_AddRefed<Animation>
Animate(JSContext* aContext,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
ErrorResult& aError);
// Given an element:pseudoType pair, returns the CSSPseudoElement stored as a
// property on |aElement|. If there is no CSSPseudoElement for the specified
// pseudo-type on element, a new CSSPseudoElement will be created and stored
// on the element.
static already_AddRefed<CSSPseudoElement>
GetCSSPseudoElement(Element* aElement, CSSPseudoElementType aType);
private:
// Only ::before and ::after are supported.
CSSPseudoElement(Element* aElement, CSSPseudoElementType aType);
static nsIAtom* GetCSSPseudoElementPropertyAtom(CSSPseudoElementType aType);
// mParentElement needs to be an owning reference since if script is holding
// on to the pseudo-element, it needs to continue to be able to refer to
// the parent element.
RefPtr<Element> mParentElement;
CSSPseudoElementType mPseudoType;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_CSSPseudoElement_h

View file

@ -0,0 +1,78 @@
/* -*- 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/. */
#ifndef mozilla_ComputedTiming_h
#define mozilla_ComputedTiming_h
#include "mozilla/dom/Nullable.h"
#include "mozilla/StickyTimeDuration.h"
#include "mozilla/ComputedTimingFunction.h"
// X11 has a #define for None
#ifdef None
#undef None
#endif
#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // FillMode
namespace mozilla {
/**
* Stores the results of calculating the timing properties of an animation
* at a given sample time.
*/
struct ComputedTiming
{
// The total duration of the animation including all iterations.
// Will equal StickyTimeDuration::Forever() if the animation repeats
// indefinitely.
StickyTimeDuration mActiveDuration;
// The effect end time in local time (i.e. an offset from the effect's
// start time). Will equal StickyTimeDuration::Forever() if the animation
// plays indefinitely.
StickyTimeDuration mEndTime;
// Progress towards the end of the current iteration. If the effect is
// being sampled backwards, this will go from 1.0 to 0.0.
// Will be null if the animation is neither animating nor
// filling at the sampled time.
Nullable<double> mProgress;
// Zero-based iteration index (meaningless if mProgress is null).
uint64_t mCurrentIteration = 0;
// Unlike TimingParams::mIterations, this value is
// guaranteed to be in the range [0, Infinity].
double mIterations = 1.0;
double mIterationStart = 0.0;
StickyTimeDuration mDuration;
// This is the computed fill mode so it is never auto
dom::FillMode mFill = dom::FillMode::None;
bool FillsForwards() const {
MOZ_ASSERT(mFill != dom::FillMode::Auto,
"mFill should not be Auto in ComputedTiming.");
return mFill == dom::FillMode::Both ||
mFill == dom::FillMode::Forwards;
}
bool FillsBackwards() const {
MOZ_ASSERT(mFill != dom::FillMode::Auto,
"mFill should not be Auto in ComputedTiming.");
return mFill == dom::FillMode::Both ||
mFill == dom::FillMode::Backwards;
}
enum class AnimationPhase {
Null, // Not sampled (null sample time)
Before, // Sampled prior to the start of the active interval
Active, // Sampled within the active interval
After // Sampled after (or at) the end of the active interval
};
AnimationPhase mPhase = AnimationPhase::Null;
ComputedTimingFunction::BeforeFlag mBeforeFlag =
ComputedTimingFunction::BeforeFlag::Unset;
};
} // namespace mozilla
#endif // mozilla_ComputedTiming_h

View file

@ -0,0 +1,194 @@
/* -*- 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 "ComputedTimingFunction.h"
#include "nsAlgorithm.h" // For clamped()
#include "nsStyleUtil.h"
namespace mozilla {
void
ComputedTimingFunction::Init(const nsTimingFunction &aFunction)
{
mType = aFunction.mType;
if (nsTimingFunction::IsSplineType(mType)) {
mTimingFunction.Init(aFunction.mFunc.mX1, aFunction.mFunc.mY1,
aFunction.mFunc.mX2, aFunction.mFunc.mY2);
} else {
mSteps = aFunction.mSteps;
}
}
static inline double
StepTiming(uint32_t aSteps,
double aPortion,
ComputedTimingFunction::BeforeFlag aBeforeFlag,
nsTimingFunction::Type aType)
{
MOZ_ASSERT(0.0 <= aPortion && aPortion <= 1.0, "out of range");
MOZ_ASSERT(aType == nsTimingFunction::Type::StepStart ||
aType == nsTimingFunction::Type::StepEnd, "invalid type");
if (aPortion == 1.0) {
return 1.0;
}
// Calculate current step using step-end behavior
uint32_t step = uint32_t(aPortion * aSteps); // floor
// step-start is one step ahead
if (aType == nsTimingFunction::Type::StepStart) {
step++;
}
// If the "before flag" is set and we are at a transition point,
// drop back a step (but only if we are not already at the zero point--
// we do this clamping here since |step| is an unsigned integer)
if (step != 0 &&
aBeforeFlag == ComputedTimingFunction::BeforeFlag::Set &&
fmod(aPortion * aSteps, 1) == 0) {
step--;
}
// Convert to a progress value
return double(step) / double(aSteps);
}
double
ComputedTimingFunction::GetValue(
double aPortion,
ComputedTimingFunction::BeforeFlag aBeforeFlag) const
{
if (HasSpline()) {
// Check for a linear curve.
// (GetSplineValue(), below, also checks this but doesn't work when
// aPortion is outside the range [0.0, 1.0]).
if (mTimingFunction.X1() == mTimingFunction.Y1() &&
mTimingFunction.X2() == mTimingFunction.Y2()) {
return aPortion;
}
// Ensure that we return 0 or 1 on both edges.
if (aPortion == 0.0) {
return 0.0;
}
if (aPortion == 1.0) {
return 1.0;
}
// For negative values, try to extrapolate with tangent (p1 - p0) or,
// if p1 is coincident with p0, with (p2 - p0).
if (aPortion < 0.0) {
if (mTimingFunction.X1() > 0.0) {
return aPortion * mTimingFunction.Y1() / mTimingFunction.X1();
} else if (mTimingFunction.Y1() == 0 && mTimingFunction.X2() > 0.0) {
return aPortion * mTimingFunction.Y2() / mTimingFunction.X2();
}
// If we can't calculate a sensible tangent, don't extrapolate at all.
return 0.0;
}
// For values greater than 1, try to extrapolate with tangent (p2 - p3) or,
// if p2 is coincident with p3, with (p1 - p3).
if (aPortion > 1.0) {
if (mTimingFunction.X2() < 1.0) {
return 1.0 + (aPortion - 1.0) *
(mTimingFunction.Y2() - 1) / (mTimingFunction.X2() - 1);
} else if (mTimingFunction.Y2() == 1 && mTimingFunction.X1() < 1.0) {
return 1.0 + (aPortion - 1.0) *
(mTimingFunction.Y1() - 1) / (mTimingFunction.X1() - 1);
}
// If we can't calculate a sensible tangent, don't extrapolate at all.
return 1.0;
}
return mTimingFunction.GetSplineValue(aPortion);
}
// Since we use endpoint-exclusive timing, the output of a steps(start) timing
// function when aPortion = 0.0 is the top of the first step. When aPortion is
// negative, however, we should use the bottom of the first step. We handle
// negative values of aPortion specially here since once we clamp aPortion
// to [0,1] below we will no longer be able to distinguish to the two cases.
if (aPortion < 0.0) {
return 0.0;
}
// Clamp in case of steps(end) and steps(start) for values greater than 1.
aPortion = clamped(aPortion, 0.0, 1.0);
return StepTiming(mSteps, aPortion, aBeforeFlag, mType);
}
int32_t
ComputedTimingFunction::Compare(const ComputedTimingFunction& aRhs) const
{
if (mType != aRhs.mType) {
return int32_t(mType) - int32_t(aRhs.mType);
}
if (mType == nsTimingFunction::Type::CubicBezier) {
int32_t order = mTimingFunction.Compare(aRhs.mTimingFunction);
if (order != 0) {
return order;
}
} else if (mType == nsTimingFunction::Type::StepStart ||
mType == nsTimingFunction::Type::StepEnd) {
if (mSteps != aRhs.mSteps) {
return int32_t(mSteps) - int32_t(aRhs.mSteps);
}
}
return 0;
}
void
ComputedTimingFunction::AppendToString(nsAString& aResult) const
{
switch (mType) {
case nsTimingFunction::Type::CubicBezier:
nsStyleUtil::AppendCubicBezierTimingFunction(mTimingFunction.X1(),
mTimingFunction.Y1(),
mTimingFunction.X2(),
mTimingFunction.Y2(),
aResult);
break;
case nsTimingFunction::Type::StepStart:
case nsTimingFunction::Type::StepEnd:
nsStyleUtil::AppendStepsTimingFunction(mType, mSteps, aResult);
break;
default:
nsStyleUtil::AppendCubicBezierKeywordTimingFunction(mType, aResult);
break;
}
}
/* static */ int32_t
ComputedTimingFunction::Compare(const Maybe<ComputedTimingFunction>& aLhs,
const Maybe<ComputedTimingFunction>& aRhs)
{
// We can't use |operator<| for const Maybe<>& here because
// 'ease' is prior to 'linear' which is represented by Nothing().
// So we have to convert Nothing() as 'linear' and check it first.
nsTimingFunction::Type lhsType = aLhs.isNothing() ?
nsTimingFunction::Type::Linear : aLhs->GetType();
nsTimingFunction::Type rhsType = aRhs.isNothing() ?
nsTimingFunction::Type::Linear : aRhs->GetType();
if (lhsType != rhsType) {
return int32_t(lhsType) - int32_t(rhsType);
}
// Both of them are Nothing().
if (lhsType == nsTimingFunction::Type::Linear) {
return 0;
}
// Other types.
return aLhs->Compare(aRhs.value());
}
} // namespace mozilla

View file

@ -0,0 +1,65 @@
/* -*- 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/. */
#ifndef mozilla_ComputedTimingFunction_h
#define mozilla_ComputedTimingFunction_h
#include "nsSMILKeySpline.h" // nsSMILKeySpline
#include "nsStyleStruct.h" // nsTimingFunction
namespace mozilla {
class ComputedTimingFunction
{
public:
// BeforeFlag is used in step timing function.
// https://w3c.github.io/web-animations/#before-flag
enum class BeforeFlag {
Unset,
Set
};
void Init(const nsTimingFunction &aFunction);
double GetValue(double aPortion, BeforeFlag aBeforeFlag) const;
const nsSMILKeySpline* GetFunction() const
{
NS_ASSERTION(HasSpline(), "Type mismatch");
return &mTimingFunction;
}
nsTimingFunction::Type GetType() const { return mType; }
bool HasSpline() const { return nsTimingFunction::IsSplineType(mType); }
uint32_t GetSteps() const { return mSteps; }
bool operator==(const ComputedTimingFunction& aOther) const
{
return mType == aOther.mType &&
(HasSpline() ?
mTimingFunction == aOther.mTimingFunction :
mSteps == aOther.mSteps);
}
bool operator!=(const ComputedTimingFunction& aOther) const
{
return !(*this == aOther);
}
int32_t Compare(const ComputedTimingFunction& aRhs) const;
void AppendToString(nsAString& aResult) const;
static double GetPortion(const Maybe<ComputedTimingFunction>& aFunction,
double aPortion,
BeforeFlag aBeforeFlag)
{
return aFunction ? aFunction->GetValue(aPortion, aBeforeFlag) : aPortion;
}
static int32_t Compare(const Maybe<ComputedTimingFunction>& aLhs,
const Maybe<ComputedTimingFunction>& aRhs);
private:
nsTimingFunction::Type mType;
nsSMILKeySpline mTimingFunction;
uint32_t mSteps;
};
} // namespace mozilla
#endif // mozilla_ComputedTimingFunction_h

View file

@ -0,0 +1,283 @@
/* -*- 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 "DocumentTimeline.h"
#include "mozilla/dom/DocumentTimelineBinding.h"
#include "AnimationUtils.h"
#include "nsContentUtils.h"
#include "nsDOMMutationObserver.h"
#include "nsDOMNavigationTiming.h"
#include "nsIPresShell.h"
#include "nsPresContext.h"
#include "nsRefreshDriver.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentTimeline)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
tmp->UnregisterFromRefreshDriver();
if (tmp->isInList()) {
tmp->remove();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DocumentTimeline,
AnimationTimeline)
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DocumentTimeline)
NS_INTERFACE_MAP_END_INHERITING(AnimationTimeline)
NS_IMPL_ADDREF_INHERITED(DocumentTimeline, AnimationTimeline)
NS_IMPL_RELEASE_INHERITED(DocumentTimeline, AnimationTimeline)
JSObject*
DocumentTimeline::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return DocumentTimelineBinding::Wrap(aCx, this, aGivenProto);
}
/* static */ already_AddRefed<DocumentTimeline>
DocumentTimeline::Constructor(const GlobalObject& aGlobal,
const DocumentTimelineOptions& aOptions,
ErrorResult& aRv)
{
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
if (!doc) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
TimeDuration originTime =
TimeDuration::FromMilliseconds(aOptions.mOriginTime);
if (originTime == TimeDuration::Forever() ||
originTime == -TimeDuration::Forever()) {
aRv.ThrowTypeError<dom::MSG_TIME_VALUE_OUT_OF_RANGE>(
NS_LITERAL_STRING("Origin time"));
return nullptr;
}
RefPtr<DocumentTimeline> timeline = new DocumentTimeline(doc, originTime);
return timeline.forget();
}
Nullable<TimeDuration>
DocumentTimeline::GetCurrentTime() const
{
return ToTimelineTime(GetCurrentTimeStamp());
}
TimeStamp
DocumentTimeline::GetCurrentTimeStamp() const
{
nsRefreshDriver* refreshDriver = GetRefreshDriver();
TimeStamp refreshTime = refreshDriver
? refreshDriver->MostRecentRefresh()
: TimeStamp();
// Always return the same object to benefit from return-value optimization.
TimeStamp result = !refreshTime.IsNull()
? refreshTime
: mLastRefreshDriverTime;
// If we don't have a refresh driver and we've never had one use the
// timeline's zero time.
if (result.IsNull()) {
RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
if (timing) {
result = timing->GetNavigationStartTimeStamp();
// Also, let this time represent the current refresh time. This way
// we'll save it as the last refresh time and skip looking up
// navigation timing each time.
refreshTime = result;
}
}
if (!refreshTime.IsNull()) {
mLastRefreshDriverTime = refreshTime;
}
return result;
}
Nullable<TimeDuration>
DocumentTimeline::ToTimelineTime(const TimeStamp& aTimeStamp) const
{
Nullable<TimeDuration> result; // Initializes to null
if (aTimeStamp.IsNull()) {
return result;
}
RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
if (MOZ_UNLIKELY(!timing)) {
return result;
}
result.SetValue(aTimeStamp
- timing->GetNavigationStartTimeStamp()
- mOriginTime);
return result;
}
void
DocumentTimeline::NotifyAnimationUpdated(Animation& aAnimation)
{
AnimationTimeline::NotifyAnimationUpdated(aAnimation);
if (!mIsObservingRefreshDriver) {
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (refreshDriver) {
MOZ_ASSERT(isInList(),
"We should not register with the refresh driver if we are not"
" in the document's list of timelines");
refreshDriver->AddRefreshObserver(this, Flush_Style);
mIsObservingRefreshDriver = true;
}
}
}
void
DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime)
{
MOZ_ASSERT(mIsObservingRefreshDriver);
MOZ_ASSERT(GetRefreshDriver(),
"Should be able to reach refresh driver from within WillRefresh");
bool needsTicks = false;
nsTArray<Animation*> animationsToRemove(mAnimations.Count());
nsAutoAnimationMutationBatch mb(mDocument);
for (Animation* animation = mAnimationOrder.getFirst(); animation;
animation = animation->getNext()) {
// Skip any animations that are longer need associated with this timeline.
if (animation->GetTimeline() != this) {
// If animation has some other timeline, it better not be also in the
// animation list of this timeline object!
MOZ_ASSERT(!animation->GetTimeline());
animationsToRemove.AppendElement(animation);
continue;
}
needsTicks |= animation->NeedsTicks();
// Even if |animation| doesn't need future ticks, we should still
// Tick it this time around since it might just need a one-off tick in
// order to dispatch events.
animation->Tick();
if (!animation->IsRelevant() && !animation->NeedsTicks()) {
animationsToRemove.AppendElement(animation);
}
}
for (Animation* animation : animationsToRemove) {
RemoveAnimation(animation);
}
if (!needsTicks) {
// We already assert that GetRefreshDriver() is non-null at the beginning
// of this function but we check it again here to be sure that ticking
// animations does not have any side effects that cause us to lose the
// connection with the refresh driver, such as triggering the destruction
// of mDocument's PresShell.
MOZ_ASSERT(GetRefreshDriver(),
"Refresh driver should still be valid at end of WillRefresh");
UnregisterFromRefreshDriver();
}
}
void
DocumentTimeline::NotifyRefreshDriverCreated(nsRefreshDriver* aDriver)
{
MOZ_ASSERT(!mIsObservingRefreshDriver,
"Timeline should not be observing the refresh driver before"
" it is created");
if (!mAnimationOrder.isEmpty()) {
MOZ_ASSERT(isInList(),
"We should not register with the refresh driver if we are not"
" in the document's list of timelines");
aDriver->AddRefreshObserver(this, Flush_Style);
mIsObservingRefreshDriver = true;
}
}
void
DocumentTimeline::NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver)
{
if (!mIsObservingRefreshDriver) {
return;
}
aDriver->RemoveRefreshObserver(this, Flush_Style);
mIsObservingRefreshDriver = false;
}
void
DocumentTimeline::RemoveAnimation(Animation* aAnimation)
{
AnimationTimeline::RemoveAnimation(aAnimation);
if (mIsObservingRefreshDriver && mAnimations.IsEmpty()) {
UnregisterFromRefreshDriver();
}
}
TimeStamp
DocumentTimeline::ToTimeStamp(const TimeDuration& aTimeDuration) const
{
TimeStamp result;
RefPtr<nsDOMNavigationTiming> timing = mDocument->GetNavigationTiming();
if (MOZ_UNLIKELY(!timing)) {
return result;
}
result =
timing->GetNavigationStartTimeStamp() + (aTimeDuration + mOriginTime);
return result;
}
nsRefreshDriver*
DocumentTimeline::GetRefreshDriver() const
{
nsIPresShell* presShell = mDocument->GetShell();
if (MOZ_UNLIKELY(!presShell)) {
return nullptr;
}
nsPresContext* presContext = presShell->GetPresContext();
if (MOZ_UNLIKELY(!presContext)) {
return nullptr;
}
return presContext->RefreshDriver();
}
void
DocumentTimeline::UnregisterFromRefreshDriver()
{
if (!mIsObservingRefreshDriver) {
return;
}
nsRefreshDriver* refreshDriver = GetRefreshDriver();
if (!refreshDriver) {
return;
}
refreshDriver->RemoveRefreshObserver(this, Flush_Style);
mIsObservingRefreshDriver = false;
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,111 @@
/* -*- 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/. */
#ifndef mozilla_dom_DocumentTimeline_h
#define mozilla_dom_DocumentTimeline_h
#include "mozilla/dom/DocumentTimelineBinding.h"
#include "mozilla/LinkedList.h"
#include "mozilla/TimeStamp.h"
#include "AnimationTimeline.h"
#include "nsIDocument.h"
#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
#include "nsRefreshDriver.h"
struct JSContext;
// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
// GetTickCount().
#ifdef GetCurrentTime
#undef GetCurrentTime
#endif
namespace mozilla {
namespace dom {
class DocumentTimeline final
: public AnimationTimeline
, public nsARefreshObserver
, public LinkedListElement<DocumentTimeline>
{
public:
DocumentTimeline(nsIDocument* aDocument, const TimeDuration& aOriginTime)
: AnimationTimeline(aDocument->GetParentObject())
, mDocument(aDocument)
, mIsObservingRefreshDriver(false)
, mOriginTime(aOriginTime)
{
if (mDocument) {
mDocument->Timelines().insertBack(this);
}
}
protected:
virtual ~DocumentTimeline()
{
MOZ_ASSERT(!mIsObservingRefreshDriver, "Timeline should have disassociated"
" from the refresh driver before being destroyed");
if (isInList()) {
remove();
}
}
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(DocumentTimeline,
AnimationTimeline)
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
static already_AddRefed<DocumentTimeline>
Constructor(const GlobalObject& aGlobal,
const DocumentTimelineOptions& aOptions,
ErrorResult& aRv);
// AnimationTimeline methods
virtual Nullable<TimeDuration> GetCurrentTime() const override;
bool TracksWallclockTime() const override
{
nsRefreshDriver* refreshDriver = GetRefreshDriver();
return !refreshDriver ||
!refreshDriver->IsTestControllingRefreshesEnabled();
}
Nullable<TimeDuration> ToTimelineTime(const TimeStamp& aTimeStamp) const
override;
TimeStamp ToTimeStamp(const TimeDuration& aTimelineTime) const override;
void NotifyAnimationUpdated(Animation& aAnimation) override;
void RemoveAnimation(Animation* aAnimation) override;
// nsARefreshObserver methods
void WillRefresh(TimeStamp aTime) override;
void NotifyRefreshDriverCreated(nsRefreshDriver* aDriver);
void NotifyRefreshDriverDestroying(nsRefreshDriver* aDriver);
protected:
TimeStamp GetCurrentTimeStamp() const;
nsRefreshDriver* GetRefreshDriver() const;
void UnregisterFromRefreshDriver();
nsCOMPtr<nsIDocument> mDocument;
// The most recently used refresh driver time. This is used in cases where
// we don't have a refresh driver (e.g. because we are in a display:none
// iframe).
mutable TimeStamp mLastRefreshDriverTime;
bool mIsObservingRefreshDriver;
TimeDuration mOriginTime;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_DocumentTimeline_h

View file

@ -0,0 +1,920 @@
/* -*- 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 "EffectCompositor.h"
#include "mozilla/dom/Animation.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/KeyframeEffectReadOnly.h"
#include "mozilla/AnimationComparator.h"
#include "mozilla/AnimationPerformanceWarning.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/EffectSet.h"
#include "mozilla/LayerAnimationInfo.h"
#include "mozilla/RestyleManagerHandle.h"
#include "mozilla/RestyleManagerHandleInlines.h"
#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetPresShellForContent
#include "nsCSSPropertyIDSet.h"
#include "nsCSSProps.h"
#include "nsIPresShell.h"
#include "nsLayoutUtils.h"
#include "nsRuleNode.h" // For nsRuleNode::ComputePropertiesOverridingAnimation
#include "nsRuleProcessorData.h" // For ElementRuleProcessorData etc.
#include "nsTArray.h"
#include <bitset>
#include <initializer_list>
using mozilla::dom::Animation;
using mozilla::dom::Element;
using mozilla::dom::KeyframeEffectReadOnly;
namespace mozilla {
NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor)
for (auto& elementSet : tmp->mElementsToRestyle) {
elementSet.Clear();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EffectCompositor)
for (auto& elementSet : tmp->mElementsToRestyle) {
for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
CycleCollectionNoteChild(cb, iter.Key().mElement,
"EffectCompositor::mElementsToRestyle[]",
cb.Flags());
}
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(EffectCompositor, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(EffectCompositor, Release)
// Helper function to factor out the common logic from
// GetAnimationsForCompositor and HasAnimationsForCompositor.
//
// Takes an optional array to fill with eligible animations.
//
// Returns true if there are eligible animations, false otherwise.
bool
FindAnimationsForCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty,
nsTArray<RefPtr<dom::Animation>>* aMatches /*out*/)
{
MOZ_ASSERT(!aMatches || aMatches->IsEmpty(),
"Matches array, if provided, should be empty");
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
if (!effects || effects->IsEmpty()) {
return false;
}
// If the property will be added to the animations level of the cascade but
// there is an !important rule for that property in the cascade then the
// animation will not be applied since the !important rule overrides it.
if (effects->PropertiesWithImportantRules().HasProperty(aProperty) &&
effects->PropertiesForAnimationsLevel().HasProperty(aProperty)) {
return false;
}
if (aFrame->RefusedAsyncAnimation()) {
return false;
}
// The animation cascade will almost always be up-to-date by this point
// but there are some cases such as when we are restoring the refresh driver
// from test control after seeking where it might not be the case.
//
// Those cases are probably not important but just to be safe, let's make
// sure the cascade is up to date since if it *is* up to date, this is
// basically a no-op.
Maybe<NonOwningAnimationTarget> pseudoElement =
EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame);
if (pseudoElement) {
EffectCompositor::MaybeUpdateCascadeResults(pseudoElement->mElement,
pseudoElement->mPseudoType,
aFrame->StyleContext());
}
if (!nsLayoutUtils::AreAsyncAnimationsEnabled()) {
if (nsLayoutUtils::IsAnimationLoggingEnabled()) {
nsCString message;
message.AppendLiteral("Performance warning: Async animations are "
"disabled");
AnimationUtils::LogAsyncAnimationFailure(message);
}
return false;
}
// Disable async animations if we have a rendering observer that
// depends on our content (svg masking, -moz-element etc) so that
// it gets updated correctly.
nsIContent* content = aFrame->GetContent();
while (content) {
if (content->HasRenderingObservers()) {
EffectCompositor::SetPerformanceWarning(
aFrame, aProperty,
AnimationPerformanceWarning(
AnimationPerformanceWarning::Type::HasRenderingObserver));
return false;
}
content = content->GetParent();
}
bool foundSome = false;
for (KeyframeEffectReadOnly* effect : *effects) {
MOZ_ASSERT(effect && effect->GetAnimation());
Animation* animation = effect->GetAnimation();
if (!animation->IsPlayableOnCompositor()) {
continue;
}
AnimationPerformanceWarning::Type warningType;
if (aProperty == eCSSProperty_transform &&
effect->ShouldBlockAsyncTransformAnimations(aFrame,
warningType)) {
if (aMatches) {
aMatches->Clear();
}
EffectCompositor::SetPerformanceWarning(
aFrame, aProperty,
AnimationPerformanceWarning(warningType));
return false;
}
if (!effect->HasEffectiveAnimationOfProperty(aProperty)) {
continue;
}
if (aMatches) {
aMatches->AppendElement(animation);
}
foundSome = true;
}
MOZ_ASSERT(!foundSome || !aMatches || !aMatches->IsEmpty(),
"If return value is true, matches array should be non-empty");
if (aMatches && foundSome) {
aMatches->Sort(AnimationPtrComparator<RefPtr<dom::Animation>>());
}
return foundSome;
}
void
EffectCompositor::RequestRestyle(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
RestyleType aRestyleType,
CascadeLevel aCascadeLevel)
{
if (!mPresContext) {
// Pres context will be null after the effect compositor is disconnected.
return;
}
// Ignore animations on orphaned elements.
if (!aElement->IsInComposedDoc()) {
return;
}
auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
if (aRestyleType == RestyleType::Throttled) {
if (!elementsToRestyle.Contains(key)) {
elementsToRestyle.Put(key, false);
}
mPresContext->Document()->SetNeedStyleFlush();
} else {
// Get() returns 0 if the element is not found. It will also return
// false if the element is found but does not have a pending restyle.
bool hasPendingRestyle = elementsToRestyle.Get(key);
if (!hasPendingRestyle) {
PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel);
}
elementsToRestyle.Put(key, true);
}
if (aRestyleType == RestyleType::Layer) {
// Prompt layers to re-sync their animations.
MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
"stylo: Servo-backed style system should not be using "
"EffectCompositor");
mPresContext->RestyleManager()->AsGecko()->IncrementAnimationGeneration();
EffectSet* effectSet =
EffectSet::GetEffectSet(aElement, aPseudoType);
if (effectSet) {
effectSet->UpdateAnimationGeneration(mPresContext);
}
}
}
void
EffectCompositor::PostRestyleForAnimation(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel)
{
if (!mPresContext) {
return;
}
dom::Element* element = GetElementToRestyle(aElement, aPseudoType);
if (!element) {
return;
}
nsRestyleHint hint = aCascadeLevel == CascadeLevel::Transitions ?
eRestyle_CSSTransitions :
eRestyle_CSSAnimations;
mPresContext->PresShell()->RestyleForAnimation(element, hint);
}
void
EffectCompositor::PostRestyleForThrottledAnimations()
{
for (size_t i = 0; i < kCascadeLevelCount; i++) {
CascadeLevel cascadeLevel = CascadeLevel(i);
auto& elementSet = mElementsToRestyle[cascadeLevel];
for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
bool& postedRestyle = iter.Data();
if (postedRestyle) {
continue;
}
PostRestyleForAnimation(iter.Key().mElement,
iter.Key().mPseudoType,
cascadeLevel);
postedRestyle = true;
}
}
}
void
EffectCompositor::UpdateEffectProperties(nsStyleContext* aStyleContext,
dom::Element* aElement,
CSSPseudoElementType aPseudoType)
{
EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
if (!effectSet) {
return;
}
// Style context change might cause CSS cascade level,
// e.g removing !important, so we should update the cascading result.
effectSet->MarkCascadeNeedsUpdate();
for (KeyframeEffectReadOnly* effect : *effectSet) {
effect->UpdateProperties(aStyleContext);
}
}
void
EffectCompositor::MaybeUpdateAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
nsStyleContext* aStyleContext)
{
// First update cascade results since that may cause some elements to
// be marked as needing a restyle.
MaybeUpdateCascadeResults(aElement, aPseudoType, aStyleContext);
auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
if (!mPresContext || !elementsToRestyle.Contains(key)) {
return;
}
ComposeAnimationRule(aElement, aPseudoType, aCascadeLevel,
mPresContext->RefreshDriver()->MostRecentRefresh());
elementsToRestyle.Remove(key);
}
nsIStyleRule*
EffectCompositor::GetAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
nsStyleContext* aStyleContext)
{
// NOTE: We need to be careful about early returns in this method where
// we *don't* update mElementsToRestyle. When we get a call to
// RequestRestyle that results in a call to PostRestyleForAnimation, we
// will set a bool flag in mElementsToRestyle indicating that we've
// called PostRestyleForAnimation so we don't need to call it again
// until that restyle happens. During that restyle, if we arrive here
// and *don't* update mElementsToRestyle we'll continue to skip calling
// PostRestyleForAnimation from RequestRestyle.
if (!mPresContext || !mPresContext->IsDynamic()) {
// For print or print preview, ignore animations.
return nullptr;
}
MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
"stylo: Servo-backed style system should not be using "
"EffectCompositor");
if (mPresContext->RestyleManager()->AsGecko()->SkipAnimationRules()) {
// We don't need to worry about updating mElementsToRestyle in this case
// since this is not the animation restyle we requested when we called
// PostRestyleForAnimation (see comment at start of this method).
return nullptr;
}
MaybeUpdateAnimationRule(aElement, aPseudoType, aCascadeLevel, aStyleContext);
#ifdef DEBUG
{
auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
PseudoElementHashEntry::KeyType key = { aElement, aPseudoType };
MOZ_ASSERT(!elementsToRestyle.Contains(key),
"Element should no longer require a restyle after its "
"animation rule has been updated");
}
#endif
EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
if (!effectSet) {
return nullptr;
}
return effectSet->AnimationRule(aCascadeLevel);
}
/* static */ dom::Element*
EffectCompositor::GetElementToRestyle(dom::Element* aElement,
CSSPseudoElementType aPseudoType)
{
if (aPseudoType == CSSPseudoElementType::NotPseudo) {
return aElement;
}
nsIFrame* primaryFrame = aElement->GetPrimaryFrame();
if (!primaryFrame) {
return nullptr;
}
nsIFrame* pseudoFrame;
if (aPseudoType == CSSPseudoElementType::before) {
pseudoFrame = nsLayoutUtils::GetBeforeFrame(primaryFrame);
} else if (aPseudoType == CSSPseudoElementType::after) {
pseudoFrame = nsLayoutUtils::GetAfterFrame(primaryFrame);
} else {
NS_NOTREACHED("Should not try to get the element to restyle for a pseudo "
"other that :before or :after");
return nullptr;
}
if (!pseudoFrame) {
return nullptr;
}
return pseudoFrame->GetContent()->AsElement();
}
bool
EffectCompositor::HasPendingStyleUpdates() const
{
for (auto& elementSet : mElementsToRestyle) {
if (elementSet.Count()) {
return true;
}
}
return false;
}
bool
EffectCompositor::HasThrottledStyleUpdates() const
{
for (auto& elementSet : mElementsToRestyle) {
for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) {
if (!iter.Data()) {
return true;
}
}
}
return false;
}
void
EffectCompositor::AddStyleUpdatesTo(RestyleTracker& aTracker)
{
if (!mPresContext) {
return;
}
for (size_t i = 0; i < kCascadeLevelCount; i++) {
CascadeLevel cascadeLevel = CascadeLevel(i);
auto& elementSet = mElementsToRestyle[cascadeLevel];
// Copy the list of elements to restyle to a separate array that we can
// iterate over. This is because we need to call MaybeUpdateCascadeResults
// on each element, but doing that can mutate elementSet. In this case
// it will only mutate the bool value associated with each element in the
// set but even doing that will cause assertions in PLDHashTable to fail
// if we are iterating over the hashtable at the same time.
nsTArray<PseudoElementHashEntry::KeyType> elementsToRestyle(
elementSet.Count());
for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
// Skip animations on elements that have been orphaned since they
// requested a restyle.
if (iter.Key().mElement->IsInComposedDoc()) {
elementsToRestyle.AppendElement(iter.Key());
}
}
for (auto& pseudoElem : elementsToRestyle) {
MaybeUpdateCascadeResults(pseudoElem.mElement,
pseudoElem.mPseudoType,
nullptr);
ComposeAnimationRule(pseudoElem.mElement,
pseudoElem.mPseudoType,
cascadeLevel,
mPresContext->RefreshDriver()->MostRecentRefresh());
dom::Element* elementToRestyle =
GetElementToRestyle(pseudoElem.mElement, pseudoElem.mPseudoType);
if (elementToRestyle) {
nsRestyleHint rshint = cascadeLevel == CascadeLevel::Transitions ?
eRestyle_CSSTransitions :
eRestyle_CSSAnimations;
aTracker.AddPendingRestyle(elementToRestyle, rshint, nsChangeHint(0));
}
}
elementSet.Clear();
// Note: mElement pointers in elementsToRestyle might now dangle
}
}
/* static */ bool
EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty)
{
return FindAnimationsForCompositor(aFrame, aProperty, nullptr);
}
/* static */ nsTArray<RefPtr<dom::Animation>>
EffectCompositor::GetAnimationsForCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty)
{
nsTArray<RefPtr<dom::Animation>> result;
#ifdef DEBUG
bool foundSome =
#endif
FindAnimationsForCompositor(aFrame, aProperty, &result);
MOZ_ASSERT(!foundSome || !result.IsEmpty(),
"If return value is true, matches array should be non-empty");
return result;
}
/* static */ void
EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame *aFrame,
nsCSSPropertyID aProperty)
{
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
if (!effects) {
return;
}
for (KeyframeEffectReadOnly* effect : *effects) {
effect->SetIsRunningOnCompositor(aProperty, false);
}
}
/* static */ void
EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext)
{
EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
if (!effects || !effects->CascadeNeedsUpdate()) {
return;
}
nsStyleContext* styleContext = aStyleContext;
if (!styleContext) {
dom::Element* elementToRestyle = GetElementToRestyle(aElement, aPseudoType);
if (elementToRestyle) {
nsIFrame* frame = elementToRestyle->GetPrimaryFrame();
if (frame) {
styleContext = frame->StyleContext();
}
}
}
UpdateCascadeResults(*effects, aElement, aPseudoType, styleContext);
MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state");
}
namespace {
class EffectCompositeOrderComparator {
public:
bool Equals(const KeyframeEffectReadOnly* a,
const KeyframeEffectReadOnly* b) const
{
return a == b;
}
bool LessThan(const KeyframeEffectReadOnly* a,
const KeyframeEffectReadOnly* b) const
{
MOZ_ASSERT(a->GetAnimation() && b->GetAnimation());
MOZ_ASSERT(
Equals(a, b) ||
a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) !=
b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation()));
return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation());
}
};
}
/* static */ void
EffectCompositor::UpdateCascadeResults(Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext)
{
EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
if (!effects) {
return;
}
UpdateCascadeResults(*effects, aElement, aPseudoType, aStyleContext);
}
/* static */ Maybe<NonOwningAnimationTarget>
EffectCompositor::GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame)
{
// Always return the same object to benefit from return-value optimization.
Maybe<NonOwningAnimationTarget> result;
CSSPseudoElementType pseudoType =
aFrame->StyleContext()->GetPseudoType();
if (pseudoType != CSSPseudoElementType::NotPseudo &&
pseudoType != CSSPseudoElementType::before &&
pseudoType != CSSPseudoElementType::after) {
return result;
}
nsIContent* content = aFrame->GetContent();
if (!content) {
return result;
}
if (pseudoType == CSSPseudoElementType::before ||
pseudoType == CSSPseudoElementType::after) {
content = content->GetParent();
if (!content) {
return result;
}
}
if (!content->IsElement()) {
return result;
}
result.emplace(content->AsElement(), pseudoType);
return result;
}
/* static */ void
EffectCompositor::ComposeAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
TimeStamp aRefreshTime)
{
EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
if (!effects) {
return;
}
// The caller is responsible for calling MaybeUpdateCascadeResults first.
MOZ_ASSERT(!effects->CascadeNeedsUpdate(),
"Animation cascade out of date when composing animation rule");
// Get a list of effects sorted by composite order.
nsTArray<KeyframeEffectReadOnly*> sortedEffectList(effects->Count());
for (KeyframeEffectReadOnly* effect : *effects) {
sortedEffectList.AppendElement(effect);
}
sortedEffectList.Sort(EffectCompositeOrderComparator());
RefPtr<AnimValuesStyleRule>& animationRule =
effects->AnimationRule(aCascadeLevel);
animationRule = nullptr;
// If multiple animations affect the same property, animations with higher
// composite order (priority) override or add or animations with lower
// priority except properties in propertiesToSkip.
const nsCSSPropertyIDSet& propertiesToSkip =
aCascadeLevel == CascadeLevel::Animations
? effects->PropertiesForAnimationsLevel().Invert()
: effects->PropertiesForAnimationsLevel();
for (KeyframeEffectReadOnly* effect : sortedEffectList) {
effect->GetAnimation()->ComposeStyle(animationRule, propertiesToSkip);
}
MOZ_ASSERT(effects == EffectSet::GetEffectSet(aElement, aPseudoType),
"EffectSet should not change while composing style");
effects->UpdateAnimationRuleRefreshTime(aCascadeLevel, aRefreshTime);
}
/* static */ void
EffectCompositor::GetOverriddenProperties(nsStyleContext* aStyleContext,
EffectSet& aEffectSet,
nsCSSPropertyIDSet&
aPropertiesOverridden)
{
AutoTArray<nsCSSPropertyID, LayerAnimationInfo::kRecords> propertiesToTrack;
{
nsCSSPropertyIDSet propertiesToTrackAsSet;
for (KeyframeEffectReadOnly* effect : aEffectSet) {
for (const AnimationProperty& property : effect->Properties()) {
if (nsCSSProps::PropHasFlags(property.mProperty,
CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) &&
!propertiesToTrackAsSet.HasProperty(property.mProperty)) {
propertiesToTrackAsSet.AddProperty(property.mProperty);
propertiesToTrack.AppendElement(property.mProperty);
}
}
// Skip iterating over the rest of the effects if we've already
// found all the compositor-animatable properties.
if (propertiesToTrack.Length() == LayerAnimationInfo::kRecords) {
break;
}
}
}
if (propertiesToTrack.IsEmpty()) {
return;
}
nsRuleNode::ComputePropertiesOverridingAnimation(propertiesToTrack,
aStyleContext,
aPropertiesOverridden);
}
/* static */ void
EffectCompositor::UpdateCascadeResults(EffectSet& aEffectSet,
Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext)
{
MOZ_ASSERT(EffectSet::GetEffectSet(aElement, aPseudoType) == &aEffectSet,
"Effect set should correspond to the specified (pseudo-)element");
if (aEffectSet.IsEmpty()) {
aEffectSet.MarkCascadeUpdated();
return;
}
// Get a list of effects sorted by composite order.
nsTArray<KeyframeEffectReadOnly*> sortedEffectList(aEffectSet.Count());
for (KeyframeEffectReadOnly* effect : aEffectSet) {
sortedEffectList.AppendElement(effect);
}
sortedEffectList.Sort(EffectCompositeOrderComparator());
// Get properties that override the *animations* level of the cascade.
//
// We only do this for properties that we can animate on the compositor
// since we will apply other properties on the main thread where the usual
// cascade applies.
nsCSSPropertyIDSet overriddenProperties;
if (aStyleContext) {
GetOverriddenProperties(aStyleContext, aEffectSet, overriddenProperties);
}
// Returns a bitset the represents which properties from
// LayerAnimationInfo::sRecords are present in |aPropertySet|.
auto compositorPropertiesInSet =
[](nsCSSPropertyIDSet& aPropertySet) ->
std::bitset<LayerAnimationInfo::kRecords> {
std::bitset<LayerAnimationInfo::kRecords> result;
for (size_t i = 0; i < LayerAnimationInfo::kRecords; i++) {
if (aPropertySet.HasProperty(
LayerAnimationInfo::sRecords[i].mProperty)) {
result.set(i);
}
}
return result;
};
nsCSSPropertyIDSet& propertiesWithImportantRules =
aEffectSet.PropertiesWithImportantRules();
nsCSSPropertyIDSet& propertiesForAnimationsLevel =
aEffectSet.PropertiesForAnimationsLevel();
// Record which compositor-animatable properties were originally set so we can
// compare for changes later.
std::bitset<LayerAnimationInfo::kRecords>
prevCompositorPropertiesWithImportantRules =
compositorPropertiesInSet(propertiesWithImportantRules);
std::bitset<LayerAnimationInfo::kRecords>
prevCompositorPropertiesForAnimationsLevel =
compositorPropertiesInSet(propertiesForAnimationsLevel);
propertiesWithImportantRules.Empty();
propertiesForAnimationsLevel.Empty();
bool hasCompositorPropertiesForTransition = false;
for (const KeyframeEffectReadOnly* effect : sortedEffectList) {
MOZ_ASSERT(effect->GetAnimation(),
"Effects on a target element should have an Animation");
CascadeLevel cascadeLevel = effect->GetAnimation()->CascadeLevel();
for (const AnimationProperty& prop : effect->Properties()) {
if (overriddenProperties.HasProperty(prop.mProperty)) {
propertiesWithImportantRules.AddProperty(prop.mProperty);
}
if (cascadeLevel == EffectCompositor::CascadeLevel::Animations) {
propertiesForAnimationsLevel.AddProperty(prop.mProperty);
}
if (nsCSSProps::PropHasFlags(prop.mProperty,
CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR) &&
cascadeLevel == EffectCompositor::CascadeLevel::Transitions) {
hasCompositorPropertiesForTransition = true;
}
}
}
aEffectSet.MarkCascadeUpdated();
nsPresContext* presContext = GetPresContext(aElement);
if (!presContext) {
return;
}
// If properties for compositor are newly overridden by !important rules, or
// released from being overridden by !important rules, we need to update
// layers for animations level because it's a trigger to send animations to
// the compositor or pull animations back from the compositor.
if (prevCompositorPropertiesWithImportantRules !=
compositorPropertiesInSet(propertiesWithImportantRules)) {
presContext->EffectCompositor()->
RequestRestyle(aElement, aPseudoType,
EffectCompositor::RestyleType::Layer,
EffectCompositor::CascadeLevel::Animations);
}
// If we have transition properties for compositor and if the same propery
// for animations level is newly added or removed, we need to update layers
// for transitions level because composite order has been changed now.
if (hasCompositorPropertiesForTransition &&
prevCompositorPropertiesForAnimationsLevel !=
compositorPropertiesInSet(propertiesForAnimationsLevel)) {
presContext->EffectCompositor()->
RequestRestyle(aElement, aPseudoType,
EffectCompositor::RestyleType::Layer,
EffectCompositor::CascadeLevel::Transitions);
}
}
/* static */ nsPresContext*
EffectCompositor::GetPresContext(Element* aElement)
{
MOZ_ASSERT(aElement);
nsIPresShell* shell = nsComputedDOMStyle::GetPresShellForContent(aElement);
if (!shell) {
return nullptr;
}
return shell->GetPresContext();
}
/* static */ void
EffectCompositor::SetPerformanceWarning(
const nsIFrame *aFrame,
nsCSSPropertyID aProperty,
const AnimationPerformanceWarning& aWarning)
{
EffectSet* effects = EffectSet::GetEffectSet(aFrame);
if (!effects) {
return;
}
for (KeyframeEffectReadOnly* effect : *effects) {
effect->SetPerformanceWarning(aProperty, aWarning);
}
}
// ---------------------------------------------------------
//
// Nested class: AnimationStyleRuleProcessor
//
// ---------------------------------------------------------
NS_IMPL_ISUPPORTS(EffectCompositor::AnimationStyleRuleProcessor,
nsIStyleRuleProcessor)
nsRestyleHint
EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle(
StateRuleProcessorData* aData)
{
return nsRestyleHint(0);
}
nsRestyleHint
EffectCompositor::AnimationStyleRuleProcessor::HasStateDependentStyle(
PseudoElementStateRuleProcessorData* aData)
{
return nsRestyleHint(0);
}
bool
EffectCompositor::AnimationStyleRuleProcessor::HasDocumentStateDependentStyle(
StateRuleProcessorData* aData)
{
return false;
}
nsRestyleHint
EffectCompositor::AnimationStyleRuleProcessor::HasAttributeDependentStyle(
AttributeRuleProcessorData* aData,
RestyleHintData& aRestyleHintDataResult)
{
return nsRestyleHint(0);
}
bool
EffectCompositor::AnimationStyleRuleProcessor::MediumFeaturesChanged(
nsPresContext* aPresContext)
{
return false;
}
void
EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
ElementRuleProcessorData* aData)
{
nsIStyleRule *rule =
mCompositor->GetAnimationRule(aData->mElement,
CSSPseudoElementType::NotPseudo,
mCascadeLevel,
nullptr);
if (rule) {
aData->mRuleWalker->Forward(rule);
aData->mRuleWalker->CurrentNode()->SetIsAnimationRule();
}
}
void
EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
PseudoElementRuleProcessorData* aData)
{
if (aData->mPseudoType != CSSPseudoElementType::before &&
aData->mPseudoType != CSSPseudoElementType::after) {
return;
}
nsIStyleRule *rule =
mCompositor->GetAnimationRule(aData->mElement,
aData->mPseudoType,
mCascadeLevel,
nullptr);
if (rule) {
aData->mRuleWalker->Forward(rule);
aData->mRuleWalker->CurrentNode()->SetIsAnimationRule();
}
}
void
EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
AnonBoxRuleProcessorData* aData)
{
}
#ifdef MOZ_XUL
void
EffectCompositor::AnimationStyleRuleProcessor::RulesMatching(
XULTreeRuleProcessorData* aData)
{
}
#endif
size_t
EffectCompositor::AnimationStyleRuleProcessor::SizeOfExcludingThis(
MallocSizeOf aMallocSizeOf) const
{
return 0;
}
size_t
EffectCompositor::AnimationStyleRuleProcessor::SizeOfIncludingThis(
MallocSizeOf aMallocSizeOf) const
{
return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
}
} // namespace mozilla

View file

@ -0,0 +1,307 @@
/* -*- 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/. */
#ifndef mozilla_EffectCompositor_h
#define mozilla_EffectCompositor_h
#include "mozilla/EnumeratedArray.h"
#include "mozilla/Maybe.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/PseudoElementHashEntry.h"
#include "mozilla/RefPtr.h"
#include "nsCSSPropertyID.h"
#include "nsCycleCollectionParticipant.h"
#include "nsDataHashtable.h"
#include "nsIStyleRuleProcessor.h"
#include "nsTArray.h"
class nsCSSPropertyIDSet;
class nsIFrame;
class nsIStyleRule;
class nsPresContext;
class nsStyleContext;
namespace mozilla {
class EffectSet;
class RestyleTracker;
struct AnimationPerformanceWarning;
struct NonOwningAnimationTarget;
namespace dom {
class Animation;
class Element;
}
class EffectCompositor
{
public:
explicit EffectCompositor(nsPresContext* aPresContext)
: mPresContext(aPresContext)
{
for (size_t i = 0; i < kCascadeLevelCount; i++) {
CascadeLevel cascadeLevel = CascadeLevel(i);
mRuleProcessors[cascadeLevel] =
new AnimationStyleRuleProcessor(this, cascadeLevel);
}
}
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor)
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EffectCompositor)
void Disconnect() {
mPresContext = nullptr;
}
// Animations can be applied at two different levels in the CSS cascade:
enum class CascadeLevel {
// The animations sheet (CSS animations, script-generated animations,
// and CSS transitions that are no longer tied to CSS markup)
Animations,
// The transitions sheet (CSS transitions that are tied to CSS markup)
Transitions
};
// We don't define this as part of CascadeLevel as then we'd have to add
// explicit checks for the Count enum value everywhere CascadeLevel is used.
static const size_t kCascadeLevelCount =
static_cast<size_t>(CascadeLevel::Transitions) + 1;
// NOTE: This can return null after Disconnect().
nsPresContext* PresContext() const { return mPresContext; }
enum class RestyleType {
// Animation style has changed but the compositor is applying the same
// change so we might be able to defer updating the main thread until it
// becomes necessary.
Throttled,
// Animation style has changed and needs to be updated on the main thread.
Standard,
// Animation style has changed and needs to be updated on the main thread
// as well as forcing animations on layers to be updated.
// This is needed in cases such as when an animation becomes paused or has
// its playback rate changed. In such cases, although the computed style
// and refresh driver time might not change, we still need to ensure the
// corresponding animations on layers are updated to reflect the new
// configuration of the animation.
Layer
};
// Notifies the compositor that the animation rule for the specified
// (pseudo-)element at the specified cascade level needs to be updated.
// The specified steps taken to update the animation rule depend on
// |aRestyleType| whose values are described above.
void RequestRestyle(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
RestyleType aRestyleType,
CascadeLevel aCascadeLevel);
// Schedule an animation restyle. This is called automatically by
// RequestRestyle when necessary. However, it is exposed here since we also
// need to perform this step when triggering transitions *without* also
// invalidating the animation style rule (which RequestRestyle would do).
void PostRestyleForAnimation(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel);
// Posts an animation restyle for any elements whose animation style rule
// is out of date but for which an animation restyle has not yet been
// posted because updates on the main thread are throttled.
void PostRestyleForThrottledAnimations();
// Called when the style context on the specified (pseudo-) element might
// have changed so that any context-sensitive values stored within
// animation effects (e.g. em-based endpoints used in keyframe effects)
// can be re-resolved to computed values.
void UpdateEffectProperties(nsStyleContext* aStyleContext,
dom::Element* aElement,
CSSPseudoElementType aPseudoType);
// Updates the animation rule stored on the EffectSet for the
// specified (pseudo-)element for cascade level |aLevel|.
// If the animation rule is not marked as needing an update,
// no work is done.
// |aStyleContext| is used for UpdateCascadingResults.
// |aStyleContext| can be nullptr if style context, which is associated with
// the primary frame of the specified (pseudo-)element, is the current style
// context.
// If we are resolving a new style context, we shoud pass the newly created
// style context, otherwise we may use an old style context, it will result
// unexpected cascading results.
void MaybeUpdateAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
nsStyleContext *aStyleContext);
// We need to pass the newly resolved style context as |aStyleContext| when
// we call this function during resolving style context because this function
// calls UpdateCascadingResults with a style context if necessary, at the
// time, we end up using the previous style context if we don't pass the new
// style context.
// When we are not resolving style context, |aStyleContext| can be nullptr, we
// will use a style context associated with the primary frame of the specified
// (pseudo-)element.
nsIStyleRule* GetAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
nsStyleContext* aStyleContext);
bool HasPendingStyleUpdates() const;
bool HasThrottledStyleUpdates() const;
// Tell the restyle tracker about all the animated styles that have
// pending updates so that it can update the animation rule for these
// elements.
void AddStyleUpdatesTo(RestyleTracker& aTracker);
nsIStyleRuleProcessor* RuleProcessor(CascadeLevel aCascadeLevel) const
{
return mRuleProcessors[aCascadeLevel];
}
static bool HasAnimationsForCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty);
static nsTArray<RefPtr<dom::Animation>>
GetAnimationsForCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty);
static void ClearIsRunningOnCompositor(const nsIFrame* aFrame,
nsCSSPropertyID aProperty);
// Update animation cascade results for the specified (pseudo-)element
// but only if we have marked the cascade as needing an update due a
// the change in the set of effects or a change in one of the effects'
// "in effect" state.
// |aStyleContext| may be nullptr in which case we will use the
// nsStyleContext of the primary frame of the specified (pseudo-)element.
//
// This method does NOT detect if other styles that apply above the
// animation level of the cascade have changed.
static void
MaybeUpdateCascadeResults(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext);
// Update the mPropertiesWithImportantRules and
// mPropertiesForAnimationsLevel members of the corresponding EffectSet.
//
// This can be expensive so we should only call it if styles that apply
// above the animation level of the cascade might have changed. For all
// other cases we should call MaybeUpdateCascadeResults.
static void
UpdateCascadeResults(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext);
// Helper to fetch the corresponding element and pseudo-type from a frame.
//
// For frames corresponding to pseudo-elements, the returned element is the
// element on which we store the animations (i.e. the EffectSet and/or
// AnimationCollection), *not* the generated content.
//
// Returns an empty result when a suitable element cannot be found including
// when the frame represents a pseudo-element on which we do not support
// animations.
static Maybe<NonOwningAnimationTarget>
GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame);
// Associates a performance warning with effects on |aFrame| that animates
// |aProperty|.
static void SetPerformanceWarning(
const nsIFrame* aFrame,
nsCSSPropertyID aProperty,
const AnimationPerformanceWarning& aWarning);
private:
~EffectCompositor() = default;
// Rebuilds the animation rule corresponding to |aCascadeLevel| on the
// EffectSet associated with the specified (pseudo-)element.
static void ComposeAnimationRule(dom::Element* aElement,
CSSPseudoElementType aPseudoType,
CascadeLevel aCascadeLevel,
TimeStamp aRefreshTime);
static dom::Element* GetElementToRestyle(dom::Element* aElement,
CSSPseudoElementType
aPseudoType);
// Get the properties in |aEffectSet| that we are able to animate on the
// compositor but which are also specified at a higher level in the cascade
// than the animations level in |aStyleContext|.
static void
GetOverriddenProperties(nsStyleContext* aStyleContext,
EffectSet& aEffectSet,
nsCSSPropertyIDSet& aPropertiesOverridden);
static void
UpdateCascadeResults(EffectSet& aEffectSet,
dom::Element* aElement,
CSSPseudoElementType aPseudoType,
nsStyleContext* aStyleContext);
static nsPresContext* GetPresContext(dom::Element* aElement);
nsPresContext* mPresContext;
// Elements with a pending animation restyle. The associated bool value is
// true if a pending animation restyle has also been dispatched. For
// animations that can be throttled, we will add an entry to the hashtable to
// indicate that the style rule on the element is out of date but without
// posting a restyle to update it.
EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
nsDataHashtable<PseudoElementHashEntry, bool>>
mElementsToRestyle;
class AnimationStyleRuleProcessor final : public nsIStyleRuleProcessor
{
public:
AnimationStyleRuleProcessor(EffectCompositor* aCompositor,
CascadeLevel aCascadeLevel)
: mCompositor(aCompositor)
, mCascadeLevel(aCascadeLevel)
{
MOZ_ASSERT(aCompositor);
}
NS_DECL_ISUPPORTS
// nsIStyleRuleProcessor (parts)
nsRestyleHint HasStateDependentStyle(
StateRuleProcessorData* aData) override;
nsRestyleHint HasStateDependentStyle(
PseudoElementStateRuleProcessorData* aData) override;
bool HasDocumentStateDependentStyle(StateRuleProcessorData* aData) override;
nsRestyleHint HasAttributeDependentStyle(
AttributeRuleProcessorData* aData,
RestyleHintData& aRestyleHintDataResult) override;
bool MediumFeaturesChanged(nsPresContext* aPresContext) override;
void RulesMatching(ElementRuleProcessorData* aData) override;
void RulesMatching(PseudoElementRuleProcessorData* aData) override;
void RulesMatching(AnonBoxRuleProcessorData* aData) override;
#ifdef MOZ_XUL
void RulesMatching(XULTreeRuleProcessorData* aData) override;
#endif
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)
const MOZ_MUST_OVERRIDE override;
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
const MOZ_MUST_OVERRIDE override;
private:
~AnimationStyleRuleProcessor() = default;
EffectCompositor* mCompositor;
CascadeLevel mCascadeLevel;
};
EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
OwningNonNull<AnimationStyleRuleProcessor>>
mRuleProcessors;
};
} // namespace mozilla
#endif // mozilla_EffectCompositor_h

177
dom/animation/EffectSet.cpp Normal file
View file

@ -0,0 +1,177 @@
/* -*- 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 "EffectSet.h"
#include "mozilla/dom/Element.h" // For Element
#include "mozilla/RestyleManagerHandle.h"
#include "mozilla/RestyleManagerHandleInlines.h"
#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
#include "nsCycleCollectionNoteChild.h" // For CycleCollectionNoteChild
#include "nsPresContext.h"
#include "nsLayoutUtils.h"
namespace mozilla {
/* static */ void
EffectSet::PropertyDtor(void* aObject, nsIAtom* aPropertyName,
void* aPropertyValue, void* aData)
{
EffectSet* effectSet = static_cast<EffectSet*>(aPropertyValue);
#ifdef DEBUG
MOZ_ASSERT(!effectSet->mCalledPropertyDtor, "Should not call dtor twice");
effectSet->mCalledPropertyDtor = true;
#endif
delete effectSet;
}
void
EffectSet::Traverse(nsCycleCollectionTraversalCallback& aCallback)
{
for (auto iter = mEffects.Iter(); !iter.Done(); iter.Next()) {
CycleCollectionNoteChild(aCallback, iter.Get()->GetKey(),
"EffectSet::mEffects[]", aCallback.Flags());
}
}
/* static */ EffectSet*
EffectSet::GetEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType)
{
nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
return static_cast<EffectSet*>(aElement->GetProperty(propName));
}
/* static */ EffectSet*
EffectSet::GetEffectSet(const nsIFrame* aFrame)
{
Maybe<NonOwningAnimationTarget> target =
EffectCompositor::GetAnimationElementAndPseudoForFrame(aFrame);
if (!target) {
return nullptr;
}
if (!target->mElement->MayHaveAnimations()) {
return nullptr;
}
return GetEffectSet(target->mElement, target->mPseudoType);
}
/* static */ EffectSet*
EffectSet::GetOrCreateEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType)
{
EffectSet* effectSet = GetEffectSet(aElement, aPseudoType);
if (effectSet) {
return effectSet;
}
nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
effectSet = new EffectSet();
nsresult rv = aElement->SetProperty(propName, effectSet,
&EffectSet::PropertyDtor, true);
if (NS_FAILED(rv)) {
NS_WARNING("SetProperty failed");
// The set must be destroyed via PropertyDtor, otherwise
// mCalledPropertyDtor assertion is triggered in destructor.
EffectSet::PropertyDtor(aElement, propName, effectSet, nullptr);
return nullptr;
}
aElement->SetMayHaveAnimations();
return effectSet;
}
/* static */ void
EffectSet::DestroyEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType)
{
nsIAtom* propName = GetEffectSetPropertyAtom(aPseudoType);
EffectSet* effectSet =
static_cast<EffectSet*>(aElement->GetProperty(propName));
if (!effectSet) {
return;
}
MOZ_ASSERT(!effectSet->IsBeingEnumerated(),
"Should not destroy an effect set while it is being enumerated");
effectSet = nullptr;
aElement->DeleteProperty(propName);
}
void
EffectSet::UpdateAnimationGeneration(nsPresContext* aPresContext)
{
MOZ_ASSERT(aPresContext->RestyleManager()->IsGecko(),
"stylo: Servo-backed style system should not be using "
"EffectSet");
mAnimationGeneration =
aPresContext->RestyleManager()->AsGecko()->GetAnimationGeneration();
}
/* static */ nsIAtom**
EffectSet::GetEffectSetPropertyAtoms()
{
static nsIAtom* effectSetPropertyAtoms[] =
{
nsGkAtoms::animationEffectsProperty,
nsGkAtoms::animationEffectsForBeforeProperty,
nsGkAtoms::animationEffectsForAfterProperty,
nullptr
};
return effectSetPropertyAtoms;
}
/* static */ nsIAtom*
EffectSet::GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType)
{
switch (aPseudoType) {
case CSSPseudoElementType::NotPseudo:
return nsGkAtoms::animationEffectsProperty;
case CSSPseudoElementType::before:
return nsGkAtoms::animationEffectsForBeforeProperty;
case CSSPseudoElementType::after:
return nsGkAtoms::animationEffectsForAfterProperty;
default:
NS_NOTREACHED("Should not try to get animation effects for a pseudo "
"other that :before or :after");
return nullptr;
}
}
void
EffectSet::AddEffect(dom::KeyframeEffectReadOnly& aEffect)
{
if (mEffects.Contains(&aEffect)) {
return;
}
mEffects.PutEntry(&aEffect);
MarkCascadeNeedsUpdate();
}
void
EffectSet::RemoveEffect(dom::KeyframeEffectReadOnly& aEffect)
{
if (!mEffects.Contains(&aEffect)) {
return;
}
mEffects.RemoveEntry(&aEffect);
MarkCascadeNeedsUpdate();
}
} // namespace mozilla

261
dom/animation/EffectSet.h Normal file
View file

@ -0,0 +1,261 @@
/* -*- 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/. */
#ifndef mozilla_EffectSet_h
#define mozilla_EffectSet_h
#include "mozilla/AnimValuesStyleRule.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/EffectCompositor.h"
#include "mozilla/EnumeratedArray.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/KeyframeEffectReadOnly.h"
#include "nsHashKeys.h" // For nsPtrHashKey
#include "nsTHashtable.h" // For nsTHashtable
class nsPresContext;
namespace mozilla {
namespace dom {
class Element;
} // namespace dom
enum class CSSPseudoElementType : uint8_t;
// A wrapper around a hashset of AnimationEffect objects to handle
// storing the set as a property of an element.
class EffectSet
{
public:
EffectSet()
: mCascadeNeedsUpdate(false)
, mAnimationGeneration(0)
#ifdef DEBUG
, mActiveIterators(0)
, mCalledPropertyDtor(false)
#endif
{
MOZ_COUNT_CTOR(EffectSet);
}
~EffectSet()
{
MOZ_ASSERT(mCalledPropertyDtor,
"must call destructor through element property dtor");
MOZ_ASSERT(mActiveIterators == 0,
"Effect set should not be destroyed while it is being "
"enumerated");
MOZ_COUNT_DTOR(EffectSet);
}
static void PropertyDtor(void* aObject, nsIAtom* aPropertyName,
void* aPropertyValue, void* aData);
// Methods for supporting cycle-collection
void Traverse(nsCycleCollectionTraversalCallback& aCallback);
static EffectSet* GetEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType);
static EffectSet* GetEffectSet(const nsIFrame* aFrame);
static EffectSet* GetOrCreateEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType);
static void DestroyEffectSet(dom::Element* aElement,
CSSPseudoElementType aPseudoType);
void AddEffect(dom::KeyframeEffectReadOnly& aEffect);
void RemoveEffect(dom::KeyframeEffectReadOnly& aEffect);
private:
typedef nsTHashtable<nsRefPtrHashKey<dom::KeyframeEffectReadOnly>>
OwningEffectSet;
public:
// A simple iterator to support iterating over the effects in this object in
// range-based for loops.
//
// This allows us to avoid exposing mEffects directly and saves the
// caller from having to dereference hashtable iterators using
// the rather complicated: iter.Get()->GetKey().
class Iterator
{
public:
explicit Iterator(EffectSet& aEffectSet)
: mEffectSet(aEffectSet)
, mHashIterator(mozilla::Move(aEffectSet.mEffects.Iter()))
, mIsEndIterator(false)
{
#ifdef DEBUG
mEffectSet.mActiveIterators++;
#endif
}
Iterator(Iterator&& aOther)
: mEffectSet(aOther.mEffectSet)
, mHashIterator(mozilla::Move(aOther.mHashIterator))
, mIsEndIterator(aOther.mIsEndIterator)
{
#ifdef DEBUG
mEffectSet.mActiveIterators++;
#endif
}
static Iterator EndIterator(EffectSet& aEffectSet)
{
Iterator result(aEffectSet);
result.mIsEndIterator = true;
return result;
}
~Iterator()
{
#ifdef DEBUG
MOZ_ASSERT(mEffectSet.mActiveIterators > 0);
mEffectSet.mActiveIterators--;
#endif
}
bool operator!=(const Iterator& aOther) const {
if (Done() || aOther.Done()) {
return Done() != aOther.Done();
}
return mHashIterator.Get() != aOther.mHashIterator.Get();
}
Iterator& operator++() {
MOZ_ASSERT(!Done());
mHashIterator.Next();
return *this;
}
dom::KeyframeEffectReadOnly* operator* ()
{
MOZ_ASSERT(!Done());
return mHashIterator.Get()->GetKey();
}
private:
Iterator() = delete;
Iterator(const Iterator&) = delete;
Iterator& operator=(const Iterator&) = delete;
Iterator& operator=(const Iterator&&) = delete;
bool Done() const {
return mIsEndIterator || mHashIterator.Done();
}
EffectSet& mEffectSet;
OwningEffectSet::Iterator mHashIterator;
bool mIsEndIterator;
};
friend class Iterator;
Iterator begin() { return Iterator(*this); }
Iterator end() { return Iterator::EndIterator(*this); }
#ifdef DEBUG
bool IsBeingEnumerated() const { return mActiveIterators != 0; }
#endif
bool IsEmpty() const { return mEffects.IsEmpty(); }
size_t Count() const { return mEffects.Count(); }
RefPtr<AnimValuesStyleRule>& AnimationRule(EffectCompositor::CascadeLevel
aCascadeLevel)
{
return mAnimationRule[aCascadeLevel];
}
const TimeStamp& AnimationRuleRefreshTime(EffectCompositor::CascadeLevel
aCascadeLevel) const
{
return mAnimationRuleRefreshTime[aCascadeLevel];
}
void UpdateAnimationRuleRefreshTime(EffectCompositor::CascadeLevel
aCascadeLevel,
const TimeStamp& aRefreshTime)
{
mAnimationRuleRefreshTime[aCascadeLevel] = aRefreshTime;
}
bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; }
void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; }
void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; }
void UpdateAnimationGeneration(nsPresContext* aPresContext);
uint64_t GetAnimationGeneration() const { return mAnimationGeneration; }
static nsIAtom** GetEffectSetPropertyAtoms();
nsCSSPropertyIDSet& PropertiesWithImportantRules()
{
return mPropertiesWithImportantRules;
}
nsCSSPropertyIDSet& PropertiesForAnimationsLevel()
{
return mPropertiesForAnimationsLevel;
}
private:
static nsIAtom* GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType);
OwningEffectSet mEffects;
// These style rules contain the style data for currently animating
// values. They only match when styling with animation. When we
// style without animation, we need to not use them so that we can
// detect any new changes; if necessary we restyle immediately
// afterwards with animation.
EnumeratedArray<EffectCompositor::CascadeLevel,
EffectCompositor::CascadeLevel(
EffectCompositor::kCascadeLevelCount),
RefPtr<AnimValuesStyleRule>> mAnimationRule;
// A parallel array to mAnimationRule that records the refresh driver
// timestamp when the rule was last updated. This is used for certain
// animations which are updated only periodically (e.g. transform animations
// running on the compositor that affect the scrollable overflow region).
EnumeratedArray<EffectCompositor::CascadeLevel,
EffectCompositor::CascadeLevel(
EffectCompositor::kCascadeLevelCount),
TimeStamp> mAnimationRuleRefreshTime;
// Dirty flag to represent when the mPropertiesWithImportantRules and
// mPropertiesForAnimationsLevel on effects in this set might need to be
// updated.
//
// Set to true any time the set of effects is changed or when
// one the effects goes in or out of the "in effect" state.
bool mCascadeNeedsUpdate;
// RestyleManager keeps track of the number of animation restyles.
// 'mini-flushes' (see nsTransitionManager::UpdateAllThrottledStyles()).
// mAnimationGeneration is the sequence number of the last flush where a
// transition/animation changed. We keep a similar count on the
// corresponding layer so we can check that the layer is up to date with
// the animation manager.
uint64_t mAnimationGeneration;
// Specifies the compositor-animatable properties that are overridden by
// !important rules.
nsCSSPropertyIDSet mPropertiesWithImportantRules;
// Specifies the properties for which the result will be added to the
// animations level of the cascade and hence should be skipped when we are
// composing the animation style for the transitions level of the cascede.
nsCSSPropertyIDSet mPropertiesForAnimationsLevel;
#ifdef DEBUG
// Track how many iterators are referencing this effect set when we are
// destroyed, we can assert that nothing is still pointing to us.
uint64_t mActiveIterators;
bool mCalledPropertyDtor;
#endif
};
} // namespace mozilla
#endif // mozilla_EffectSet_h

View file

@ -0,0 +1,211 @@
/* -*- 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 "mozilla/dom/KeyframeEffect.h"
#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
// For UnrestrictedDoubleOrKeyframeAnimationOptions
#include "mozilla/dom/AnimationEffectTiming.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
#include "mozilla/KeyframeUtils.h"
#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
#include "nsIScriptError.h"
namespace mozilla {
namespace dom {
KeyframeEffect::KeyframeEffect(nsIDocument* aDocument,
const Maybe<OwningAnimationTarget>& aTarget,
const TimingParams& aTiming,
const KeyframeEffectParams& aOptions)
: KeyframeEffectReadOnly(aDocument, aTarget,
new AnimationEffectTiming(aDocument, aTiming, this),
aOptions)
{
}
JSObject*
KeyframeEffect::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto)
{
return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto);
}
/* static */ already_AddRefed<KeyframeEffect>
KeyframeEffect::Constructor(
const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
ErrorResult& aRv)
{
return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
aOptions, aRv);
}
/* static */ already_AddRefed<KeyframeEffect>
KeyframeEffect::Constructor(const GlobalObject& aGlobal,
KeyframeEffectReadOnly& aSource,
ErrorResult& aRv)
{
return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aSource, aRv);
}
/* static */ already_AddRefed<KeyframeEffect>
KeyframeEffect::Constructor(
const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
ErrorResult& aRv)
{
return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aKeyframes,
aOptions, aRv);
}
void
KeyframeEffect::NotifySpecifiedTimingUpdated()
{
// Use the same document for a pseudo element and its parent element.
// Use nullptr if we don't have mTarget, so disable the mutation batch.
nsAutoAnimationMutationBatch mb(mTarget ? mTarget->mElement->OwnerDoc()
: nullptr);
if (mAnimation) {
mAnimation->NotifyEffectTimingUpdated();
if (mAnimation->IsRelevant()) {
nsNodeUtils::AnimationChanged(mAnimation);
}
RequestRestyle(EffectCompositor::RestyleType::Layer);
}
}
void
KeyframeEffect::SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget)
{
Maybe<OwningAnimationTarget> newTarget = ConvertTarget(aTarget);
if (mTarget == newTarget) {
// Assign the same target, skip it.
return;
}
if (mTarget) {
UnregisterTarget();
ResetIsRunningOnCompositor();
RequestRestyle(EffectCompositor::RestyleType::Layer);
nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
if (mAnimation) {
nsNodeUtils::AnimationRemoved(mAnimation);
}
}
mTarget = newTarget;
if (mTarget) {
UpdateTargetRegistration();
RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
if (styleContext) {
UpdateProperties(styleContext);
} else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
}
MaybeUpdateFrameForCompositor();
RequestRestyle(EffectCompositor::RestyleType::Layer);
nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
if (mAnimation) {
nsNodeUtils::AnimationAdded(mAnimation);
}
} else if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
// New target is null, so fall back to distribute spacing.
KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
}
}
void
KeyframeEffect::SetIterationComposite(
const IterationCompositeOperation& aIterationComposite)
{
// Ignore iterationComposite if the Web Animations API is not enabled,
// then the default value 'Replace' will be used.
if (!AnimationUtils::IsCoreAPIEnabledForCaller()) {
return;
}
if (mEffectOptions.mIterationComposite == aIterationComposite) {
return;
}
if (mAnimation && mAnimation->IsRelevant()) {
nsNodeUtils::AnimationChanged(mAnimation);
}
mEffectOptions.mIterationComposite = aIterationComposite;
RequestRestyle(EffectCompositor::RestyleType::Layer);
}
void
KeyframeEffect::SetSpacing(JSContext* aCx,
const nsAString& aSpacing,
ErrorResult& aRv)
{
SpacingMode spacingMode = SpacingMode::distribute;
nsCSSPropertyID pacedProperty = eCSSProperty_UNKNOWN;
nsAutoString invalidPacedProperty;
KeyframeEffectParams::ParseSpacing(aSpacing,
spacingMode,
pacedProperty,
invalidPacedProperty,
aRv);
if (aRv.Failed()) {
return;
}
if (!invalidPacedProperty.IsEmpty()) {
const char16_t* params[] = { invalidPacedProperty.get() };
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aCx);
nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
NS_LITERAL_CSTRING("Animation"),
doc,
nsContentUtils::eDOM_PROPERTIES,
"UnanimatablePacedProperty",
params, ArrayLength(params));
}
if (mEffectOptions.mSpacingMode == spacingMode &&
mEffectOptions.mPacedProperty == pacedProperty) {
return;
}
mEffectOptions.mSpacingMode = spacingMode;
mEffectOptions.mPacedProperty = pacedProperty;
// Apply spacing. We apply distribute here. If the new spacing is paced,
// UpdateProperties() will apply it.
if (mEffectOptions.mSpacingMode == SpacingMode::distribute) {
KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
}
if (mAnimation && mAnimation->IsRelevant()) {
nsNodeUtils::AnimationChanged(mAnimation);
}
if (mTarget) {
RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
if (styleContext) {
UpdateProperties(styleContext);
}
}
}
} // namespace dom
} // namespace mozilla

View file

@ -0,0 +1,82 @@
/* -*- 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/. */
#ifndef mozilla_dom_KeyframeEffect_h
#define mozilla_dom_KeyframeEffect_h
#include "nsWrapperCache.h"
#include "mozilla/dom/KeyframeEffectReadOnly.h"
#include "mozilla/AnimationTarget.h" // For (Non)OwningAnimationTarget
#include "mozilla/Maybe.h"
struct JSContext;
class JSObject;
class nsIDocument;
namespace mozilla {
class ErrorResult;
struct KeyframeEffectParams;
struct TimingParams;
namespace dom {
class ElementOrCSSPseudoElement;
class GlobalObject;
class UnrestrictedDoubleOrKeyframeAnimationOptions;
class UnrestrictedDoubleOrKeyframeEffectOptions;
class KeyframeEffect : public KeyframeEffectReadOnly
{
public:
KeyframeEffect(nsIDocument* aDocument,
const Maybe<OwningAnimationTarget>& aTarget,
const TimingParams& aTiming,
const KeyframeEffectParams& aOptions);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
static already_AddRefed<KeyframeEffect>
Constructor(const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
ErrorResult& aRv);
static already_AddRefed<KeyframeEffect>
Constructor(const GlobalObject& aGlobal,
KeyframeEffectReadOnly& aSource,
ErrorResult& aRv);
// Variant of Constructor that accepts a KeyframeAnimationOptions object
// for use with for Animatable.animate.
// Not exposed to content.
static already_AddRefed<KeyframeEffect>
Constructor(const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
ErrorResult& aRv);
void NotifySpecifiedTimingUpdated();
// This method calls GetTargetStyleContext which is not safe to use when
// we are in the middle of updating style. If we need to use this when
// updating style, we should pass the nsStyleContext into this method and use
// that to update the properties rather than calling
// GetStyleContextForElement.
void SetTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
void SetSpacing(JSContext* aCx, const nsAString& aSpacing, ErrorResult& aRv);
void SetIterationComposite(
const IterationCompositeOperation& aIterationComposite);
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_KeyframeEffect_h

View file

@ -0,0 +1,169 @@
/* -*- 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 "mozilla/KeyframeEffectParams.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/KeyframeUtils.h"
#include "mozilla/RangedPtr.h"
#include "nsReadableUtils.h"
namespace mozilla {
static inline bool
IsLetter(char16_t aCh)
{
return (0x41 <= aCh && aCh <= 0x5A) || (0x61 <= aCh && aCh <= 0x7A);
}
static inline bool
IsDigit(char16_t aCh)
{
return 0x30 <= aCh && aCh <= 0x39;
}
static inline bool
IsNameStartCode(char16_t aCh)
{
return IsLetter(aCh) || aCh >= 0x80 || aCh == '_';
}
static inline bool
IsNameCode(char16_t aCh)
{
return IsNameStartCode(aCh) || IsDigit(aCh) || aCh == '-';
}
static inline bool
IsNewLine(char16_t aCh)
{
// 0x0A (LF), 0x0C (FF), 0x0D (CR), or pairs of CR followed by LF are
// replaced by LF.
return aCh == 0x0A || aCh == 0x0C || aCh == 0x0D;
}
static inline bool
IsValidEscape(char16_t aFirst, char16_t aSecond)
{
return aFirst == '\\' && !IsNewLine(aSecond);
}
static bool
IsIdentStart(RangedPtr<const char16_t> aIter,
const char16_t* const aEnd)
{
if (aIter == aEnd) {
return false;
}
if (*aIter == '-') {
if (aIter + 1 == aEnd) {
return false;
}
char16_t second = *(aIter + 1);
return IsNameStartCode(second) ||
second == '-' ||
(aIter + 2 != aEnd && IsValidEscape(second, *(aIter + 2)));
}
return IsNameStartCode(*aIter) ||
(aIter + 1 != aEnd && IsValidEscape(*aIter, *(aIter + 1)));
}
static void
ConsumeIdentToken(RangedPtr<const char16_t>& aIter,
const char16_t* const aEnd,
nsAString& aResult)
{
aResult.Truncate();
// Check if it starts with an identifier.
if (!IsIdentStart(aIter, aEnd)) {
return;
}
// Start to consume.
while (aIter != aEnd) {
if (IsNameCode(*aIter)) {
aResult.Append(*aIter);
} else if (*aIter == '\\') {
const RangedPtr<const char16_t> secondChar = aIter + 1;
if (secondChar == aEnd || !IsValidEscape(*aIter, *secondChar)) {
break;
}
// Consume '\\' and append the character following this '\\'.
++aIter;
aResult.Append(*aIter);
} else {
break;
}
++aIter;
}
}
/* static */ void
KeyframeEffectParams::ParseSpacing(const nsAString& aSpacing,
SpacingMode& aSpacingMode,
nsCSSPropertyID& aPacedProperty,
nsAString& aInvalidPacedProperty,
ErrorResult& aRv)
{
aInvalidPacedProperty.Truncate();
// Ignore spacing if the core API is not enabled since it is not yet ready to
// ship.
if (!AnimationUtils::IsCoreAPIEnabledForCaller()) {
aSpacingMode = SpacingMode::distribute;
return;
}
// Parse spacing.
// distribute | paced({ident})
// https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing
// 1. distribute spacing.
if (aSpacing.EqualsLiteral("distribute")) {
aSpacingMode = SpacingMode::distribute;
return;
}
// 2. paced spacing.
static const nsLiteralString kPacedPrefix = NS_LITERAL_STRING("paced(");
if (!StringBeginsWith(aSpacing, kPacedPrefix)) {
aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
return;
}
RangedPtr<const char16_t> iter(aSpacing.Data() + kPacedPrefix.Length(),
aSpacing.Data(), aSpacing.Length());
const char16_t* const end = aSpacing.EndReading();
nsAutoString identToken;
ConsumeIdentToken(iter, end, identToken);
if (identToken.IsEmpty()) {
aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
return;
}
aPacedProperty =
nsCSSProps::LookupProperty(identToken, CSSEnabledState::eForAllContent);
if (aPacedProperty == eCSSProperty_UNKNOWN ||
aPacedProperty == eCSSPropertyExtra_variable ||
!KeyframeUtils::IsAnimatableProperty(aPacedProperty)) {
aPacedProperty = eCSSProperty_UNKNOWN;
aInvalidPacedProperty = identToken;
}
if (end - iter.get() != 1 || *iter != ')') {
aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
return;
}
aSpacingMode = aPacedProperty == eCSSProperty_UNKNOWN
? SpacingMode::distribute
: SpacingMode::paced;
}
} // namespace mozilla

View file

@ -0,0 +1,68 @@
/* -*- 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/. */
#ifndef mozilla_KeyframeEffectParams_h
#define mozilla_KeyframeEffectParams_h
#include "nsCSSProps.h"
#include "nsString.h"
// X11 has a #define for None
#ifdef None
#undef None
#endif
#include "mozilla/dom/KeyframeEffectBinding.h" // IterationCompositeOperation
namespace mozilla {
class ErrorResult;
enum class SpacingMode
{
distribute,
paced
};
struct KeyframeEffectParams
{
void GetSpacingAsString(nsAString& aSpacing) const
{
if (mSpacingMode == SpacingMode::distribute) {
aSpacing.AssignLiteral("distribute");
} else {
aSpacing.AssignLiteral("paced(");
aSpacing.AppendASCII(nsCSSProps::GetStringValue(mPacedProperty).get());
aSpacing.AppendLiteral(")");
}
}
/**
* Parse spacing string.
*
* @param aSpacing The input spacing string.
* @param [out] aSpacingMode The parsed spacing mode.
* @param [out] aPacedProperty The parsed CSS property if using paced spacing.
* @param [out] aInvalidPacedProperty A string that, if we parsed a string of
* the form 'paced(<ident>)' where <ident>
* is not a recognized animatable property,
* will be set to <ident>.
* @param [out] aRv The error result.
*/
static void ParseSpacing(const nsAString& aSpacing,
SpacingMode& aSpacingMode,
nsCSSPropertyID& aPacedProperty,
nsAString& aInvalidPacedProperty,
ErrorResult& aRv);
dom::IterationCompositeOperation mIterationComposite =
dom::IterationCompositeOperation::Replace;
// FIXME: Bug 1216844: Add CompositeOperation
SpacingMode mSpacingMode = SpacingMode::distribute;
nsCSSPropertyID mPacedProperty = eCSSProperty_UNKNOWN;
};
} // namespace mozilla
#endif // mozilla_KeyframeEffectParams_h

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,439 @@
/* -*- 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/. */
#ifndef mozilla_dom_KeyframeEffectReadOnly_h
#define mozilla_dom_KeyframeEffectReadOnly_h
#include "nsChangeHint.h"
#include "nsCSSPropertyID.h"
#include "nsCSSValue.h"
#include "nsCycleCollectionParticipant.h"
#include "nsTArray.h"
#include "nsWrapperCache.h"
#include "mozilla/AnimationPerformanceWarning.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/Attributes.h"
#include "mozilla/ComputedTimingFunction.h"
#include "mozilla/EffectCompositor.h"
#include "mozilla/KeyframeEffectParams.h"
#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords
#include "mozilla/ServoBindingTypes.h" // RawServoDeclarationBlock and
// associated RefPtrTraits
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/dom/AnimationEffectReadOnly.h"
#include "mozilla/dom/Element.h"
struct JSContext;
class JSObject;
class nsCSSPropertyIDSet;
class nsIContent;
class nsIDocument;
class nsIFrame;
class nsIPresShell;
class nsPresContext;
namespace mozilla {
class AnimValuesStyleRule;
enum class CSSPseudoElementType : uint8_t;
class ErrorResult;
struct TimingParams;
namespace dom {
class ElementOrCSSPseudoElement;
class GlobalObject;
class OwningElementOrCSSPseudoElement;
class UnrestrictedDoubleOrKeyframeAnimationOptions;
class UnrestrictedDoubleOrKeyframeEffectOptions;
enum class IterationCompositeOperation : uint32_t;
enum class CompositeOperation : uint32_t;
struct AnimationPropertyDetails;
}
/**
* A property-value pair specified on a keyframe.
*/
struct PropertyValuePair
{
nsCSSPropertyID mProperty;
// The specified value for the property. For shorthand properties or invalid
// property values, we store the specified property value as a token stream
// (string).
nsCSSValue mValue;
// The specified value when using the Servo backend. However, even when
// using the Servo backend, we still fill in |mValue| in the case where we
// fail to parse the value since we use it to store the original string.
RefPtr<RawServoDeclarationBlock> mServoDeclarationBlock;
bool operator==(const PropertyValuePair&) const;
};
/**
* A single keyframe.
*
* This is the canonical form in which keyframe effects are stored and
* corresponds closely to the type of objects returned via the getKeyframes()
* API.
*
* Before computing an output animation value, however, we flatten these frames
* down to a series of per-property value arrays where we also resolve any
* overlapping shorthands/longhands, convert specified CSS values to computed
* values, etc.
*
* When the target element or style context changes, however, we rebuild these
* per-property arrays from the original list of keyframes objects. As a result,
* these objects represent the master definition of the effect's values.
*/
struct Keyframe
{
Keyframe() = default;
Keyframe(const Keyframe& aOther) = default;
Keyframe(Keyframe&& aOther)
{
*this = Move(aOther);
}
Keyframe& operator=(const Keyframe& aOther) = default;
Keyframe& operator=(Keyframe&& aOther)
{
mOffset = aOther.mOffset;
mComputedOffset = aOther.mComputedOffset;
mTimingFunction = Move(aOther.mTimingFunction);
mPropertyValues = Move(aOther.mPropertyValues);
return *this;
}
Maybe<double> mOffset;
static constexpr double kComputedOffsetNotSet = -1.0;
double mComputedOffset = kComputedOffsetNotSet;
Maybe<ComputedTimingFunction> mTimingFunction; // Nothing() here means
// "linear"
nsTArray<PropertyValuePair> mPropertyValues;
};
struct AnimationPropertySegment
{
float mFromKey, mToKey;
StyleAnimationValue mFromValue, mToValue;
Maybe<ComputedTimingFunction> mTimingFunction;
bool operator==(const AnimationPropertySegment& aOther) const
{
return mFromKey == aOther.mFromKey &&
mToKey == aOther.mToKey &&
mFromValue == aOther.mFromValue &&
mToValue == aOther.mToValue &&
mTimingFunction == aOther.mTimingFunction;
}
bool operator!=(const AnimationPropertySegment& aOther) const
{
return !(*this == aOther);
}
};
struct AnimationProperty
{
nsCSSPropertyID mProperty = eCSSProperty_UNKNOWN;
// If true, the propery is currently being animated on the compositor.
//
// Note that when the owning Animation requests a non-throttled restyle, in
// between calling RequestRestyle on its EffectCompositor and when the
// restyle is performed, this member may temporarily become false even if
// the animation remains on the layer after the restyle.
//
// **NOTE**: This member is not included when comparing AnimationProperty
// objects for equality.
bool mIsRunningOnCompositor = false;
Maybe<AnimationPerformanceWarning> mPerformanceWarning;
InfallibleTArray<AnimationPropertySegment> mSegments;
// The copy constructor/assignment doesn't copy mIsRunningOnCompositor and
// mPerformanceWarning.
AnimationProperty() = default;
AnimationProperty(const AnimationProperty& aOther)
: mProperty(aOther.mProperty), mSegments(aOther.mSegments) { }
AnimationProperty& operator=(const AnimationProperty& aOther)
{
mProperty = aOther.mProperty;
mSegments = aOther.mSegments;
return *this;
}
// NOTE: This operator does *not* compare the mIsRunningOnCompositor member.
// This is because AnimationProperty objects are compared when recreating
// CSS animations to determine if mutation observer change records need to
// be created or not. However, at the point when these objects are compared
// the mIsRunningOnCompositor will not have been set on the new objects so
// we ignore this member to avoid generating spurious change records.
bool operator==(const AnimationProperty& aOther) const
{
return mProperty == aOther.mProperty &&
mSegments == aOther.mSegments;
}
bool operator!=(const AnimationProperty& aOther) const
{
return !(*this == aOther);
}
};
struct ElementPropertyTransition;
namespace dom {
class Animation;
class KeyframeEffectReadOnly : public AnimationEffectReadOnly
{
public:
KeyframeEffectReadOnly(nsIDocument* aDocument,
const Maybe<OwningAnimationTarget>& aTarget,
const TimingParams& aTiming,
const KeyframeEffectParams& aOptions);
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(KeyframeEffectReadOnly,
AnimationEffectReadOnly)
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
KeyframeEffectReadOnly* AsKeyframeEffect() override { return this; }
// KeyframeEffectReadOnly interface
static already_AddRefed<KeyframeEffectReadOnly>
Constructor(const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
ErrorResult& aRv);
static already_AddRefed<KeyframeEffectReadOnly>
Constructor(const GlobalObject& aGlobal,
KeyframeEffectReadOnly& aSource,
ErrorResult& aRv);
void GetTarget(Nullable<OwningElementOrCSSPseudoElement>& aRv) const;
Maybe<NonOwningAnimationTarget> GetTarget() const
{
Maybe<NonOwningAnimationTarget> result;
if (mTarget) {
result.emplace(*mTarget);
}
return result;
}
void GetKeyframes(JSContext*& aCx,
nsTArray<JSObject*>& aResult,
ErrorResult& aRv);
void GetProperties(nsTArray<AnimationPropertyDetails>& aProperties,
ErrorResult& aRv) const;
IterationCompositeOperation IterationComposite() const;
CompositeOperation Composite() const;
void GetSpacing(nsString& aRetVal) const
{
mEffectOptions.GetSpacingAsString(aRetVal);
}
void NotifyAnimationTimingUpdated();
void SetAnimation(Animation* aAnimation) override;
void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
ErrorResult& aRv);
void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
nsStyleContext* aStyleContext);
// Returns true if the effect includes |aProperty| regardless of whether the
// property is overridden by !important rule.
bool HasAnimationOfProperty(nsCSSPropertyID aProperty) const;
// GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
// to a given CSS property if the effect includes the property and the
// property is not overridden by !important rules.
// Also EffectiveAnimationOfProperty returns true under the same condition.
//
// NOTE: We don't currently check for !important rules for properties that
// can't run on the compositor.
bool HasEffectiveAnimationOfProperty(nsCSSPropertyID aProperty) const
{
return GetEffectiveAnimationOfProperty(aProperty) != nullptr;
}
const AnimationProperty* GetEffectiveAnimationOfProperty(
nsCSSPropertyID aProperty) const;
const InfallibleTArray<AnimationProperty>& Properties() const
{
return mProperties;
}
// Update |mProperties| by recalculating from |mKeyframes| using
// |aStyleContext| to resolve specified values.
void UpdateProperties(nsStyleContext* aStyleContext);
// Updates |aStyleRule| with the animation values produced by this
// AnimationEffect for the current time except any properties contained
// in |aPropertiesToSkip|.
void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
const nsCSSPropertyIDSet& aPropertiesToSkip);
// Returns true if at least one property is being animated on compositor.
bool IsRunningOnCompositor() const;
void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
void ResetIsRunningOnCompositor();
// Returns true if this effect, applied to |aFrame|, contains properties
// that mean we shouldn't run transform compositor animations on this element.
//
// For example, if we have an animation of geometric properties like 'left'
// and 'top' on an element, we force all 'transform' animations running at
// the same time on the same element to run on the main thread.
//
// When returning true, |aPerformanceWarning| stores the reason why
// we shouldn't run the transform animations.
bool ShouldBlockAsyncTransformAnimations(
const nsIFrame* aFrame,
AnimationPerformanceWarning::Type& aPerformanceWarning) const;
nsIDocument* GetRenderedDocument() const;
nsPresContext* GetPresContext() const;
nsIPresShell* GetPresShell() const;
// Associates a warning with the animated property on the specified frame
// indicating why, for example, the property could not be animated on the
// compositor. |aParams| and |aParamsLength| are optional parameters which
// will be used to generate a localized message for devtools.
void SetPerformanceWarning(
nsCSSPropertyID aProperty,
const AnimationPerformanceWarning& aWarning);
// Cumulative change hint on each segment for each property.
// This is used for deciding the animation is paint-only.
void CalculateCumulativeChangeHint(nsStyleContext* aStyleContext);
// Returns true if all of animation properties' change hints
// can ignore painting if the animation is not visible.
// See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
// in detail which change hint can be ignored.
bool CanIgnoreIfNotVisible() const;
protected:
KeyframeEffectReadOnly(nsIDocument* aDocument,
const Maybe<OwningAnimationTarget>& aTarget,
AnimationEffectTimingReadOnly* aTiming,
const KeyframeEffectParams& aOptions);
~KeyframeEffectReadOnly() override = default;
static Maybe<OwningAnimationTarget>
ConvertTarget(const Nullable<ElementOrCSSPseudoElement>& aTarget);
template<class KeyframeEffectType, class OptionsType>
static already_AddRefed<KeyframeEffectType>
ConstructKeyframeEffect(const GlobalObject& aGlobal,
const Nullable<ElementOrCSSPseudoElement>& aTarget,
JS::Handle<JSObject*> aKeyframes,
const OptionsType& aOptions,
ErrorResult& aRv);
template<class KeyframeEffectType>
static already_AddRefed<KeyframeEffectType>
ConstructKeyframeEffect(const GlobalObject& aGlobal,
KeyframeEffectReadOnly& aSource,
ErrorResult& aRv);
// Build properties by recalculating from |mKeyframes| using |aStyleContext|
// to resolve specified values. This function also applies paced spacing if
// needed.
nsTArray<AnimationProperty> BuildProperties(nsStyleContext* aStyleContext);
// This effect is registered with its target element so long as:
//
// (a) It has a target element, and
// (b) It is "relevant" (i.e. yet to finish but not idle, or finished but
// filling forwards)
//
// As a result, we need to make sure this gets called whenever anything
// changes with regards to this effects's timing including changes to the
// owning Animation's timing.
void UpdateTargetRegistration();
// Remove the current effect target from its EffectSet.
void UnregisterTarget();
void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
// Update the associated frame state bits so that, if necessary, a stacking
// context will be created and the effect sent to the compositor. We
// typically need to do this when the properties referenced by the keyframe
// have changed, or when the target frame might have changed.
void MaybeUpdateFrameForCompositor();
// Looks up the style context associated with the target element, if any.
// We need to be careful to *not* call this when we are updating the style
// context. That's because calling GetStyleContextForElement when we are in
// the process of building a style context may trigger various forms of
// infinite recursion.
already_AddRefed<nsStyleContext>
GetTargetStyleContext();
// A wrapper for marking cascade update according to the current
// target and its effectSet.
void MarkCascadeNeedsUpdate();
Maybe<OwningAnimationTarget> mTarget;
KeyframeEffectParams mEffectOptions;
// The specified keyframes.
nsTArray<Keyframe> mKeyframes;
// A set of per-property value arrays, derived from |mKeyframes|.
nsTArray<AnimationProperty> mProperties;
// The computed progress last time we composed the style rule. This is
// used to detect when the progress is not changing (e.g. due to a step
// timing function) so we can avoid unnecessary style updates.
Nullable<double> mProgressOnLastCompose;
// The purpose of this value is the same as mProgressOnLastCompose but
// this is used to detect when the current iteration is not changing
// in the case when iterationComposite is accumulate.
uint64_t mCurrentIterationOnLastCompose = 0;
// We need to track when we go to or from being "in effect" since
// we need to re-evaluate the cascade of animations when that changes.
bool mInEffectOnLastAnimationTimingUpdate;
private:
nsChangeHint mCumulativeChangeHint;
nsIFrame* GetAnimationFrame() const;
bool CanThrottle() const;
bool CanThrottleTransformChanges(nsIFrame& aFrame) const;
// Returns true if the computedTiming has changed since the last
// composition.
bool HasComputedTimingChanged() const;
// Returns true unless Gecko limitations prevent performing transform
// animations for |aFrame|. When returning true, the reason for the
// limitation is stored in |aOutPerformanceWarning|.
static bool CanAnimateTransformOnCompositor(
const nsIFrame* aFrame,
AnimationPerformanceWarning::Type& aPerformanceWarning);
static bool IsGeometricProperty(const nsCSSPropertyID aProperty);
static const TimeDuration OverflowRegionRefreshInterval();
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_KeyframeEffectReadOnly_h

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,151 @@
/* -*- 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/. */
#ifndef mozilla_KeyframeUtils_h
#define mozilla_KeyframeUtils_h
#include "nsTArrayForwardDeclare.h" // For nsTArray
#include "js/RootingAPI.h" // For JS::Handle
#include "mozilla/KeyframeEffectParams.h" // SpacingMode
struct JSContext;
class JSObject;
class nsIDocument;
class nsStyleContext;
namespace mozilla {
struct AnimationProperty;
enum class CSSPseudoElementType : uint8_t;
class ErrorResult;
struct Keyframe;
struct PropertyStyleAnimationValuePair;
namespace dom {
class Element;
} // namespace dom
} // namespace mozilla
namespace mozilla {
// Represents the set of property-value pairs on a Keyframe converted to
// computed values.
using ComputedKeyframeValues = nsTArray<PropertyStyleAnimationValuePair>;
/**
* Utility methods for processing keyframes.
*/
class KeyframeUtils
{
public:
/**
* Converts a JS value representing a property-indexed keyframe or a sequence
* of keyframes to an array of Keyframe objects.
*
* @param aCx The JSContext that corresponds to |aFrames|.
* @param aDocument The document to use when parsing CSS properties.
* @param aFrames The JS value, provided as an optional IDL |object?| value,
* that is the keyframe list specification.
* @param aRv (out) Out-param to hold any error returned by this function.
* Must be initially empty.
* @return The set of processed keyframes. If an error occurs, aRv will be
* filled-in with the appropriate error code and an empty array will be
* returned.
*/
static nsTArray<Keyframe>
GetKeyframesFromObject(JSContext* aCx,
nsIDocument* aDocument,
JS::Handle<JSObject*> aFrames,
ErrorResult& aRv);
/**
* Calculate the StyleAnimationValues of properties of each keyframe.
* This involves expanding shorthand properties into longhand properties,
* removing the duplicated properties for each keyframe, and creating an
* array of |property:computed value| pairs for each keyframe.
*
* These computed values are used *both* when computing the final set of
* per-property animation values (see GetAnimationPropertiesFromKeyframes) as
* well when applying paced spacing. By returning these values here, we allow
* the result to be re-used in both operations.
*
* @param aKeyframes The input keyframes.
* @param aElement The context element.
* @param aStyleContext The style context to use when computing values.
* @return The set of ComputedKeyframeValues. The length will be the same as
* aFrames.
*/
static nsTArray<ComputedKeyframeValues>
GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
dom::Element* aElement,
nsStyleContext* aStyleContext);
/**
* Fills in the mComputedOffset member of each keyframe in the given array
* using the specified spacing mode.
*
* https://w3c.github.io/web-animations/#spacing-keyframes
*
* @param aKeyframes The set of keyframes to adjust.
* @param aSpacingMode The spacing mode to apply.
* @param aProperty The paced property. Only used when |aSpacingMode| is
* SpacingMode::paced. In all other cases it is ignored and hence may be
* any value, e.g. eCSSProperty_UNKNOWN.
* @param aComputedValues The set of computed keyframe values as returned by
* GetComputedKeyframeValues. Only used when |aSpacingMode| is
* SpacingMode::paced. In all other cases this parameter is unused and may
* be any value including an empty array.
* @param aStyleContext The style context used for calculating paced spacing
* on transform.
*/
static void ApplySpacing(nsTArray<Keyframe>& aKeyframes,
SpacingMode aSpacingMode,
nsCSSPropertyID aProperty,
nsTArray<ComputedKeyframeValues>& aComputedValues,
nsStyleContext* aStyleContext);
/**
* Wrapper for ApplySpacing to simplify using distribute spacing.
*
* @param aKeyframes The set of keyframes to adjust.
*/
static void ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes);
/**
* Converts an array of Keyframe objects into an array of AnimationProperty
* objects. This involves creating an array of computed values for each
* longhand property and determining the offset and timing function to use
* for each value.
*
* @param aKeyframes The input keyframes.
* @param aComputedValues The computed keyframe values (as returned by
* GetComputedKeyframeValues) used to fill in the individual
* AnimationPropertySegment objects. Although these values could be
* calculated from |aKeyframes|, passing them in as a separate parameter
* allows the result of GetComputedKeyframeValues to be re-used both
* here and in ApplySpacing.
* @param aStyleContext The style context to calculate the style difference.
* @return The set of animation properties. If an error occurs, the returned
* array will be empty.
*/
static nsTArray<AnimationProperty> GetAnimationPropertiesFromKeyframes(
const nsTArray<Keyframe>& aKeyframes,
const nsTArray<ComputedKeyframeValues>& aComputedValues,
nsStyleContext* aStyleContext);
/**
* Check if the property or, for shorthands, one or more of
* its subproperties, is animatable.
*
* @param aProperty The property to check.
* @return true if |aProperty| is animatable.
*/
static bool IsAnimatableProperty(nsCSSPropertyID aProperty);
};
} // namespace mozilla
#endif // mozilla_KeyframeUtils_h

View file

@ -0,0 +1,124 @@
/* -*- 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 "PendingAnimationTracker.h"
#include "mozilla/dom/AnimationTimeline.h"
#include "nsIFrame.h"
#include "nsIPresShell.h"
using namespace mozilla;
namespace mozilla {
NS_IMPL_CYCLE_COLLECTION(PendingAnimationTracker,
mPlayPendingSet,
mPausePendingSet,
mDocument)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PendingAnimationTracker, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PendingAnimationTracker, Release)
void
PendingAnimationTracker::AddPending(dom::Animation& aAnimation,
AnimationSet& aSet)
{
aSet.PutEntry(&aAnimation);
// Schedule a paint. Otherwise animations that don't trigger a paint by
// themselves (e.g. CSS animations with an empty keyframes rule) won't
// start until something else paints.
EnsurePaintIsScheduled();
}
void
PendingAnimationTracker::RemovePending(dom::Animation& aAnimation,
AnimationSet& aSet)
{
aSet.RemoveEntry(&aAnimation);
}
bool
PendingAnimationTracker::IsWaiting(const dom::Animation& aAnimation,
const AnimationSet& aSet) const
{
return aSet.Contains(const_cast<dom::Animation*>(&aAnimation));
}
void
PendingAnimationTracker::TriggerPendingAnimationsOnNextTick(const TimeStamp&
aReadyTime)
{
auto triggerAnimationsAtReadyTime = [aReadyTime](AnimationSet& aAnimationSet)
{
for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) {
dom::Animation* animation = iter.Get()->GetKey();
dom::AnimationTimeline* timeline = animation->GetTimeline();
// If the animation does not have a timeline, just drop it from the map.
// The animation will detect that it is not being tracked and will trigger
// itself on the next tick where it has a timeline.
if (!timeline) {
iter.Remove();
continue;
}
// When the timeline's refresh driver is under test control, its values
// have no correspondance to wallclock times so we shouldn't try to
// convert aReadyTime (which is a wallclock time) to a timeline value.
// Instead, the animation will be started/paused when the refresh driver
// is next advanced since this will trigger a call to
// TriggerPendingAnimationsNow.
if (!timeline->TracksWallclockTime()) {
continue;
}
Nullable<TimeDuration> readyTime = timeline->ToTimelineTime(aReadyTime);
animation->TriggerOnNextTick(readyTime);
iter.Remove();
}
};
triggerAnimationsAtReadyTime(mPlayPendingSet);
triggerAnimationsAtReadyTime(mPausePendingSet);
}
void
PendingAnimationTracker::TriggerPendingAnimationsNow()
{
auto triggerAndClearAnimations = [](AnimationSet& aAnimationSet) {
for (auto iter = aAnimationSet.Iter(); !iter.Done(); iter.Next()) {
iter.Get()->GetKey()->TriggerNow();
}
aAnimationSet.Clear();
};
triggerAndClearAnimations(mPlayPendingSet);
triggerAndClearAnimations(mPausePendingSet);
}
void
PendingAnimationTracker::EnsurePaintIsScheduled()
{
if (!mDocument) {
return;
}
nsIPresShell* presShell = mDocument->GetShell();
if (!presShell) {
return;
}
nsIFrame* rootFrame = presShell->GetRootFrame();
if (!rootFrame) {
return;
}
rootFrame->SchedulePaint();
}
} // namespace mozilla

View file

@ -0,0 +1,84 @@
/* -*- 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/. */
#ifndef mozilla_dom_PendingAnimationTracker_h
#define mozilla_dom_PendingAnimationTracker_h
#include "mozilla/dom/Animation.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIDocument.h"
#include "nsTHashtable.h"
class nsIFrame;
namespace mozilla {
class PendingAnimationTracker final
{
public:
explicit PendingAnimationTracker(nsIDocument* aDocument)
: mDocument(aDocument)
{ }
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PendingAnimationTracker)
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PendingAnimationTracker)
void AddPlayPending(dom::Animation& aAnimation)
{
MOZ_ASSERT(!IsWaitingToPause(aAnimation),
"Animation is already waiting to pause");
AddPending(aAnimation, mPlayPendingSet);
}
void RemovePlayPending(dom::Animation& aAnimation)
{
RemovePending(aAnimation, mPlayPendingSet);
}
bool IsWaitingToPlay(const dom::Animation& aAnimation) const
{
return IsWaiting(aAnimation, mPlayPendingSet);
}
void AddPausePending(dom::Animation& aAnimation)
{
MOZ_ASSERT(!IsWaitingToPlay(aAnimation),
"Animation is already waiting to play");
AddPending(aAnimation, mPausePendingSet);
}
void RemovePausePending(dom::Animation& aAnimation)
{
RemovePending(aAnimation, mPausePendingSet);
}
bool IsWaitingToPause(const dom::Animation& aAnimation) const
{
return IsWaiting(aAnimation, mPausePendingSet);
}
void TriggerPendingAnimationsOnNextTick(const TimeStamp& aReadyTime);
void TriggerPendingAnimationsNow();
bool HasPendingAnimations() const {
return mPlayPendingSet.Count() > 0 || mPausePendingSet.Count() > 0;
}
private:
~PendingAnimationTracker() { }
void EnsurePaintIsScheduled();
typedef nsTHashtable<nsRefPtrHashKey<dom::Animation>> AnimationSet;
void AddPending(dom::Animation& aAnimation, AnimationSet& aSet);
void RemovePending(dom::Animation& aAnimation, AnimationSet& aSet);
bool IsWaiting(const dom::Animation& aAnimation,
const AnimationSet& aSet) const;
AnimationSet mPlayPendingSet;
AnimationSet mPausePendingSet;
nsCOMPtr<nsIDocument> mDocument;
};
} // namespace mozilla
#endif // mozilla_dom_PendingAnimationTracker_h

View file

@ -0,0 +1,58 @@
/* -*- 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/. */
#ifndef mozilla_PseudoElementHashEntry_h
#define mozilla_PseudoElementHashEntry_h
#include "mozilla/dom/Element.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/HashFunctions.h"
#include "PLDHashTable.h"
namespace mozilla {
// A hash entry that uses a RefPtr<dom::Element>, CSSPseudoElementType pair
class PseudoElementHashEntry : public PLDHashEntryHdr
{
public:
typedef NonOwningAnimationTarget KeyType;
typedef const NonOwningAnimationTarget* KeyTypePointer;
explicit PseudoElementHashEntry(KeyTypePointer aKey)
: mElement(aKey->mElement)
, mPseudoType(aKey->mPseudoType) { }
explicit PseudoElementHashEntry(const PseudoElementHashEntry& aCopy)=default;
~PseudoElementHashEntry() = default;
KeyType GetKey() const { return { mElement, mPseudoType }; }
bool KeyEquals(KeyTypePointer aKey) const
{
return mElement == aKey->mElement &&
mPseudoType == aKey->mPseudoType;
}
static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
static PLDHashNumber HashKey(KeyTypePointer aKey)
{
if (!aKey)
return 0;
// Convert the scoped enum into an integer while adding it to hash.
// Note: CSSPseudoElementTypeBase is uint8_t, so we convert it into
// uint8_t directly to avoid including the header.
return mozilla::HashGeneric(aKey->mElement,
static_cast<uint8_t>(aKey->mPseudoType));
}
enum { ALLOW_MEMMOVE = true };
RefPtr<dom::Element> mElement;
CSSPseudoElementType mPseudoType;
};
} // namespace mozilla
#endif // mozilla_PseudoElementHashEntry_h

View file

@ -0,0 +1,182 @@
/* -*- 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 "mozilla/TimingParams.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/dom/AnimatableBinding.h"
#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
#include "nsCSSParser.h" // For nsCSSParser
#include "nsIDocument.h"
#include "nsRuleNode.h"
namespace mozilla {
template <class OptionsType>
static const dom::AnimationEffectTimingProperties&
GetTimingProperties(const OptionsType& aOptions);
template <>
/* static */ const dom::AnimationEffectTimingProperties&
GetTimingProperties(
const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
{
MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
return aOptions.GetAsKeyframeEffectOptions();
}
template <>
/* static */ const dom::AnimationEffectTimingProperties&
GetTimingProperties(
const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
{
MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
return aOptions.GetAsKeyframeAnimationOptions();
}
template <class OptionsType>
static TimingParams
TimingParamsFromOptionsUnion(const OptionsType& aOptions,
nsIDocument* aDocument,
ErrorResult& aRv)
{
TimingParams result;
if (aOptions.IsUnrestrictedDouble()) {
double durationInMs = aOptions.GetAsUnrestrictedDouble();
if (durationInMs >= 0) {
result.mDuration.emplace(
StickyTimeDuration::FromMilliseconds(durationInMs));
} else {
aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
}
} else {
const dom::AnimationEffectTimingProperties& timing =
GetTimingProperties(aOptions);
Maybe<StickyTimeDuration> duration =
TimingParams::ParseDuration(timing.mDuration, aRv);
if (aRv.Failed()) {
return result;
}
TimingParams::ValidateIterationStart(timing.mIterationStart, aRv);
if (aRv.Failed()) {
return result;
}
TimingParams::ValidateIterations(timing.mIterations, aRv);
if (aRv.Failed()) {
return result;
}
Maybe<ComputedTimingFunction> easing =
TimingParams::ParseEasing(timing.mEasing, aDocument, aRv);
if (aRv.Failed()) {
return result;
}
result.mDuration = duration;
result.mDelay = TimeDuration::FromMilliseconds(timing.mDelay);
result.mEndDelay = TimeDuration::FromMilliseconds(timing.mEndDelay);
result.mIterations = timing.mIterations;
result.mIterationStart = timing.mIterationStart;
result.mDirection = timing.mDirection;
result.mFill = timing.mFill;
result.mFunction = easing;
}
return result;
}
/* static */ TimingParams
TimingParams::FromOptionsUnion(
const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
nsIDocument* aDocument,
ErrorResult& aRv)
{
return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv);
}
/* static */ TimingParams
TimingParams::FromOptionsUnion(
const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
nsIDocument* aDocument,
ErrorResult& aRv)
{
return TimingParamsFromOptionsUnion(aOptions, aDocument, aRv);
}
/* static */ Maybe<ComputedTimingFunction>
TimingParams::ParseEasing(const nsAString& aEasing,
nsIDocument* aDocument,
ErrorResult& aRv)
{
MOZ_ASSERT(aDocument);
nsCSSValue value;
nsCSSParser parser;
parser.ParseLonghandProperty(eCSSProperty_animation_timing_function,
aEasing,
aDocument->GetDocumentURI(),
aDocument->GetDocumentURI(),
aDocument->NodePrincipal(),
value);
switch (value.GetUnit()) {
case eCSSUnit_List: {
const nsCSSValueList* list = value.GetListValue();
if (list->mNext) {
// don't support a list of timing functions
break;
}
switch (list->mValue.GetUnit()) {
case eCSSUnit_Enumerated:
// Return Nothing() if "linear" is passed in.
if (list->mValue.GetIntValue() ==
NS_STYLE_TRANSITION_TIMING_FUNCTION_LINEAR) {
return Nothing();
}
MOZ_FALLTHROUGH;
case eCSSUnit_Cubic_Bezier:
case eCSSUnit_Steps: {
nsTimingFunction timingFunction;
nsRuleNode::ComputeTimingFunction(list->mValue, timingFunction);
ComputedTimingFunction computedTimingFunction;
computedTimingFunction.Init(timingFunction);
return Some(computedTimingFunction);
}
default:
MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function list "
"item unit");
break;
}
break;
}
case eCSSUnit_Inherit:
case eCSSUnit_Initial:
case eCSSUnit_Unset:
case eCSSUnit_TokenStream:
case eCSSUnit_Null:
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected animation-timing-function unit");
break;
}
aRv.ThrowTypeError<dom::MSG_INVALID_EASING_ERROR>(aEasing);
return Nothing();
}
bool
TimingParams::operator==(const TimingParams& aOther) const
{
return mDuration == aOther.mDuration &&
mDelay == aOther.mDelay &&
mIterations == aOther.mIterations &&
mIterationStart == aOther.mIterationStart &&
mDirection == aOther.mDirection &&
mFill == aOther.mFill &&
mFunction == aOther.mFunction;
}
} // namespace mozilla

View file

@ -0,0 +1,130 @@
/* -*- 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/. */
#ifndef mozilla_TimingParams_h
#define mozilla_TimingParams_h
#include "nsStringFwd.h"
#include "mozilla/dom/Nullable.h"
#include "mozilla/dom/UnionTypes.h" // For OwningUnrestrictedDoubleOrString
#include "mozilla/ComputedTimingFunction.h"
#include "mozilla/Maybe.h"
#include "mozilla/StickyTimeDuration.h"
#include "mozilla/TimeStamp.h" // for TimeDuration
// X11 has a #define for None
#ifdef None
#undef None
#endif
#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for FillMode
// and PlaybackDirection
class nsIDocument;
namespace mozilla {
namespace dom {
class UnrestrictedDoubleOrKeyframeEffectOptions;
class UnrestrictedDoubleOrKeyframeAnimationOptions;
}
struct TimingParams
{
TimingParams() = default;
static TimingParams FromOptionsUnion(
const dom::UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
nsIDocument* aDocument, ErrorResult& aRv);
static TimingParams FromOptionsUnion(
const dom::UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
nsIDocument* aDocument, ErrorResult& aRv);
// Range-checks and validates an UnrestrictedDoubleOrString or
// OwningUnrestrictedDoubleOrString object and converts to a
// StickyTimeDuration value or Nothing() if aDuration is "auto".
// Caller must check aRv.Failed().
template <class DoubleOrString>
static Maybe<StickyTimeDuration> ParseDuration(DoubleOrString& aDuration,
ErrorResult& aRv)
{
Maybe<StickyTimeDuration> result;
if (aDuration.IsUnrestrictedDouble()) {
double durationInMs = aDuration.GetAsUnrestrictedDouble();
if (durationInMs >= 0) {
result.emplace(StickyTimeDuration::FromMilliseconds(durationInMs));
} else {
aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
NS_LITERAL_STRING("duration"));
}
} else if (!aDuration.GetAsString().EqualsLiteral("auto")) {
aRv.ThrowTypeError<dom::MSG_INVALID_DURATION_ERROR>(
aDuration.GetAsString());
}
return result;
}
static void ValidateIterationStart(double aIterationStart,
ErrorResult& aRv)
{
if (aIterationStart < 0) {
aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
NS_LITERAL_STRING("iterationStart"));
}
}
static void ValidateIterations(double aIterations, ErrorResult& aRv)
{
if (IsNaN(aIterations) || aIterations < 0) {
aRv.ThrowTypeError<dom::MSG_ENFORCE_RANGE_OUT_OF_RANGE>(
NS_LITERAL_STRING("iterations"));
}
}
static Maybe<ComputedTimingFunction> ParseEasing(const nsAString& aEasing,
nsIDocument* aDocument,
ErrorResult& aRv);
// mDuration.isNothing() represents the "auto" value
Maybe<StickyTimeDuration> mDuration;
TimeDuration mDelay; // Initializes to zero
TimeDuration mEndDelay;
double mIterations = 1.0; // Can be NaN, negative, +/-Infinity
double mIterationStart = 0.0;
dom::PlaybackDirection mDirection = dom::PlaybackDirection::Normal;
dom::FillMode mFill = dom::FillMode::Auto;
Maybe<ComputedTimingFunction> mFunction;
// Return the duration of the active interval calculated by duration and
// iteration count.
StickyTimeDuration ActiveDuration() const
{
// If either the iteration duration or iteration count is zero,
// Web Animations says that the active duration is zero. This is to
// ensure that the result is defined when the other argument is Infinity.
static const StickyTimeDuration zeroDuration;
if (!mDuration || *mDuration == zeroDuration || mIterations == 0.0) {
return zeroDuration;
}
return mDuration->MultDouble(mIterations);
}
StickyTimeDuration EndTime() const
{
return std::max(mDelay + ActiveDuration() + mEndDelay,
StickyTimeDuration());
}
bool operator==(const TimingParams& aOther) const;
bool operator!=(const TimingParams& aOther) const
{
return !(*this == aOther);
}
};
} // namespace mozilla
#endif // mozilla_TimingParams_h

67
dom/animation/moz.build Normal file
View file

@ -0,0 +1,67 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
MOCHITEST_MANIFESTS += ['test/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
EXPORTS.mozilla.dom += [
'Animation.h',
'AnimationEffectReadOnly.h',
'AnimationEffectTiming.h',
'AnimationEffectTimingReadOnly.h',
'AnimationTimeline.h',
'CSSPseudoElement.h',
'DocumentTimeline.h',
'KeyframeEffect.h',
'KeyframeEffectReadOnly.h',
]
EXPORTS.mozilla += [
'AnimationComparator.h',
'AnimationPerformanceWarning.h',
'AnimationTarget.h',
'AnimationUtils.h',
'AnimValuesStyleRule.h',
'ComputedTiming.h',
'ComputedTimingFunction.h',
'EffectCompositor.h',
'EffectSet.h',
'KeyframeEffectParams.h',
'KeyframeUtils.h',
'PendingAnimationTracker.h',
'PseudoElementHashEntry.h',
'TimingParams.h',
]
UNIFIED_SOURCES += [
'Animation.cpp',
'AnimationEffectReadOnly.cpp',
'AnimationEffectTiming.cpp',
'AnimationEffectTimingReadOnly.cpp',
'AnimationPerformanceWarning.cpp',
'AnimationTimeline.cpp',
'AnimationUtils.cpp',
'AnimValuesStyleRule.cpp',
'ComputedTimingFunction.cpp',
'CSSPseudoElement.cpp',
'DocumentTimeline.cpp',
'EffectCompositor.cpp',
'EffectSet.cpp',
'KeyframeEffect.cpp',
'KeyframeEffectParams.cpp',
'KeyframeEffectReadOnly.cpp',
'KeyframeUtils.cpp',
'PendingAnimationTracker.cpp',
'TimingParams.cpp',
]
LOCAL_INCLUDES += [
'/dom/base',
'/layout/base',
'/layout/style',
]
FINAL_LIBRARY = 'xul'

View file

@ -0,0 +1,17 @@
[DEFAULT]
support-files =
testcommon.js
../../imptests/testharness.js
../../imptests/testharnessreport.js
!/dom/animation/test/chrome/file_animate_xrays.html
[chrome/test_animate_xrays.html]
# file_animate_xrays.html needs to go in mochitest.ini since it is served
# over HTTP
[chrome/test_animation_observers.html]
[chrome/test_animation_performance_warning.html]
[chrome/test_animation_properties.html]
[chrome/test_generated_content_getAnimations.html]
[chrome/test_observers_for_sync_api.html]
[chrome/test_restyles.html]
[chrome/test_running_on_compositor.html]

View file

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<script>
Element.prototype.animate = function() {
throw 'Called animate() as defined in content document';
}
// Bug 1211783: Use KeyframeEffect (not KeyframeEffectReadOnly) here
for (var obj of [KeyframeEffectReadOnly, Animation]) {
obj = function() {
throw 'Called overridden ' + String(obj) + ' constructor';
};
}
</script>
<body>
<div id="target"></div>
</body>
</html>

View file

@ -0,0 +1,31 @@
<!doctype html>
<head>
<meta charset=utf-8>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
target="_blank">Mozilla Bug 1045994</a>
<div id="log"></div>
<iframe id="iframe"
src="http://example.org/tests/dom/animation/test/chrome/file_animate_xrays.html"></iframe>
<script>
'use strict';
var win = document.getElementById('iframe').contentWindow;
async_test(function(t) {
window.addEventListener('load', t.step_func(function() {
var target = win.document.getElementById('target');
var anim = target.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
// In the x-ray case, the frames object will be given an opaque wrapper
// so it won't be possible to fetch any frames from it.
assert_equals(anim.effect.getKeyframes().length, 0);
t.done();
}));
}, 'Calling animate() across x-rays');
</script>
</body>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,957 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
@keyframes fade {
from { opacity: 1 }
to { opacity: 0 }
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';
// This is used for obtaining localized strings.
var gStringBundle;
W3CTest.runner.requestLongerTimeout(2);
SpecialPowers.pushPrefEnv({ "set": [
["general.useragent.locale", "en-US"],
// Need to set devPixelsPerPx explicitly to gain
// consistent pixel values in warning messages
// regardless of platform DPIs.
["layout.css.devPixelsPerPx", 1],
] },
start);
function compare_property_state(a, b) {
if (a.property > b.property) {
return -1;
} else if (a.property < b.property) {
return 1;
}
if (a.runningOnCompositor != b.runningOnCompositor) {
return a.runningOnCompositor ? 1 : -1;
}
return a.warning > b.warning ? -1 : 1;
}
function assert_animation_property_state_equals(actual, expected) {
assert_equals(actual.length, expected.length, 'Number of properties');
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(sortedActual[i].runningOnCompositor,
sortedExpected[i].runningOnCompositor,
'runningOnCompositor property should match');
if (sortedExpected[i].warning instanceof RegExp) {
assert_regexp_match(sortedActual[i].warning,
sortedExpected[i].warning,
'warning message should match');
} else if (sortedExpected[i].warning) {
assert_equals(sortedActual[i].warning,
gStringBundle.GetStringFromName(sortedExpected[i].warning),
'warning message should match');
}
}
}
// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_property_state_on_compositor(actual, expected) {
assert_equals(actual.length, expected.length);
var sortedActual = actual.sort(compare_property_state);
var sortedExpected = expected.sort(compare_property_state);
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_true(sortedActual[i].runningOnCompositor,
'runningOnCompositor property should be true on ' +
sortedActual[i].property);
assert_not_exists(sortedActual[i], 'warning',
'warning property should not be set');
}
}
var gAnimationsTests = [
{
desc: 'animations on compositor',
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
{
desc: 'animations on main thread',
frames: {
backgroundColor: ['white', 'red']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
}
]
},
{
desc: 'animations on both threads',
frames: {
backgroundColor: ['white', 'red'],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'background-color',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'two animation properties on compositor thread',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
]
},
{
desc: 'opacity on compositor with animation of geometric properties',
frames: {
width: ['100px', '200px'],
opacity: [0, 1]
},
expected: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
}
]
},
];
// Test cases that check results of adding/removing a 'width' property on the
// same animation object.
var gAnimationWithGeometricKeyframeTests = [
{
desc: 'transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
desc: 'opacity and transform',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false
},
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
];
// Performance warning tests that set and clear a style property.
var gPerformanceWarningTestsStyle = [
{
desc: 'preserve-3d transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'transform with backface-visibility:hidden',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
desc: 'opacity and transform with preserve-3d',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'transform-style: preserve-3d',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
desc: 'opacity and transform with backface-visibility:hidden',
frames: {
opacity: [0, 1],
transform: ['translate(0px)', 'translate(100px)']
},
style: 'backface-visibility: hidden;',
expected: [
{
property: 'opacity',
runningOnCompositor: true
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
];
// Performance warning tests that set and clear the id property
var gPerformanceWarningTestsId= [
{
desc: 'moz-element referencing a transform',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
id: 'transformed',
createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningHasRenderingObserver'
}
]
},
];
var gMultipleAsyncAnimationsTests = [
{
desc: 'opacity and transform with preserve-3d',
style: 'transform-style: preserve-3d',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformPreserve3D'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
{
desc: 'opacity and transform with backface-visibility:hidden',
style: 'backface-visibility: hidden;',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
// Test cases that check results of adding/removing a 'width' keyframe on the
// same animation object, where multiple animation objects belong to the same
// element.
// The 'width' property is added to animations[1].
var gMultipleAsyncAnimationsWithGeometricKeyframeTests = [
{
desc: 'transform and opacity with geometric keyframes',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
},
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
}
],
},
{
desc: 'opacity and transform with geometric keyframes',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: {
withoutGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
],
withGeometric: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
},
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: {
withoutGeometric: [
{
property: 'transform',
runningOnCompositor: true
}
],
withGeometric: [
{
property: 'width',
runningOnCompositor: false,
},
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
}
}
]
},
];
// Test cases that check results of adding/removing 'width' animation on the
// same element which has async animations.
var gMultipleAsyncAnimationsWithGeometricAnimationTests = [
{
desc: 'transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
]
},
{
desc: 'opacity',
animations: [
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true
}
]
},
]
},
{
desc: 'opacity and transform',
animations: [
{
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
}
]
},
{
frames: {
opacity: [0, 1]
},
expected: [
{
property: 'opacity',
runningOnCompositor: true,
}
]
}
],
},
];
var gAnimationsOnTooSmallElementTests = [
{
desc: 'opacity on too small element',
frames: {
opacity: [0, 1]
},
style: { style: 'width: 8px; height: 8px; background-color: red;' +
// We need to set transform here to try creating an
// individual frame for this opacity element.
// Without this, this small element is created on the same
// nsIFrame of mochitest iframe, i.e. the document which are
// running this test, as a result the layer corresponding
// to the frame is sent to compositor.
'transform: translateX(100px);' },
expected: [
{
property: 'opacity',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
{
desc: 'transform on too small element',
frames: {
transform: ['translate(0px)', 'translate(100px)']
},
style: { style: 'width: 8px; height: 8px; background-color: red;' },
expected: [
{
property: 'transform',
runningOnCompositor: false,
warning: /Animation cannot be run on the compositor because frame size \(8, 8\) is smaller than \(16, 16\)/
}
]
},
];
function start() {
var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
.getService(SpecialPowers.Ci.nsIStringBundleService);
gStringBundle = bundleService
.createBundle("chrome://global/locale/layout_errors.properties");
gAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gAnimationWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
// First, a transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
}).then(function() {
// Add a 'width' property.
var keyframes = animation.effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withGeometric);
}).then(function() {
// Remove the 'width' property.
var keyframes = animation.effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animation.effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, the transform animation is running on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected.withoutGeometric);
});
}, 'An animation has: ' + subtest.desc);
});
gPerformanceWarningTestsStyle.forEach(function(subtest) {
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = subtest.style;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.style = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gPerformanceWarningTestsId.forEach(function(subtest) {
promise_test(function(t) {
if (subtest.createelement) {
addDiv(t, { style: subtest.createelement });
}
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = subtest.id;
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
animation.effect.target.id = '';
return waitForFrame();
}).then(function() {
assert_property_state_on_compositor(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
gMultipleAsyncAnimationsTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
div.style = subtest.style;
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
div.style = '';
return waitForFrame();
}).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple animations: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricKeyframeTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
return waitForAllAnimations(animations).then(function() {
// First, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
}).then(function() {
// Add a 'width' property to animations[1].
var keyframes = animations[1].effect.getKeyframes();
keyframes[0].width = '100px';
keyframes[1].width = '200px';
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Now the transform animation is not running on compositor because of
// the 'width' property.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withGeometric);
});
}).then(function() {
// Remove the 'width' property from animations[1].
var keyframes = animations[1].effect.getKeyframes();
delete keyframes[0].width;
delete keyframes[1].width;
animations[1].effect.setKeyframes(keyframes);
return waitForFrame();
}).then(function() {
// Finally, all animations are running on compositor.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected.withoutGeometric);
});
});
}, 'Multiple animations with geometric property: ' + subtest.desc);
});
gMultipleAsyncAnimationsWithGeometricAnimationTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable' });
var animations = subtest.animations.map(function(anim) {
var animation = div.animate(anim.frames, 100 * MS_PER_SEC);
// Bind expected values to animation object.
animation.expected = anim.expected;
return animation;
});
var widthAnimation;
return waitForAllAnimations(animations).then(function() {
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
}).then(function() {
// Append 'width' animation on the same element.
widthAnimation = div.animate({ width: ['100px', '200px'] },
100 * MS_PER_SEC);
return waitForFrame();
}).then(function() {
// Now transform animations are not running on compositor because of
// the 'width' animation.
animations.forEach(function(anim) {
assert_animation_property_state_equals(
anim.effect.getProperties(),
anim.expected);
});
// Remove the 'width' animation.
widthAnimation.cancel();
return waitForFrame();
}).then(function() {
// Now all animations are running on compositor.
animations.forEach(function(anim) {
assert_property_state_on_compositor(
anim.effect.getProperties(),
anim.expected);
});
});
}, 'Multiple async animations and geometric animation: ' + subtest.desc);
});
gAnimationsOnTooSmallElementTests.forEach(function(subtest) {
promise_test(function(t) {
var div = addDiv(t, subtest.style);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
subtest.expected);
});
}, subtest.desc);
});
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{ class: 'compositable' },
{ transform: [ 'translate(0px)',
'translate(100px)'] },
100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
animation.effect.target.style = 'width: 10000px; height: 10000px';
return waitForFrame();
}).then(function() {
// viewport depends on test environment.
var expectedWarning = new RegExp(
"Animation cannot be run on the compositor because the frame size " +
"\\(10000, 10000\\) is bigger than the viewport \\(\\d+, \\d+\\) " +
"or the visual rectangle \\(10000, 10000\\) is larger than the " +
"maximum allowed value \\(\\d+\\)");
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: expectedWarning
} ]);
animation.effect.target.style = 'width: 100px; height: 100px';
return waitForFrame();
}).then(function() {
// FIXME: Bug 1253164: the animation should get back on compositor.
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: false } ]);
});
}, 'transform on too big element');
promise_test(function(t) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100');
svg.setAttribute('height', '100');
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', '100');
rect.setAttribute('height', '100');
rect.setAttribute('fill', 'red');
svg.appendChild(rect);
document.body.appendChild(svg);
t.add_cleanup(function() {
svg.remove();
});
var animation = svg.animate(
{ transform: ['translate(0px)', 'translate(100px)'] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
svg.setAttribute('transform', 'translate(10, 20)');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ {
property: 'transform',
runningOnCompositor: false,
warning: 'CompositorAnimationWarningTransformSVG'
} ]);
svg.removeAttribute('transform');
return waitForFrame();
}).then(function() {
assert_animation_property_state_equals(
animation.effect.getProperties(),
[ { property: 'transform', runningOnCompositor: true } ]);
});
}, 'transform of nsIFrame with SVG transform');
promise_test(function(t) {
var div = addDiv(t, { class: 'compositable',
style: 'animation: fade 100s' });
var cssAnimation = div.getAnimations()[0];
var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);
return scriptAnimation.ready.then(function() {
assert_animation_property_state_equals(
cssAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
assert_animation_property_state_equals(
scriptAnimation.effect.getProperties(),
[ { property: 'opacity', runningOnCompositor: true } ]);
});
}, 'overridden animation');
}
</script>
</body>

View file

@ -0,0 +1,993 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1254419 - Test the values returned by
KeyframeEffectReadOnly.getProperties()</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
target="_blank">Mozilla Bug 1254419</a>
<div id="log"></div>
<style>
:root {
--var-100px: 100px;
--var-100px-200px: 100px 200px;
}
div {
font-size: 10px; /* For calculating em-based units */
}
</style>
<script>
'use strict';
function assert_properties_equal(actual, expected) {
assert_equals(actual.length, expected.length);
var compareProperties = (a, b) =>
a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
var sortedActual = actual.sort(compareProperties);
var sortedExpected = expected.sort(compareProperties);
// We want to serialize the values in the following form:
//
// { offset: 0, easing: linear, composite: replace, value: 5px }, ...
//
// So that we can just compare strings and, in the failure case,
// easily see where the differences lie.
var serializeMember = value => {
return typeof value === 'undefined' ? '<not set>' : value;
}
var serializeValues = values =>
values.map(value =>
'{ ' +
[ 'offset', 'value', 'easing', 'composite' ].map(
member => `${member}: ${serializeMember(value[member])}`
).join(', ') +
' }')
.join(', ');
for (var i = 0; i < sortedActual.length; i++) {
assert_equals(sortedActual[i].property,
sortedExpected[i].property,
'CSS property name should match');
assert_equals(serializeValues(sortedActual[i].values),
serializeValues(sortedExpected[i].values),
`Values arrays do not match for `
+ `${sortedActual[i].property} property`);
}
}
// Shorthand for constructing a value object
function value(offset, value, composite, easing) {
return { offset: offset, value: value, easing: easing, composite: composite };
}
var gTests = [
// ---------------------------------------------------------------------
//
// Tests for property-indexed specifications
//
// ---------------------------------------------------------------------
{ desc: 'a one-property two-value property-indexed specification',
frames: { left: ['10px', '20px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ]
},
{ desc: 'a one-shorthand-property two-value property-indexed'
+ ' specification',
frames: { margin: ['10px', '10px 20px 30px 40px'] },
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '10px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a two-property (one shorthand and one of its longhand'
+ ' components) two-value property-indexed specification',
frames: { marginTop: ['50px', '60px'],
margin: ['10px', '10px 20px 30px 40px'] },
expected: [ { property: 'margin-top',
values: [ value(0, '50px', 'replace', 'linear'),
value(1, '60px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a two-property property-indexed specification with different'
+ ' numbers of values',
frames: { left: ['10px', '20px', '30px'],
top: ['40px', '50px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'top',
values: [ value(0, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a property-indexed specification with an invalid value',
frames: { left: ['10px', '20px', '30px', '40px', '50px'],
top: ['15px', '25px', 'invalid', '45px', '55px'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'top',
values: [ value(0, '15px', 'replace', 'linear'),
value(0.25, '25px', 'replace', 'linear'),
value(0.75, '45px', 'replace', 'linear'),
value(1, '55px', 'replace') ] } ]
},
{ desc: 'a one-property two-value property-indexed specification that'
+ ' needs to stringify its values',
frames: { opacity: [0, 1] },
expected: [ { property: 'opacity',
values: [ value(0, '0', 'replace', 'linear'),
value(1, '1', 'replace') ] } ]
},
{ desc: 'a property-indexed keyframe where a lesser shorthand precedes'
+ ' a greater shorthand',
frames: { borderLeft: [ '1px solid rgb(1, 2, 3)',
'2px solid rgb(4, 5, 6)' ],
border: [ '3px dotted rgb(7, 8, 9)',
'4px dashed rgb(10, 11, 12)' ] },
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(4, 5, 6)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '2px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'solid', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
{ desc: 'a property-indexed keyframe where a greater shorthand precedes'
+ ' a lesser shorthand',
frames: { border: [ '3px dotted rgb(7, 8, 9)',
'4px dashed rgb(10, 11, 12)' ],
borderLeft: [ '1px solid rgb(1, 2, 3)',
'2px solid rgb(4, 5, 6)' ] },
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(4, 5, 6)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
value(1, 'rgb(10, 11, 12)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '2px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '3px', 'replace', 'linear'),
value(1, '4px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'solid', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for keyframe sequences
//
// ---------------------------------------------------------------------
{ desc: 'a keyframe sequence specification with repeated values at'
+ ' offset 0/1 with different easings',
frames: [ { offset: 0.0, left: '100px', easing: 'ease' },
{ offset: 0.0, left: '200px', easing: 'ease' },
{ offset: 0.5, left: '300px', easing: 'linear' },
{ offset: 1.0, left: '400px', easing: 'ease-out' },
{ offset: 1.0, left: '500px', easing: 'step-end' } ],
expected: [ { property: 'left',
values: [ value(0, '100px', 'replace'),
value(0, '200px', 'replace', 'ease'),
value(0.5, '300px', 'replace', 'linear'),
value(1, '400px', 'replace'),
value(1, '500px', 'replace') ] } ]
},
{ desc: 'a one-property two-keyframe sequence',
frames: [ { offset: 0, left: '10px' },
{ offset: 1, left: '20px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ]
},
{ desc: 'a two-property two-keyframe sequence',
frames: [ { offset: 0, left: '10px', top: '30px' },
{ offset: 1, left: '20px', top: '40px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'top',
values: [ value(0, '30px', 'replace', 'linear'),
value(1, '40px', 'replace') ] } ]
},
{ desc: 'a one shorthand property two-keyframe sequence',
frames: [ { offset: 0, margin: '10px' },
{ offset: 1, margin: '20px 30px 40px 50px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a two-property (a shorthand and one of its component longhands)'
+ ' two-keyframe sequence',
frames: [ { offset: 0, margin: '10px', marginTop: '20px' },
{ offset: 1, marginTop: '70px',
margin: '30px 40px 50px 60px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '70px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with duplicate values for a given interior'
+ ' offset',
frames: [ { offset: 0.0, left: '10px' },
{ offset: 0.5, left: '20px' },
{ offset: 0.5, left: '30px' },
{ offset: 0.5, left: '40px' },
{ offset: 1.0, left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace'),
value(0.5, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with duplicate values for offsets 0 and 1',
frames: [ { offset: 0, left: '10px' },
{ offset: 0, left: '20px' },
{ offset: 0, left: '30px' },
{ offset: 1, left: '40px' },
{ offset: 1, left: '50px' },
{ offset: 1, left: '60px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace'),
value(0, '30px', 'replace', 'linear'),
value(1, '40px', 'replace'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a two-property four-keyframe sequence',
frames: [ { offset: 0, left: '10px' },
{ offset: 0, top: '20px' },
{ offset: 1, top: '30px' },
{ offset: 1, left: '40px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '40px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a one-property keyframe sequence with some omitted offsets',
frames: [ { offset: 0.00, left: '10px' },
{ offset: 0.25, left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ offset: 1.00, left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a two-property keyframe sequence with some omitted offsets',
frames: [ { offset: 0.00, left: '10px', top: '20px' },
{ offset: 0.25, left: '30px' },
{ left: '40px' },
{ left: '50px', top: '60px' },
{ offset: 1.00, left: '70px', top: '80px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '30px', 'replace', 'linear'),
value(0.5, '40px', 'replace', 'linear'),
value(0.75, '50px', 'replace', 'linear'),
value(1, '70px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'linear'),
value(0.75, '60px', 'replace', 'linear'),
value(1, '80px', 'replace') ] } ]
},
{ desc: 'a one-property keyframe sequence with all omitted offsets',
frames: [ { left: '10px' },
{ left: '20px' },
{ left: '30px' },
{ left: '40px' },
{ left: '50px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.25, '20px', 'replace', 'linear'),
value(0.5, '30px', 'replace', 'linear'),
value(0.75, '40px', 'replace', 'linear'),
value(1, '50px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence with different easing values, but the'
+ ' same easing value for a given offset',
frames: [ { offset: 0.0, easing: 'ease', left: '10px'},
{ offset: 0.0, easing: 'ease', top: '20px'},
{ offset: 0.5, easing: 'linear', left: '30px' },
{ offset: 0.5, easing: 'linear', top: '40px' },
{ offset: 1.0, easing: 'step-end', left: '50px' },
{ offset: 1.0, easing: 'step-end', top: '60px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'ease'),
value(0.5, '30px', 'replace', 'linear'),
value(1, '50px', 'replace') ] },
{ property: 'top',
values: [ value(0, '20px', 'replace', 'ease'),
value(0.5, '40px', 'replace', 'linear'),
value(1, '60px', 'replace') ] } ]
},
{ desc: 'a one-property two-keyframe sequence that needs to'
+ ' stringify its values',
frames: [ { offset: 0, opacity: 0 },
{ offset: 1, opacity: 1 } ],
expected: [ { property: 'opacity',
values: [ value(0, '0', 'replace', 'linear'),
value(1, '1', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where shorthand precedes longhand',
frames: [ { offset: 0, margin: '10px', marginRight: '20px' },
{ offset: 1, margin: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where longhand precedes shorthand',
frames: [ { offset: 0, marginRight: '20px', margin: '10px' },
{ offset: 1, margin: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where lesser shorthand precedes greater'
+ ' shorthand',
frames: [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
border: '2px dotted rgb(4, 5, 6)' },
{ offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
{ desc: 'a keyframe sequence where greater shorthand precedes'
+ ' lesser shorthand',
frames: [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
borderLeft: '1px solid rgb(1, 2, 3)' },
{ offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
expected: [ { property: 'border-bottom-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-left-color',
values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-right-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-top-color',
values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
value(1, 'rgb(7, 8, 9)', 'replace') ] },
{ property: 'border-bottom-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-left-width',
values: [ value(0, '1px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-right-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-top-width',
values: [ value(0, '2px', 'replace', 'linear'),
value(1, '3px', 'replace') ] },
{ property: 'border-bottom-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-left-style',
values: [ value(0, 'solid', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-right-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-top-style',
values: [ value(0, 'dotted', 'replace', 'linear'),
value(1, 'dashed', 'replace') ] },
{ property: 'border-image-outset',
values: [ value(0, '0 0 0 0', 'replace', 'linear'),
value(1, '0 0 0 0', 'replace') ] },
{ property: 'border-image-repeat',
values: [ value(0, 'stretch stretch', 'replace', 'linear'),
value(1, 'stretch stretch', 'replace') ] },
{ property: 'border-image-slice',
values: [ value(0, '100% 100% 100% 100%',
'replace', 'linear'),
value(1, '100% 100% 100% 100%', 'replace') ] },
{ property: 'border-image-source',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: 'border-image-width',
values: [ value(0, '1 1 1 1', 'replace', 'linear'),
value(1, '1 1 1 1', 'replace') ] },
{ property: '-moz-border-bottom-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-left-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-right-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] },
{ property: '-moz-border-top-colors',
values: [ value(0, 'none', 'replace', 'linear'),
value(1, 'none', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for unit conversion
//
// ---------------------------------------------------------------------
{ desc: 'em units are resolved to px values',
frames: { left: ['10em', '20em'] },
expected: [ { property: 'left',
values: [ value(0, '100px', 'replace', 'linear'),
value(1, '200px', 'replace') ] } ]
},
{ desc: 'calc() expressions are resolved to the equivalent units',
frames: { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
expected: [ { property: 'left',
values: [ value(0, 'calc(110px)', 'replace', 'linear'),
value(1, 'calc(100px + 10%)', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for CSS variable handling conversion
//
// ---------------------------------------------------------------------
{ desc: 'CSS variables are resolved to their corresponding values',
frames: { left: ['10px', 'var(--var-100px)'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] } ]
},
{ desc: 'CSS variables in calc() expressions are resolved',
frames: { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, 'calc(50px + -10%)', 'replace') ] } ]
},
{ desc: 'CSS variables in shorthands are resolved to their corresponding'
+ ' values',
frames: { margin: ['10px', 'var(--var-100px-200px)'] },
expected: [ { property: 'margin-top',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '200px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '100px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '200px', 'replace') ] } ]
},
// ---------------------------------------------------------------------
//
// Tests for properties that parse correctly but which we fail to
// convert to computed values.
//
// ---------------------------------------------------------------------
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe',
frames: [ { margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe where we have enough values to create'
+ ' a final segment',
frames: [ { margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' },
{ margin: '5px' } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (first in series of two)',
frames: [ { margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 0 },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (second in series of two)',
frames: [ { margin: '5px', offset: 0 },
{ margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial overlapping keyframes (second in series of three)',
frames: [ { margin: '5px', offset: 0 },
{ margin: '5px', offset: 0,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 0 },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace'),
value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe',
frames: [ { margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe where it forms the last segment in the series',
frames: [ { margin: '5px' },
{ margin: '5px',
marginLeft: '5px',
marginRight: '5px',
marginBottom: '5px',
// margin-top sorts last and only it will be missing since
// the other longhand components are specified
simulateComputeValuesFailure: true } ],
expected: [ { property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe where we have enough values to create'
+ ' an initial segment',
frames: [ { margin: '5px' },
{ margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true } ],
expected: [ ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (first in series of two)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 1 } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (second in series of two)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1 },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final overlapping keyframes (second in series of three)',
frames: [ { margin: '5px' },
{ margin: '5px', offset: 1 },
{ margin: '5px', offset: 1,
simulateComputeValuesFailure: true },
{ margin: '5px', offset: 1 } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' intermediate keyframe',
frames: [ { margin: '5px' },
{ margin: '5px', simulateComputeValuesFailure: true },
{ margin: '5px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe along with other values',
// simulateComputeValuesFailure only applies to shorthands so we can set
// it on the same keyframe and it will only apply to |margin| and not
// |left|.
frames: [ { margin: '77%', left: '10px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '20px' } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' initial keyframe along with other values where those'
+ ' values sort after the property with missing values',
frames: [ { margin: '77%', right: '10px',
simulateComputeValuesFailure: true },
{ margin: '5px', right: '20px' } ],
expected: [ { property: 'right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe along with other values',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px', left: '20px',
simulateComputeValuesFailure: true } ],
expected: [ { property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' final keyframe along with other values where those'
+ ' values sort after the property with missing values',
frames: [ { margin: '5px', right: '10px' },
{ margin: '5px', right: '20px',
simulateComputeValuesFailure: true } ],
expected: [ { property: 'right',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '20px', 'replace') ] } ],
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' an intermediate keyframe along with other values',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px', left: '20px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(0.5, '20px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
{ desc: 'a property that can\'t be resolved to computed values in'
+ ' an intermediate keyframe by itself',
frames: [ { margin: '5px', left: '10px' },
{ margin: '5px',
simulateComputeValuesFailure: true },
{ margin: '5px', left: '30px' } ],
expected: [ { property: 'margin-top',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-right',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-bottom',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'margin-left',
values: [ value(0, '5px', 'replace', 'linear'),
value(1, '5px', 'replace') ] },
{ property: 'left',
values: [ value(0, '10px', 'replace', 'linear'),
value(1, '30px', 'replace') ] } ]
},
];
gTests.forEach(function(subtest) {
test(function(t) {
var div = addDiv(t);
var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
assert_properties_equal(animation.effect.getProperties(),
subtest.expected);
}, subtest.desc);
});
</script>
</body>

View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<head>
<meta charset=utf-8>
<title>Test getAnimations() for generated-content elements</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
@keyframes anim { }
@keyframes anim2 { }
.before::before {
content: '';
animation: anim 100s;
}
.after::after {
content: '';
animation: anim 100s, anim2 100s;
}
</style>
</head>
<body>
<div id='root' class='before after'>
<div class='before'></div>
<div></div>
</div>
<script>
'use strict';
const {Cc, Ci, Cu} = SpecialPowers;
function getWalker(node) {
var walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
createInstance(Ci.inIDeepTreeWalker);
walker.showAnonymousContent = true;
walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
walker.currentNode = node;
return walker;
}
test(function(t) {
var root = document.getElementById('root');
// Flush first to make sure the generated-content elements are ready
// in the tree.
flushComputedStyle(root);
var before = getWalker(root).firstChild();
var after = getWalker(root).lastChild();
// Sanity Checks
assert_equals(document.getAnimations().length, 4,
'All animations in this document');
assert_equals(before.tagName, '_moz_generated_content_before',
'First child is ::before element');
assert_equals(after.tagName, '_moz_generated_content_after',
'Last child is ::after element');
// Test Element.getAnimations() for generated-content elements
assert_equals(before.getAnimations().length, 1,
'Animations of ::before generated-content element');
assert_equals(after.getAnimations().length, 2,
'Animations of ::after generated-content element');
}, 'Element.getAnimations() used on generated-content elements');
test(function(t) {
var root = document.getElementById('root');
flushComputedStyle(root);
var walker = getWalker(root);
var animations = [];
var element = walker.currentNode;
while (element) {
if (element.getAnimations) {
animations = [...animations, ...element.getAnimations()];
}
element = walker.nextNode();
}
assert_equals(animations.length, document.getAnimations().length,
'The number of animations got by DeepTreeWalker and ' +
'document.getAnimations() should be the same');
}, 'Element.getAnimations() used by traversing DeepTreeWalker');
</script>
</body>

View file

@ -0,0 +1,854 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>
Test chrome-only MutationObserver animation notifications for sync APIs
</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<div id="log"></div>
<style>
@keyframes anim {
to { transform: translate(100px); }
}
@keyframes anotherAnim {
to { transform: translate(0px); }
}
</style>
<script>
function assert_record_list(actual, expected, desc, index, listName) {
assert_equals(actual.length, expected.length,
`${desc} - record[${index}].${listName} length`);
if (actual.length != expected.length) {
return;
}
for (var i = 0; i < actual.length; i++) {
assert_not_equals(actual.indexOf(expected[i]), -1,
`${desc} - record[${index}].${listName} contains expected Animation`);
}
}
function assert_equals_records(actual, expected, desc) {
assert_equals(actual.length, expected.length, `${desc} - number of records`);
if (actual.length != expected.length) {
return;
}
for (var i = 0; i < actual.length; i++) {
assert_record_list(actual[i].addedAnimations,
expected[i].added, desc, i, "addedAnimations");
assert_record_list(actual[i].changedAnimations,
expected[i].changed, desc, i, "changedAnimations");
assert_record_list(actual[i].removedAnimations,
expected[i].removed, desc, i, "removedAnimations");
}
}
// Create a pseudo element
function createPseudo(test, element, type) {
addStyle(test, { '@keyframes anim': '',
['.pseudo::' + type]: 'animation: anim 10s;' });
element.classList.add('pseudo');
var anims = document.getAnimations();
assert_true(anims.length >= 1);
var anim = anims[anims.length - 1];
assert_equals(anim.effect.target.parentElement, element);
assert_equals(anim.effect.target.type, '::' + type);
anim.cancel();
return anim.effect.target;
}
[ { subtree: false },
{ subtree: true }
].forEach(aOptions => {
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after duration is changed");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = anim.effect.timing.duration * 2;
anim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.duration = anim.effect.timing.duration * 3;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after duration set \"auto\"");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value \"auto\"");
}, "change_duration_and_currenttime");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.endDelay = 10 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after endDelay is changed");
anim.effect.timing.endDelay = 10 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = 109 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after currentTime during endDelay");
anim.effect.timing.endDelay = -110 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning negative value");
}, "change_enddelay_and_currenttime");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
endDelay: -100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[], "records after animation is added");
}, "zero_end_time");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.iterations = 2;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after iterations is changed");
anim.effect.timing.iterations = 2;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.effect.timing.iterations = 0;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.iterations = Infinity;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
}, "change_iterations");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.delay = 100;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after delay is changed");
anim.effect.timing.delay = 100;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.effect.timing.delay = -100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.delay = 0;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
}, "change_delay");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
easing: "steps(2, start)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.easing = "steps(2, end)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after easing is changed");
anim.effect.timing.easing = "steps(2, end)";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
}, "change_easing");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100, delay: -100 });
assert_equals_records(observer.takeRecords(),
[], "records after assigning negative value");
}, "negative_delay_in_constructor");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var effect = new KeyframeEffectReadOnly(null,
{ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var anim = new Animation(effect, document.timeline);
anim.play();
assert_equals_records(observer.takeRecords(),
[], "no records after animation is added");
}, "create_animation_without_target");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.target = div;
assert_equals_records(observer.takeRecords(),
[], "no records after setting the same target");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after setting null");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[], "records after setting redundant null");
}, "set_redundant_animation_target");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation is removed");
}, "set_null_animation_effect");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = new Animation();
anim.play();
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
}, "set_effect_on_null_effect_animation");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after replace effects");
}, "replace_effect_targeting_on_the_same_element");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate({ marginLeft: [ "0px", "100px" ] },
100 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.currentTime = 60 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after animation is changed");
anim.effect = new KeyframeEffect(div, { opacity: [ 0, 1 ] },
50 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after replacing effects");
}, "replace_effect_targeting_on_the_same_element_not_in_effect");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(margin-left)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after animation is changed");
}, "set_spacing");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC,
spacing: "paced(margin-left)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(animation-duration)";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after setting a non-animatable paced property");
}, "set_spacing_on_a_non-animatable_property");
test(t => {
var div = addDiv(t);
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var anim = div.animate([ { marginLeft: "0px" },
{ marginLeft: "-20px" },
{ marginLeft: "100px" },
{ marginLeft: "50px" } ],
{ duration: 100 * MS_PER_SEC,
spacing: "paced(margin-left)" });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.spacing = "paced(margin-left)";
assert_equals_records(observer.takeRecords(),
[], "no record after setting the same spacing");
}, "set_the_same_spacing");
// Test that starting a single animation that is cancelled by calling
// cancel() dispatches an added notification and then a removed
// notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].cancel();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: animations }],
"records after animation end");
// Re-trigger the animation.
animations[0].play();
// Single MutationRecord for the Animation (re-)addition.
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
}, "single_animation_cancelled_api");
// Test that updating a property on the Animation object dispatches a changed
// notification.
[
{ prop: "playbackRate", val: 0.5 },
{ prop: "startTime", val: 50 * MS_PER_SEC },
{ prop: "currentTime", val: 50 * MS_PER_SEC },
].forEach(function(aChangeTest) {
test(t => {
// We use a forwards fill mode so that even if the change we make causes
// the animation to become finished, it will still be "relevant" so we
// won't mark it as removed.
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
// Update the property.
animations[0][aChangeTest.prop] = aChangeTest.val;
// Make a redundant change.
animations[0][aChangeTest.prop] = animations[0][aChangeTest.prop];
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after animation property change");
}, `single_animation_api_change_${aChangeTest.prop}`);
});
// Test that making a redundant change to currentTime while an Animation
// is pause-pending still generates a change MutationRecord since setting
// the currentTime to any value in this state aborts the pending pause.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].pause();
// We are now pause-pending. Even if we make a redundant change to the
// currentTime, we should still get a change record because setting the
// currentTime while pause-pending has the effect of cancelling a pause.
animations[0].currentTime = animations[0].currentTime;
// Two MutationRecords for the Animation changes: one for pausing, one
// for aborting the pause.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] },
{ added: [], changed: animations, removed: [] }],
"records after pausing then seeking");
}, "change_currentTime_while_pause_pending");
// Test that calling finish() on a forwards-filling Animation dispatches
// a changed notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s forwards" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after finish()");
// Redundant finish.
animations[0].finish();
// Ensure no change records.
assert_equals_records(observer.takeRecords(),
[], "records after redundant finish()");
}, "finish_with_forwards_fill");
// Test that calling finish() on an Animation that does not fill forwards,
// dispatches a removal notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].finish();
// Single MutationRecord for the Animation removal.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: animations }],
"records after finishing");
}, "finish_without_fill");
// Test that calling finish() on a forwards-filling Animation dispatches
test(t => {
var div = addDiv(t, { style: "animation: anim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animation = div.getAnimations()[0];
assert_equals_records(observer.takeRecords(),
[{ added: [animation], changed: [], removed: []}],
"records after creation");
animation.id = "new id";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [animation], removed: []}],
"records after id is changed");
animation.id = "new id";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value with id");
}, "change_id");
// Test that calling reverse() dispatches a changed notification.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s both" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
animations[0].reverse();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] }],
"records after calling reverse()");
}, "reverse");
// Test that calling reverse() does *not* dispatch a changed notification
// when playbackRate == 0.
test(t => {
var div = addDiv(t, { style: "animation: anim 100s both" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals(animations.length, 1,
"getAnimations().length after animation start");
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: [] }],
"records after animation start");
// Seek to the middle and set playbackRate to zero.
animations[0].currentTime = 50 * MS_PER_SEC;
animations[0].playbackRate = 0;
// Two MutationRecords, one for each change.
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: [] },
{ added: [], changed: animations, removed: [] }],
"records after seeking and setting playbackRate");
animations[0].reverse();
// We should get no notifications.
assert_equals_records(observer.takeRecords(),
[], "records after calling reverse()");
}, "reverse_with_zero_playbackRate");
// Test that attempting to start an animation that should already be finished
// does not send any notifications.
test(t => {
// Start an animation that should already be finished.
var div = addDiv(t, { style: "animation: anim 1s -2s;" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
// The animation should cause no Animations to be created.
var animations = div.getAnimations();
assert_equals(animations.length, 0,
"getAnimations().length after animation start");
// And we should get no notifications.
assert_equals_records(observer.takeRecords(),
[], "records after attempted animation start");
}, "already_finished");
test(t => {
var div = addDiv(t, { style: "animation: anim 100s, anotherAnim 100s" });
var observer =
setupSynchronousObserver(t,
aOptions.subtree ? div.parentNode : div,
aOptions.subtree);
var animations = div.getAnimations();
assert_equals_records(observer.takeRecords(),
[{ added: animations, changed: [], removed: []}],
"records after creation");
div.style.animation = "anotherAnim 100s, anim 100s";
animations = div.getAnimations();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: animations, removed: []}],
"records after the order is changed");
div.style.animation = "anotherAnim 100s, anim 100s";
assert_equals_records(observer.takeRecords(),
[], "no records after applying the same order");
}, "animtion_order_change");
});
test(t => {
var div = addDiv(t);
var observer = setupSynchronousObserver(t, div, true);
var child = document.createElement("div");
div.appendChild(child);
var anim1 = div.animate({ marginLeft: [ "0px", "50px" ] },
100 * MS_PER_SEC);
var anim2 = child.animate({ marginLeft: [ "0px", "100px" ] },
50 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim1], changed: [], removed: [] },
{ added: [anim2], changed: [], removed: [] }],
"records after animation is added");
// After setting a new effect, we remove the current animation, anim1,
// because it is no longer attached to |div|, and then remove the previous
// animation, anim2. Finally, add back the anim1 which is in effect on
// |child| now. In addition, we sort them by tree order and they are
// batched.
anim1.effect = anim2.effect;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim1] }, // div
{ added: [anim1], changed: [], removed: [anim2] }], // child
"records after animation effects are changed");
}, "set_effect_with_previous_animation");
test(t => {
var div = addDiv(t);
var observer = setupSynchronousObserver(t, document, true);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var newTarget = document.createElement("div");
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.target = null;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after setting null");
anim.effect.target = div;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after setting a target");
anim.effect.target = addDiv(t);
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] },
{ added: [anim], changed: [], removed: [] }],
"records after setting a different target");
}, "set_animation_target");
test(t => {
var div = addDiv(t);
var pseudoTarget = createPseudo(t, div, 'before');
var observer = setupSynchronousObserver(t, div, true);
var anim = pseudoTarget.animate({ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [anim], removed: [] }],
"records after duration is changed");
anim.effect.timing.duration = 100 * MS_PER_SEC;
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value");
anim.currentTime = anim.effect.timing.duration * 2;
anim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation end");
anim.effect.timing.duration = anim.effect.timing.duration * 3;
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation restarted");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after duration set \"auto\"");
anim.effect.timing.duration = "auto";
assert_equals_records(observer.takeRecords(),
[], "records after assigning same value \"auto\"");
}, "change_duration_and_currenttime_on_pseudo_elements");
test(t => {
var div = addDiv(t);
var pseudoTarget = createPseudo(t, div, 'before');
var observer = setupSynchronousObserver(t, div, false);
var anim = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
var pAnim = pseudoTarget.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC });
assert_equals_records(observer.takeRecords(),
[{ added: [anim], changed: [], removed: [] }],
"records after animation is added");
anim.finish();
pAnim.finish();
assert_equals_records(observer.takeRecords(),
[{ added: [], changed: [], removed: [anim] }],
"records after animation is finished");
}, "exclude_animations_targeting_pseudo_elements");
</script>

View file

@ -0,0 +1,815 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Tests restyles caused by animations</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style>
@keyframes opacity {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes background-color {
from { background-color: red; }
to { background-color: blue; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
div {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
</style>
</head>
<body>
<script>
'use strict';
function observeStyling(frameCount, onFrame) {
var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsIWebNavigation)
.QueryInterface(SpecialPowers.Ci.nsIDocShell);
docShell.recordProfileTimelineMarkers = true;
docShell.popProfileTimelineMarkers();
return new Promise(function(resolve) {
return waitForAnimationFrames(frameCount, onFrame).then(function() {
var markers = docShell.popProfileTimelineMarkers();
docShell.recordProfileTimelineMarkers = false;
var stylingMarkers = markers.filter(function(marker, index) {
return marker.name == 'Styles' &&
(marker.restyleHint == 'eRestyle_CSSAnimations' ||
marker.restyleHint == 'eRestyle_CSSTransitions');
});
resolve(stylingMarkers);
});
});
}
function ensureElementRemoval(aElement) {
return new Promise(function(resolve) {
aElement.remove();
waitForAllPaintsFlushed(resolve);
});
}
SimpleTest.waitForExplicitFinish();
var omtaEnabled = isOMTAEnabled();
var isAndroid = !!navigator.userAgent.includes("Android");
function add_task_if_omta_enabled(test) {
if (!omtaEnabled) {
info(test.name + " is skipped because OMTA is disabled");
return;
}
add_task(test);
}
// We need to wait for all paints before running tests to avoid contaminations
// from styling of this document itself.
waitForAllPaints(function() {
add_task(function* restyling_for_main_thread_animations() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 5,
'CSS animations running on the main-thread should update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS animations running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS transitions running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
div.animationDuration = '200s';
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor should not update style ' +
'on the main thread');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.finish();
var markers = yield observeStyling(5);
is(markers.length, 1,
'Animations running on the compositor should only update style ' +
'once after finish() is called');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_transition() {
var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished transitions should never cause restyles ' +
'when mouse is moved on the animations');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_animation() {
var div = addDiv(null, { style: 'animation: opacity 1ms' });
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished animations should never cause restyles ' +
'when mouse is moved on the animations');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var div = addDiv(null,
{ style: 'animation: opacity 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor in an out-of-view element ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var div = addDiv(null,
{ style: 'animation: background-color 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the main-thread in an out-of-view element ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android the opacity animation runs on the compositor even if it is
scrolled out of view. We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: opacity 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor for elements ' +
'which are scrolled out should never cause restyles');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the main-thread for elements ' +
'which are scrolled out should never cause restyles');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_nested_scrolled_out_element() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var grandParent = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 100px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
grandParent.appendChild(parentElement);
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the main-thread which are in nested elements ' +
'which are scrolled out should never cause restyles');
yield ensureElementRemoval(grandParent);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: opacity 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the compositor in ' +
'visibility hidden element should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
var parentRect = parentElement.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
var markers = yield observeStyling(1, function() {
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 100 });
});
is(markers.length, 1,
'Animations running on the main-thread which were in scrolled out ' +
'elements should update restyling soon after the element moved in ' +
'view by scrolling');
yield ensureElementRemoval(parentElement);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var grandParent = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 200px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
grandParent.appendChild(parentElement);
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
var parentRect = grandParent.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
var markers = yield observeStyling(1, function() {
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 100 });
});
// FIXME: We should reduce a redundant restyle here.
ok(markers.length >= 1,
'Animations running on the main-thread which were in nested scrolled ' +
'out elements should update restyle soon after the element moved ' +
'in view by scrolling');
yield ensureElementRemoval(grandParent);
});
add_task(function* restyling_main_thread_animations_move_out_of_view_by_scrolling() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
/*
On Android throttled animations are left behind on the main thread in some
frames, We will fix this in bug 1247800.
*/
if (isAndroid) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 200px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s;' });
var pad = addDiv(null,
{ style: 'height: 400px;' });
parentElement.appendChild(div);
parentElement.appendChild(pad);
var animation = div.getAnimations()[0];
var parentRect = parentElement.getBoundingClientRect();
var centerX = parentRect.left + parentRect.width / 2;
var centerY = parentRect.top + parentRect.height / 2;
yield animation.ready;
// We can't use synthesizeWheel here since synthesizeWheel causes
// layout flush.
synthesizeWheelAtPoint(centerX, centerY,
{ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
deltaY: 200 });
var markers = yield observeStyling(5);
// FIXME: We should reduce a redundant restyle here.
ok(markers.length >= 0,
'Animations running on the main-thread which are in scrolled out ' +
'elements should throttle restyling');
yield ensureElementRemoval(parentElement);
});
add_task(function* restyling_main_thread_animations_moved_in_view_by_resizing() {
if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
return;
}
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(1, function() {
parentElement.style.height = '100px';
});
is(markers.length, 1,
'Animations running on the main-thread which was in scrolled out ' +
'elements should update restyling soon after the element moved in ' +
'view by resizing');
yield ensureElementRemoval(parentElement);
});
add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: background-color 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the main-thread in ' +
'visibility hidden element should never cause restyles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the compositor should ' +
'never cause restyles once after pause() is called');
yield ensureElementRemoval(div);
});
add_task(function* no_restyling_main_thread_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the main-thread should ' +
'never cause restyles after pause() is called');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* only_one_restyling_when_current_time_is_set_to_middle_of_duration() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
animation.currentTime = 50 * MS_PER_SEC;
var markers = yield observeStyling(5);
is(markers.length, 1,
'Bug 1235478: Animations running on the compositor should only once ' +
'update style when currentTime is set to middle of duration time');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* change_duration_and_currenttime() {
var div = addDiv(null);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
yield animation.ready;
ok(animation.isRunningOnCompositor);
// Set currentTime to a time longer than duration.
animation.currentTime = 500 * MS_PER_SEC;
// Now the animation immediately get back from compositor.
ok(!animation.isRunningOnCompositor);
// Extend the duration.
animation.effect.timing.duration = 800 * MS_PER_SEC;
var markers = yield observeStyling(5);
is(markers.length, 1,
'Animations running on the compositor should update style ' +
'when timing.duration is made longer than the current time');
yield ensureElementRemoval(div);
});
add_task(function* script_animation_on_display_none_element() {
var div = addDiv(null);
var animation = div.animate({ backgroundColor: [ 'red', 'blue' ] },
100 * MS_PER_SEC);
yield animation.ready;
div.style.display = 'none';
// We need to wait a frame to apply display:none style.
yield waitForFrame();
is(animation.playState, 'running',
'Script animations keep running even when the target element has ' +
'"display: none" style');
ok(!animation.isRunningOnCompositor,
'Script animations on "display:none" element should not run on the ' +
'compositor');
var markers = yield observeStyling(5);
is(markers.length, 0,
'Script animations on "display: none" element should not update styles');
div.style.display = '';
// We need to wait a frame to unapply display:none style.
yield waitForFrame();
var markers = yield observeStyling(5);
is(markers.length, 5,
'Script animations restored from "display: none" state should update ' +
'styles');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* compositable_script_animation_on_display_none_element() {
var div = addDiv(null);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
yield animation.ready;
div.style.display = 'none';
// We need to wait a frame to apply display:none style.
yield waitForFrame();
is(animation.playState, 'running',
'Opacity script animations keep running even when the target element ' +
'has "display: none" style');
ok(!animation.isRunningOnCompositor,
'Opacity script animations on "display:none" element should not ' +
'run on the compositor');
var markers = yield observeStyling(5);
is(markers.length, 0,
'Opacity script animations on "display: none" element should not ' +
'update styles');
div.style.display = '';
// We need to wait a frame to unapply display:none style.
yield waitForFrame();
ok(animation.isRunningOnCompositor,
'Opacity script animations restored from "display: none" should be ' +
'run on the compositor');
yield ensureElementRemoval(div);
});
add_task(function* restyling_for_empty_keyframes() {
var div = addDiv(null);
var animation = div.animate({ }, 100 * MS_PER_SEC);
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations with no keyframes should not cause restyles');
animation.effect.setKeyframes({ backgroundColor: ['red', 'blue'] });
markers = yield observeStyling(5);
is(markers.length, 5,
'Setting valid keyframes should cause regular animation restyles to ' +
'occur');
animation.effect.setKeyframes({ });
markers = yield observeStyling(5);
is(markers.length, 1,
'Setting an empty set of keyframes should trigger a single restyle ' +
'to remove the previous animated style');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(function* no_restyling_when_animation_style_when_re_setting_same_animation_property() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
// Apply the same animation style
div.style.animation = 'opacity 100s';
var markers = yield observeStyling(5);
is(markers.length, 0,
'Applying same animation style ' +
'should never cause restyles');
yield ensureElementRemoval(div);
});
add_task(function* necessary_update_should_be_invoked() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
yield waitForAnimationFrames(5);
// Apply another animation style
div.style.animation = 'background-color 110s';
var animation = div.getAnimations()[0];
var markers = yield observeStyling(5);
is(markers.length, 5,
'Applying animation style with different duration ' +
'should cause restyles on every frame.');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(
function* changing_cascading_result_for_main_thread_animation() {
var div = addDiv(null, { style: 'background-color: blue' });
var animation = div.animate({ opacity: [0, 1],
backgroundColor: ['green', 'red'] },
100 * MS_PER_SEC);
yield animation.ready;
ok(animation.isRunningOnCompositor,
'The opacity animation is running on the compositor');
// Make the background-color style as !important to cause an update
// to the cascade.
// Bug 1300982: The background-color animation should be no longer
// running on the main thread.
div.style.setProperty('background-color', '1', 'important');
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Changing cascading result for the property running on the main ' +
'thread does not cause synchronization layer of opacity animation ' +
'running on the compositor');
yield ensureElementRemoval(div);
}
);
add_task(function* restyling_for_animation_on_orphaned_element() {
var div = addDiv(null);
var animation = div.animate({ marginLeft: [ '0px', '100px' ] },
100 * MS_PER_SEC);
yield animation.ready;
div.remove();
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animation on orphaned element should not cause restyles');
document.body.appendChild(div);
markers = yield observeStyling(1);
// We are observing restyles in rAF callback which is processed before
// restyling process in each frame, so in the first frame there should be
// no observed restyle since we don't process restyle while the element
// is not attached to the document.
is(markers.length, 0,
'We observe no restyle in the first frame right after re-atatching ' +
'to the document');
markers = yield observeStyling(5);
is(markers.length, 5,
'Animation on re-attached to the document begins to update style');
yield ensureElementRemoval(div);
});
add_task_if_omta_enabled(
// Tests that if we remove an element from the document whose animation
// cascade needs recalculating, that it is correctly updated when it is
// re-attached to the document.
function* restyling_for_opacity_animation_on_re_attached_element() {
var div = addDiv(null, { style: 'opacity: 1 ! important' });
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
yield animation.ready;
ok(!animation.isRunningOnCompositor,
'The opacity animation overridden by an !important rule is NOT ' +
'running on the compositor');
// Drop the !important rule to update the cascade.
div.style.setProperty('opacity', '1', '');
div.remove();
var markers = yield observeStyling(5);
is(markers.length, 0,
'Opacity animation on orphaned element should not cause restyles');
document.body.appendChild(div);
// Need a frame to give the animation a chance to be sent to the
// compositor.
yield waitForFrame();
ok(animation.isRunningOnCompositor,
'The opacity animation which is no longer overridden by the ' +
'!important rule begins running on the compositor even if the ' +
'!important rule had been dropped before the target element was ' +
'removed');
yield ensureElementRemoval(div);
}
);
});
</script>
</body>

View file

@ -0,0 +1,966 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1045994 - Add a chrome-only property to inspect if an animation is
running on the compositor or not</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
@keyframes anim {
to { transform: translate(100px) }
}
@keyframes transform-starts-with-none {
0% { transform: none }
99% { transform: none }
100% { transform: translate(100px) }
}
@keyframes opacity {
to { opacity: 0 }
}
@keyframes background_and_translate {
to { background-color: red; transform: translate(100px); }
}
@keyframes background {
to { background-color: red; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes rotate-and-opacity {
from { transform: rotate(0deg); opacity: 1;}
to { transform: rotate(360deg); opacity: 0;}
}
div {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045994"
target="_blank">Mozilla Bug 1045994</a>
<div id="log"></div>
<script>
'use strict';
/** Test for bug 1045994 - Add a chrome-only property to inspect if an
animation is running on the compositor or not **/
var omtaEnabled = isOMTAEnabled();
function assert_animation_is_running_on_compositor(animation, desc) {
assert_equals(animation.isRunningOnCompositor, omtaEnabled,
desc + ' at ' + animation.currentTime + 'ms');
}
function assert_animation_is_not_running_on_compositor(animation, desc) {
assert_equals(animation.isRunningOnCompositor, false,
desc + ' at ' + animation.currentTime + 'ms');
}
promise_test(function(t) {
// FIXME: When we implement Element.animate, use that here instead of CSS
// so that we remove any dependency on the CSS mapping.
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' during playback');
div.style.animationPlayState = 'paused';
return animation.ready;
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when paused');
});
}, '');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: background 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' for animation of "background"');
});
}, 'isRunningOnCompositor is false for animation of "background"');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: background_and_translate 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when the animation has two properties, where one can run'
+ ' on the compositor, the other cannot');
});
}, 'isRunningOnCompositor is true if the animation has at least one ' +
'property can run on compositor');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.pause();
return animation.ready;
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when animation.pause() is called');
});
}, 'isRunningOnCompositor is false when the animation.pause() is called');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.finish();
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after animation.finish() is called');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after animation.finish() is called');
});
}, 'isRunningOnCompositor is false when the animation.finish() is called');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.currentTime = 100 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after manually seeking the animation to the end');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after manually seeking the animation to the end');
});
}, 'isRunningOnCompositor is false when manually seeking the animation to ' +
'the end');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.cancel();
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' immediately after animation.cancel() is called');
// Check that we don't set the flag back again on the next tick.
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' on the next tick after animation.cancel() is called');
});
}, 'isRunningOnCompositor is false when animation.cancel() is called');
// This is to test that we don't simply clobber the flag when ticking
// animations and then set it again during painting.
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
window.requestAnimationFrame(function() {
t.step(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in requestAnimationFrame callback');
});
resolve();
});
});
});
}, 'isRunningOnCompositor is true in requestAnimationFrame callback');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
var observer = new MutationObserver(function(records) {
var changedAnimation;
records.forEach(function(record) {
changedAnimation =
record.changedAnimations.find(function(changedAnim) {
return changedAnim == animation;
});
});
t.step(function() {
assert_true(!!changedAnimation, 'The animation should be recorded '
+ 'as one of the changedAnimations');
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in MutationObserver callback');
});
resolve();
});
observer.observe(div, { animations: true, subtree: false });
t.add_cleanup(function() {
observer.disconnect();
});
div.style.animationDuration = "200s";
});
});
}, 'isRunningOnCompositor is true in MutationObserver callback');
// This is to test that we don't temporarily clear the flag when forcing
// an unthrottled sample.
promise_test(function(t) {
return new Promise(function(resolve) {
// Needs scrollbars to cause overflow.
SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] },
resolve);
}).then(function() {
var div = addDiv(t, { style: 'animation: rotate 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
return new Promise(function(resolve) {
var timeAtStart = window.performance.now();
function handleFrame() {
t.step(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' in requestAnimationFrame callback');
});
// we have to wait at least 200ms because this animation is
// unthrottled on every 200ms.
// See http://hg.mozilla.org/mozilla-central/file/cafb1c90f794/layout/style/AnimationCommon.cpp#l863
if (window.performance.now() - timeAtStart > 200) {
resolve();
return;
}
window.requestAnimationFrame(handleFrame);
}
window.requestAnimationFrame(handleFrame);
});
});
});
}, 'isRunningOnCompositor remains true in requestAnimationFrameCallback for ' +
'overflow animation');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Transition reports that it is running on the compositor'
+ ' during playback for opacity transition');
});
}, 'isRunningOnCompositor for transitions');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: rotate-and-opacity 100s; ' +
'backface-visibility: hidden; ' +
'transform: none !important;' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'If an animation has a property that can run on the compositor and a '
+ 'property that cannot (due to Gecko limitations) but where the latter'
+ 'property is overridden in the CSS cascade, the animation should '
+ 'still report that it is running on the compositor');
});
}, 'isRunningOnCompositor is true when a property that would otherwise block ' +
'running on the compositor is overridden in the CSS cascade');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.currentTime = 150 * MS_PER_SEC;
animation.effect.timing.duration = 100 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when the animation is set a shorter duration than current time');
});
}, 'animation is immediately removed from compositor' +
'when timing.duration is made shorter than the current time');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.currentTime = 500 * MS_PER_SEC;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when finished');
animation.effect.timing.duration = 1000 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when restarted');
});
}, 'animation is added to compositor' +
' when timing.duration is made longer than the current time');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = 100 * MS_PER_SEC;
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when endDelay is changed');
animation.currentTime = 110 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when currentTime is during endDelay');
});
}, 'animation is removed from compositor' +
' when current time is made longer than the duration even during endDelay');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = -200 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when endTime is negative value');
});
}, 'animation is removed from compositor' +
' when endTime is negative value');
promise_test(function(t) {
var animation = addDivAndAnimate(t,
{},
{ opacity: [ 0, 1 ] }, 200 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.timing.endDelay = -100 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor'
+ ' when endTime is positive and endDelay is negative');
animation.currentTime = 110 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the compositor'
+ ' when currentTime is after endTime');
});
}, 'animation is NOT running on compositor' +
' when endTime is positive and endDelay is negative');
promise_test(function(t) {
var effect = new KeyframeEffect(null,
{ opacity: [ 0, 1 ] },
100 * MS_PER_SEC);
var animation = new Animation(effect, document.timeline);
animation.play();
var div = addDiv(t);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation with null target reports that it is not running ' +
'on the compositor');
animation.effect.target = div;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor ' +
'after setting a valid target');
});
}, 'animation is added to the compositor when setting a valid target');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation reports that it is running on the compositor');
animation.effect.target = null;
assert_animation_is_not_running_on_compositor(animation,
'Animation reports that it is NOT running on the ' +
'compositor after setting null target');
});
}, 'animation is removed from the compositor when setting null target');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ opacity: [ 0, 1 ] },
{ duration: 100 * MS_PER_SEC,
delay: 100 * MS_PER_SEC,
fill: 'backwards' });
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation with fill:backwards in delay phase reports ' +
'that it is running on the compositor');
animation.currentTime = 100 * MS_PER_SEC;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Animation with fill:backwards in delay phase reports ' +
'that it is running on the compositor after delay phase');
});
}, 'animation with fill:backwards in delay phase is running on the ' +
' main-thread while it is in delay phase');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate([{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }], 100 * MS_PER_SEC);
var another = addDiv(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on a 100% opacity keyframe reports ' +
'that it is running on the compositor from the begining');
animation.effect.target = another;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on a 100% opacity keyframe keeps ' +
'running on the compositor after changing the target ' +
'element');
});
}, '100% opacity animations with keeps running on the ' +
'compositor after changing the target element');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Color animation reports that it is not running on the ' +
'compositor');
animation.effect.setKeyframes([{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }]);
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'100% opacity animation set by using setKeyframes reports ' +
'that it is running on the compositor');
});
}, '100% opacity animation set up by converting an existing animation with ' +
'cannot be run on the compositor, is running on the compositor');
promise_test(function(t) {
var div = addDiv(t);
var animation = div.animate({ color: ['red', 'black'] }, 100 * MS_PER_SEC);
var effect = new KeyframeEffect(div,
[{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.99 },
{ opacity: 0, offset: 1 }],
100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Color animation reports that it is not running on the ' +
'compositor');
animation.effect = effect;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'100% opacity animation set up by changing effects reports ' +
'that it is running on the compositor');
});
}, '100% opacity animation set up by changing the effects on an existing ' +
'animation which cannot be run on the compositor, is running on the ' +
'compositor');
promise_test(function(t) {
var div = addDiv(t, { style: "opacity: 1 ! important" });
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has 100% opacity style with ' +
'!important flag reports that it is not running on the compositor');
// Clear important flag from the opacity style on the target element.
div.style.setProperty("opacity", "1", "");
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation reports that it is running on the compositor after '
+ 'clearing the !important flag');
});
}, 'Clearing *important* opacity style on the target element sends the ' +
'animation to the compositor');
promise_test(function(t) {
var div = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority opacity animation on an element ' +
'reports that it is running on the compositor');
assert_animation_is_running_on_compositor(lowerAnimation,
'A lower-priority opacity animation on the same ' +
'element also reports that it is running on the compositor');
});
}, 'Opacity animations on the same element run on the compositor');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; opacity: 1' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
getComputedStyle(div).opacity;
var transition = div.getAnimations()[0];
var animation = div.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([transition.ready, animation.ready]).then(function() {
assert_animation_is_running_on_compositor(animation,
'An opacity animation on an element reports that' +
'that it is running on the compositor');
assert_animation_is_running_on_compositor(transition,
'An opacity transition on the same element reports that ' +
'it is running on the compositor');
});
}, 'Both of transition and script animation on the same element run on the ' +
'compositor');
promise_test(function(t) {
var div = addDiv(t);
var importantOpacityElement = addDiv(t, { style: "opacity: 1 ! important" });
var animation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation on an element reports ' +
'that it is running on the compositor');
animation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation is no longer running on the compositor after ' +
'removing from the element');
animation.effect.target = importantOpacityElement;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Animation is NOT running on the compositor even after ' +
'being applied to a different element which has an ' +
'!important opacity declaration');
});
}, 'Animation continues not running on the compositor after being ' +
'applied to an element which has an important declaration and ' +
'having previously been temporarily associated with no target element');
promise_test(function(t) {
var div = addDiv(t);
var another = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'An opacity animation on an element reports that ' +
'it is running on the compositor');
assert_animation_is_running_on_compositor(higherAnimation,
'Opacity animation on a different element reports ' +
'that it is running on the compositor');
lowerAnimation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(lowerAnimation,
'Animation is no longer running on the compositor after ' +
'being removed from the element');
lowerAnimation.effect.target = another;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'A lower-priority animation begins running ' +
'on the compositor after being applied to an element ' +
'which has a higher-priority animation');
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority animation continues to run on the ' +
'compositor even after a lower-priority animation is ' +
'applied to the same element');
});
}, 'Animation begins running on the compositor after being applied ' +
'to an element which has a higher-priority animation and after ' +
'being temporarily associated with no target element');
promise_test(function(t) {
var div = addDiv(t);
var another = addDiv(t);
var lowerAnimation = div.animate({ opacity: [1, 0] }, 100 * MS_PER_SEC);
var higherAnimation = another.animate({ opacity: [0, 1] }, 100 * MS_PER_SEC);
return Promise.all([lowerAnimation.ready, higherAnimation.ready]).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'An opacity animation on an element reports that ' +
'it is running on the compositor');
assert_animation_is_running_on_compositor(higherAnimation,
'Opacity animation on a different element reports ' +
'that it is running on the compositor');
higherAnimation.effect.target = null;
return waitForFrame();
}).then(function() {
assert_animation_is_not_running_on_compositor(higherAnimation,
'Animation is no longer running on the compositor after ' +
'being removed from the element');
higherAnimation.effect.target = div;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(lowerAnimation,
'Animation continues running on the compositor after ' +
'a higher-priority animation applied to the same element');
assert_animation_is_running_on_compositor(higherAnimation,
'A higher-priority animation begins to running on the ' +
'compositor after being applied to an element which has ' +
'a lower-priority-animation');
});
}, 'Animation begins running on the compositor after being applied ' +
'to an element which has a lower-priority animation once after ' +
'disassociating with an element');
var delayPhaseTests = [
{
desc: 'script animation of opacity',
setupAnimation: function(t) {
return addDiv(t).animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'script animation of transform',
setupAnimation: function(t) {
return addDiv(t).animate(
{ transform: ['translateX(0px)', 'translateX(100px)'] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'CSS animation of opacity',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: opacity 100s 100s' })
.getAnimations()[0];
},
},
{
desc: 'CSS animation of transform',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: anim 100s 100s' })
.getAnimations()[0];
},
},
{
desc: 'CSS transition of opacity',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s 100s' });
getComputedStyle(div).opacity;
div.style.opacity = 0;
return div.getAnimations()[0];
},
},
{
desc: 'CSS transition of transform',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: transform 100s 100s' });
getComputedStyle(div).transform;
div.style.transform = 'translateX(100px)';
return div.getAnimations()[0];
},
},
];
delayPhaseTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in the delay phase');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
'it is in the delay phase');
});
// The purpose of thie test cases is to check that
// NS_FRAME_MAY_BE_TRANSFORMED flag on the associated nsIFrame persists
// after transform style on the frame is removed.
var delayPhaseWithTransformStyleTests = [
{
desc: 'script animation of transform with transform style',
setupAnimation: function(t) {
return addDiv(t, { style: 'transform: translateX(10px)' }).animate(
{ transform: ['translateX(0px)', 'translateX(100px)'] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
},
},
{
desc: 'CSS animation of transform with transform style',
setupAnimation: function(t) {
return addDiv(t, { style: 'animation: anim 100s 100s;' +
'transform: translateX(10px)' })
.getAnimations()[0];
},
},
{
desc: 'CSS transition of transform with transform style',
setupAnimation: function(t) {
var div = addDiv(t, { style: 'transition: transform 100s 100s;' +
'transform: translateX(10px)'});
getComputedStyle(div).transform;
div.style.transform = 'translateX(100px)';
return div.getAnimations()[0];
},
},
];
delayPhaseWithTransformStyleTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in the delay phase');
}).then(function() {
// Remove the initial transform style during delay phase.
animation.effect.target.style.transform = 'none';
return animation.ready;
}).then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it keeps running on the '
+ 'compositor after removing the initial transform style');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true after removing ' +
'the initial transform style during the delay phase');
});
var startsWithNoneTests = [
{
desc: 'script animation of transform starts with transform:none segment',
setupAnimation: function(t) {
return addDiv(t).animate(
{ transform: ['none', 'none', 'translateX(100px)'] }, 100 * MS_PER_SEC);
},
},
{
desc: 'CSS animation of transform starts with transform:none segment',
setupAnimation: function(t) {
return addDiv(t,
{ style: 'animation: transform-starts-with-none 100s 100s' })
.getAnimations()[0];
},
},
];
startsWithNoneTests.forEach(function(test) {
promise_test(function(t) {
var animation = test.setupAnimation(t);
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
test.desc + ' reports that it is running on the '
+ 'compositor even though it is in transform:none segment');
});
}, 'isRunningOnCompositor for ' + test.desc + ' is true even though ' +
'it is in transform:none segment');
});
promise_test(function(t) {
var div = addDiv(t, { style: 'opacity: 1 ! important' });
var animation = div.animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has opacity:1 important style'
+ 'reports that it is not running on the compositor');
// Clear the opacity style on the target element.
div.style.setProperty("opacity", "1", "");
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animations reports that it is running on the compositor after '
+ 'clearing the opacity style on the element');
});
}, 'Clearing *important* opacity style on the target element sends the ' +
'animation to the compositor even if the animation is in the delay phase');
promise_test(function(t) {
var opaqueDiv = addDiv(t, { style: 'opacity: 1 ! important' });
var anotherDiv = addDiv(t);
var animation = opaqueDiv.animate(
{ opacity: [0, 1] },
{ delay: 100 * MS_PER_SEC, duration: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Opacity animation on an element which has opacity:1 important style'
+ 'reports that it is not running on the compositor');
// Changing target element to another element which has no opacity style.
animation.effect.target = anotherDiv;
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animations reports that it is running on the compositor after '
+ 'changing the target element to another elemenent having no '
+ 'opacity style');
});
}, 'Changing target element of opacity animation sends the animation to the ' +
'the compositor even if the animation is in the delay phase');
promise_test(function(t) {
var animation =
addDivAndAnimate(t,
{},
{ width: ['100px', '200px'] },
{ duration: 100 * MS_PER_SEC, delay: 100 * MS_PER_SEC });
return animation.ready.then(function() {
assert_animation_is_not_running_on_compositor(animation,
'Width animation reports that it is not running on the compositor '
+ 'in the delay phase');
// Changing to property runnable on the compositor.
animation.effect.setKeyframes({ opacity: [0, 1] });
return waitForFrame();
}).then(function() {
assert_animation_is_running_on_compositor(animation,
'Opacity animation reports that it is running on the compositor '
+ 'after changing the property from width property in the delay phase');
});
}, 'Dynamic change to a property runnable on the compositor ' +
'in the delay phase');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; ' +
'opacity: 0 !important' });
getComputedStyle(div).opacity;
div.style.setProperty('opacity', '1', 'important');
getComputedStyle(div).opacity;
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_animation_is_running_on_compositor(animation,
'Transition reports that it is running on the compositor even if the ' +
'property is overridden by an !important rule');
});
}, 'Transitions override important rules');
promise_test(function(t) {
var div = addDiv(t, { style: 'transition: opacity 100s; ' +
'opacity: 0 !important' });
getComputedStyle(div).opacity;
div.animate({ opacity: [ 0, 1 ] }, 100 * MS_PER_SEC);
div.style.setProperty('opacity', '1', 'important');
getComputedStyle(div).opacity;
var [transition, animation] = div.getAnimations();
return Promise.all([transition.ready, animation.ready]).then(function() {
assert_animation_is_not_running_on_compositor(transition,
'Transition suppressed by an animation which is overridden by an ' +
'!important rule reports that it is NOT running on the compositor');
assert_animation_is_not_running_on_compositor(animation,
'Animation overridden by an !important rule reports that it is ' +
'NOT running on the compositor');
});
}, 'Neither transition nor animation does run on the compositor if the ' +
'property is overridden by an !important rule');
</script>
</body>

View file

@ -0,0 +1,35 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>Bug 1216842: effect-level easing function produces negative values (compositor thread)</title>
<style>
#target {
width: 100px; height: 100px;
background: blue;
}
</style>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ opacity: [0, 1] },
{
fill: "forwards",
/* The function produces negative values in (0, 0.766312060) */
easing: "cubic-bezier(0,-0.5,1,-0.5)",
duration: 100,
iterations: 0.75 /* To finish in the extraporation range */
}
);
var animation = new Animation(effect, document.timeline);
animation.play();
animation.finished.then(function() {
document.documentElement.className = "";
});
</script>
</html>

View file

@ -0,0 +1,35 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>Bug 1216842: effect-level easing function produces values greater than 1 (compositor thread)</title>
<style>
#target {
width: 100px; height: 100px;
background: blue;
}
</style>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ opacity: [0, 1] },
{
fill: "forwards",
/* The function produces values greater than 1 in (0.23368794, 1) */
easing: "cubic-bezier(0,1.5,1,1.5)",
duration: 100,
iterations: 0.25 /* To finish in the extraporation range */
}
);
var animation = new Animation(effect, document.timeline);
animation.play();
animation.finished.then(function() {
document.documentElement.className = "";
});
</script>
</html>

View file

@ -0,0 +1,27 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>Bug 1216842: effect-level easing function produces values greater than 1 (main-thread)</title>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ color: ["red", "blue"] },
{
fill: "forwards",
/* The function produces values greater than 1 in (0.23368794, 1) */
easing: "cubic-bezier(0,1.5,1,1.5)",
duration: 100
}
);
var animation = new Animation(effect, document.timeline);
animation.pause();
animation.currentTime = 250;
document.documentElement.className = "";
</script>
</html>

View file

@ -0,0 +1,27 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>Bug 1216842: effect-level easing function produces negative values (main-thread)</title>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ color: ["red", "blue"] },
{
fill: "forwards",
/* The function produces negative values in (0, 0.766312060) */
easing: "cubic-bezier(0,-0.5,1,-0.5)",
duration: 100
}
);
var animation = new Animation(effect, document.timeline);
animation.pause();
animation.currentTime = 250;
document.documentElement.className = "";
</script>
</html>

View file

@ -0,0 +1,38 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>
Bug 1216842: effect-level easing function produces negative values passed
to step-end function (compositor thread)
</title>
<style>
#target {
width: 100px; height: 100px;
background: blue;
}
</style>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ opacity: [0, 1], easing: "step-end" },
{
fill: "forwards",
/* The function produces negative values in (0, 0.766312060) */
easing: "cubic-bezier(0,-0.5,1,-0.5)",
duration: 100,
iterations: 0.75 /* To finish in the extraporation range */
}
);
var animation = new Animation(effect, document.timeline);
animation.play();
animation.finished.then(function() {
document.documentElement.className = "";
});
</script>
</html>

View file

@ -0,0 +1,38 @@
<!doctype html>
<html class="reftest-wait">
<head>
<title>
Bug 1216842: effect-level easing function produces values greater than 1
which are passed to step-end function (compositor thread)
</title>
<style>
#target {
width: 100px; height: 100px;
background: blue;
}
</style>
</head>
<body>
<div id="target"></div>
</body>
<script>
var target = document.getElementById("target");
var effect =
new KeyframeEffectReadOnly(
target,
{ opacity: [0, 1], easing: "step-end" },
{
fill: "forwards",
/* The function produces values greater than 1 in (0.23368794, 1) */
easing: "cubic-bezier(0,1.5,1,1.5)",
duration: 100,
iterations: 0.25 /* To finish in the extraporation range */
}
);
var animation = new Animation(effect, document.timeline);
animation.play();
animation.finished.then(function() {
document.documentElement.className = "";
});
</script>
</html>

View file

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<title>Bug 1239889</title>
</head>
<body>
</body>
<script>
var div = document.createElement('div');
var effect = new KeyframeEffectReadOnly(div, { opacity: [0, 1] });
</script>
</html>

View file

@ -0,0 +1,3 @@
<div id=target><script>
var player = target.animate([{background: 'green'}, {background: 'green'}]);
</script>

View file

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<title>Bug 1272475 - scale function with an extreme large value</title>
<script>
function test() {
var div = document.createElement("div");
div.setAttribute("style", "width: 1px; height: 1px; " +
"background: red;");
document.body.appendChild(div);
div.animate([ { "transform": "scale(8)" },
{ "transform": "scale(9.5e+307)" },
{ "transform": "scale(32)" } ],
{ "duration": 1000, "fill": "both" });
}
</script>
</head>
<body onload="test()">
</body>
</html>

View file

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<title>Bug 1272475 - rotate function with an extreme large value</title>
<script>
function test() {
var div = document.createElement("div");
div.setAttribute("style", "width: 100px; height: 100px; " +
"background: red;");
document.body.appendChild(div);
div.animate([ { "transform": "rotate(8rad)" },
{ "transform": "rotate(9.5e+307rad)" },
{ "transform": "rotate(32rad)" } ],
{ "duration": 1000, "fill": "both" });
}
</script>
</head>
<body onload="test()">
</body>
</html>

View file

@ -0,0 +1,19 @@
<!doctype html>
<head>
<script>
function start() {
var animation = document.body.animate([{marks: 'crop'},{marks: 'crop'}], 12);
document.write('<html><body></body></html>');
setTimeout(function() { animation.play(); }, 4);
setTimeout(function() {
animation.timeline = undefined;
SpecialPowers.Cu.forceGC();
window.top.continueTest();
}, 5);
}
</script>
</head>
<body onload="start()"></body>
</html>

View file

@ -0,0 +1,26 @@
<!doctype html>
<html class="reftest-wait">
<head>
<script>
var count = 0;
function start() {
if (++count > 10) {
document.documentElement.className = "";
return;
}
var frame = document.getElementById("frame");
frame.src = "./1277272-1-inner.html";
}
function continueTest() {
setTimeout(start.bind(window), 1);
}
</script>
</head>
<body onload="start()"></body>
<iframe id="frame">
</html>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
function boom()
{
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, -1e+39, 0, 0)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 1e+39, 0, 0)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 0, 0, -1e+39)" });
document.body.animate([],
{ duration: 6,
easing: "cubic-bezier(0, 0, 0, 1e+39)" });
}
</script>
</head>
<body onload="boom();"></body>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Bug 1290535 - Sort paced subproperties of a shorthand property</title>
<meta charset="UTF-8">
<script>
function test()
{
var div = document.createElement('div');
document.documentElement.appendChild(div);
div.animate([ { borderRadius: "0", borderTopRightRadius: "0" },
{ borderRadius: "50%" } ],
{ spacing:"paced(border-radius)" });
}
</script>
</head>
<body onload="test();"></body>
</html>

View file

@ -0,0 +1,13 @@
pref(dom.animations-api.core.enabled,true) load 1239889-1.html
pref(dom.animations-api.core.enabled,true) load 1244595-1.html
pref(dom.animations-api.core.enabled,true) load 1216842-1.html
pref(dom.animations-api.core.enabled,true) load 1216842-2.html
pref(dom.animations-api.core.enabled,true) load 1216842-3.html
pref(dom.animations-api.core.enabled,true) load 1216842-4.html
pref(dom.animations-api.core.enabled,true) load 1216842-5.html
pref(dom.animations-api.core.enabled,true) load 1216842-6.html
pref(dom.animations-api.core.enabled,true) load 1272475-1.html
pref(dom.animations-api.core.enabled,true) load 1272475-2.html
pref(dom.animations-api.core.enabled,true) load 1278485-1.html
pref(dom.animations-api.core.enabled,true) load 1277272-1.html
pref(dom.animations-api.core.enabled,true) load 1290535-1.html

View file

@ -0,0 +1,154 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes translateAnim {
to { transform: translate(100px) }
}
@keyframes marginLeftAnim {
to { margin-left: 100px }
}
@keyframes marginLeftAnim100To200 {
from { margin-left: 100px }
to { margin-left: 200px }
}
</style>
<body>
<script>
'use strict';
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: translateAnim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_not_equals(getComputedStyle(div).transform, 'none',
'transform style is animated before cancelling');
animation.cancel();
assert_equals(getComputedStyle(div).transform, 'none',
'transform style is no longer animated after cancelling');
});
}, 'Animated style is cleared after cancelling a running CSS animation');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: translateAnim 100s forwards' });
var animation = div.getAnimations()[0];
animation.finish();
return animation.ready.then(function() {
assert_not_equals(getComputedStyle(div).transform, 'none',
'transform style is filling before cancelling');
animation.cancel();
assert_equals(getComputedStyle(div).transform, 'none',
'fill style is cleared after cancelling');
});
}, 'Animated style is cleared after cancelling a filling CSS animation');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: translateAnim 100s' });
var animation = div.getAnimations()[0];
div.addEventListener('animationend', t.step_func(function() {
assert_unreached('Got unexpected end event on cancelled animation');
}));
return animation.ready.then(function() {
// Seek to just before the end then cancel
animation.currentTime = 99.9 * 1000;
animation.cancel();
// Then wait a couple of frames and check that no event was dispatched
return waitForAnimationFrames(2);
});
}, 'Cancelled CSS animations do not dispatch events');
test(function(t) {
var div = addDiv(t, { style: 'animation: marginLeftAnim 100s linear' });
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after cancelling');
animation.currentTime = 50 * 1000;
assert_equals(getComputedStyle(div).marginLeft, '50px',
'margin-left style is updated when cancelled animation is'
+ ' seeked');
}, 'After cancelling an animation, it can still be seeked');
promise_test(function(t) {
var div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after cancelling');
animation.play();
assert_equals(getComputedStyle(div).marginLeft, '100px',
'margin-left style is animated after re-starting animation');
return animation.ready;
}).then(function() {
assert_equals(animation.playState, 'running',
'Animation succeeds in running after being re-started');
});
}, 'After cancelling an animation, it can still be re-used');
test(function(t) {
var div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after cancelling');
// Trigger a change to some animation properties and check that this
// doesn't cause the animation to become live again
div.style.animationDuration = '200s';
flushComputedStyle(div);
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is still not animated after updating'
+ ' animation-duration');
assert_equals(animation.playState, 'idle',
'Animation is still idle after updating animation-duration');
}, 'After cancelling an animation, updating animation properties doesn\'t make'
+ ' it live again');
test(function(t) {
var div =
addDiv(t, { style: 'animation: marginLeftAnim100To200 100s linear' });
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(getComputedStyle(div).marginLeft, '0px',
'margin-left style is not animated after cancelling');
// Make some changes to animation-play-state and check that the
// animation doesn't become live again. This is because it should be
// possible to cancel an animation from script such that all future
// changes to style are ignored.
// Redundant change
div.style.animationPlayState = 'running';
assert_equals(animation.playState, 'idle',
'Animation is still idle after a redundant change to'
+ ' animation-play-state');
// Pause
div.style.animationPlayState = 'paused';
assert_equals(animation.playState, 'idle',
'Animation is still idle after setting'
+ ' animation-play-state: paused');
// Play
div.style.animationPlayState = 'running';
assert_equals(animation.playState, 'idle',
'Animation is still idle after re-setting'
+ ' animation-play-state: running');
}, 'After cancelling an animation, updating animation-play-state doesn\'t'
+ ' make it live again');
done();
</script>
</body>
</html>

View file

@ -0,0 +1,566 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes moveAnimation {
from { margin-left: 100px }
to { margin-left: 200px }
}
</style>
<body>
<script>
'use strict';
// --------------------
// delay
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().delay, 0,
'Initial value of delay');
}, 'delay of a new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().delay, -10 * MS_PER_SEC,
'Initial value of delay');
}, 'Negative delay of a new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 10s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().delay, 10 * MS_PER_SEC,
'Initial value of delay');
}, 'Positive delay of a new animation');
// --------------------
// endDelay
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().endDelay, 0,
'Initial value of endDelay');
}, 'endDelay of a new animation');
// --------------------
// fill
// --------------------
test(function(t) {
var getEffectWithFill = function(fill) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + fill});
return div.getAnimations()[0].effect;
};
var effect = getEffectWithFill('');
assert_equals(effect.getComputedTiming().fill, 'none',
'Initial value of fill');
effect = getEffectWithFill('forwards');
assert_equals(effect.getComputedTiming().fill, 'forwards',
'Fill forwards');
effect = getEffectWithFill('backwards');
assert_equals(effect.getComputedTiming().fill, 'backwards',
'Fill backwards');
effect = getEffectWithFill('both');
assert_equals(effect.getComputedTiming().fill, 'both',
'Fill forwards and backwards');
}, 'fill of a new animation');
// --------------------
// iterationStart
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().iterationStart, 0,
'Initial value of iterationStart');
}, 'iterationStart of a new animation');
// --------------------
// iterations
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().iterations, 1,
'Initial value of iterations');
}, 'iterations of a new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 2016.5'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().iterations, 2016.5,
'Initial value of iterations');
}, 'iterations of a finitely repeating animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().iterations, Infinity,
'Initial value of iterations');
}, 'iterations of an infinitely repeating animation');
// --------------------
// duration
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s -10s infinite'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().duration, 100 * MS_PER_SEC,
'Initial value of duration');
}, 'duration of a new animation');
// --------------------
// direction
// --------------------
test(function(t) {
var getEffectWithDir = function(dir) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s ' + dir});
return div.getAnimations()[0].effect;
};
var effect = getEffectWithDir('');
assert_equals(effect.getComputedTiming().direction, 'normal',
'Initial value of normal direction');
effect = getEffectWithDir('reverse');
assert_equals(effect.getComputedTiming().direction, 'reverse',
'Initial value of reverse direction');
effect = getEffectWithDir('alternate');
assert_equals(effect.getComputedTiming().direction, 'alternate',
'Initial value of alternate direction');
effect = getEffectWithDir('alternate-reverse');
assert_equals(effect.getComputedTiming().direction, 'alternate-reverse',
'Initial value of alternate-reverse direction');
}, 'direction of a new animation');
// --------------------
// easing
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().easing, 'linear',
'Initial value of easing');
}, 'easing of a new animation');
// ------------------------------
// endTime
// = max(start delay + active duration + end delay, 0)
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
'Initial value of endTime');
}, 'endTime of an new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s -5s'});
var effect = div.getAnimations()[0].effect;
var answer = (100 - 5) * MS_PER_SEC;
assert_equals(effect.getComputedTiming().endTime, answer,
'Initial value of endTime');
}, 'endTime of an animation with a negative delay');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s infinite'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().endTime, Infinity,
'Initial value of endTime');
}, 'endTime of an infinitely repeating animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 100s infinite'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().endTime, 100 * MS_PER_SEC,
'Initial value of endTime');
}, 'endTime of an infinitely repeating zero-duration animation');
test(function(t) {
// Fill forwards so div.getAnimations()[0] won't return an
// undefined value.
var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s forwards'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().endTime, 0,
'Initial value of endTime');
}, 'endTime of an animation that finishes before its startTime');
// --------------------
// activeDuration
// = iteration duration * iteration count
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 5'});
var effect = div.getAnimations()[0].effect;
var answer = 100 * MS_PER_SEC * 5;
assert_equals(effect.getComputedTiming().activeDuration, answer,
'Initial value of activeDuration');
}, 'activeDuration of a new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s infinite'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().activeDuration, Infinity,
'Initial value of activeDuration');
}, 'activeDuration of an infinitely repeating animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 1s infinite'});
var effect = div.getAnimations()[0].effect;
// If either the iteration duration or iteration count are zero,
// the active duration is zero.
assert_equals(effect.getComputedTiming().activeDuration, 0,
'Initial value of activeDuration');
}, 'activeDuration of an infinitely repeating zero-duration animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 1s 0'});
var effect = div.getAnimations()[0].effect;
// If either the iteration duration or iteration count are zero,
// the active duration is zero.
assert_equals(effect.getComputedTiming().activeDuration, 0,
'Initial value of activeDuration');
}, 'activeDuration of an animation with zero iterations');
// --------------------
// localTime
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().localTime, 0,
'Initial value of localTime');
}, 'localTime of a new animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var anim = div.getAnimations()[0];
anim.currentTime = 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
'current localTime after setting currentTime');
}, 'localTime of an animation is always equal to currentTime');
promise_test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var anim = div.getAnimations()[0];
anim.playbackRate = 2; // 2 times faster
return anim.ready.then(function() {
assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
'localTime is equal to currentTime');
return waitForFrame();
}).then(function() {
assert_equals(anim.effect.getComputedTiming().localTime, anim.currentTime,
'localTime is equal to currentTime');
});
}, 'localTime reflects playbackRate immediately');
test(function(t) {
var div = addDiv(t);
var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
assert_equals(effect.getComputedTiming().localTime, null,
'localTime for orphaned effect');
}, 'localTime of an AnimationEffect without an Animation');
// --------------------
// progress
// Note: Default timing function is linear.
// --------------------
test(function(t) {
[{fill: '', progress: [ null, null ]},
{fill: 'none', progress: [ null, null ]},
{fill: 'forwards', progress: [ null, 1.0 ]},
{fill: 'backwards', progress: [ 0.0, null ]},
{fill: 'both', progress: [ 0.0, 1.0 ]}]
.forEach(function(test) {
var div =
addDiv(t, {style: 'animation: moveAnimation 100s 10s ' + test.fill});
var anim = div.getAnimations()[0];
assert_true(anim.effect.getComputedTiming().progress === test.progress[0],
'initial progress with "' + test.fill + '" fill');
anim.finish();
assert_true(anim.effect.getComputedTiming().progress === test.progress[1],
'finished progress with "' + test.fill + '" fill');
});
}, 'progress of an animation with different fill modes');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 10s 10 both'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Initial value of progress');
anim.currentTime += 2.5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.finish()
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Value of progress');
}, 'progress of an integral repeating animation with normal direction');
test(function(t) {
var div = addDiv(t);
// Note: FillMode here is "both" because
// 1. Since this a zero-duration animation, it will already have finished
// so it won't be returned by getAnimations() unless it fills forwards.
// 2. Fill backwards, so the progress before phase wouldn't be
// unresolved (null value).
var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Initial value of progress in after phase');
// Seek backwards
anim.currentTime -= 1 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Value of progress before phase');
}, 'progress of an infinitely repeating zero-duration animation');
test(function(t) {
// Default iterations = 1
var div = addDiv(t, {style: 'animation: moveAnimation 0s both'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Initial value of progress in after phase');
// Seek backwards
anim.currentTime -= 1 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Value of progress before phase');
}, 'progress of a finitely repeating zero-duration animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Initial value of progress (before phase)');
// Using iteration duration of 1 now.
// currentIteration now is floor(10.25) = 10, so progress should be 25%.
anim.finish();
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress in after phase');
}, 'progress of a non-integral repeating zero-duration animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 5s 10.25 both reverse'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Initial value of progress (before phase)');
// Seek forwards
anim.finish();
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress in after phase');
}, 'Progress of a non-integral repeating zero-duration animation ' +
'with reversing direction');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Initial value of progress');
anim.currentTime += 2.5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
anim.finish()
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
}, 'progress of a non-integral repeating animation ' +
'with alternate direction');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 10s 10.25 both alternate-reverse'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Initial value of progress');
anim.currentTime += 2.5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.currentTime += 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.finish()
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
}, 'progress of a non-integral repeating animation ' +
'with alternate-reversing direction');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Initial value of progress');
anim.currentTime += 2.5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
anim.currentTime -= 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.0,
'Value of progress');
anim.finish()
assert_equals(anim.effect.getComputedTiming().progress, 0.25,
'Value of progress');
}, 'progress of a non-integral repeating zero-duration animation ' +
'with alternate direction');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.25 both alternate-reverse'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Initial value of progress');
anim.currentTime += 2.5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
anim.currentTime -= 5 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().progress, 1.0,
'Value of progress');
anim.finish()
assert_equals(anim.effect.getComputedTiming().progress, 0.75,
'Value of progress');
}, 'progress of a non-integral repeating zero-duration animation ' +
'with alternate-reverse direction');
// --------------------
// currentIteration
// --------------------
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 2s'});
var effect = div.getAnimations()[0].effect;
assert_equals(effect.getComputedTiming().currentIteration, null,
'Initial value of currentIteration before phase');
}, 'currentIteration of a new animation with no backwards fill is unresolved ' +
'in before phase');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Initial value of currentIteration');
}, 'currentIteration of a new animation is zero');
test(function(t) {
// Note: FillMode here is "both" because
// 1. Since this a zero-duration animation, it will already have finished
// so it won't be returned by getAnimations() unless it fills forwards.
// 2. Fill backwards, so the currentIteration (before phase) wouldn't be
// unresolved (null value).
var div = addDiv(t, {style: 'animation: moveAnimation 0s infinite both'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().currentIteration, Infinity,
'Initial value of currentIteration in after phase');
// Seek backwards
anim.currentTime -= 2 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Value of currentIteration count during before phase');
}, 'currentIteration of an infinitely repeating zero-duration animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 0s 10.5 both'});
var anim = div.getAnimations()[0];
// Note: currentIteration = ceil(iteration start + iteration count) - 1
assert_equals(anim.effect.getComputedTiming().currentIteration, 10,
'Initial value of currentIteration');
// Seek backwards
anim.currentTime -= 2 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Value of currentIteration count during before phase');
}, 'currentIteration of a finitely repeating zero-duration animation');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 5.5 forwards'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Initial value of currentIteration');
// The 3rd iteration
// Note: currentIteration = floor(scaled active time / iteration duration)
anim.currentTime = 250 * MS_PER_SEC;
assert_equals(anim.effect.getComputedTiming().currentIteration, 2,
'Value of currentIteration during the 3rd iteration');
// Finish
anim.finish();
assert_equals(anim.effect.getComputedTiming().currentIteration, 5,
'Value of currentIteration in after phase');
}, 'currentIteration of an animation with a non-integral iteration count');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s 2 forwards'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Initial value of currentIteration');
// Finish
anim.finish();
assert_equals(anim.effect.getComputedTiming().currentIteration, 1,
'Value of currentIteration in after phase');
}, 'currentIteration of an animation with an integral iteration count');
test(function(t) {
var div = addDiv(t, {style: 'animation: moveAnimation 100s forwards'});
var anim = div.getAnimations()[0];
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Initial value of currentIteration');
// Finish
anim.finish();
assert_equals(anim.effect.getComputedTiming().currentIteration, 0,
'Value of currentIteration in after phase');
}, 'currentIteration of an animation with a default iteration count');
test(function(t) {
var div = addDiv(t);
var effect = new KeyframeEffectReadOnly(div, {left: ["0px", "100px"]});
assert_equals(effect.getComputedTiming().currentIteration, null,
'currentIteration for orphaned effect');
}, 'currentIteration of an AnimationEffect without an Animation');
// TODO: If iteration duration is Infinity, currentIteration is 0.
// However, we cannot set iteration duration to Infinity in CSS Animation now.
done();
</script>
</body>

View file

@ -0,0 +1,345 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Tests for the effect of setting a CSS animation's
Animation.currentTime</title>
<style>
.animated-div {
margin-left: 10px;
/* Make it easier to calculate expected values: */
animation-timing-function: linear ! important;
}
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<script src="../testcommon.js"></script>
</head>
<body>
<script type="text/javascript">
'use strict';
// TODO: We should separate this test(Testing for CSS Animation events /
// Testing for currentTime of Web Animation).
// e.g:
// CSS Animation events test :
// - check the firing an event using Animation.currentTime
// The current Time of Web Animation test :
// - check an current time value on several situation(init / processing..)
// - Based on W3C Spec, check the behavior of setting current time.
// TODO: Once the computedTiming property is implemented, add checks to the
// checker helpers to ensure that computedTiming's properties are updated as
// expected.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
const CSS_ANIM_EVENTS =
['animationstart', 'animationiteration', 'animationend'];
test(function(t)
{
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = "anim 100s";
var animation = div.getAnimations()[0];
// Animations shouldn't start until the next paint tick, so:
assert_equals(animation.currentTime, 0,
'Animation.currentTime should be zero when an animation ' +
'is initially created');
// Make sure the animation is running before we set the current time.
animation.startTime = animation.timeline.currentTime;
animation.currentTime = 50 * MS_PER_SEC;
assert_times_equal(animation.currentTime, 50 * MS_PER_SEC,
'Check setting of currentTime actually works');
}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
'currentTime');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
// the 0.0001 here is for rounding error
assert_less_than_equal(animation.currentTime,
animation.timeline.currentTime - animation.startTime + 0.0001,
'Animation.currentTime should be less than the local time ' +
'equivalent of the timeline\'s currentTime on the first paint tick ' +
'after animation creation');
animation.currentTime = 100 * MS_PER_SEC;
return eventWatcher.wait_for('animationstart');
}).then(function() {
animation.currentTime = 200 * MS_PER_SEC;
return eventWatcher.wait_for('animationend');
});
}, 'Skipping forward through animation');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
animation.currentTime = 200 * MS_PER_SEC;
var previousTimelineTime = animation.timeline.currentTime;
return eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
assert_true(document.timeline.currentTime - previousTimelineTime <
100 * MS_PER_SEC,
'Sanity check that seeking worked rather than the events ' +
'firing after normal playback through the very long ' +
'animation duration');
animation.currentTime = 150 * MS_PER_SEC;
return eventWatcher.wait_for('animationstart');
}).then(function() {
animation.currentTime = 0;
return eventWatcher.wait_for('animationend');
});
}, 'Skipping backwards through animation');
// Next we have multiple tests to check that redundant currentTime changes do
// NOT dispatch events. It's impossible to distinguish between events not being
// dispatched and events just taking an incredibly long time to dispatch
// without waiting an infinitely long time. Obviously we don't want to do that
// (block this test from finishing forever), so instead we just listen for
// events until two animation frames (i.e. requestAnimationFrame callbacks)
// have happened, then assume that no events will ever be dispatched for the
// redundant changes if no events were detected in that time.
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
animation.currentTime = 150 * MS_PER_SEC;
animation.currentTime = 50 * MS_PER_SEC;
return waitForAnimationFrames(2);
}, 'Redundant change, before -> active, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
animation.currentTime = 250 * MS_PER_SEC;
animation.currentTime = 50 * MS_PER_SEC;
return waitForAnimationFrames(2);
}, 'Redundant change, before -> after, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for('animationstart').then(function() {
animation.currentTime = 50 * MS_PER_SEC;
animation.currentTime = 150 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.currentTime = 150 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, active -> before, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for('animationstart').then(function() {
animation.currentTime = 250 * MS_PER_SEC;
animation.currentTime = 150 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.currentTime = 150 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, active -> after, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.currentTime = 50 * MS_PER_SEC;
animation.currentTime = 250 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.currentTime = 250 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, after -> before, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.currentTime = 150 * MS_PER_SEC;
animation.currentTime = 250 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.currentTime = 250 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, after -> active, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s"
var animation = div.getAnimations()[0];
animation.pause();
animation.currentTime = 150 * MS_PER_SEC;
return eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.currentTime = 50 * MS_PER_SEC;
return eventWatcher.wait_for('animationstart');
});
}, 'Seeking finished -> paused dispatches animationstart');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = "anim 100s";
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
var exception;
try {
animation.currentTime = null;
} catch (e) {
exception = e;
}
assert_equals(exception.name, 'TypeError',
'Expect TypeError exception on trying to set ' +
'Animation.currentTime to null');
});
}, 'Setting currentTime to null');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
var pauseTime;
return animation.ready.then(function() {
assert_not_equals(animation.currentTime, null,
'Animation.currentTime not null on ready Promise resolve');
animation.pause();
return animation.ready;
}).then(function() {
pauseTime = animation.currentTime;
return waitForFrame();
}).then(function() {
assert_equals(animation.currentTime, pauseTime,
'Animation.currentTime is unchanged after pausing');
});
}, 'Animation.currentTime after pausing');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = "anim 100s";
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
// just before animation ends:
animation.currentTime = 100 * MS_PER_SEC - 1;
return waitForAnimationFrames(2);
}).then(function() {
assert_equals(animation.currentTime, 100 * MS_PER_SEC,
'Animation.currentTime should not continue to increase after the ' +
'animation has finished');
});
}, 'Animation.currentTime clamping');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = "anim 100s";
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
// play backwards:
animation.playbackRate = -1;
// just before animation ends (at the "start"):
animation.currentTime = 1;
return waitForAnimationFrames(2);
}).then(function() {
assert_equals(animation.currentTime, 0,
'Animation.currentTime should not continue to decrease after an ' +
'animation running in reverse has finished and currentTime is zero');
});
}, 'Animation.currentTime clamping for reversed animation');
test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(animation.currentTime, null,
'The currentTime of a cancelled animation should be null');
}, 'Animation.currentTime after cancelling');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.finish();
// Initiate a pause then abort it
animation.pause();
animation.play();
// Wait to return to running state
return animation.ready;
}).then(function() {
assert_true(animation.currentTime < 100 * 1000,
'After aborting a pause when finished, the currentTime should'
+ ' jump back towards the start of the animation');
});
}, 'After aborting a pause when finished, the call to play() should rewind'
+ ' the current time');
done();
</script>
</body>
</html>

View file

@ -0,0 +1,97 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<body>
<script>
'use strict';
const ANIM_PROP_VAL = 'anim 100s';
const ANIM_DURATION = 100000; // ms
test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
div.style.animationIterationCount = 'infinite';
var animation = div.getAnimations()[0];
var threw = false;
try {
animation.finish();
} catch (e) {
threw = true;
assert_equals(e.name, 'InvalidStateError',
'Exception should be an InvalidStateError exception when ' +
'trying to finish an infinite animation');
}
assert_true(threw,
'Expect InvalidStateError exception trying to finish an ' +
'infinite animation');
}, 'Test exceptions when finishing infinite animation');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL + ' paused';
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
animation.finish();
assert_equals(animation.playState, 'finished',
'The play state of a paused animation should become ' +
'"finished" after finish() is called');
assert_approx_equals(animation.startTime,
animation.timeline.currentTime - ANIM_DURATION,
0.0001,
'The start time of a paused animation should be set ' +
'after calling finish()');
t.done();
}));
}, 'Test finish() while paused');
test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL + ' paused';
var animation = div.getAnimations()[0];
// Update playbackRate so we can test that the calculated startTime
// respects it
animation.playbackRate = 2;
// While animation is still pause-pending call finish()
animation.finish();
assert_equals(animation.playState, 'finished',
'The play state of a pause-pending animation should become ' +
'"finished" after finish() is called');
assert_approx_equals(animation.startTime,
animation.timeline.currentTime - ANIM_DURATION / 2,
0.0001,
'The start time of a pause-pending animation should ' +
'be set after calling finish()');
}, 'Test finish() while pause-pending with positive playbackRate');
test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL + ' paused';
var animation = div.getAnimations()[0];
animation.playbackRate = -2;
animation.finish();
assert_equals(animation.playState, 'finished',
'The play state of a pause-pending animation should become ' +
'"finished" after finish() is called');
assert_equals(animation.startTime, animation.timeline.currentTime,
'The start time of a pause-pending animation should be ' +
'set after calling finish()');
}, 'Test finish() while pause-pending with negative playbackRate');
done();
</script>
</body>

View file

@ -0,0 +1,93 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes abc {
to { transform: translate(10px) }
}
@keyframes def {}
</style>
<body>
<script>
'use strict';
const ANIM_PROP_VAL = 'abc 100s';
const ANIM_DURATION = 100 * MS_PER_SEC;
promise_test(function(t) {
var div = addDiv(t);
// Set up pending animation
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
// Set up listeners on finished promise
var retPromise = animation.finished.then(function() {
assert_unreached('finished promise is fulfilled');
}).catch(function(err) {
assert_equals(err.name, 'AbortError',
'finished promise is rejected with AbortError');
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should change after the original is ' +
'rejected');
});
// Now cancel the animation and flush styles
div.style.animation = '';
window.getComputedStyle(div).animation;
return retPromise;
}, 'finished promise is rejected when an animation is cancelled by resetting ' +
'the animation property');
promise_test(function(t) {
var div = addDiv(t);
// As before, but this time instead of removing all animations, simply update
// the list of animations. At least for Firefox, updating is a different
// code path.
// Set up pending animation
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
// Set up listeners on finished promise
var retPromise = animation.finished.then(function() {
assert_unreached('finished promise was fulfilled');
}).catch(function(err) {
assert_equals(err.name, 'AbortError',
'finished promise is rejected with AbortError');
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should change after the original is ' +
'rejected');
});
// Now update the animation and flush styles
div.style.animation = 'def 100s';
window.getComputedStyle(div).animation;
return retPromise;
}, 'finished promise is rejected when an animation is cancelled by changing ' +
'the animation property');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
animation.currentTime = ANIM_DURATION;
return animation.finished.then(function() {
div.style.animationPlayState = 'running';
return waitForAnimationFrames(2);
}).then(function() {
assert_equals(animation.finished, previousFinishedPromise,
'Should not replay when animation-play-state changes to ' +
'"running" on finished animation');
assert_equals(animation.currentTime, ANIM_DURATION,
'currentTime should not change when animation-play-state ' +
'changes to "running" on finished animation');
});
}, 'Test finished promise changes when animationPlayState set to running');
done();
</script>
</body>

View file

@ -0,0 +1,24 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes abc { }
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t);
div.style.animation = 'abc 100s';
var animation = div.getAnimations()[0];
assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
animation.id = 'anim'
assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
}, 'Animation.id for CSS Animations');
done();
</script>
</body>
</html>

View file

@ -0,0 +1,165 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim {
0% { margin-left: 0px }
100% { margin-left: 10000px }
}
</style>
<body>
<script>
'use strict';
function getMarginLeft(cs) {
return parseFloat(cs.marginLeft);
}
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
assert_equals(getMarginLeft(cs), 0,
'Initial value of margin-left is zero');
animation.play();
return animation.ready.then(waitForFrame).then(function() {
assert_true(getMarginLeft(cs) > 0,
'Playing value of margin-left is greater than zero');
});
}, 'play() overrides animation-play-state');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
assert_equals(getMarginLeft(cs), 0,
'Initial value of margin-left is zero');
animation.pause();
div.style.animationPlayState = 'running';
return animation.ready.then(waitForFrame).then(function() {
assert_equals(cs.animationPlayState, 'running',
'animation-play-state is running');
assert_equals(getMarginLeft(cs), 0,
'Paused value of margin-left is zero');
});
}, 'pause() overrides animation-play-state');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
assert_equals(getMarginLeft(cs), 0,
'Initial value of margin-left is zero');
animation.play();
var previousAnimVal;
return animation.ready.then(function() {
div.style.animationPlayState = 'running';
cs.animationPlayState; // Trigger style resolution
return waitForFrame();
}).then(function() {
assert_equals(cs.animationPlayState, 'running',
'animation-play-state is running');
div.style.animationPlayState = 'paused';
return animation.ready;
}).then(function() {
assert_equals(cs.animationPlayState, 'paused',
'animation-play-state is paused');
previousAnimVal = getMarginLeft(cs);
return waitForFrame();
}).then(function() {
assert_equals(getMarginLeft(cs), previousAnimVal,
'Animated value of margin-left does not change when'
+ ' paused by style');
});
}, 'play() is overridden by later setting "animation-play-state: paused"');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s';
var animation = div.getAnimations()[0];
assert_equals(getMarginLeft(cs), 0,
'Initial value of margin-left is zero');
// Set the specified style first. If implementations fail to
// apply the style changes first, they will ignore the redundant
// call to play() and fail to correctly override the pause style.
div.style.animationPlayState = 'paused';
animation.play();
var previousAnimVal = getMarginLeft(cs);
return animation.ready.then(waitForFrame).then(function() {
assert_equals(cs.animationPlayState, 'paused',
'animation-play-state is paused');
assert_true(getMarginLeft(cs) > previousAnimVal,
'Playing value of margin-left is increasing');
});
}, 'play() flushes pending changes to animation-play-state first');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
assert_equals(getMarginLeft(cs), 0,
'Initial value of margin-left is zero');
// Unlike the previous test for play(), since calling pause() is sticky,
// we'll apply it even if the underlying style also says we're paused.
//
// We would like to test that implementations flush styles before running
// pause() but actually there's no style we can currently set that will
// change the behavior of pause(). That may change in the future
// (e.g. if we introduce animation-timeline or animation-playback-rate etc.).
//
// For now this just serves as a sanity check that we do the same thing
// even if we set style before calling the API.
div.style.animationPlayState = 'running';
animation.pause();
var previousAnimVal = getMarginLeft(cs);
return animation.ready.then(waitForFrame).then(function() {
assert_equals(cs.animationPlayState, 'running',
'animation-play-state is running');
assert_equals(getMarginLeft(cs), previousAnimVal,
'Paused value of margin-left does not change');
});
}, 'pause() applies pending changes to animation-play-state first');
// (Note that we can't actually test for this; see comment above, in test-body.)
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 1000s' });
var animation = div.getAnimations()[0];
var readyPromiseRun = false;
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
assert_equals(animation.playState, 'pending', 'Animation is pause pending');
// Set current time
animation.currentTime = 5 * MS_PER_SEC;
assert_equals(animation.playState, 'paused',
'Animation is paused immediately after setting currentTime');
assert_equals(animation.startTime, null,
'Animation startTime is unresolved immediately after ' +
'setting currentTime');
assert_equals(animation.currentTime, 5 * MS_PER_SEC,
'Animation currentTime does not change when forcing a ' +
'pause operation to complete');
// The ready promise should now be resolved. If it's not then test will
// probably time out before anything else happens that causes it to resolve.
return animation.ready;
});
}, 'Setting the current time completes a pending pause');
done();
</script>
</body>

View file

@ -0,0 +1,71 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim { }
</style>
<body>
<script>
'use strict';
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s';
var animation = div.getAnimations()[0];
assert_equals(animation.playState, 'pending');
return animation.ready.then(function() {
assert_equals(animation.playState, 'running');
});
}, 'Animation returns correct playState when running');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
assert_equals(animation.playState, 'pending');
return animation.ready.then(function() {
assert_equals(animation.playState, 'paused');
});
}, 'Animation returns correct playState when paused');
promise_test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s';
var animation = div.getAnimations()[0];
animation.pause();
assert_equals(animation.playState, 'pending');
return animation.ready.then(function() {
assert_equals(animation.playState, 'paused');
});
}, 'Animation.playState updates when paused by script');
test(function(t) {
var div = addDiv(t);
var cs = window.getComputedStyle(div);
div.style.animation = 'anim 1000s paused';
var animation = div.getAnimations()[0];
div.style.animationPlayState = 'running';
// This test also checks that calling playState flushes style
assert_equals(animation.playState, 'pending',
'Animation.playState reports pending after updating'
+ ' animation-play-state (got: ' + animation.playState + ')');
}, 'Animation.playState updates when resumed by setting style');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim 1000s';
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(animation.playState, 'idle');
}, 'Animation returns correct playState when cancelled');
done();
</script>
</body>

View file

@ -0,0 +1,149 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes abc {
to { transform: translate(10px) }
}
</style>
<body>
<script>
'use strict';
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'abc 100s paused';
var animation = div.getAnimations()[0];
var originalReadyPromise = animation.ready;
return animation.ready.then(function() {
div.style.animationPlayState = 'running';
assert_not_equals(animation.ready, originalReadyPromise,
'After updating animation-play-state a new ready promise'
+ ' object is created');
});
}, 'A new ready promise is created when setting animation-play-state: running');
promise_test(function(t) {
var div = addDiv(t);
// Set up pending animation
div.style.animation = 'abc 100s';
var animation = div.getAnimations()[0];
assert_equals(animation.playState, 'pending',
'Animation is initially pending');
// Set up listeners on ready promise
var retPromise = animation.ready.then(function() {
assert_unreached('ready promise is fulfilled');
}).catch(function(err) {
assert_equals(err.name, 'AbortError',
'ready promise is rejected with AbortError');
});
// Now cancel the animation and flush styles
div.style.animation = '';
window.getComputedStyle(div).animation;
return retPromise;
}, 'ready promise is rejected when an animation is cancelled by resetting'
+ ' the animation property');
promise_test(function(t) {
var div = addDiv(t);
// As before, but this time instead of removing all animations, simply update
// the list of animations. At least for Firefox, updating is a different
// code path.
// Set up pending animation
div.style.animation = 'abc 100s';
var animation = div.getAnimations()[0];
assert_equals(animation.playState, 'pending',
'Animation is initially pending');
// Set up listeners on ready promise
var retPromise = animation.ready.then(function() {
assert_unreached('ready promise was fulfilled');
}).catch(function(err) {
assert_equals(err.name, 'AbortError',
'ready promise is rejected with AbortError');
});
// Now update the animation and flush styles
div.style.animation = 'def 100s';
window.getComputedStyle(div).animation;
return retPromise;
}, 'ready promise is rejected when an animation is cancelled by updating'
+ ' the animation property');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: abc 100s' });
var animation = div.getAnimations()[0];
var originalReadyPromise = animation.ready;
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
assert_not_equals(animation.ready, originalReadyPromise,
'A new Promise object is generated when setting'
+ ' animation-play-state: paused');
});
}, 'A new ready promise is created when setting animation-play-state: paused');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: abc 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
var firstReadyPromise = animation.ready;
animation.pause();
assert_equals(animation.ready, firstReadyPromise,
'Ready promise objects are identical after redundant pause');
});
}, 'Pausing twice re-uses the same Promise');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: abc 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
// Flush style and verify we're pending at the same time
assert_equals(animation.playState, 'pending', 'Animation is pending');
var pauseReadyPromise = animation.ready;
// Now play again immediately
div.style.animationPlayState = 'running';
assert_equals(animation.playState, 'pending', 'Animation is still pending');
assert_equals(animation.ready, pauseReadyPromise,
'The pause Promise is re-used when playing while waiting'
+ ' to pause');
return animation.ready;
}).then(function() {
assert_equals(animation.playState, 'running',
'Animation is running after aborting a pause');
});
}, 'If a pause operation is interrupted, the ready promise is reused');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: abc 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
return animation.ready;
}).then(function(resolvedAnimation) {
assert_equals(resolvedAnimation, animation,
'Promise received when ready Promise for a pause operation'
+ ' is completed is the animation on which the pause was'
+ ' performed');
});
}, 'When a pause is complete the Promise callback gets the correct animation');
done();
</script>
</body>

View file

@ -0,0 +1,29 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim {
to { transform: translate(100px) }
}
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
div.style.animation = "";
flushComputedStyle(div);
assert_equals(animation.currentTime, null);
animation.reverse();
assert_equals(animation.currentTime, 100 * MS_PER_SEC,
'animation.currentTime should be its effect end');
}, 'reverse() from idle state starts playing the animation');
done();
</script>
</body>

View file

@ -0,0 +1,383 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Tests for the effect of setting a CSS animation's
Animation.startTime</title>
<style>
.animated-div {
margin-left: 10px;
/* Make it easier to calculate expected values: */
animation-timing-function: linear ! important;
}
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<script src="../testcommon.js"></script>
</head>
<body>
<script type="text/javascript">
'use strict';
// TODO: We should separate this test(Testing for CSS Animation events /
// Testing for start time of Web Animation).
// e.g:
// CSS Animation events test:
// - check the firing an event after setting an Animation.startTime
// The start time of Web Animation test:
// - check an start time value on several situation(init / processing..)
// - Based on W3C Spec, check the behavior of setting current time.
// TODO: Once the computedTiming property is implemented, add checks to the
// checker helpers to ensure that computedTiming's properties are updated as
// expected.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
const CSS_ANIM_EVENTS =
['animationstart', 'animationiteration', 'animationend'];
test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var animation = div.getAnimations()[0];
assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (play-pending) animation is unresolved');
test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
var animation = div.getAnimations()[0];
assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (pause-pending) animation is unresolved');
promise_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_true(animation.startTime > 0,
'startTime is resolved when running');
});
}, 'startTime is resolved when running');
promise_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_equals(animation.startTime, null,
'startTime is unresolved when paused');
});
}, 'startTime is unresolved when paused');
promise_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
assert_not_equals(animation.startTime, null,
'startTime is resolved when pause-pending');
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
assert_not_equals(animation.startTime, null,
'startTime is preserved when a pause is aborted');
});
}, 'startTime while pause-pending and play-pending');
promise_test(function(t) {
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var animation = div.getAnimations()[0];
// Seek to end to put us in the finished state
animation.currentTime = 100 * MS_PER_SEC;
return animation.ready.then(function() {
// Call play() which puts us back in the running state
animation.play();
assert_equals(animation.startTime, null, 'startTime is unresolved');
});
}, 'startTime while play-pending from finished state');
test(function(t) {
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var animation = div.getAnimations()[0];
animation.finish();
// Call play() which puts us back in the running state
animation.play();
assert_equals(animation.startTime, null, 'startTime is unresolved');
}, 'startTime while play-pending from finished state using finish()');
promise_test(function(t) {
var div = addDiv(t, { style: 'animation: anim 100s' });
var animation = div.getAnimations()[0];
assert_equals(animation.startTime, null, 'The initial startTime is null');
var initialTimelineTime = document.timeline.currentTime;
return animation.ready.then(function() {
assert_true(animation.startTime > initialTimelineTime,
'After the animation has started, startTime is greater than ' +
'the time when it was started');
var startTimeBeforePausing = animation.startTime;
div.style.animationPlayState = 'paused';
// Flush styles just in case querying animation.startTime doesn't flush
// styles (which would be a bug in of itself and could mask a further bug
// by causing startTime to appear to not change).
getComputedStyle(div).animationPlayState;
assert_equals(animation.startTime, startTimeBeforePausing,
'The startTime does not change when pausing-pending');
return animation.ready;
}).then(function() {
assert_equals(animation.startTime, null,
'After actually pausing, the startTime of an animation ' +
'is null');
});
}, 'Pausing should make the startTime become null');
test(function(t)
{
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s 100s';
var animation = div.getAnimations()[0];
var currentTime = animation.timeline.currentTime;
animation.startTime = currentTime;
assert_times_equal(animation.startTime, currentTime,
'Check setting of startTime actually works');
}, 'Sanity test to check round-tripping assigning to a new animation\'s ' +
'startTime');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = 'anim 100s 100s';
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
'Animation.startTime should be less than the timeline\'s ' +
'currentTime on the first paint tick after animation creation');
animation.startTime = animation.timeline.currentTime - 100 * MS_PER_SEC;
return eventWatcher.wait_for('animationstart');
}).then(function() {
animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
return eventWatcher.wait_for('animationend');
});
}, 'Skipping forward through animation');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = 'anim 100s 100s';
var animation = div.getAnimations()[0];
animation.startTime = animation.timeline.currentTime - 200 * MS_PER_SEC;
var previousTimelineTime = animation.timeline.currentTime;
return eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
assert_true(document.timeline.currentTime - previousTimelineTime <
100 * MS_PER_SEC,
'Sanity check that seeking worked rather than the events ' +
'firing after normal playback through the very long ' +
'animation duration');
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
// Despite going backwards from after the end of the animation (to being
// in the active interval), we now expect an 'animationstart' event
// because the animation should go from being inactive to active.
return eventWatcher.wait_for('animationstart');
}).then(function() {
animation.startTime = animation.timeline.currentTime;
// Despite going backwards from just after the active interval starts to
// the animation start time, we now expect an animationend event
// because we went from inside to outside the active interval.
return eventWatcher.wait_for('animationend');
}).then(function() {
assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
'Animation.startTime should be less than the timeline\'s ' +
'currentTime on the first paint tick after animation creation');
});
}, 'Skipping backwards through animation');
// Next we have multiple tests to check that redundant startTime changes do NOT
// dispatch events. It's impossible to distinguish between events not being
// dispatched and events just taking an incredibly long time to dispatch
// without waiting an infinitely long time. Obviously we don't want to do that
// (block this test from finishing forever), so instead we just listen for
// events until two animation frames (i.e. requestAnimationFrame callbacks)
// have happened, then assume that no events will ever be dispatched for the
// redundant changes if no events were detected in that time.
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
return waitForAnimationFrames(2);
}, 'Redundant change, before -> active, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
return waitForAnimationFrames(2);
}, 'Redundant change, before -> after, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for('animationstart').then(function() {
animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, active -> before, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for('animationstart').then(function() {
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, active -> after, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.startTime = animation.timeline.currentTime - 50 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, after -> before, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
div.style.animation = "anim 100s 100s";
var animation = div.getAnimations()[0];
var retPromise = eventWatcher.wait_for(['animationstart',
'animationend']).then(function() {
animation.startTime = animation.timeline.currentTime - 150 * MS_PER_SEC;
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
return waitForAnimationFrames(2);
});
// get us into the initial state:
animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
return retPromise;
}, 'Redundant change, after -> active, then back');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s 100s';
var animation = div.getAnimations()[0];
var storedCurrentTime;
return animation.ready.then(function() {
storedCurrentTime = animation.currentTime;
animation.startTime = null;
return animation.ready;
}).then(function() {
assert_equals(animation.currentTime, storedCurrentTime,
'Test that hold time is correct');
});
}, 'Setting startTime to null');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
var savedStartTime = animation.startTime;
assert_not_equals(animation.startTime, null,
'Animation.startTime not null on ready Promise resolve');
animation.pause();
return animation.ready;
}).then(function() {
assert_equals(animation.startTime, null,
'Animation.startTime is null after paused');
assert_equals(animation.playState, 'paused',
'Animation.playState is "paused" after pause() call');
});
}, 'Animation.startTime after pausing');
promise_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
return animation.ready.then(function() {
animation.cancel();
assert_equals(animation.startTime, null,
'The startTime of a cancelled animation should be null');
});
}, 'Animation.startTime after cancelling');
done();
</script>
</body>
</html>

View file

@ -0,0 +1,154 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim1 {
to { left: 100px }
}
@keyframes anim2 { }
</style>
<body>
<script>
'use strict';
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s';
var originalAnimation = div.getAnimations()[0];
var originalStartTime;
var originalCurrentTime;
// Wait a moment so we can confirm the startTime doesn't change (and doesn't
// simply reflect the current time).
return originalAnimation.ready.then(function() {
originalStartTime = originalAnimation.startTime;
originalCurrentTime = originalAnimation.currentTime;
// Wait a moment so we can confirm the startTime doesn't change (and
// doesn't simply reflect the current time).
return waitForFrame();
}).then(function() {
div.style.animationDuration = '200s';
var animation = div.getAnimations()[0];
assert_equals(animation, originalAnimation,
'The same Animation is returned after updating'
+ ' animation duration');
assert_equals(animation.startTime, originalStartTime,
'Animations returned by getAnimations preserve'
+ ' their startTime even when they are updated');
// Sanity check
assert_not_equals(animation.currentTime, originalCurrentTime,
'Animation.currentTime has updated in next'
+ ' requestAnimationFrame callback');
});
}, 'Animations preserve their startTime when changed');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s, anim1 100s';
// Store original state
var animations = div.getAnimations();
var animation1 = animations[0];
var animation2 = animations[1];
// Update first in list
div.style.animationDuration = '200s, 100s';
animations = div.getAnimations();
assert_equals(animations[0], animation1,
'First Animation is in same position after update');
assert_equals(animations[1], animation2,
'Second Animation is in same position after update');
}, 'Updated Animations maintain their order in the list');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 200s, anim1 100s';
// Store original state
var animations = div.getAnimations();
var animation1 = animations[0];
var animation2 = animations[1];
// Wait before continuing so we can compare start times (otherwise the
// new Animation objects and existing Animation objects will all have the same
// start time).
return waitForAllAnimations(animations).then(waitForFrame).then(function() {
// Swap duration of first and second in list and prepend animation at the
// same time
div.style.animation = 'anim1 100s, anim1 100s, anim1 200s';
animations = div.getAnimations();
assert_true(animations[0] !== animation1 && animations[0] !== animation2,
'New Animation is prepended to start of list');
assert_equals(animations[1], animation1,
'First Animation is in second position after update');
assert_equals(animations[2], animation2,
'Second Animation is in third position after update');
assert_equals(animations[1].startTime, animations[2].startTime,
'Old Animations have the same start time');
// TODO: Check that animations[0].startTime === null
return animations[0].ready;
}).then(function() {
assert_true(animations[0].startTime > animations[1].startTime,
'New Animation has later start time');
});
}, 'Only the startTimes of existing animations are preserved');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s, anim1 100s';
var secondAnimation = div.getAnimations()[1];
// Wait before continuing so we can compare start times
return secondAnimation.ready.then(waitForFrame).then(function() {
// Trim list of animations
div.style.animationName = 'anim1';
var animations = div.getAnimations();
assert_equals(animations.length, 1, 'List of Animations was trimmed');
assert_equals(animations[0], secondAnimation,
'Remaining Animation is the second one in the list');
assert_equals(typeof(animations[0].startTime), 'number',
'Remaining Animation has resolved startTime');
assert_true(animations[0].startTime < animations[0].timeline.currentTime,
'Remaining Animation preserves startTime');
});
}, 'Animations are removed from the start of the list while preserving'
+ ' the state of existing Animations');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s';
var firstAddedAnimation = div.getAnimations()[0],
secondAddedAnimation,
animations;
// Wait and add second Animation
return firstAddedAnimation.ready.then(waitForFrame).then(function() {
div.style.animation = 'anim1 100s, anim1 100s';
secondAddedAnimation = div.getAnimations()[0];
// Wait again and add another Animation
return secondAddedAnimation.ready.then(waitForFrame);
}).then(function() {
div.style.animation = 'anim1 100s, anim2 100s, anim1 100s';
animations = div.getAnimations();
assert_not_equals(firstAddedAnimation, secondAddedAnimation,
'New Animations are added to start of the list');
assert_equals(animations[0], secondAddedAnimation,
'Second Animation remains in same position after'
+ ' interleaving');
assert_equals(animations[2], firstAddedAnimation,
'First Animation remains in same position after'
+ ' interleaving');
return animations[1].ready;
}).then(function() {
assert_true(animations[1].startTime > animations[0].startTime,
'Interleaved animation starts later than existing animations');
assert_true(animations[0].startTime > animations[2].startTime,
'Original animations retain their start time');
});
}, 'Animation state is preserved when interleaving animations in list');
done();
</script>
</body>

View file

@ -0,0 +1,37 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes xyz {
to { left: 100px }
}
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t);
div.style.animation = 'xyz 100s';
assert_equals(div.getAnimations()[0].animationName, 'xyz',
'Animation name matches keyframes rule name');
}, 'Animation name makes keyframe rule');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'x\\yz 100s';
assert_equals(div.getAnimations()[0].animationName, 'xyz',
'Escaped animation name matches keyframes rule name');
}, 'Escaped animation name');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'x\\79 z 100s';
assert_equals(div.getAnimations()[0].animationName, 'xyz',
'Hex-escaped animation name matches keyframes rule'
+ ' name');
}, 'Animation name with hex-escape');
done();
</script>
</body>

View file

@ -0,0 +1,276 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes animLeft {
to { left: 100px }
}
@keyframes animTop {
to { top: 100px }
}
@keyframes animBottom {
to { bottom: 100px }
}
@keyframes animRight {
to { right: 100px }
}
</style>
<body>
<script>
'use strict';
test(function(t) {
assert_equals(document.getAnimations().length, 0,
'getAnimations returns an empty sequence for a document'
+ ' with no animations');
}, 'getAnimations for non-animated content');
test(function(t) {
var div = addDiv(t);
// Add an animation
div.style.animation = 'animLeft 100s';
assert_equals(document.getAnimations().length, 1,
'getAnimations returns a running CSS Animation');
// Add another animation
div.style.animation = 'animLeft 100s, animTop 100s';
assert_equals(document.getAnimations().length, 2,
'getAnimations returns two running CSS Animations');
// Remove both
div.style.animation = '';
assert_equals(document.getAnimations().length, 0,
'getAnimations returns no running CSS Animations');
}, 'getAnimations for CSS Animations');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'animLeft 100s, animTop 100s, animRight 100s, ' +
'animBottom 100s';
var animations = document.getAnimations();
assert_equals(animations.length, 4,
'getAnimations returns all running CSS Animations');
assert_equals(animations[0].animationName, 'animLeft',
'Order of first animation returned');
assert_equals(animations[1].animationName, 'animTop',
'Order of second animation returned');
assert_equals(animations[2].animationName, 'animRight',
'Order of third animation returned');
assert_equals(animations[3].animationName, 'animBottom',
'Order of fourth animation returned');
}, 'Order of CSS Animations - within an element');
test(function(t) {
var div1 = addDiv(t, { style: 'animation: animLeft 100s' });
var div2 = addDiv(t, { style: 'animation: animLeft 100s' });
var div3 = addDiv(t, { style: 'animation: animLeft 100s' });
var div4 = addDiv(t, { style: 'animation: animLeft 100s' });
var animations = document.getAnimations();
assert_equals(animations.length, 4,
'getAnimations returns all running CSS Animations');
assert_equals(animations[0].effect.target, div1,
'Order of first animation returned');
assert_equals(animations[1].effect.target, div2,
'Order of second animation returned');
assert_equals(animations[2].effect.target, div3,
'Order of third animation returned');
assert_equals(animations[3].effect.target, div4,
'Order of fourth animation returned');
// Order should be depth-first pre-order so add some depth as follows:
//
// <parent>
// / |
// 2 3
// / \
// 1 4
//
// Which should give: 2, 1, 4, 3
div2.appendChild(div1);
div2.appendChild(div4);
animations = document.getAnimations();
assert_equals(animations[0].effect.target, div2,
'Order of first animation returned after tree surgery');
assert_equals(animations[1].effect.target, div1,
'Order of second animation returned after tree surgery');
assert_equals(animations[2].effect.target, div4,
'Order of third animation returned after tree surgery');
assert_equals(animations[3].effect.target, div3,
'Order of fourth animation returned after tree surgery');
}, 'Order of CSS Animations - across elements');
test(function(t) {
var div1 = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
var div2 = addDiv(t, { style: 'animation: animBottom 100s' });
var expectedResults = [ [ div1, 'animLeft' ],
[ div1, 'animTop' ],
[ div2, 'animBottom' ] ];
var animations = document.getAnimations();
assert_equals(animations.length, expectedResults.length,
'getAnimations returns all running CSS Animations');
animations.forEach(function(anim, i) {
assert_equals(anim.effect.target, expectedResults[i][0],
'Target of animation in position ' + i);
assert_equals(anim.animationName, expectedResults[i][1],
'Name of animation in position ' + i);
});
// Modify tree structure and animation list
div2.appendChild(div1);
div1.style.animation = 'animLeft 100s, animRight 100s, animTop 100s';
expectedResults = [ [ div2, 'animBottom' ],
[ div1, 'animLeft' ],
[ div1, 'animRight' ],
[ div1, 'animTop' ] ];
animations = document.getAnimations();
assert_equals(animations.length, expectedResults.length,
'getAnimations returns all running CSS Animations after ' +
'making changes');
animations.forEach(function(anim, i) {
assert_equals(anim.effect.target, expectedResults[i][0],
'Target of animation in position ' + i + ' after changes');
assert_equals(anim.animationName, expectedResults[i][1],
'Name of animation in position ' + i + ' after changes');
});
}, 'Order of CSS Animations - across and within elements');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
var animLeft = document.getAnimations()[0];
assert_equals(animLeft.animationName, 'animLeft',
'Originally, animLeft animation comes first');
// Disassociate animLeft from markup and restart
div.style.animation = 'animTop 100s';
animLeft.play();
var animations = document.getAnimations();
assert_equals(animations.length, 2,
'getAnimations returns markup-bound and free animations');
assert_equals(animations[0].animationName, 'animTop',
'Markup-bound animations come first');
assert_equals(animations[1], animLeft, 'Free animations come last');
}, 'Order of CSS Animations - markup-bound vs free animations');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s, animTop 100s' });
var animLeft = document.getAnimations()[0];
var animTop = document.getAnimations()[1];
// Disassociate both animations from markup and restart in opposite order
div.style.animation = '';
animTop.play();
animLeft.play();
var animations = document.getAnimations();
assert_equals(animations.length, 2,
'getAnimations returns free animations');
assert_equals(animations[0], animTop,
'Free animations are returned in the order they are started');
assert_equals(animations[1], animLeft,
'Animations started later are returned later');
// Restarting an animation should have no effect
animTop.cancel();
animTop.play();
assert_equals(document.getAnimations()[0], animTop,
'After restarting, the ordering of free animations' +
' does not change');
}, 'Order of CSS Animations - free animations');
test(function(t) {
// Add an animation first
var div = addDiv(t, { style: 'animation: animLeft 100s' });
div.style.top = '0px';
div.style.transition = 'all 100s';
flushComputedStyle(div);
// *Then* add a transition
div.style.top = '100px';
flushComputedStyle(div);
// Although the transition was added later, it should come first in the list
var animations = document.getAnimations();
assert_equals(animations.length, 2,
'Both CSS animations and transitions are returned');
assert_class_string(animations[0], 'CSSTransition', 'Transition comes first');
assert_class_string(animations[1], 'CSSAnimation', 'Animation comes second');
}, 'Order of CSS Animations and CSS Transitions');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s forwards' });
div.getAnimations()[0].finish();
assert_equals(document.getAnimations().length, 1,
'Forwards-filling CSS animations are returned');
}, 'Finished but filling CSS Animations are returned');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s' });
div.getAnimations()[0].finish();
assert_equals(document.getAnimations().length, 0,
'Non-filling finished CSS animations are not returned');
}, 'Finished but not filling CSS Animations are not returned');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s 100s' });
assert_equals(document.getAnimations().length, 1,
'Yet-to-start CSS animations are returned');
}, 'Yet-to-start CSS Animations are returned');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s' });
div.getAnimations()[0].cancel();
assert_equals(document.getAnimations().length, 0,
'CSS animations cancelled by the API are not returned');
}, 'CSS Animations cancelled via the API are not returned');
test(function(t) {
var div = addDiv(t, { style: 'animation: animLeft 100s' });
var anim = div.getAnimations()[0];
anim.cancel();
anim.play();
assert_equals(document.getAnimations().length, 1,
'CSS animations cancelled and restarted by the API are ' +
'returned');
}, 'CSS Animations cancelled and restarted via the API are returned');
test(function(t) {
addStyle(t, { '#parent::after': 'animation: animLeft 10s;',
'#parent::before': 'animation: animRight 10s;' });
// create two divs with these arrangement:
// parent
// ::before,
// ::after
// |
// child
var parent = addDiv(t, { 'id': 'parent' });
var child = addDiv(t);
parent.appendChild(child);
[parent, child].forEach((div) => {
div.setAttribute('style', 'animation: animBottom 10s');
});
var anims = document.getAnimations();
assert_equals(anims.length, 4,
'CSS animations on both pseudo-elements and elements ' +
'are returned');
assert_equals(anims[0].effect.target, parent,
'The animation targeting the parent element comes first');
assert_equals(anims[1].effect.target.type, '::before',
'The animation targeting the ::before element comes second');
assert_equals(anims[2].effect.target.type, '::after',
'The animation targeting the ::after element comes third');
assert_equals(anims[3].effect.target, child,
'The animation targeting the child element comes last');
}, 'CSS Animations targetting (pseudo-)elements should have correct order ' +
'after sorting');
done();
</script>
</body>

View file

@ -0,0 +1,54 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim { }
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim 100s';
var animation = div.getAnimations()[0];
assert_equals(animation.effect.target, div,
'Animation.target is the animatable div');
}, 'Returned CSS animations have the correct effect target');
test(function(t) {
addStyle(t, { '.after::after': 'animation: anim 10s, anim 100s;' });
var div = addDiv(t, { class: 'after' });
var anims = document.getAnimations();
assert_equals(anims.length, 2,
'Got animations running on ::after pseudo element');
assert_equals(anims[0].effect.target, anims[1].effect.target,
'Both animations return the same target object');
}, 'effect.target should return the same CSSPseudoElement object each time');
test(function(t) {
addStyle(t, { '.after::after': 'animation: anim 10s;' });
var div = addDiv(t, { class: 'after' });
var pseudoTarget = document.getAnimations()[0].effect.target;
var effect = new KeyframeEffectReadOnly(pseudoTarget,
{ background: ["blue", "red"] },
3 * MS_PER_SEC);
var newAnim = new Animation(effect, document.timeline);
newAnim.play();
var anims = document.getAnimations();
assert_equals(anims.length, 2,
'Got animations running on ::after pseudo element');
assert_not_equals(anims[0], newAnim,
'The scriped-generated animation appears last');
assert_equals(newAnim.effect.target, pseudoTarget,
'The effect.target of the scripted-generated animation is ' +
'the same as the one from the argument of ' +
'KeyframeEffectReadOnly constructor');
assert_equals(anims[0].effect.target, newAnim.effect.target,
'Both animations return the same target object');
}, 'effect.target from the script-generated animation should return the same ' +
'CSSPseudoElement object as that from the CSS generated animation');
done();
</script>
</body>

View file

@ -0,0 +1,445 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim1 {
to { left: 100px }
}
@keyframes anim2 {
to { top: 100px }
}
@keyframes multiPropAnim {
to { background: green, opacity: 0.5, left: 100px, top: 100px }
}
@keyframes empty { }
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t);
assert_equals(div.getAnimations().length, 0,
'getAnimations returns an empty sequence for an element'
+ ' with no animations');
}, 'getAnimations for non-animated content');
promise_test(function(t) {
var div = addDiv(t);
// FIXME: This test does too many things. It should be split up.
// Add an animation
div.style.animation = 'anim1 100s';
var animations = div.getAnimations();
assert_equals(animations.length, 1,
'getAnimations returns an Animation running CSS Animations');
return animations[0].ready.then(function() {
var startTime = animations[0].startTime;
assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
'CSS animation has a sensible start time');
// Wait a moment then add a second animation.
//
// We wait for the next frame so that we can test that the start times of
// the animations differ.
return waitForFrame();
}).then(function() {
div.style.animation = 'anim1 100s, anim2 100s';
animations = div.getAnimations();
assert_equals(animations.length, 2,
'getAnimations returns one Animation for each value of'
+ ' animation-name');
// Wait until both Animations are ready
// (We don't make any assumptions about the order of the Animations since
// that is the purpose of the following test.)
return waitForAllAnimations(animations);
}).then(function() {
assert_true(animations[0].startTime < animations[1].startTime,
'Additional Animations for CSS animations start after the original'
+ ' animation and appear later in the list');
});
}, 'getAnimations for CSS Animations');
test(function(t) {
var div = addDiv(t, { style: 'animation: anim1 100s' });
assert_class_string(div.getAnimations()[0], 'CSSAnimation',
'Interface of returned animation is CSSAnimation');
}, 'getAnimations returns CSSAnimation objects for CSS Animations');
test(function(t) {
var div = addDiv(t);
// Add an animation that targets multiple properties
div.style.animation = 'multiPropAnim 100s';
assert_equals(div.getAnimations().length, 1,
'getAnimations returns only one Animation for a CSS Animation'
+ ' that targets multiple properties');
}, 'getAnimations for multi-property animations');
promise_test(function(t) {
var div = addDiv(t);
// Add an animation
div.style.backgroundColor = 'red';
div.style.animation = 'anim1 100s';
window.getComputedStyle(div).backgroundColor;
// Wait until a frame after the animation starts, then add a transition
var animations = div.getAnimations();
return animations[0].ready.then(waitForFrame).then(function() {
div.style.transition = 'all 100s';
div.style.backgroundColor = 'green';
animations = div.getAnimations();
assert_equals(animations.length, 2,
'getAnimations returns Animations for both animations and'
+ ' transitions that run simultaneously');
assert_class_string(animations[0], 'CSSTransition',
'First-returned animation is the CSS Transition');
assert_class_string(animations[1], 'CSSAnimation',
'Second-returned animation is the CSS Animation');
});
}, 'getAnimations for both CSS Animations and CSS Transitions at once');
async_test(function(t) {
var div = addDiv(t);
// Set up event listener
div.addEventListener('animationend', t.step_func(function() {
assert_equals(div.getAnimations().length, 0,
'getAnimations does not return Animations for finished '
+ ' (and non-forwards-filling) CSS Animations');
t.done();
}));
// Add a very short animation
div.style.animation = 'anim1 0.01s';
}, 'getAnimations for CSS Animations that have finished');
async_test(function(t) {
var div = addDiv(t);
// Set up event listener
div.addEventListener('animationend', t.step_func(function() {
assert_equals(div.getAnimations().length, 1,
'getAnimations returns Animations for CSS Animations that have'
+ ' finished but are filling forwards');
t.done();
}));
// Add a very short animation
div.style.animation = 'anim1 0.01s forwards';
}, 'getAnimations for CSS Animations that have finished but are'
+ ' forwards filling');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'none 100s';
var animations = div.getAnimations();
assert_equals(animations.length, 0,
'getAnimations returns an empty sequence for an element'
+ ' with animation-name: none');
div.style.animation = 'none 100s, anim1 100s';
animations = div.getAnimations();
assert_equals(animations.length, 1,
'getAnimations returns Animations only for those CSS Animations whose'
+ ' animation-name is not none');
}, 'getAnimations for CSS Animations with animation-name: none');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'missing 100s';
var animations = div.getAnimations();
assert_equals(animations.length, 0,
'getAnimations returns an empty sequence for an element'
+ ' with animation-name: missing');
div.style.animation = 'anim1 100s, missing 100s';
animations = div.getAnimations();
assert_equals(animations.length, 1,
'getAnimations returns Animations only for those CSS Animations whose'
+ ' animation-name is found');
}, 'getAnimations for CSS Animations with animation-name: missing');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s, notyet 100s';
var animations = div.getAnimations();
assert_equals(animations.length, 1,
'getAnimations initally only returns Animations for CSS Animations whose'
+ ' animation-name is found');
return animations[0].ready.then(waitForFrame).then(function() {
var keyframes = '@keyframes notyet { to { left: 100px; } }';
document.styleSheets[0].insertRule(keyframes, 0);
animations = div.getAnimations();
assert_equals(animations.length, 2,
'getAnimations includes Animation when @keyframes rule is added'
+ ' later');
return waitForAllAnimations(animations);
}).then(function() {
assert_true(animations[0].startTime < animations[1].startTime,
'Newly added animation has a later start time');
document.styleSheets[0].deleteRule(0);
});
}, 'getAnimations for CSS Animations where the @keyframes rule is added'
+ ' later');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s, anim1 100s';
assert_equals(div.getAnimations().length, 2,
'getAnimations returns one Animation for each CSS animation-name'
+ ' even if the names are duplicated');
}, 'getAnimations for CSS Animations with duplicated animation-name');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'empty 100s';
assert_equals(div.getAnimations().length, 1,
'getAnimations returns Animations for CSS animations with an'
+ ' empty keyframes rule');
}, 'getAnimations for CSS Animations with empty keyframes rule');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s 100s';
var animations = div.getAnimations();
assert_equals(animations.length, 1,
'getAnimations returns animations for CSS animations whose'
+ ' delay makes them start later');
return animations[0].ready.then(waitForFrame).then(function() {
assert_true(animations[0].startTime <= document.timeline.currentTime,
'For CSS Animations in delay phase, the start time of the Animation is'
+ ' not in the future');
});
}, 'getAnimations for CSS animations in delay phase');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 0s 100s';
assert_equals(div.getAnimations().length, 1,
'getAnimations returns animations for CSS animations whose'
+ ' duration is zero');
div.remove();
}, 'getAnimations for zero-duration CSS Animations');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s';
var originalAnimation = div.getAnimations()[0];
// Update pause state (an Animation change)
div.style.animationPlayState = 'paused';
var pendingAnimation = div.getAnimations()[0];
assert_equals(pendingAnimation.playState, 'pending',
'animation\'s play state is updated');
assert_equals(originalAnimation, pendingAnimation,
'getAnimations returns the same objects even when their'
+ ' play state changes');
// Update duration (an Animation change)
div.style.animationDuration = '200s';
var extendedAnimation = div.getAnimations()[0];
// FIXME: Check extendedAnimation.effect.timing.duration has changed once the
// API is available
assert_equals(originalAnimation, extendedAnimation,
'getAnimations returns the same objects even when their'
+ ' duration changes');
}, 'getAnimations returns objects with the same identity');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim1 100s';
assert_equals(div.getAnimations().length, 1,
'getAnimations returns an animation before cancelling');
var animation = div.getAnimations()[0];
animation.cancel();
assert_equals(div.getAnimations().length, 0,
'getAnimations does not return cancelled animations');
animation.play();
assert_equals(div.getAnimations().length, 1,
'getAnimations returns cancelled animations that have been re-started');
}, 'getAnimations for CSS Animations that are cancelled');
promise_test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim2 100s';
return div.getAnimations()[0].ready.then(function() {
// Prepend to the list and test that even though anim1 was triggered
// *after* anim2, it should come first because it appears first
// in the animation-name property.
div.style.animation = 'anim1 100s, anim2 100s';
var anims = div.getAnimations();
assert_equals(anims[0].animationName, 'anim1',
'animation order after prepending to list');
assert_equals(anims[1].animationName, 'anim2',
'animation order after prepending to list');
// Normally calling cancel and play would this push anim1 to the top of
// the stack but it shouldn't for CSS animations that map an the
// animation-name property.
var anim1 = anims[0];
anim1.cancel();
anim1.play();
anims = div.getAnimations();
assert_equals(anims[0].animationName, 'anim1',
'animation order after cancelling and restarting');
assert_equals(anims[1].animationName, 'anim2',
'animation order after cancelling and restarting');
});
}, 'getAnimations for CSS Animations follows animation-name order');
test(function(t) {
addStyle(t, { '#target::after': 'animation: anim1 10s;',
'#target::before': 'animation: anim1 10s;' });
var target = addDiv(t, { 'id': 'target' });
target.style.animation = 'anim1 100s';
var animations = target.getAnimations({ subtree: false });
assert_equals(animations.length, 1,
'Should find only the element');
assert_equals(animations[0].effect.target, target,
'Effect target should be the element');
}, 'Test AnimationFilter{ subtree: false } with single element');
test(function(t) {
addStyle(t, { '#target::after': 'animation: anim1 10s;',
'#target::before': 'animation: anim1 10s;' });
var target = addDiv(t, { 'id': 'target' });
target.style.animation = 'anim1 100s';
var animations = target.getAnimations({ subtree: true });
assert_equals(animations.length, 3,
'getAnimations({ subtree: true }) ' +
'should return animations on pseudo-elements');
assert_equals(animations[0].effect.target, target,
'The animation targeting the parent element ' +
'should be returned first');
assert_equals(animations[1].effect.target.type, '::before',
'The animation targeting the ::before pseudo-element ' +
'should be returned second');
assert_equals(animations[2].effect.target.type, '::after',
'The animation targeting the ::after pesudo-element ' +
'should be returned last');
}, 'Test AnimationFilter{ subtree: true } with single element');
test(function(t) {
addStyle(t, { '#parent::after': 'animation: anim1 10s;',
'#parent::before': 'animation: anim1 10s;',
'#child::after': 'animation: anim1 10s;',
'#child::before': 'animation: anim1 10s;' });
var parent = addDiv(t, { 'id': 'parent' });
parent.style.animation = 'anim1 100s';
var child = addDiv(t, { 'id': 'child' });
child.style.animation = 'anim1 100s';
parent.appendChild(child);
var animations = parent.getAnimations({ subtree: false });
assert_equals(animations.length, 1,
'Should find only the element even if it has a child');
assert_equals(animations[0].effect.target, parent,
'Effect target shuld be the element');
}, 'Test AnimationFilter{ subtree: false } with element that has a child');
test(function(t) {
addStyle(t, { '#parent::after': 'animation: anim1 10s;',
'#parent::before': 'animation: anim1 10s;',
'#child::after': 'animation: anim1 10s;',
'#child::before': 'animation: anim1 10s;' });
var parent = addDiv(t, { 'id': 'parent' });
var child = addDiv(t, { 'id': 'child' });
parent.style.animation = 'anim1 100s';
child.style.animation = 'anim1 100s';
parent.appendChild(child);
var animations = parent.getAnimations({ subtree: true });
assert_equals(animations.length, 6,
'Should find all elements, pesudo-elements that parent has');
assert_equals(animations[0].effect.target, parent,
'The animation targeting the parent element ' +
'should be returned first');
assert_equals(animations[1].effect.target.type, '::before',
'The animation targeting the ::before pseudo-element ' +
'should be returned second');
assert_equals(animations[1].effect.target.parentElement, parent,
'This ::before element should be child of parent element');
assert_equals(animations[2].effect.target.type, '::after',
'The animation targeting the ::after pesudo-element ' +
'should be returned third');
assert_equals(animations[2].effect.target.parentElement, parent,
'This ::after element should be child of parent element');
assert_equals(animations[3].effect.target, child,
'The animation targeting the child element ' +
'should be returned fourth');
assert_equals(animations[4].effect.target.type, '::before',
'The animation targeting the ::before pseudo-element ' +
'should be returned fifth');
assert_equals(animations[4].effect.target.parentElement, child,
'This ::before element should be child of child element');
assert_equals(animations[5].effect.target.type, '::after',
'The animation targeting the ::after pesudo-element ' +
'should be returned last');
assert_equals(animations[5].effect.target.parentElement, child,
'This ::after element should be child of child element');
}, 'Test AnimationFilter{ subtree: true } with element that has a child');
test(function(t) {
var parent = addDiv(t, { 'id': 'parent' });
var child1 = addDiv(t, { 'id': 'child1' });
var grandchild1 = addDiv(t, { 'id': 'grandchild1' });
var grandchild2 = addDiv(t, { 'id': 'grandchild2' });
var child2 = addDiv(t, { 'id': 'child2' });
parent.style.animation = 'anim1 100s';
child1.style.animation = 'anim1 100s';
grandchild1.style.animation = 'anim1 100s';
grandchild2.style.animation = 'anim1 100s';
child2.style.animation = 'anim1 100s';
parent.appendChild(child1);
child1.appendChild(grandchild1);
child1.appendChild(grandchild2);
parent.appendChild(child2);
var animations = parent.getAnimations({ subtree: true });
assert_equals(
parent.getAnimations({ subtree: true }).length, 5,
'Should find all descendants of the element');
assert_equals(animations[0].effect.target, parent,
'The animation targeting the parent element ' +
'should be returned first');
assert_equals(animations[1].effect.target, child1,
'The animation targeting the child1 element ' +
'should be returned second');
assert_equals(animations[2].effect.target, grandchild1,
'The animation targeting the grandchild1 element ' +
'should be returned third');
assert_equals(animations[3].effect.target, grandchild2,
'The animation targeting the grandchild2 element ' +
'should be returned fourth');
assert_equals(animations[4].effect.target, child2,
'The animation targeting the child2 element ' +
'should be returned last');
}, 'Test AnimationFilter{ subtree: true } with element that has many descendant');
done();
</script>
</body>

View file

@ -0,0 +1,672 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim-empty { }
@keyframes anim-empty-frames {
from { }
to { }
}
@keyframes anim-only-timing {
from { animation-timing-function: linear; }
to { }
}
@keyframes anim-only-non-animatable {
from { animation-duration: 3s; }
to { animation-duration: 5s; }
}
@keyframes anim-simple {
from { color: black; }
to { color: white; }
}
@keyframes anim-simple-three {
from { color: black; }
50% { color: blue; }
to { color: white; }
}
@keyframes anim-simple-timing {
from { color: black; animation-timing-function: linear; }
50% { color: blue; animation-timing-function: ease-in-out; }
to { color: white; animation-timing-function: step-end; }
}
@keyframes anim-simple-timing-some {
from { color: black; animation-timing-function: linear; }
50% { color: blue; }
to { color: white; }
}
@keyframes anim-simple-shorthand {
from { margin: 8px; }
to { margin: 16px; }
}
@keyframes anim-omit-to {
from { color: blue; }
}
@keyframes anim-omit-from {
to { color: blue; }
}
@keyframes anim-omit-from-to {
50% { color: blue; }
}
@keyframes anim-partially-omit-to {
from { margin-top: 50px;
margin-bottom: 100px; }
to { margin-top: 150px !important; /* ignored */
margin-bottom: 200px; }
}
@keyframes anim-different-props {
from { color: black; margin-top: 8px; }
25% { color: blue; }
75% { margin-top: 12px; }
to { color: white; margin-top: 16px }
}
@keyframes anim-different-props-and-easing {
from { color: black; margin-top: 8px; animation-timing-function: linear; }
25% { color: blue; animation-timing-function: step-end; }
75% { margin-top: 12px; animation-timing-function: ease-in; }
to { color: white; margin-top: 16px }
}
@keyframes anim-merge-offset {
from { color: black; }
to { color: white; }
from { margin-top: 8px; }
to { margin-top: 16px; }
}
@keyframes anim-merge-offset-and-easing {
from { color: black; animation-timing-function: step-end; }
to { color: white; }
from { margin-top: 8px; animation-timing-function: linear; }
to { margin-top: 16px; }
from { font-size: 16px; animation-timing-function: step-end; }
to { font-size: 32px; }
from { padding-left: 2px; animation-timing-function: linear; }
to { padding-left: 4px; }
}
@keyframes anim-no-merge-equiv-easing {
from { margin-top: 0px; animation-timing-function: steps(1, end); }
from { margin-right: 0px; animation-timing-function: step-end; }
from { margin-bottom: 0px; animation-timing-function: steps(1); }
50% { margin-top: 10px; animation-timing-function: step-end; }
50% { margin-right: 10px; animation-timing-function: step-end; }
50% { margin-bottom: 10px; animation-timing-function: step-end; }
to { margin-top: 20px; margin-right: 20px; margin-bottom: 20px; }
}
@keyframes anim-overriding {
from { padding-top: 50px }
50%, from { padding-top: 30px } /* wins: 0% */
75%, 85%, 50% { padding-top: 20px } /* wins: 75%, 50% */
100%, 85% { padding-top: 70px } /* wins: 100% */
85.1% { padding-top: 60px } /* wins: 85.1% */
85% { padding-top: 30px } /* wins: 85% */
}
@keyframes anim-filter {
to { filter: blur(5px) sepia(60%) saturate(30%); }
}
@keyframes anim-text-shadow {
to { text-shadow: none; }
}
@keyframes anim-background-size {
to { background-size: 50%, 6px, contain }
}
:root {
--var-100px: 100px;
}
@keyframes anim-variables {
to { transform: translate(var(--var-100px), 0) }
}
@keyframes anim-variables-shorthand {
to { margin: var(--var-100px) }
}
</style>
<body>
<script>
"use strict";
function getKeyframes(e) {
return e.getAnimations()[0].effect.getKeyframes();
}
function assert_frames_equal(a, b, name) {
assert_equals(Object.keys(a).sort().toString(),
Object.keys(b).sort().toString(),
"properties on " + name);
for (var p in a) {
if (p === 'offset' || p === 'computedOffset') {
assert_approx_equals(a[p], b[p], 0.00001,
"value for '" + p + "' on " + name);
} else {
assert_equals(a[p], b[p], "value for '" + p + "' on " + name);
}
}
}
// animation-timing-function values to test with, where the value
// is exactly the same as its serialization, sorted by the order
// getKeyframes() will group frames with the same easing function
// together (by nsTimingFunction::Compare).
const kTimingFunctionValues = [
"ease",
"linear",
"ease-in",
"ease-out",
"ease-in-out",
"steps(1, start)",
"steps(2, start)",
"steps(1)",
"steps(2)",
"cubic-bezier(0, 0, 1, 1)",
"cubic-bezier(0, 0.25, 0.75, 1)"
];
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-empty 100s';
assert_equals(getKeyframes(div).length, 0,
"number of frames with empty @keyframes");
div.style.animation = 'anim-empty-frames 100s';
assert_equals(getKeyframes(div).length, 0,
"number of frames when @keyframes has empty keyframes");
div.style.animation = 'anim-only-timing 100s';
assert_equals(getKeyframes(div).length, 0,
"number of frames when @keyframes only has keyframes with " +
"animation-timing-function");
div.style.animation = 'anim-only-non-animatable 100s';
assert_equals(getKeyframes(div).length, 0,
"number of frames when @keyframes only has frames with " +
"non-animatable properties");
}, 'KeyframeEffectReadOnly.getKeyframes() returns no frames for various kinds'
+ ' of empty enimations');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-simple 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(0, 0, 0)" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ 'animation');
test(function(t) {
kTimingFunctionValues.forEach(function(easing) {
var div = addDiv(t);
div.style.animation = 'anim-simple-three 100s ' + easing;
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
for (var i = 0; i < frames.length; i++) {
assert_equals(frames[i].easing, easing,
"value for 'easing' on ComputedKeyframe #" + i);
}
});
}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ ' values, when the easing comes from animation-timing-function on the'
+ ' element');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-simple-timing 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
assert_equals(frames[0].easing, "linear",
"value of 'easing' on ComputedKeyframe #0");
assert_equals(frames[1].easing, "ease-in-out",
"value of 'easing' on ComputedKeyframe #1");
assert_equals(frames[2].easing, "steps(1)",
"value of 'easing' on ComputedKeyframe #2");
}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ ' values, when the easing is specified on each keyframe');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-simple-timing-some 100s step-start';
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
assert_equals(frames[0].easing, "linear",
"value of 'easing' on ComputedKeyframe #0");
assert_equals(frames[1].easing, "steps(1, start)",
"value of 'easing' on ComputedKeyframe #1");
assert_equals(frames[2].easing, "steps(1, start)",
"value of 'easing' on ComputedKeyframe #2");
}, 'KeyframeEffectReadOnly.getKeyframes() returns frames with expected easing'
+ ' values, when the easing is specified on some keyframes');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-simple-shorthand 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
marginBottom: "8px", marginLeft: "8px",
marginRight: "8px", marginTop: "8px" },
{ offset: 1, computedOffset: 1, easing: "ease",
marginBottom: "16px", marginLeft: "16px",
marginRight: "16px", marginTop: "16px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
+ ' animation that specifies a single shorthand property');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-omit-to 100s';
div.style.color = 'white';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(0, 0, 255)" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with a 0% keyframe and no 100% keyframe');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-omit-from 100s';
div.style.color = 'white';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(255, 255, 255)" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(0, 0, 255)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with a 100% keyframe and no 0% keyframe');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-omit-from-to 100s';
div.style.color = 'white';
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(255, 255, 255)" },
{ offset: 0.5, computedOffset: 0.5, easing: "ease",
color: "rgb(0, 0, 255)" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with no 0% or 100% keyframe but with a 50% keyframe');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-partially-omit-to 100s';
div.style.marginTop = '250px';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
marginTop: '50px', marginBottom: '100px' },
{ offset: 1, computedOffset: 1, easing: "ease",
marginTop: '250px', marginBottom: '200px' },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with a partially complete 100% keyframe (because the ' +
'!important rule is ignored)');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-different-props 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 4, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(0, 0, 0)", marginTop: "8px" },
{ offset: 0.25, computedOffset: 0.25, easing: "ease",
color: "rgb(0, 0, 255)" },
{ offset: 0.75, computedOffset: 0.75, easing: "ease",
marginTop: "12px" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)", marginTop: "16px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with different properties on different keyframes, all ' +
'with the same easing function');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-different-props-and-easing 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 4, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "linear",
color: "rgb(0, 0, 0)", marginTop: "8px" },
{ offset: 0.25, computedOffset: 0.25, easing: "steps(1)",
color: "rgb(0, 0, 255)" },
{ offset: 0.75, computedOffset: 0.75, easing: "ease-in",
marginTop: "12px" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)", marginTop: "16px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with different properties on different keyframes, with ' +
'a different easing function on each');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-merge-offset 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
color: "rgb(0, 0, 0)", marginTop: "8px" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)", marginTop: "16px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with multiple keyframes for the same time, and all with ' +
'the same easing function');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-merge-offset-and-easing 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "steps(1)",
color: "rgb(0, 0, 0)", fontSize: "16px" },
{ offset: 0, computedOffset: 0, easing: "linear",
marginTop: "8px", paddingLeft: "2px" },
{ offset: 1, computedOffset: 1, easing: "ease",
color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
paddingLeft: "4px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with multiple keyframes for the same time and with ' +
'different easing functions');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-no-merge-equiv-easing 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 3, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "steps(1)",
marginTop: "0px", marginRight: "0px", marginBottom: "0px" },
{ offset: 0.5, computedOffset: 0.5, easing: "steps(1)",
marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
{ offset: 1, computedOffset: 1, easing: "ease",
marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
'animation with multiple keyframes for the same time and with ' +
'different but equivalent easing functions');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-overriding 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 6, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
paddingTop: "30px" },
{ offset: 0.5, computedOffset: 0.5, easing: "ease",
paddingTop: "20px" },
{ offset: 0.75, computedOffset: 0.75, easing: "ease",
paddingTop: "20px" },
{ offset: 0.85, computedOffset: 0.85, easing: "ease",
paddingTop: "30px" },
{ offset: 0.851, computedOffset: 0.851, easing: "ease",
paddingTop: "60px" },
{ offset: 1, computedOffset: 1, easing: "ease",
paddingTop: "70px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' +
'overlapping keyframes');
// Gecko-specific test case: We are specifically concerned here that the
// computed value for filter, "none", is correctly represented.
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-filter 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
filter: "none" },
{ offset: 1, computedOffset: 1, easing: "ease",
filter: "blur(5px) sepia(60%) saturate(30%)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
'animations with filter properties and missing keyframes');
// Gecko-specific test case: We are specifically concerned here that the
// computed value for text-shadow and a "none" specified on a keyframe
// are correctly represented.
test(function(t) {
var div = addDiv(t);
div.style.textShadow = '1px 1px 2px black, 0 0 16px blue, 0 0 3.2px blue';
div.style.animation = 'anim-text-shadow 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
textShadow: "1px 1px 2px 0px rgb(0, 0, 0),"
+ " 0px 0px 16px 0px rgb(0, 0, 255),"
+ " 0px 0px 3.2px 0px rgb(0, 0, 255)" },
{ offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
'animations with text-shadow properties and missing keyframes');
// Gecko-specific test case: We are specifically concerned here that the
// initial value for background-size and the specified list are correctly
// represented.
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-background-size 100s';
var frames = getKeyframes(div);
assert_equals(frames.length, 2, "number of frames");
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
backgroundSize: "auto auto" },
{ offset: 1, computedOffset: 1, easing: "ease",
backgroundSize: "50% auto, 6px auto, contain" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
// Test inheriting a background-size value
expected[0].backgroundSize = div.style.backgroundSize =
"30px auto, 40% auto, auto auto";
frames = getKeyframes(div);
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i
+ " after updating current style");
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
'animations with background-size properties and missing keyframes');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-variables 100s';
var frames = getKeyframes(div);
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
transform: "none" },
{ offset: 1, computedOffset: 1, easing: "ease",
transform: "translate(100px, 0px)" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
'animations with CSS variables as keyframe values');
test(function(t) {
var div = addDiv(t);
div.style.animation = 'anim-variables-shorthand 100s';
var frames = getKeyframes(div);
var expected = [
{ offset: 0, computedOffset: 0, easing: "ease",
marginBottom: "0px",
marginLeft: "0px",
marginRight: "0px",
marginTop: "0px" },
{ offset: 1, computedOffset: 1, easing: "ease",
marginBottom: "100px",
marginLeft: "100px",
marginRight: "100px",
marginTop: "100px" },
];
for (var i = 0; i < frames.length; i++) {
assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
}
}, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
'animations with CSS variables as keyframe values in a shorthand property');
done();
</script>
</body>

View file

@ -0,0 +1,70 @@
<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim1 { }
@keyframes anim2 { }
.before::before {
animation: anim1 10s;
}
.after-with-mix-anims-trans::after {
content: '';
animation: anim1 10s, anim2 10s;
width: 0px;
height: 0px;
transition: all 100s;
}
.after-change::after {
width: 100px;
height: 100px;
}
</style>
<body>
<script>
'use strict';
test(function(t) {
var div = addDiv(t, { class: 'before' });
var pseudoTarget = document.getAnimations()[0].effect.target;
assert_equals(pseudoTarget.getAnimations().length, 1,
'Expected number of animations are returned');
assert_equals(pseudoTarget.getAnimations()[0].animationName, 'anim1',
'CSS animation name matches');
}, 'getAnimations returns CSSAnimation objects');
test(function(t) {
var div = addDiv(t, { class: 'after-with-mix-anims-trans' });
// Trigger transitions
flushComputedStyle(div);
div.classList.add('after-change');
// Create additional animation on the pseudo-element from script
var pseudoTarget = document.getAnimations()[0].effect.target;
var effect = new KeyframeEffectReadOnly(pseudoTarget,
{ background: ["blue", "red"] },
3 * MS_PER_SEC);
var newAnimation = new Animation(effect, document.timeline);
newAnimation.id = 'scripted-anim';
newAnimation.play();
// Check order - the script-generated animation should appear later
var anims = pseudoTarget.getAnimations();
assert_equals(anims.length, 5,
'Got expected number of animations/trnasitions running on ' +
'::after pseudo element');
assert_equals(anims[0].transitionProperty, 'height',
'1st animation is the 1st transition sorted by name');
assert_equals(anims[1].transitionProperty, 'width',
'2nd animation is the 2nd transition sorted by name ');
assert_equals(anims[2].animationName, 'anim1',
'3rd animation is the 1st animation in animation-name list');
assert_equals(anims[3].animationName, 'anim2',
'4rd animation is the 2nd animation in animation-name list');
assert_equals(anims[4].id, 'scripted-anim',
'Animation added by script appears last');
}, 'getAnimations returns css transitions/animations, and script-generated ' +
'animations in the expected order');
done();
</script>
</body>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-cancel.html");
});
</script>
</html>

View file

@ -0,0 +1,16 @@
<!doctype html>
<html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-computed-timing.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-currenttime.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-finish.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-finished.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-id.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-pausing.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-playstate.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-ready.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-reverse.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animation-starttime.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_animations-dynamic-changes.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_cssanimation-animationname.html");
});
</script>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
setup({explicit_done: true});
SpecialPowers.pushPrefEnv(
{ "set": [["dom.animations-api.core.enabled", true]]},
function() {
window.open("file_document-get-animations.html");
});
</script>
</html>

Some files were not shown because too many files have changed in this diff Show more