mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-30 20:37:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
110
dom/animation/AnimValuesStyleRule.cpp
Normal file
110
dom/animation/AnimValuesStyleRule.cpp
Normal 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
|
||||
58
dom/animation/AnimValuesStyleRule.h
Normal file
58
dom/animation/AnimValuesStyleRule.h
Normal 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
1426
dom/animation/Animation.cpp
Normal file
File diff suppressed because it is too large
Load diff
453
dom/animation/Animation.h
Normal file
453
dom/animation/Animation.h
Normal 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
|
||||
32
dom/animation/AnimationComparator.h
Normal file
32
dom/animation/AnimationComparator.h
Normal 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
|
||||
343
dom/animation/AnimationEffectReadOnly.cpp
Normal file
343
dom/animation/AnimationEffectReadOnly.cpp
Normal 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
|
||||
102
dom/animation/AnimationEffectReadOnly.h
Normal file
102
dom/animation/AnimationEffectReadOnly.h
Normal 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
|
||||
152
dom/animation/AnimationEffectTiming.cpp
Normal file
152
dom/animation/AnimationEffectTiming.cpp
Normal 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
|
||||
49
dom/animation/AnimationEffectTiming.h
Normal file
49
dom/animation/AnimationEffectTiming.h
Normal 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
|
||||
51
dom/animation/AnimationEffectTimingReadOnly.cpp
Normal file
51
dom/animation/AnimationEffectTimingReadOnly.cpp
Normal 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
|
||||
63
dom/animation/AnimationEffectTimingReadOnly.h
Normal file
63
dom/animation/AnimationEffectTimingReadOnly.h
Normal 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
|
||||
79
dom/animation/AnimationPerformanceWarning.cpp
Normal file
79
dom/animation/AnimationPerformanceWarning.cpp
Normal 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
|
||||
79
dom/animation/AnimationPerformanceWarning.h
Normal file
79
dom/animation/AnimationPerformanceWarning.h
Normal 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
|
||||
78
dom/animation/AnimationTarget.h
Normal file
78
dom/animation/AnimationTarget.h
Normal 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
|
||||
63
dom/animation/AnimationTimeline.cpp
Normal file
63
dom/animation/AnimationTimeline.cpp
Normal 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
|
||||
125
dom/animation/AnimationTimeline.h
Normal file
125
dom/animation/AnimationTimeline.h
Normal 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
|
||||
81
dom/animation/AnimationUtils.cpp
Normal file
81
dom/animation/AnimationUtils.cpp
Normal 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
|
||||
74
dom/animation/AnimationUtils.h
Normal file
74
dom/animation/AnimationUtils.h
Normal 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
|
||||
123
dom/animation/CSSPseudoElement.cpp
Normal file
123
dom/animation/CSSPseudoElement.cpp
Normal 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
|
||||
91
dom/animation/CSSPseudoElement.h
Normal file
91
dom/animation/CSSPseudoElement.h
Normal 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
|
||||
78
dom/animation/ComputedTiming.h
Normal file
78
dom/animation/ComputedTiming.h
Normal 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
|
||||
194
dom/animation/ComputedTimingFunction.cpp
Normal file
194
dom/animation/ComputedTimingFunction.cpp
Normal 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
|
||||
65
dom/animation/ComputedTimingFunction.h
Normal file
65
dom/animation/ComputedTimingFunction.h
Normal 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
|
||||
283
dom/animation/DocumentTimeline.cpp
Normal file
283
dom/animation/DocumentTimeline.cpp
Normal 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
|
||||
111
dom/animation/DocumentTimeline.h
Normal file
111
dom/animation/DocumentTimeline.h
Normal 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
|
||||
920
dom/animation/EffectCompositor.cpp
Normal file
920
dom/animation/EffectCompositor.cpp
Normal 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
|
||||
307
dom/animation/EffectCompositor.h
Normal file
307
dom/animation/EffectCompositor.h
Normal 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
177
dom/animation/EffectSet.cpp
Normal 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
261
dom/animation/EffectSet.h
Normal 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
|
||||
211
dom/animation/KeyframeEffect.cpp
Normal file
211
dom/animation/KeyframeEffect.cpp
Normal 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
|
||||
82
dom/animation/KeyframeEffect.h
Normal file
82
dom/animation/KeyframeEffect.h
Normal 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
|
||||
169
dom/animation/KeyframeEffectParams.cpp
Normal file
169
dom/animation/KeyframeEffectParams.cpp
Normal 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
|
||||
68
dom/animation/KeyframeEffectParams.h
Normal file
68
dom/animation/KeyframeEffectParams.h
Normal 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
|
||||
1430
dom/animation/KeyframeEffectReadOnly.cpp
Normal file
1430
dom/animation/KeyframeEffectReadOnly.cpp
Normal file
File diff suppressed because it is too large
Load diff
439
dom/animation/KeyframeEffectReadOnly.h
Normal file
439
dom/animation/KeyframeEffectReadOnly.h
Normal 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
|
||||
1667
dom/animation/KeyframeUtils.cpp
Normal file
1667
dom/animation/KeyframeUtils.cpp
Normal file
File diff suppressed because it is too large
Load diff
151
dom/animation/KeyframeUtils.h
Normal file
151
dom/animation/KeyframeUtils.h
Normal 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
|
||||
124
dom/animation/PendingAnimationTracker.cpp
Normal file
124
dom/animation/PendingAnimationTracker.cpp
Normal 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
|
||||
84
dom/animation/PendingAnimationTracker.h
Normal file
84
dom/animation/PendingAnimationTracker.h
Normal 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
|
||||
58
dom/animation/PseudoElementHashEntry.h
Normal file
58
dom/animation/PseudoElementHashEntry.h
Normal 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
|
||||
182
dom/animation/TimingParams.cpp
Normal file
182
dom/animation/TimingParams.cpp
Normal 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
|
||||
130
dom/animation/TimingParams.h
Normal file
130
dom/animation/TimingParams.h
Normal 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
67
dom/animation/moz.build
Normal 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'
|
||||
17
dom/animation/test/chrome.ini
Normal file
17
dom/animation/test/chrome.ini
Normal 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]
|
||||
19
dom/animation/test/chrome/file_animate_xrays.html
Normal file
19
dom/animation/test/chrome/file_animate_xrays.html
Normal 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>
|
||||
31
dom/animation/test/chrome/test_animate_xrays.html
Normal file
31
dom/animation/test/chrome/test_animate_xrays.html
Normal 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>
|
||||
1177
dom/animation/test/chrome/test_animation_observers.html
Normal file
1177
dom/animation/test/chrome/test_animation_observers.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
993
dom/animation/test/chrome/test_animation_properties.html
Normal file
993
dom/animation/test/chrome/test_animation_properties.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
854
dom/animation/test/chrome/test_observers_for_sync_api.html
Normal file
854
dom/animation/test/chrome/test_observers_for_sync_api.html
Normal 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>
|
||||
815
dom/animation/test/chrome/test_restyles.html
Normal file
815
dom/animation/test/chrome/test_restyles.html
Normal 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>
|
||||
966
dom/animation/test/chrome/test_running_on_compositor.html
Normal file
966
dom/animation/test/chrome/test_running_on_compositor.html
Normal 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>
|
||||
35
dom/animation/test/crashtests/1216842-1.html
Normal file
35
dom/animation/test/crashtests/1216842-1.html
Normal 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>
|
||||
35
dom/animation/test/crashtests/1216842-2.html
Normal file
35
dom/animation/test/crashtests/1216842-2.html
Normal 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>
|
||||
27
dom/animation/test/crashtests/1216842-3.html
Normal file
27
dom/animation/test/crashtests/1216842-3.html
Normal 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>
|
||||
27
dom/animation/test/crashtests/1216842-4.html
Normal file
27
dom/animation/test/crashtests/1216842-4.html
Normal 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>
|
||||
38
dom/animation/test/crashtests/1216842-5.html
Normal file
38
dom/animation/test/crashtests/1216842-5.html
Normal 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>
|
||||
38
dom/animation/test/crashtests/1216842-6.html
Normal file
38
dom/animation/test/crashtests/1216842-6.html
Normal 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>
|
||||
12
dom/animation/test/crashtests/1239889-1.html
Normal file
12
dom/animation/test/crashtests/1239889-1.html
Normal 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>
|
||||
3
dom/animation/test/crashtests/1244595-1.html
Normal file
3
dom/animation/test/crashtests/1244595-1.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div id=target><script>
|
||||
var player = target.animate([{background: 'green'}, {background: 'green'}]);
|
||||
</script>
|
||||
20
dom/animation/test/crashtests/1272475-1.html
Normal file
20
dom/animation/test/crashtests/1272475-1.html
Normal 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>
|
||||
20
dom/animation/test/crashtests/1272475-2.html
Normal file
20
dom/animation/test/crashtests/1272475-2.html
Normal 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>
|
||||
19
dom/animation/test/crashtests/1277272-1-inner.html
Normal file
19
dom/animation/test/crashtests/1277272-1-inner.html
Normal 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>
|
||||
|
||||
26
dom/animation/test/crashtests/1277272-1.html
Normal file
26
dom/animation/test/crashtests/1277272-1.html
Normal 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>
|
||||
|
||||
26
dom/animation/test/crashtests/1278485-1.html
Normal file
26
dom/animation/test/crashtests/1278485-1.html
Normal 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>
|
||||
20
dom/animation/test/crashtests/1290535-1.html
Normal file
20
dom/animation/test/crashtests/1290535-1.html
Normal 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>
|
||||
13
dom/animation/test/crashtests/crashtests.list
Normal file
13
dom/animation/test/crashtests/crashtests.list
Normal 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
|
||||
154
dom/animation/test/css-animations/file_animation-cancel.html
Normal file
154
dom/animation/test/css-animations/file_animation-cancel.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
97
dom/animation/test/css-animations/file_animation-finish.html
Normal file
97
dom/animation/test/css-animations/file_animation-finish.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
24
dom/animation/test/css-animations/file_animation-id.html
Normal file
24
dom/animation/test/css-animations/file_animation-id.html
Normal 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>
|
||||
165
dom/animation/test/css-animations/file_animation-pausing.html
Normal file
165
dom/animation/test/css-animations/file_animation-pausing.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
149
dom/animation/test/css-animations/file_animation-ready.html
Normal file
149
dom/animation/test/css-animations/file_animation-ready.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
383
dom/animation/test/css-animations/file_animation-starttime.html
Normal file
383
dom/animation/test/css-animations/file_animation-starttime.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
54
dom/animation/test/css-animations/file_effect-target.html
Normal file
54
dom/animation/test/css-animations/file_effect-target.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
15
dom/animation/test/css-animations/test_animation-cancel.html
Normal file
15
dom/animation/test/css-animations/test_animation-cancel.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
15
dom/animation/test/css-animations/test_animation-finish.html
Normal file
15
dom/animation/test/css-animations/test_animation-finish.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
15
dom/animation/test/css-animations/test_animation-id.html
Normal file
15
dom/animation/test/css-animations/test_animation-id.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
15
dom/animation/test/css-animations/test_animation-ready.html
Normal file
15
dom/animation/test/css-animations/test_animation-ready.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue