2015-05-03 19:32:37 +00:00
|
|
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
2014-08-10 07:06:46 +00:00
|
|
|
/* 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/. */
|
|
|
|
|
2015-04-14 23:48:21 +00:00
|
|
|
#include "mozilla/dom/KeyframeEffect.h"
|
2015-11-20 05:12:00 +00:00
|
|
|
|
2016-03-12 13:14:10 +00:00
|
|
|
#include "mozilla/dom/AnimatableBinding.h"
|
2015-04-14 23:48:21 +00:00
|
|
|
#include "mozilla/dom/KeyframeEffectBinding.h"
|
2015-12-03 23:32:53 +00:00
|
|
|
#include "mozilla/AnimationUtils.h"
|
2016-01-12 22:54:53 +00:00
|
|
|
#include "mozilla/EffectCompositor.h"
|
2014-08-10 07:06:50 +00:00
|
|
|
#include "mozilla/FloatingPoint.h"
|
2015-11-06 01:53:00 +00:00
|
|
|
#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
|
2016-03-22 07:20:37 +00:00
|
|
|
#include "mozilla/KeyframeUtils.h"
|
2015-10-22 08:22:38 +00:00
|
|
|
#include "mozilla/StyleAnimationValue.h"
|
2015-11-06 01:53:00 +00:00
|
|
|
#include "Layers.h" // For Layer
|
2016-03-22 07:35:53 +00:00
|
|
|
#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement
|
2014-10-20 04:55:46 +00:00
|
|
|
#include "nsCSSPropertySet.h"
|
2015-09-16 14:05:00 +00:00
|
|
|
#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
|
2016-03-22 07:20:37 +00:00
|
|
|
#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
|
2016-02-26 21:39:49 +00:00
|
|
|
#include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
|
2016-01-06 02:04:06 +00:00
|
|
|
#include <algorithm> // For std::max
|
2014-08-10 07:06:46 +00:00
|
|
|
|
|
|
|
namespace mozilla {
|
2014-08-10 07:06:50 +00:00
|
|
|
|
2015-11-20 05:12:00 +00:00
|
|
|
// Helper functions for generating a ComputedTimingProperties dictionary
|
2015-10-22 22:48:00 +00:00
|
|
|
static void
|
|
|
|
GetComputedTimingDictionary(const ComputedTiming& aComputedTiming,
|
|
|
|
const Nullable<TimeDuration>& aLocalTime,
|
2016-01-13 17:41:00 +00:00
|
|
|
const TimingParams& aTiming,
|
2015-10-22 22:48:00 +00:00
|
|
|
dom::ComputedTimingProperties& aRetVal)
|
|
|
|
{
|
|
|
|
// AnimationEffectTimingProperties
|
|
|
|
aRetVal.mDelay = aTiming.mDelay.ToMilliseconds();
|
2016-03-08 01:31:10 +00:00
|
|
|
aRetVal.mEndDelay = aTiming.mEndDelay.ToMilliseconds();
|
2016-01-13 17:36:00 +00:00
|
|
|
aRetVal.mFill = aComputedTiming.mFill;
|
2016-01-10 17:41:00 +00:00
|
|
|
aRetVal.mIterations = aComputedTiming.mIterations;
|
2016-03-02 07:23:34 +00:00
|
|
|
aRetVal.mIterationStart = aComputedTiming.mIterationStart;
|
2016-01-13 17:36:00 +00:00
|
|
|
aRetVal.mDuration.SetAsUnrestrictedDouble() =
|
|
|
|
aComputedTiming.mDuration.ToMilliseconds();
|
2015-11-20 05:12:00 +00:00
|
|
|
aRetVal.mDirection = aTiming.mDirection;
|
2015-10-22 22:48:00 +00:00
|
|
|
|
|
|
|
// ComputedTimingProperties
|
|
|
|
aRetVal.mActiveDuration = aComputedTiming.mActiveDuration.ToMilliseconds();
|
2016-02-18 23:37:32 +00:00
|
|
|
aRetVal.mEndTime = aComputedTiming.mEndTime.ToMilliseconds();
|
2015-12-03 23:32:53 +00:00
|
|
|
aRetVal.mLocalTime = AnimationUtils::TimeDurationToDouble(aLocalTime);
|
2015-10-22 22:48:00 +00:00
|
|
|
aRetVal.mProgress = aComputedTiming.mProgress;
|
2016-03-02 07:23:34 +00:00
|
|
|
|
2015-10-22 22:48:00 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-08-10 07:06:46 +00:00
|
|
|
namespace dom {
|
|
|
|
|
2015-11-26 07:53:54 +00:00
|
|
|
NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly,
|
|
|
|
AnimationEffectReadOnly,
|
|
|
|
mTarget,
|
|
|
|
mAnimation)
|
2014-08-10 07:06:46 +00:00
|
|
|
|
2015-04-30 13:06:43 +00:00
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly,
|
|
|
|
AnimationEffectReadOnly)
|
2015-04-14 23:48:21 +00:00
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
|
2015-04-30 13:06:43 +00:00
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly)
|
|
|
|
NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
|
2015-04-14 23:48:21 +00:00
|
|
|
|
2015-04-30 13:06:43 +00:00
|
|
|
NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
|
|
|
|
NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
|
2014-08-10 07:06:46 +00:00
|
|
|
|
2015-09-16 14:05:00 +00:00
|
|
|
KeyframeEffectReadOnly::KeyframeEffectReadOnly(
|
|
|
|
nsIDocument* aDocument,
|
|
|
|
Element* aTarget,
|
2016-02-17 20:37:00 +00:00
|
|
|
CSSPseudoElementType aPseudoType,
|
2016-01-13 17:41:00 +00:00
|
|
|
const TimingParams& aTiming)
|
2016-02-15 00:34:47 +00:00
|
|
|
: KeyframeEffectReadOnly(aDocument, aTarget, aPseudoType,
|
|
|
|
new AnimationEffectTimingReadOnly(aTiming))
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
KeyframeEffectReadOnly::KeyframeEffectReadOnly(
|
|
|
|
nsIDocument* aDocument,
|
|
|
|
Element* aTarget,
|
2016-02-17 20:37:00 +00:00
|
|
|
CSSPseudoElementType aPseudoType,
|
2016-02-15 00:34:47 +00:00
|
|
|
AnimationEffectTimingReadOnly* aTiming)
|
2015-09-16 14:05:00 +00:00
|
|
|
: AnimationEffectReadOnly(aDocument)
|
|
|
|
, mTarget(aTarget)
|
2016-02-15 00:34:47 +00:00
|
|
|
, mTiming(*aTiming)
|
2015-09-16 14:05:00 +00:00
|
|
|
, mPseudoType(aPseudoType)
|
2016-01-06 02:04:05 +00:00
|
|
|
, mInEffectOnLastAnimationTimingUpdate(false)
|
2015-09-16 14:05:00 +00:00
|
|
|
{
|
2016-02-15 00:34:47 +00:00
|
|
|
MOZ_ASSERT(aTiming);
|
2015-09-16 14:05:00 +00:00
|
|
|
MOZ_ASSERT(aTarget, "null animation target is not yet supported");
|
|
|
|
}
|
|
|
|
|
2014-08-10 07:06:46 +00:00
|
|
|
JSObject*
|
2015-04-30 13:06:43 +00:00
|
|
|
KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
|
2015-04-14 23:48:21 +00:00
|
|
|
JS::Handle<JSObject*> aGivenProto)
|
2014-08-10 07:06:46 +00:00
|
|
|
{
|
2015-04-30 13:06:43 +00:00
|
|
|
return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto);
|
2014-08-10 07:06:46 +00:00
|
|
|
}
|
|
|
|
|
2015-11-19 18:48:00 +00:00
|
|
|
IterationCompositeOperation
|
|
|
|
KeyframeEffectReadOnly::IterationComposite() const
|
|
|
|
{
|
|
|
|
return IterationCompositeOperation::Replace;
|
|
|
|
}
|
|
|
|
|
|
|
|
CompositeOperation
|
|
|
|
KeyframeEffectReadOnly::Composite() const
|
|
|
|
{
|
|
|
|
return CompositeOperation::Replace;
|
|
|
|
}
|
|
|
|
|
2016-01-13 17:37:00 +00:00
|
|
|
already_AddRefed<AnimationEffectTimingReadOnly>
|
2016-01-13 17:41:00 +00:00
|
|
|
KeyframeEffectReadOnly::Timing() const
|
2016-01-13 17:37:00 +00:00
|
|
|
{
|
|
|
|
RefPtr<AnimationEffectTimingReadOnly> temp(mTiming);
|
|
|
|
return temp.forget();
|
|
|
|
}
|
|
|
|
|
2015-08-17 18:28:00 +00:00
|
|
|
void
|
2016-01-13 17:41:00 +00:00
|
|
|
KeyframeEffectReadOnly::SetSpecifiedTiming(const TimingParams& aTiming)
|
2015-08-17 18:28:00 +00:00
|
|
|
{
|
2016-01-13 17:41:00 +00:00
|
|
|
if (mTiming->AsTimingParams() == aTiming) {
|
2015-08-17 18:28:00 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-01-13 17:41:00 +00:00
|
|
|
mTiming->SetTimingParams(aTiming);
|
2015-10-07 05:30:27 +00:00
|
|
|
if (mAnimation) {
|
|
|
|
mAnimation->NotifyEffectTimingUpdated();
|
|
|
|
}
|
2015-11-26 07:53:53 +00:00
|
|
|
// NotifyEffectTimingUpdated will eventually cause
|
|
|
|
// NotifyAnimationTimingUpdated to be called on this object which will
|
|
|
|
// update our registration with the target element.
|
2015-08-17 18:28:00 +00:00
|
|
|
}
|
|
|
|
|
2016-01-06 02:04:04 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::NotifyAnimationTimingUpdated()
|
|
|
|
{
|
|
|
|
UpdateTargetRegistration();
|
|
|
|
|
|
|
|
// If the effect is not relevant it will be removed from the target
|
|
|
|
// element's effect set. However, effects not in the effect set
|
|
|
|
// will not be included in the set of candidate effects for running on
|
|
|
|
// the compositor and hence they won't have their compositor status
|
|
|
|
// updated. As a result, we need to make sure we clear their compositor
|
|
|
|
// status here.
|
|
|
|
bool isRelevant = mAnimation && mAnimation->IsRelevant();
|
|
|
|
if (!isRelevant) {
|
|
|
|
ResetIsRunningOnCompositor();
|
|
|
|
}
|
2016-01-06 02:04:05 +00:00
|
|
|
|
|
|
|
// Detect changes to "in effect" status since we need to recalculate the
|
|
|
|
// animation cascade for this element whenever that changes.
|
|
|
|
bool inEffect = IsInEffect();
|
|
|
|
if (inEffect != mInEffectOnLastAnimationTimingUpdate) {
|
|
|
|
if (mTarget) {
|
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget, mPseudoType);
|
|
|
|
if (effectSet) {
|
|
|
|
effectSet->MarkCascadeNeedsUpdate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mInEffectOnLastAnimationTimingUpdate = inEffect;
|
|
|
|
}
|
2016-01-06 02:04:06 +00:00
|
|
|
|
|
|
|
// Request restyle if necessary.
|
2016-01-12 22:54:54 +00:00
|
|
|
//
|
2016-01-06 02:04:06 +00:00
|
|
|
// Bug 1235002: We should skip requesting a restyle when mProperties is empty.
|
|
|
|
// However, currently we don't properly encapsulate mProperties so we can't
|
|
|
|
// detect when it changes. As a result, if we skip requesting restyles when
|
|
|
|
// mProperties is empty and we play an animation and *then* add properties to
|
|
|
|
// it (as we currently do when building CSS animations), we will fail to
|
|
|
|
// request a restyle at all. Since animations without properties are rare, we
|
|
|
|
// currently just request the restyle regardless of whether mProperties is
|
|
|
|
// empty or not.
|
2016-01-12 22:54:53 +00:00
|
|
|
//
|
|
|
|
// Bug 1216843: When we implement iteration composite modes, we need to
|
|
|
|
// also detect if the current iteration has changed.
|
2016-01-12 22:54:54 +00:00
|
|
|
if (mAnimation && GetComputedTiming().mProgress != mProgressOnLastCompose) {
|
2016-01-12 22:54:53 +00:00
|
|
|
EffectCompositor::RestyleType restyleType =
|
|
|
|
CanThrottle() ?
|
|
|
|
EffectCompositor::RestyleType::Throttled :
|
|
|
|
EffectCompositor::RestyleType::Standard;
|
2016-01-12 22:54:54 +00:00
|
|
|
nsPresContext* presContext = GetPresContext();
|
|
|
|
if (presContext) {
|
|
|
|
presContext->EffectCompositor()->
|
|
|
|
RequestRestyle(mTarget, mPseudoType, restyleType,
|
|
|
|
mAnimation->CascadeLevel());
|
2016-01-12 22:54:53 +00:00
|
|
|
}
|
2016-01-15 06:15:47 +00:00
|
|
|
|
|
|
|
// If we're not relevant, we will have been removed from the EffectSet.
|
|
|
|
// As a result, when the restyle we requested above is fulfilled, our
|
|
|
|
// ComposeStyle will not get called and mProgressOnLastCompose will not
|
|
|
|
// be updated. Instead, we need to manually clear it.
|
|
|
|
if (!isRelevant) {
|
|
|
|
mProgressOnLastCompose.SetNull();
|
|
|
|
}
|
2016-01-06 02:04:06 +00:00
|
|
|
}
|
2016-01-06 02:04:04 +00:00
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:28 +00:00
|
|
|
Nullable<TimeDuration>
|
|
|
|
KeyframeEffectReadOnly::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;
|
|
|
|
}
|
|
|
|
|
2015-10-22 22:48:00 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::GetComputedTimingAsDict(ComputedTimingProperties& aRetVal) const
|
|
|
|
{
|
|
|
|
const Nullable<TimeDuration> currentTime = GetLocalTime();
|
2016-01-13 17:41:00 +00:00
|
|
|
GetComputedTimingDictionary(GetComputedTimingAt(currentTime,
|
|
|
|
SpecifiedTiming()),
|
2015-10-22 22:48:00 +00:00
|
|
|
currentTime,
|
2016-01-13 17:41:00 +00:00
|
|
|
SpecifiedTiming(),
|
2015-10-22 22:48:00 +00:00
|
|
|
aRetVal);
|
|
|
|
}
|
|
|
|
|
2014-08-10 07:06:50 +00:00
|
|
|
ComputedTiming
|
2015-04-30 13:06:43 +00:00
|
|
|
KeyframeEffectReadOnly::GetComputedTimingAt(
|
2015-04-14 23:48:21 +00:00
|
|
|
const Nullable<TimeDuration>& aLocalTime,
|
2016-01-13 17:41:00 +00:00
|
|
|
const TimingParams& aTiming)
|
2014-08-10 07:06:50 +00:00
|
|
|
{
|
2016-01-13 17:36:00 +00:00
|
|
|
const StickyTimeDuration zeroDuration;
|
2014-08-10 07:06:50 +00:00
|
|
|
|
|
|
|
// Always return the same object to benefit from return-value optimization.
|
|
|
|
ComputedTiming result;
|
|
|
|
|
2016-03-09 05:01:45 +00:00
|
|
|
if (aTiming.mDuration) {
|
|
|
|
MOZ_ASSERT(aTiming.mDuration.ref() >= zeroDuration,
|
|
|
|
"Iteration duration should be positive");
|
2016-03-09 05:14:20 +00:00
|
|
|
result.mDuration = aTiming.mDuration.ref();
|
2016-01-13 17:36:00 +00:00
|
|
|
}
|
2016-03-09 05:14:20 +00:00
|
|
|
|
2016-01-10 17:41:00 +00:00
|
|
|
result.mIterations = IsNaN(aTiming.mIterations) || aTiming.mIterations < 0.0f ?
|
|
|
|
1.0f :
|
|
|
|
aTiming.mIterations;
|
2016-03-02 07:23:34 +00:00
|
|
|
result.mIterationStart = std::max(aTiming.mIterationStart, 0.0);
|
|
|
|
|
2016-01-13 17:36:00 +00:00
|
|
|
result.mActiveDuration = ActiveDuration(result.mDuration, result.mIterations);
|
2016-03-08 01:31:10 +00:00
|
|
|
result.mEndTime = aTiming.mDelay + result.mActiveDuration +
|
|
|
|
aTiming.mEndDelay;
|
2016-01-13 17:36:00 +00:00
|
|
|
result.mFill = aTiming.mFill == dom::FillMode::Auto ?
|
|
|
|
dom::FillMode::None :
|
|
|
|
aTiming.mFill;
|
2014-08-10 07:06:50 +00:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// so we set up a flag for that case.
|
|
|
|
bool isEndOfFinalIteration = false;
|
|
|
|
|
|
|
|
// Get the normalized time within the active interval.
|
2014-09-25 05:25:50 +00:00
|
|
|
StickyTimeDuration activeTime;
|
2016-03-08 01:31:10 +00:00
|
|
|
if (localTime >=
|
|
|
|
std::min(StickyTimeDuration(aTiming.mDelay + result.mActiveDuration),
|
|
|
|
result.mEndTime)) {
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mPhase = ComputedTiming::AnimationPhase::After;
|
2016-01-13 17:36:00 +00:00
|
|
|
if (!result.FillsForwards()) {
|
2014-08-10 07:06:50 +00:00
|
|
|
// The animation isn't active or filling at this time.
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mProgress.SetNull();
|
2014-08-10 07:06:50 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
activeTime = result.mActiveDuration;
|
2016-03-02 07:23:34 +00:00
|
|
|
double finiteProgress =
|
|
|
|
(IsInfinite(result.mIterations) ? 0.0 : result.mIterations)
|
|
|
|
+ result.mIterationStart;
|
2016-01-10 17:41:00 +00:00
|
|
|
isEndOfFinalIteration = result.mIterations != 0.0 &&
|
2016-03-02 07:23:34 +00:00
|
|
|
fmod(finiteProgress, 1.0) == 0;
|
2016-03-08 01:31:10 +00:00
|
|
|
} else if (localTime <
|
|
|
|
std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime)) {
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mPhase = ComputedTiming::AnimationPhase::Before;
|
2016-01-13 17:36:00 +00:00
|
|
|
if (!result.FillsBackwards()) {
|
2014-08-10 07:06:50 +00:00
|
|
|
// The animation isn't active or filling at this time.
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mProgress.SetNull();
|
2014-08-10 07:06:50 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
// activeTime is zero
|
|
|
|
} else {
|
|
|
|
MOZ_ASSERT(result.mActiveDuration != zeroDuration,
|
|
|
|
"How can we be in the middle of a zero-duration interval?");
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mPhase = ComputedTiming::AnimationPhase::Active;
|
2014-08-10 07:06:50 +00:00
|
|
|
activeTime = localTime - aTiming.mDelay;
|
|
|
|
}
|
|
|
|
|
2016-03-02 07:23:34 +00:00
|
|
|
// Calculate the scaled active time
|
|
|
|
// (We handle the case where the iterationStart is zero separately in case
|
|
|
|
// the duration is infinity, since 0 * Infinity is undefined.)
|
|
|
|
StickyTimeDuration startOffset =
|
|
|
|
result.mIterationStart == 0.0
|
|
|
|
? StickyTimeDuration(0)
|
|
|
|
: result.mDuration.MultDouble(result.mIterationStart);
|
|
|
|
StickyTimeDuration scaledActiveTime = activeTime + startOffset;
|
|
|
|
|
2014-08-10 07:06:50 +00:00
|
|
|
// Get the position within the current iteration.
|
2014-09-25 05:25:50 +00:00
|
|
|
StickyTimeDuration iterationTime;
|
2016-03-02 07:23:34 +00:00
|
|
|
if (result.mDuration != zeroDuration &&
|
|
|
|
scaledActiveTime != StickyTimeDuration::Forever()) {
|
2014-08-10 07:06:50 +00:00
|
|
|
iterationTime = isEndOfFinalIteration
|
2016-01-13 17:36:00 +00:00
|
|
|
? result.mDuration
|
2016-03-02 07:23:34 +00:00
|
|
|
: scaledActiveTime % result.mDuration;
|
|
|
|
} /* else, either the duration is zero and iterationTime is zero,
|
|
|
|
or the scaledActiveTime is infinity in which case the iterationTime
|
|
|
|
should become infinity but we will not use the iterationTime in that
|
|
|
|
case so we just leave it as zero */
|
2014-08-10 07:06:50 +00:00
|
|
|
|
|
|
|
// Determine the 0-based index of the current iteration.
|
2016-03-02 07:23:34 +00:00
|
|
|
if (result.mPhase == ComputedTiming::AnimationPhase::Before ||
|
|
|
|
result.mIterations == 0) {
|
|
|
|
result.mCurrentIteration = static_cast<uint64_t>(result.mIterationStart);
|
|
|
|
} else if (result.mPhase == ComputedTiming::AnimationPhase::After) {
|
2014-08-10 07:06:50 +00:00
|
|
|
result.mCurrentIteration =
|
2016-03-02 07:23:34 +00:00
|
|
|
IsInfinite(result.mIterations)
|
2015-10-22 22:48:00 +00:00
|
|
|
? UINT64_MAX // In GetComputedTimingDictionary(), we will convert this
|
|
|
|
// into Infinity.
|
2016-03-02 07:23:34 +00:00
|
|
|
: static_cast<uint64_t>(ceil(result.mIterations +
|
|
|
|
result.mIterationStart)) - 1;
|
|
|
|
} else if (result.mDuration == StickyTimeDuration::Forever()) {
|
|
|
|
result.mCurrentIteration = static_cast<uint64_t>(result.mIterationStart);
|
2014-08-10 07:06:50 +00:00
|
|
|
} else {
|
|
|
|
result.mCurrentIteration =
|
2016-03-02 07:23:34 +00:00
|
|
|
static_cast<uint64_t>(scaledActiveTime / result.mDuration); // floor
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Normalize the iteration time into a fraction of the iteration duration.
|
2016-03-02 07:23:34 +00:00
|
|
|
if (result.mPhase == ComputedTiming::AnimationPhase::Before ||
|
|
|
|
result.mIterations == 0) {
|
|
|
|
double progress = fmod(result.mIterationStart, 1.0);
|
|
|
|
result.mProgress.SetValue(progress);
|
2015-10-18 22:38:00 +00:00
|
|
|
} else if (result.mPhase == ComputedTiming::AnimationPhase::After) {
|
2016-03-02 07:23:34 +00:00
|
|
|
double progress;
|
|
|
|
if (isEndOfFinalIteration) {
|
|
|
|
progress = 1.0;
|
|
|
|
} else if (IsInfinite(result.mIterations)) {
|
|
|
|
progress = fmod(result.mIterationStart, 1.0);
|
|
|
|
} else {
|
|
|
|
progress = fmod(result.mIterations + result.mIterationStart, 1.0);
|
|
|
|
}
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mProgress.SetValue(progress);
|
2014-08-10 07:06:50 +00:00
|
|
|
} else {
|
|
|
|
// We are in the active phase so the iteration duration can't be zero.
|
2016-01-13 17:36:00 +00:00
|
|
|
MOZ_ASSERT(result.mDuration != zeroDuration,
|
2014-08-10 07:06:50 +00:00
|
|
|
"In the active phase of a zero-duration animation?");
|
2016-01-13 17:36:00 +00:00
|
|
|
double progress = result.mDuration == StickyTimeDuration::Forever()
|
2016-03-02 07:23:34 +00:00
|
|
|
? fmod(result.mIterationStart, 1.0)
|
2016-01-13 17:36:00 +00:00
|
|
|
: iterationTime / result.mDuration;
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mProgress.SetValue(progress);
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool thisIterationReverse = false;
|
|
|
|
switch (aTiming.mDirection) {
|
2015-11-20 05:12:00 +00:00
|
|
|
case PlaybackDirection::Normal:
|
2014-08-10 07:06:50 +00:00
|
|
|
thisIterationReverse = false;
|
|
|
|
break;
|
2015-11-20 05:12:00 +00:00
|
|
|
case PlaybackDirection::Reverse:
|
2014-08-10 07:06:50 +00:00
|
|
|
thisIterationReverse = true;
|
|
|
|
break;
|
2015-11-20 05:12:00 +00:00
|
|
|
case PlaybackDirection::Alternate:
|
2014-08-10 07:06:50 +00:00
|
|
|
thisIterationReverse = (result.mCurrentIteration & 1) == 1;
|
|
|
|
break;
|
2015-11-20 05:12:00 +00:00
|
|
|
case PlaybackDirection::Alternate_reverse:
|
2014-08-10 07:06:50 +00:00
|
|
|
thisIterationReverse = (result.mCurrentIteration & 1) == 0;
|
|
|
|
break;
|
2015-11-20 05:12:00 +00:00
|
|
|
default:
|
|
|
|
MOZ_ASSERT(true, "Unknown PlaybackDirection type");
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
|
|
|
if (thisIterationReverse) {
|
2015-10-18 22:38:00 +00:00
|
|
|
result.mProgress.SetValue(1.0 - result.mProgress.Value());
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
|
|
|
|
2016-04-01 05:00:57 +00:00
|
|
|
if ((result.mPhase == ComputedTiming::AnimationPhase::After &&
|
|
|
|
thisIterationReverse) ||
|
|
|
|
(result.mPhase == ComputedTiming::AnimationPhase::Before &&
|
|
|
|
!thisIterationReverse)) {
|
|
|
|
result.mBeforeFlag = ComputedTimingFunction::BeforeFlag::Set;
|
|
|
|
}
|
|
|
|
|
2016-01-29 13:48:00 +00:00
|
|
|
if (aTiming.mFunction) {
|
|
|
|
result.mProgress.SetValue(
|
2016-04-01 05:00:57 +00:00
|
|
|
aTiming.mFunction->GetValue(result.mProgress.Value(),
|
|
|
|
result.mBeforeFlag));
|
2016-01-29 13:48:00 +00:00
|
|
|
}
|
|
|
|
|
2014-08-10 07:06:50 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2014-09-25 05:25:50 +00:00
|
|
|
StickyTimeDuration
|
2016-02-18 23:37:31 +00:00
|
|
|
KeyframeEffectReadOnly::ActiveDuration(
|
|
|
|
const StickyTimeDuration& aIterationDuration,
|
|
|
|
double aIterationCount)
|
2014-08-10 07:06:50 +00:00
|
|
|
{
|
2016-02-18 23:37:31 +00:00
|
|
|
// 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.
|
|
|
|
const StickyTimeDuration zeroDuration;
|
|
|
|
if (aIterationDuration == zeroDuration ||
|
|
|
|
aIterationCount == 0.0) {
|
|
|
|
return zeroDuration;
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
2016-02-18 23:37:31 +00:00
|
|
|
|
2016-01-13 17:36:00 +00:00
|
|
|
return aIterationDuration.MultDouble(aIterationCount);
|
2014-08-10 07:06:50 +00:00
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:27 +00:00
|
|
|
// https://w3c.github.io/web-animations/#in-play
|
2015-03-27 08:54:39 +00:00
|
|
|
bool
|
2015-10-07 05:30:27 +00:00
|
|
|
KeyframeEffectReadOnly::IsInPlay() const
|
2015-03-27 08:54:39 +00:00
|
|
|
{
|
2015-10-07 05:30:27 +00:00
|
|
|
if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
|
2015-03-27 08:54:39 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-10-18 22:38:00 +00:00
|
|
|
return GetComputedTiming().mPhase == ComputedTiming::AnimationPhase::Active;
|
2015-03-27 08:54:39 +00:00
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:27 +00:00
|
|
|
// https://w3c.github.io/web-animations/#current
|
2014-10-02 06:14:14 +00:00
|
|
|
bool
|
2015-10-07 05:30:27 +00:00
|
|
|
KeyframeEffectReadOnly::IsCurrent() const
|
2014-10-02 06:14:14 +00:00
|
|
|
{
|
2015-10-07 05:30:27 +00:00
|
|
|
if (!mAnimation || mAnimation->PlayState() == AnimationPlayState::Finished) {
|
2014-10-02 06:14:14 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
ComputedTiming computedTiming = GetComputedTiming();
|
2015-10-18 22:38:00 +00:00
|
|
|
return computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
|
|
|
|
computedTiming.mPhase == ComputedTiming::AnimationPhase::Active;
|
2014-10-02 06:14:14 +00:00
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:27 +00:00
|
|
|
// https://w3c.github.io/web-animations/#in-effect
|
2014-10-02 06:14:14 +00:00
|
|
|
bool
|
2015-04-30 13:06:43 +00:00
|
|
|
KeyframeEffectReadOnly::IsInEffect() const
|
2014-10-02 06:14:14 +00:00
|
|
|
{
|
|
|
|
ComputedTiming computedTiming = GetComputedTiming();
|
2015-10-18 22:38:00 +00:00
|
|
|
return !computedTiming.mProgress.IsNull();
|
2014-10-02 06:14:14 +00:00
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:27 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation)
|
|
|
|
{
|
|
|
|
mAnimation = aAnimation;
|
2015-11-26 07:53:53 +00:00
|
|
|
NotifyAnimationTimingUpdated();
|
2015-10-07 05:30:27 +00:00
|
|
|
}
|
|
|
|
|
2015-03-20 04:10:00 +00:00
|
|
|
const AnimationProperty*
|
2015-04-30 13:06:43 +00:00
|
|
|
KeyframeEffectReadOnly::GetAnimationOfProperty(nsCSSProperty aProperty) const
|
2014-08-10 07:06:49 +00:00
|
|
|
{
|
|
|
|
for (size_t propIdx = 0, propEnd = mProperties.Length();
|
|
|
|
propIdx != propEnd; ++propIdx) {
|
|
|
|
if (aProperty == mProperties[propIdx].mProperty) {
|
2015-03-31 22:05:55 +00:00
|
|
|
const AnimationProperty* result = &mProperties[propIdx];
|
|
|
|
if (!result->mWinsInCascade) {
|
|
|
|
result = nullptr;
|
|
|
|
}
|
|
|
|
return result;
|
2014-08-10 07:06:49 +00:00
|
|
|
}
|
|
|
|
}
|
2015-03-20 04:10:00 +00:00
|
|
|
return nullptr;
|
2014-08-10 07:06:49 +00:00
|
|
|
}
|
|
|
|
|
2015-04-06 02:53:51 +00:00
|
|
|
bool
|
2015-04-30 13:06:43 +00:00
|
|
|
KeyframeEffectReadOnly::HasAnimationOfProperties(
|
2015-04-14 23:48:21 +00:00
|
|
|
const nsCSSProperty* aProperties,
|
|
|
|
size_t aPropertyCount) const
|
2015-04-06 02:53:51 +00:00
|
|
|
{
|
|
|
|
for (size_t i = 0; i < aPropertyCount; i++) {
|
|
|
|
if (HasAnimationOfProperty(aProperties[i])) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-02-19 00:16:15 +00:00
|
|
|
bool
|
2016-02-19 00:16:15 +00:00
|
|
|
KeyframeEffectReadOnly::UpdateProperties(
|
|
|
|
const InfallibleTArray<AnimationProperty>& aProperties)
|
2016-01-13 04:38:16 +00:00
|
|
|
{
|
2016-02-19 00:16:15 +00:00
|
|
|
// AnimationProperty::operator== does not compare mWinsInCascade and
|
|
|
|
// mIsRunningOnCompositor, we don't need to update anything here because
|
|
|
|
// we want to preserve
|
|
|
|
if (mProperties == aProperties) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-01-13 04:38:16 +00:00
|
|
|
nsCSSPropertySet winningInCascadeProperties;
|
|
|
|
nsCSSPropertySet runningOnCompositorProperties;
|
|
|
|
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
|
|
if (property.mWinsInCascade) {
|
|
|
|
winningInCascadeProperties.AddProperty(property.mProperty);
|
|
|
|
}
|
|
|
|
if (property.mIsRunningOnCompositor) {
|
|
|
|
runningOnCompositorProperties.AddProperty(property.mProperty);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-19 00:16:15 +00:00
|
|
|
mProperties = aProperties;
|
2016-01-13 04:38:16 +00:00
|
|
|
|
|
|
|
for (AnimationProperty& property : mProperties) {
|
|
|
|
property.mWinsInCascade =
|
|
|
|
winningInCascadeProperties.HasProperty(property.mProperty);
|
|
|
|
property.mIsRunningOnCompositor =
|
|
|
|
runningOnCompositorProperties.HasProperty(property.mProperty);
|
|
|
|
}
|
2016-02-19 00:16:15 +00:00
|
|
|
|
|
|
|
if (mAnimation) {
|
|
|
|
nsPresContext* presContext = GetPresContext();
|
|
|
|
if (presContext) {
|
|
|
|
presContext->EffectCompositor()->
|
|
|
|
RequestRestyle(mTarget, mPseudoType,
|
|
|
|
EffectCompositor::RestyleType::Layer,
|
|
|
|
mAnimation->CascadeLevel());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2016-01-13 04:38:16 +00:00
|
|
|
}
|
|
|
|
|
2014-10-20 04:55:46 +00:00
|
|
|
void
|
2015-10-18 05:24:48 +00:00
|
|
|
KeyframeEffectReadOnly::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
|
2015-07-29 01:57:39 +00:00
|
|
|
nsCSSPropertySet& aSetProperties)
|
2014-10-20 04:55:46 +00:00
|
|
|
{
|
|
|
|
ComputedTiming computedTiming = GetComputedTiming();
|
2016-01-06 02:04:06 +00:00
|
|
|
mProgressOnLastCompose = computedTiming.mProgress;
|
2014-10-20 04:55:46 +00:00
|
|
|
|
2015-05-13 04:57:35 +00:00
|
|
|
// If the progress is null, we don't have fill data for the current
|
2014-10-20 04:55:46 +00:00
|
|
|
// time so we shouldn't animate.
|
2015-10-18 22:38:00 +00:00
|
|
|
if (computedTiming.mProgress.IsNull()) {
|
2014-10-20 04:55:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t propIdx = 0, propEnd = mProperties.Length();
|
|
|
|
propIdx != propEnd; ++propIdx)
|
|
|
|
{
|
|
|
|
const AnimationProperty& prop = mProperties[propIdx];
|
|
|
|
|
|
|
|
MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
|
|
|
|
MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
|
|
|
|
"incorrect last to key");
|
|
|
|
|
|
|
|
if (aSetProperties.HasProperty(prop.mProperty)) {
|
2016-01-12 22:54:54 +00:00
|
|
|
// Animations are composed by EffectCompositor by iterating
|
2014-10-20 04:55:46 +00:00
|
|
|
// from the last animation to first. For animations targetting the
|
|
|
|
// same property, the later one wins. So if this property is already set,
|
|
|
|
// we should not override it.
|
2015-03-06 21:35:45 +00:00
|
|
|
continue;
|
2014-10-20 04:55:46 +00:00
|
|
|
}
|
|
|
|
|
2015-03-20 04:10:00 +00:00
|
|
|
if (!prop.mWinsInCascade) {
|
|
|
|
// This isn't the winning declaration, so don't add it to style.
|
|
|
|
// For transitions, this is important, because it's how we
|
|
|
|
// implement the rule that CSS transitions don't run when a CSS
|
|
|
|
// animation is running on the same property and element. For
|
|
|
|
// animations, this is only skipping things that will otherwise be
|
|
|
|
// overridden.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2014-10-20 04:55:46 +00:00
|
|
|
aSetProperties.AddProperty(prop.mProperty);
|
|
|
|
|
|
|
|
MOZ_ASSERT(prop.mSegments.Length() > 0,
|
|
|
|
"property should not be in animations if it has no segments");
|
|
|
|
|
|
|
|
// FIXME: Maybe cache the current segment?
|
|
|
|
const AnimationPropertySegment *segment = prop.mSegments.Elements(),
|
|
|
|
*segmentEnd = segment + prop.mSegments.Length();
|
2016-03-15 13:13:46 +00:00
|
|
|
while (segment->mToKey <= computedTiming.mProgress.Value()) {
|
|
|
|
MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
|
2016-02-18 13:20:00 +00:00
|
|
|
if ((segment+1) == segmentEnd) {
|
|
|
|
break;
|
2014-10-20 04:55:46 +00:00
|
|
|
}
|
2016-02-18 13:20:00 +00:00
|
|
|
++segment;
|
2014-10-20 04:55:46 +00:00
|
|
|
MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys");
|
|
|
|
}
|
2016-03-15 13:13:46 +00:00
|
|
|
MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
|
2014-10-20 04:55:46 +00:00
|
|
|
MOZ_ASSERT(segment >= prop.mSegments.Elements() &&
|
|
|
|
size_t(segment - prop.mSegments.Elements()) <
|
|
|
|
prop.mSegments.Length(),
|
|
|
|
"out of array bounds");
|
|
|
|
|
|
|
|
if (!aStyleRule) {
|
|
|
|
// Allocate the style rule now that we know we have animation data.
|
2015-07-29 01:57:39 +00:00
|
|
|
aStyleRule = new AnimValuesStyleRule();
|
2014-10-20 04:55:46 +00:00
|
|
|
}
|
|
|
|
|
2016-03-15 13:13:46 +00:00
|
|
|
// Special handling for zero-length segments
|
|
|
|
if (segment->mToKey == segment->mFromKey) {
|
|
|
|
if (computedTiming.mProgress.Value() < 0) {
|
2016-03-29 23:59:01 +00:00
|
|
|
aStyleRule->AddValue(prop.mProperty, segment->mFromValue);
|
2016-03-15 13:13:46 +00:00
|
|
|
} else {
|
2016-03-29 23:59:01 +00:00
|
|
|
aStyleRule->AddValue(prop.mProperty, segment->mToValue);
|
2016-03-15 13:13:46 +00:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2014-10-20 04:55:46 +00:00
|
|
|
double positionInSegment =
|
2015-10-18 22:38:00 +00:00
|
|
|
(computedTiming.mProgress.Value() - segment->mFromKey) /
|
2014-10-20 04:55:46 +00:00
|
|
|
(segment->mToKey - segment->mFromKey);
|
|
|
|
double valuePosition =
|
2016-01-29 13:44:00 +00:00
|
|
|
ComputedTimingFunction::GetPortion(segment->mTimingFunction,
|
2016-04-01 05:00:57 +00:00
|
|
|
positionInSegment,
|
|
|
|
computedTiming.mBeforeFlag);
|
2014-10-20 04:55:46 +00:00
|
|
|
|
2016-03-29 23:59:01 +00:00
|
|
|
StyleAnimationValue val;
|
2014-10-20 04:55:46 +00:00
|
|
|
#ifdef DEBUG
|
|
|
|
bool result =
|
|
|
|
#endif
|
|
|
|
StyleAnimationValue::Interpolate(prop.mProperty,
|
|
|
|
segment->mFromValue,
|
|
|
|
segment->mToValue,
|
2016-03-29 23:59:01 +00:00
|
|
|
valuePosition, val);
|
2014-10-20 04:55:46 +00:00
|
|
|
MOZ_ASSERT(result, "interpolate must succeed now");
|
2016-03-29 23:59:01 +00:00
|
|
|
|
|
|
|
aStyleRule->AddValue(prop.mProperty, Move(val));
|
2014-10-20 04:55:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-16 14:05:00 +00:00
|
|
|
bool
|
|
|
|
KeyframeEffectReadOnly::IsRunningOnCompositor() const
|
|
|
|
{
|
|
|
|
// We consider animation is running on compositor if there is at least
|
|
|
|
// one property running on compositor.
|
|
|
|
// Animation.IsRunningOnCompotitor will return more fine grained
|
|
|
|
// information in bug 1196114.
|
2015-12-20 13:16:00 +00:00
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
|
|
if (property.mIsRunningOnCompositor) {
|
2015-09-16 14:05:00 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSProperty aProperty,
|
|
|
|
bool aIsRunning)
|
|
|
|
{
|
|
|
|
MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
|
|
|
|
CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
|
|
|
|
"Property being animated on compositor is a recognized "
|
|
|
|
"compositor-animatable property");
|
2015-12-20 13:16:00 +00:00
|
|
|
for (AnimationProperty& property : mProperties) {
|
|
|
|
if (property.mProperty == aProperty) {
|
|
|
|
property.mIsRunningOnCompositor = aIsRunning;
|
2016-03-03 21:36:36 +00:00
|
|
|
// We currently only set a performance warning message when animations
|
|
|
|
// cannot be run on the compositor, so if this animation is running
|
|
|
|
// on the compositor we don't need a message.
|
|
|
|
if (aIsRunning) {
|
|
|
|
property.mPerformanceWarning.reset();
|
|
|
|
}
|
2015-09-16 14:05:00 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-07 05:30:27 +00:00
|
|
|
KeyframeEffectReadOnly::~KeyframeEffectReadOnly()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2016-03-11 08:27:16 +00:00
|
|
|
template <class KeyframeEffectType, class OptionsType>
|
|
|
|
/* static */ already_AddRefed<KeyframeEffectType>
|
|
|
|
KeyframeEffectReadOnly::ConstructKeyframeEffect(
|
|
|
|
const GlobalObject& aGlobal,
|
|
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
|
|
JS::Handle<JSObject*> aFrames,
|
|
|
|
const OptionsType& aOptions,
|
|
|
|
ErrorResult& aRv)
|
|
|
|
{
|
2016-03-11 08:27:34 +00:00
|
|
|
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
|
|
|
|
if (!doc) {
|
|
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2016-03-11 08:27:16 +00:00
|
|
|
TimingParams timingParams =
|
2016-03-11 08:27:34 +00:00
|
|
|
TimingParams::FromOptionsUnion(aOptions, doc, aRv);
|
2016-03-11 08:27:16 +00:00
|
|
|
if (aRv.Failed()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2016-02-15 00:34:47 +00:00
|
|
|
if (aTarget.IsNull()) {
|
|
|
|
// We don't support null targets yet.
|
|
|
|
aRv.Throw(NS_ERROR_DOM_ANIM_NO_TARGET_ERR);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ElementOrCSSPseudoElement& target = aTarget.Value();
|
|
|
|
MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
|
|
|
|
"Uninitialized target");
|
|
|
|
|
|
|
|
RefPtr<Element> targetElement;
|
2016-02-17 20:37:00 +00:00
|
|
|
CSSPseudoElementType pseudoType = CSSPseudoElementType::NotPseudo;
|
2016-02-15 00:34:47 +00:00
|
|
|
if (target.IsElement()) {
|
|
|
|
targetElement = &target.GetAsElement();
|
|
|
|
} else {
|
|
|
|
targetElement = target.GetAsCSSPseudoElement().ParentElement();
|
|
|
|
pseudoType = target.GetAsCSSPseudoElement().GetType();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!targetElement->GetComposedDoc()) {
|
|
|
|
aRv.Throw(NS_ERROR_DOM_ANIM_TARGET_NOT_IN_DOC_ERR);
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2016-03-22 07:35:53 +00:00
|
|
|
nsTArray<Keyframe> keyframes =
|
|
|
|
KeyframeUtils::GetKeyframesFromObject(aGlobal.Context(), aFrames, aRv);
|
2016-02-15 00:34:47 +00:00
|
|
|
if (aRv.Failed()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2016-03-22 07:35:53 +00:00
|
|
|
KeyframeUtils::ApplyDistributeSpacing(keyframes);
|
|
|
|
|
|
|
|
RefPtr<nsStyleContext> styleContext;
|
|
|
|
nsIPresShell* shell = doc->GetShell();
|
|
|
|
if (shell && targetElement) {
|
|
|
|
nsIAtom* pseudo =
|
|
|
|
pseudoType < CSSPseudoElementType::Count ?
|
|
|
|
nsCSSPseudoElements::GetPseudoAtom(pseudoType) : nullptr;
|
|
|
|
styleContext =
|
|
|
|
nsComputedDOMStyle::GetStyleContextForElement(targetElement, pseudo,
|
|
|
|
shell);
|
|
|
|
}
|
|
|
|
|
|
|
|
nsTArray<AnimationProperty> animationProperties;
|
|
|
|
if (styleContext) {
|
|
|
|
animationProperties =
|
|
|
|
KeyframeUtils::GetAnimationPropertiesFromKeyframes(styleContext,
|
|
|
|
targetElement,
|
|
|
|
pseudoType,
|
|
|
|
keyframes);
|
|
|
|
}
|
2016-02-15 00:34:47 +00:00
|
|
|
|
|
|
|
RefPtr<KeyframeEffectType> effect =
|
|
|
|
new KeyframeEffectType(targetElement->OwnerDoc(), targetElement,
|
2016-03-12 13:14:10 +00:00
|
|
|
pseudoType, timingParams);
|
2016-02-15 00:34:47 +00:00
|
|
|
effect->mProperties = Move(animationProperties);
|
|
|
|
return effect.forget();
|
|
|
|
}
|
|
|
|
|
2015-09-16 14:05:00 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::ResetIsRunningOnCompositor()
|
|
|
|
{
|
2015-12-20 13:16:00 +00:00
|
|
|
for (AnimationProperty& property : mProperties) {
|
|
|
|
property.mIsRunningOnCompositor = false;
|
2015-09-16 14:05:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-26 07:53:53 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::UpdateTargetRegistration()
|
|
|
|
{
|
|
|
|
if (!mTarget) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool isRelevant = mAnimation && mAnimation->IsRelevant();
|
|
|
|
|
|
|
|
// Animation::IsRelevant() returns a cached value. It only updates when
|
|
|
|
// something calls Animation::UpdateRelevance. Whenever our timing changes,
|
|
|
|
// we should be notifying our Animation before calling this, so
|
|
|
|
// Animation::IsRelevant() should be up-to-date by the time we get here.
|
|
|
|
MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(),
|
|
|
|
"Out of date Animation::IsRelevant value");
|
|
|
|
|
|
|
|
if (isRelevant) {
|
|
|
|
EffectSet* effectSet = EffectSet::GetOrCreateEffectSet(mTarget,
|
|
|
|
mPseudoType);
|
|
|
|
effectSet->AddEffect(*this);
|
|
|
|
} else {
|
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget, mPseudoType);
|
|
|
|
if (effectSet) {
|
|
|
|
effectSet->RemoveEffect(*this);
|
2016-01-15 06:15:47 +00:00
|
|
|
if (effectSet->IsEmpty()) {
|
|
|
|
EffectSet::DestroyEffectSet(mTarget, mPseudoType);
|
|
|
|
}
|
2015-11-26 07:53:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
#ifdef DEBUG
|
|
|
|
void
|
|
|
|
DumpAnimationProperties(nsTArray<AnimationProperty>& aAnimationProperties)
|
|
|
|
{
|
|
|
|
for (auto& p : aAnimationProperties) {
|
|
|
|
printf("%s\n", nsCSSProps::GetStringValue(p.mProperty).get());
|
|
|
|
for (auto& s : p.mSegments) {
|
|
|
|
nsString fromValue, toValue;
|
|
|
|
StyleAnimationValue::UncomputeValue(p.mProperty,
|
|
|
|
s.mFromValue,
|
|
|
|
fromValue);
|
|
|
|
StyleAnimationValue::UncomputeValue(p.mProperty,
|
|
|
|
s.mToValue,
|
|
|
|
toValue);
|
|
|
|
printf(" %f..%f: %s..%s\n", s.mFromKey, s.mToKey,
|
|
|
|
NS_ConvertUTF16toUTF8(fromValue).get(),
|
|
|
|
NS_ConvertUTF16toUTF8(toValue).get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/**
|
2015-10-22 08:22:38 +00:00
|
|
|
* A property and StyleAnimationValue pair.
|
2015-10-22 08:22:38 +00:00
|
|
|
*/
|
2015-10-22 08:22:38 +00:00
|
|
|
struct KeyframeValue
|
2015-09-29 02:20:14 +00:00
|
|
|
{
|
|
|
|
nsCSSProperty mProperty;
|
2015-10-22 08:22:38 +00:00
|
|
|
StyleAnimationValue mValue;
|
|
|
|
};
|
2015-10-22 08:22:37 +00:00
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
/**
|
|
|
|
* Represents a relative position for a value in a keyframe animation.
|
|
|
|
*/
|
|
|
|
enum class ValuePosition
|
|
|
|
{
|
|
|
|
First, // value at 0 used for reverse filling
|
|
|
|
Left, // value coming in to a given offset
|
|
|
|
Right, // value coming out from a given offset
|
|
|
|
Last // value at 1 used for forward filling
|
2015-10-22 08:22:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-10-22 08:22:38 +00:00
|
|
|
* A single value in a keyframe animation, used by GetFrames to produce a
|
2016-03-22 07:16:39 +00:00
|
|
|
* minimal set of keyframe objects.
|
2015-10-22 08:22:37 +00:00
|
|
|
*/
|
2015-10-22 08:22:38 +00:00
|
|
|
struct OrderedKeyframeValueEntry : KeyframeValue
|
2015-10-22 08:22:37 +00:00
|
|
|
{
|
2015-10-22 08:22:38 +00:00
|
|
|
float mOffset;
|
2016-01-29 13:47:00 +00:00
|
|
|
const Maybe<ComputedTimingFunction>* mTimingFunction;
|
2015-10-22 08:22:38 +00:00
|
|
|
ValuePosition mPosition;
|
2015-09-29 02:20:14 +00:00
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
bool SameKeyframe(const OrderedKeyframeValueEntry& aOther) const
|
2015-09-29 02:20:14 +00:00
|
|
|
{
|
2015-10-22 08:22:38 +00:00
|
|
|
return mOffset == aOther.mOffset &&
|
|
|
|
!!mTimingFunction == !!aOther.mTimingFunction &&
|
|
|
|
(!mTimingFunction || *mTimingFunction == *aOther.mTimingFunction) &&
|
|
|
|
mPosition == aOther.mPosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ForKeyframeGenerationComparator
|
|
|
|
{
|
|
|
|
static bool Equals(const OrderedKeyframeValueEntry& aLhs,
|
|
|
|
const OrderedKeyframeValueEntry& aRhs)
|
2015-10-22 08:22:37 +00:00
|
|
|
{
|
2015-10-22 08:22:38 +00:00
|
|
|
return aLhs.SameKeyframe(aRhs) &&
|
|
|
|
aLhs.mProperty == aRhs.mProperty;
|
2015-09-29 02:20:14 +00:00
|
|
|
}
|
2015-10-22 08:22:38 +00:00
|
|
|
static bool LessThan(const OrderedKeyframeValueEntry& aLhs,
|
|
|
|
const OrderedKeyframeValueEntry& aRhs)
|
2015-10-22 08:22:37 +00:00
|
|
|
{
|
|
|
|
// First, sort by offset.
|
|
|
|
if (aLhs.mOffset != aRhs.mOffset) {
|
|
|
|
return aLhs.mOffset < aRhs.mOffset;
|
|
|
|
}
|
2015-09-29 02:20:14 +00:00
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
// Second, by position.
|
|
|
|
if (aLhs.mPosition != aRhs.mPosition) {
|
|
|
|
return aLhs.mPosition < aRhs.mPosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Third, by easing.
|
|
|
|
if (aLhs.mTimingFunction) {
|
|
|
|
if (aRhs.mTimingFunction) {
|
2016-01-29 13:47:00 +00:00
|
|
|
int32_t order =
|
|
|
|
ComputedTimingFunction::Compare(*aLhs.mTimingFunction,
|
|
|
|
*aRhs.mTimingFunction);
|
2015-10-22 08:22:38 +00:00
|
|
|
if (order != 0) {
|
|
|
|
return order < 0;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (aRhs.mTimingFunction) {
|
|
|
|
return false;
|
|
|
|
}
|
2015-10-22 08:22:37 +00:00
|
|
|
}
|
2015-09-29 02:20:14 +00:00
|
|
|
|
2015-10-22 08:22:37 +00:00
|
|
|
// Last, by property IDL name.
|
|
|
|
return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
|
|
|
|
nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
|
|
|
|
}
|
|
|
|
};
|
2015-09-29 02:20:14 +00:00
|
|
|
};
|
|
|
|
|
2016-03-11 08:27:16 +00:00
|
|
|
/* static */ already_AddRefed<KeyframeEffectReadOnly>
|
|
|
|
KeyframeEffectReadOnly::Constructor(
|
|
|
|
const GlobalObject& aGlobal,
|
|
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
|
|
JS::Handle<JSObject*> aFrames,
|
|
|
|
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
|
|
|
|
ErrorResult& aRv)
|
|
|
|
{
|
|
|
|
return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget,
|
|
|
|
aFrames, aOptions,
|
|
|
|
aRv);
|
|
|
|
}
|
|
|
|
|
2016-02-05 18:01:00 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::GetTarget(
|
|
|
|
Nullable<OwningElementOrCSSPseudoElement>& aRv) const
|
|
|
|
{
|
2016-02-01 22:59:00 +00:00
|
|
|
if (!mTarget) {
|
|
|
|
aRv.SetNull();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (mPseudoType) {
|
2016-02-16 22:07:00 +00:00
|
|
|
case CSSPseudoElementType::before:
|
|
|
|
case CSSPseudoElementType::after:
|
2016-02-01 22:59:00 +00:00
|
|
|
aRv.SetValue().SetAsCSSPseudoElement() =
|
|
|
|
CSSPseudoElement::GetCSSPseudoElement(mTarget, mPseudoType);
|
|
|
|
break;
|
|
|
|
|
2016-02-16 22:07:00 +00:00
|
|
|
case CSSPseudoElementType::NotPseudo:
|
2016-02-01 22:59:00 +00:00
|
|
|
aRv.SetValue().SetAsElement() = mTarget;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
NS_NOTREACHED("Animation of unsupported pseudo-type");
|
|
|
|
aRv.SetNull();
|
|
|
|
}
|
2016-02-05 18:01:00 +00:00
|
|
|
}
|
|
|
|
|
2016-03-15 13:42:14 +00:00
|
|
|
static void
|
|
|
|
CreatePropertyValue(nsCSSProperty aProperty,
|
|
|
|
float aOffset,
|
|
|
|
const Maybe<ComputedTimingFunction>& aTimingFunction,
|
|
|
|
const StyleAnimationValue& aValue,
|
|
|
|
AnimationPropertyValueDetails& aResult)
|
|
|
|
{
|
2016-03-17 02:13:50 +00:00
|
|
|
aResult.mOffset = aOffset;
|
2016-03-15 13:42:14 +00:00
|
|
|
|
|
|
|
nsString stringValue;
|
|
|
|
StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
|
2016-03-17 02:13:50 +00:00
|
|
|
aResult.mValue = stringValue;
|
2016-03-15 13:42:14 +00:00
|
|
|
|
|
|
|
if (aTimingFunction) {
|
|
|
|
aResult.mEasing.Construct();
|
|
|
|
aTimingFunction->AppendToString(aResult.mEasing.Value());
|
|
|
|
} else {
|
|
|
|
aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
|
|
|
|
}
|
|
|
|
|
2016-03-17 02:13:50 +00:00
|
|
|
aResult.mComposite = CompositeOperation::Replace;
|
2016-03-15 13:42:14 +00:00
|
|
|
}
|
|
|
|
|
2016-03-15 13:18:50 +00:00
|
|
|
void
|
2016-03-13 11:10:10 +00:00
|
|
|
KeyframeEffectReadOnly::GetProperties(
|
2016-03-17 04:47:14 +00:00
|
|
|
nsTArray<AnimationPropertyDetails>& aProperties,
|
|
|
|
ErrorResult& aRv) const
|
2016-03-15 13:18:50 +00:00
|
|
|
{
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
2016-03-13 11:10:10 +00:00
|
|
|
AnimationPropertyDetails propertyDetails;
|
2016-03-17 02:13:50 +00:00
|
|
|
propertyDetails.mProperty =
|
|
|
|
NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
|
|
|
|
propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
|
2016-03-15 13:18:50 +00:00
|
|
|
|
|
|
|
nsXPIDLString localizedString;
|
|
|
|
if (property.mPerformanceWarning &&
|
|
|
|
property.mPerformanceWarning->ToLocalizedString(localizedString)) {
|
2016-03-13 11:10:10 +00:00
|
|
|
propertyDetails.mWarning.Construct(localizedString);
|
2016-03-15 13:18:50 +00:00
|
|
|
}
|
|
|
|
|
2016-03-17 04:47:14 +00:00
|
|
|
if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
|
|
|
|
mozilla::fallible)) {
|
|
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
return;
|
2016-03-15 13:42:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t segmentIdx = 0, segmentLen = property.mSegments.Length();
|
|
|
|
segmentIdx < segmentLen;
|
|
|
|
segmentIdx++)
|
|
|
|
{
|
|
|
|
const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
|
|
|
|
|
|
|
|
binding_detail::FastAnimationPropertyValueDetails fromValue;
|
|
|
|
CreatePropertyValue(property.mProperty, segment.mFromKey,
|
|
|
|
segment.mTimingFunction, segment.mFromValue,
|
|
|
|
fromValue);
|
|
|
|
// We don't apply timing functions for zero-length segments, so
|
|
|
|
// don't return one here.
|
|
|
|
if (segment.mFromKey == segment.mToKey) {
|
|
|
|
fromValue.mEasing.Reset();
|
|
|
|
}
|
|
|
|
// The following won't fail since we have already allocated the capacity
|
|
|
|
// above.
|
2016-03-17 02:13:50 +00:00
|
|
|
propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
|
2016-03-15 13:42:14 +00:00
|
|
|
|
|
|
|
// Normally we can ignore the to-value for this segment since it is
|
|
|
|
// identical to the from-value from the next segment. However, we need
|
|
|
|
// to add it if either:
|
|
|
|
// a) this is the last segment, or
|
|
|
|
// b) the next segment's from-value differs.
|
|
|
|
if (segmentIdx == segmentLen - 1 ||
|
|
|
|
property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
|
|
|
|
binding_detail::FastAnimationPropertyValueDetails toValue;
|
|
|
|
CreatePropertyValue(property.mProperty, segment.mToKey,
|
|
|
|
Nothing(), segment.mToValue, toValue);
|
|
|
|
// It doesn't really make sense to have a timing function on the
|
|
|
|
// last property value or before a sudden jump so we just drop the
|
|
|
|
// easing property altogether.
|
|
|
|
toValue.mEasing.Reset();
|
2016-03-17 02:13:50 +00:00
|
|
|
propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
|
2016-03-15 13:42:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-13 11:10:10 +00:00
|
|
|
aProperties.AppendElement(propertyDetails);
|
2016-03-15 13:18:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-29 02:20:14 +00:00
|
|
|
void
|
|
|
|
KeyframeEffectReadOnly::GetFrames(JSContext*& aCx,
|
|
|
|
nsTArray<JSObject*>& aResult,
|
|
|
|
ErrorResult& aRv)
|
|
|
|
{
|
2015-10-22 08:22:38 +00:00
|
|
|
nsTArray<OrderedKeyframeValueEntry> entries;
|
|
|
|
|
2015-09-29 02:20:14 +00:00
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
|
|
for (size_t i = 0, n = property.mSegments.Length(); i < n; i++) {
|
|
|
|
const AnimationPropertySegment& segment = property.mSegments[i];
|
2015-10-22 08:22:38 +00:00
|
|
|
|
|
|
|
// We append the mFromValue for each segment. If the mToValue
|
|
|
|
// differs from the following segment's mFromValue, or if we're on
|
|
|
|
// the last segment, then we append the mToValue as well.
|
|
|
|
//
|
|
|
|
// Each value is annotated with whether it is a "first", "left", "right",
|
|
|
|
// or "last" value. "left" and "right" values represent the value coming
|
|
|
|
// in to and out of a given offset, in the middle of an animation. For
|
|
|
|
// most segments, the mToValue is the "left" and the following segment's
|
|
|
|
// mFromValue is the "right". The "first" and "last" values are the
|
|
|
|
// additional values assigned to offset 0 or 1 for reverse and forward
|
|
|
|
// filling. These annotations are used to ensure multiple values for a
|
|
|
|
// given property are sorted correctly and that we do not merge Keyframes
|
|
|
|
// with different values for the same offset.
|
|
|
|
|
|
|
|
OrderedKeyframeValueEntry* entry = entries.AppendElement();
|
2015-09-29 02:20:14 +00:00
|
|
|
entry->mProperty = property.mProperty;
|
2015-10-22 08:22:38 +00:00
|
|
|
entry->mValue = segment.mFromValue;
|
|
|
|
entry->mOffset = segment.mFromKey;
|
2016-01-29 13:47:00 +00:00
|
|
|
entry->mTimingFunction = &segment.mTimingFunction;
|
2015-10-22 08:22:38 +00:00
|
|
|
entry->mPosition =
|
|
|
|
segment.mFromKey == segment.mToKey && segment.mFromKey == 0.0f ?
|
|
|
|
ValuePosition::First :
|
|
|
|
ValuePosition::Right;
|
|
|
|
|
|
|
|
if (i == n - 1 ||
|
|
|
|
segment.mToValue != property.mSegments[i + 1].mFromValue) {
|
|
|
|
entry = entries.AppendElement();
|
|
|
|
entry->mProperty = property.mProperty;
|
|
|
|
entry->mValue = segment.mToValue;
|
|
|
|
entry->mOffset = segment.mToKey;
|
2016-01-29 13:44:00 +00:00
|
|
|
entry->mTimingFunction = segment.mToKey == 1.0f ?
|
2016-01-29 13:47:00 +00:00
|
|
|
nullptr : &segment.mTimingFunction;
|
2015-10-22 08:22:38 +00:00
|
|
|
entry->mPosition =
|
|
|
|
segment.mFromKey == segment.mToKey && segment.mToKey == 1.0f ?
|
|
|
|
ValuePosition::Last :
|
|
|
|
ValuePosition::Left;
|
|
|
|
}
|
2015-09-29 02:20:14 +00:00
|
|
|
}
|
|
|
|
}
|
2015-10-22 08:22:38 +00:00
|
|
|
|
|
|
|
entries.Sort(OrderedKeyframeValueEntry::ForKeyframeGenerationComparator());
|
2015-09-29 02:20:14 +00:00
|
|
|
|
|
|
|
for (size_t i = 0, n = entries.Length(); i < n; ) {
|
2015-10-22 08:22:38 +00:00
|
|
|
OrderedKeyframeValueEntry* entry = &entries[i];
|
|
|
|
OrderedKeyframeValueEntry* previousEntry = nullptr;
|
|
|
|
|
2016-03-22 07:16:39 +00:00
|
|
|
// Create a JS object with the BaseComputedKeyframe dictionary members.
|
|
|
|
BaseComputedKeyframe keyframeDict;
|
2015-10-22 08:22:38 +00:00
|
|
|
keyframeDict.mOffset.SetValue(entry->mOffset);
|
|
|
|
keyframeDict.mComputedOffset.Construct(entry->mOffset);
|
2016-01-29 13:47:00 +00:00
|
|
|
if (entry->mTimingFunction && entry->mTimingFunction->isSome()) {
|
2015-10-22 08:22:38 +00:00
|
|
|
// If null, leave easing as its default "linear".
|
|
|
|
keyframeDict.mEasing.Truncate();
|
2016-01-29 13:47:00 +00:00
|
|
|
entry->mTimingFunction->value().AppendToString(keyframeDict.mEasing);
|
2015-10-22 08:22:38 +00:00
|
|
|
}
|
2015-09-29 02:20:14 +00:00
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
JS::Rooted<JS::Value> keyframeJSValue(aCx);
|
|
|
|
if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
|
2015-09-29 02:20:14 +00:00
|
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-22 08:22:38 +00:00
|
|
|
JS::Rooted<JSObject*> keyframe(aCx, &keyframeJSValue.toObject());
|
2015-09-29 02:20:14 +00:00
|
|
|
do {
|
2015-10-22 08:22:38 +00:00
|
|
|
const char* name = nsCSSProps::PropertyIDLName(entry->mProperty);
|
|
|
|
nsString stringValue;
|
|
|
|
StyleAnimationValue::UncomputeValue(entry->mProperty,
|
|
|
|
entry->mValue,
|
|
|
|
stringValue);
|
2015-09-29 02:20:14 +00:00
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
2015-10-22 08:22:38 +00:00
|
|
|
if (!ToJSValue(aCx, stringValue, &value) ||
|
2015-09-29 02:20:14 +00:00
|
|
|
!JS_DefineProperty(aCx, keyframe, name, value, JSPROP_ENUMERATE)) {
|
|
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
|
|
return;
|
|
|
|
}
|
2015-10-22 08:22:38 +00:00
|
|
|
if (++i == n) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
previousEntry = entry;
|
|
|
|
entry = &entries[i];
|
|
|
|
} while (entry->SameKeyframe(*previousEntry));
|
2015-09-29 02:20:14 +00:00
|
|
|
|
|
|
|
aResult.AppendElement(keyframe);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:55:00 +00:00
|
|
|
/* static */ const TimeDuration
|
|
|
|
KeyframeEffectReadOnly::OverflowRegionRefreshInterval()
|
|
|
|
{
|
|
|
|
// The amount of time we can wait between updating throttled animations
|
|
|
|
// on the main thread that influence the overflow region.
|
|
|
|
static const TimeDuration kOverflowRegionRefreshInterval =
|
|
|
|
TimeDuration::FromMilliseconds(200);
|
|
|
|
|
|
|
|
return kOverflowRegionRefreshInterval;
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:49:00 +00:00
|
|
|
bool
|
|
|
|
KeyframeEffectReadOnly::CanThrottle() const
|
|
|
|
{
|
2016-01-06 02:04:06 +00:00
|
|
|
// Unthrottle if we are not in effect or current. This will be the case when
|
|
|
|
// our owning animation has finished, is idle, or when we are in the delay
|
|
|
|
// phase (but without a backwards fill). In each case the computed progress
|
|
|
|
// value produced on each tick will be the same so we will skip requesting
|
|
|
|
// unnecessary restyles in NotifyAnimationTimingUpdated. Any calls we *do* get
|
|
|
|
// here will be because of a change in state (e.g. we are newly finished or
|
|
|
|
// newly no longer in effect) in which case we shouldn't throttle the sample.
|
|
|
|
if (!IsInEffect() || !IsCurrent()) {
|
2015-11-06 01:57:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
2015-11-06 01:53:00 +00:00
|
|
|
|
|
|
|
nsIFrame* frame = GetAnimationFrame();
|
|
|
|
if (!frame) {
|
|
|
|
// There are two possible cases here.
|
|
|
|
// a) No target element
|
|
|
|
// b) The target element has no frame, e.g. because it is in a display:none
|
|
|
|
// subtree.
|
|
|
|
// In either case we can throttle the animation because there is no
|
|
|
|
// need to update on the main thread.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// First we need to check layer generation and transform overflow
|
2015-12-20 13:16:00 +00:00
|
|
|
// prior to the property.mIsRunningOnCompositor check because we should
|
2015-11-06 01:53:00 +00:00
|
|
|
// occasionally unthrottle these animations even if the animations are
|
|
|
|
// already running on compositor.
|
|
|
|
for (const LayerAnimationInfo::Record& record :
|
|
|
|
LayerAnimationInfo::sRecords) {
|
|
|
|
// Skip properties that are overridden in the cascade.
|
|
|
|
// (GetAnimationOfProperty, as called by HasAnimationOfProperty,
|
|
|
|
// only returns an animation if it currently wins in the cascade.)
|
|
|
|
if (!HasAnimationOfProperty(record.mProperty)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2016-01-06 02:04:05 +00:00
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget, mPseudoType);
|
|
|
|
MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
|
|
|
|
"associated with a target element");
|
2015-11-06 01:53:00 +00:00
|
|
|
layers::Layer* layer =
|
|
|
|
FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType);
|
2016-01-06 02:04:05 +00:00
|
|
|
// Unthrottle if the layer needs to be brought up to date
|
2015-11-06 01:53:00 +00:00
|
|
|
if (!layer ||
|
2016-01-06 02:04:05 +00:00
|
|
|
effectSet->GetAnimationGeneration() !=
|
|
|
|
layer->GetAnimationGeneration()) {
|
2015-11-06 01:53:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is a transform animation that affects the overflow region,
|
|
|
|
// we should unthrottle the animation periodically.
|
|
|
|
if (record.mProperty == eCSSProperty_transform &&
|
|
|
|
!CanThrottleTransformChanges(*frame)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:49:00 +00:00
|
|
|
for (const AnimationProperty& property : mProperties) {
|
2015-12-20 13:16:00 +00:00
|
|
|
if (!property.mIsRunningOnCompositor) {
|
2015-11-06 01:49:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2015-11-06 01:53:00 +00:00
|
|
|
|
2015-11-06 01:49:00 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:53:00 +00:00
|
|
|
bool
|
|
|
|
KeyframeEffectReadOnly::CanThrottleTransformChanges(nsIFrame& aFrame) const
|
|
|
|
{
|
|
|
|
// If we know that the animation cannot cause overflow,
|
|
|
|
// we can just disable flushes for this animation.
|
|
|
|
|
|
|
|
// If we don't show scrollbars, we don't care about overflow.
|
|
|
|
if (LookAndFeel::GetInt(LookAndFeel::eIntID_ShowHideScrollbars) == 0) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
nsPresContext* presContext = GetPresContext();
|
|
|
|
// CanThrottleTransformChanges is only called as part of a refresh driver tick
|
|
|
|
// in which case we expect to has a pres context.
|
|
|
|
MOZ_ASSERT(presContext);
|
|
|
|
|
|
|
|
TimeStamp now =
|
|
|
|
presContext->RefreshDriver()->MostRecentRefresh();
|
|
|
|
|
2016-01-12 22:54:54 +00:00
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget, mPseudoType);
|
|
|
|
MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
|
|
|
|
" on an effect in an effect set");
|
|
|
|
MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
|
|
|
|
" on an effect with a parent animation");
|
|
|
|
TimeStamp animationRuleRefreshTime =
|
|
|
|
effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel());
|
2015-11-06 01:53:00 +00:00
|
|
|
// If this animation can cause overflow, we can throttle some of the ticks.
|
2016-01-12 22:54:54 +00:00
|
|
|
if (!animationRuleRefreshTime.IsNull() &&
|
|
|
|
(now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) {
|
2015-11-06 01:53:00 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the nearest scrollable ancestor has overflow:hidden,
|
|
|
|
// we don't care about overflow.
|
|
|
|
nsIScrollableFrame* scrollable =
|
|
|
|
nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
|
|
|
|
if (!scrollable) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ScrollbarStyles ss = scrollable->GetScrollbarStyles();
|
|
|
|
if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
|
|
|
|
ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
|
|
|
|
scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:42:00 +00:00
|
|
|
nsIFrame*
|
|
|
|
KeyframeEffectReadOnly::GetAnimationFrame() const
|
|
|
|
{
|
|
|
|
if (!mTarget) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
nsIFrame* frame = mTarget->GetPrimaryFrame();
|
|
|
|
if (!frame) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2016-02-16 22:07:00 +00:00
|
|
|
if (mPseudoType == CSSPseudoElementType::before) {
|
2015-11-06 01:42:00 +00:00
|
|
|
frame = nsLayoutUtils::GetBeforeFrame(frame);
|
2016-02-16 22:07:00 +00:00
|
|
|
} else if (mPseudoType == CSSPseudoElementType::after) {
|
2015-11-06 01:42:00 +00:00
|
|
|
frame = nsLayoutUtils::GetAfterFrame(frame);
|
|
|
|
} else {
|
2016-02-16 22:07:00 +00:00
|
|
|
MOZ_ASSERT(mPseudoType == CSSPseudoElementType::NotPseudo,
|
2015-11-06 01:42:00 +00:00
|
|
|
"unknown mPseudoType");
|
|
|
|
}
|
|
|
|
if (!frame) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return nsLayoutUtils::GetStyleFrame(frame);
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:51:00 +00:00
|
|
|
nsIDocument*
|
|
|
|
KeyframeEffectReadOnly::GetRenderedDocument() const
|
|
|
|
{
|
|
|
|
if (!mTarget) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
return mTarget->GetComposedDoc();
|
|
|
|
}
|
|
|
|
|
|
|
|
nsPresContext*
|
|
|
|
KeyframeEffectReadOnly::GetPresContext() const
|
|
|
|
{
|
|
|
|
nsIDocument* doc = GetRenderedDocument();
|
|
|
|
if (!doc) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
nsIPresShell* shell = doc->GetShell();
|
|
|
|
if (!shell) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
return shell->GetPresContext();
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:38:00 +00:00
|
|
|
/* static */ bool
|
|
|
|
KeyframeEffectReadOnly::IsGeometricProperty(
|
|
|
|
const nsCSSProperty aProperty)
|
|
|
|
{
|
|
|
|
switch (aProperty) {
|
|
|
|
case eCSSProperty_bottom:
|
|
|
|
case eCSSProperty_height:
|
|
|
|
case eCSSProperty_left:
|
|
|
|
case eCSSProperty_right:
|
|
|
|
case eCSSProperty_top:
|
|
|
|
case eCSSProperty_width:
|
|
|
|
return true;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-06 01:38:00 +00:00
|
|
|
/* static */ bool
|
|
|
|
KeyframeEffectReadOnly::CanAnimateTransformOnCompositor(
|
|
|
|
const nsIFrame* aFrame,
|
2016-03-04 08:54:25 +00:00
|
|
|
AnimationPerformanceWarning::Type& aPerformanceWarning)
|
2015-11-06 01:38:00 +00:00
|
|
|
{
|
2016-02-07 10:51:11 +00:00
|
|
|
// Disallow OMTA for preserve-3d transform. Note that we check the style property
|
|
|
|
// rather than Extend3DContext() since that can recurse back into this function
|
2016-03-04 06:48:50 +00:00
|
|
|
// via HasOpacity(). See bug 779598.
|
2015-11-06 01:38:00 +00:00
|
|
|
if (aFrame->Combines3DTransformWithAncestors() ||
|
2016-02-07 10:51:11 +00:00
|
|
|
aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
|
2016-03-04 08:54:25 +00:00
|
|
|
aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
|
2015-11-06 01:38:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Note that testing BackfaceIsHidden() is not a sufficient test for
|
|
|
|
// what we need for animating backface-visibility correctly if we
|
|
|
|
// remove the above test for Extend3DContext(); that would require
|
2016-03-04 06:48:50 +00:00
|
|
|
// looking at backface-visibility on descendants as well. See bug 1186204.
|
2015-11-06 01:38:00 +00:00
|
|
|
if (aFrame->StyleDisplay()->BackfaceIsHidden()) {
|
2016-03-04 08:54:25 +00:00
|
|
|
aPerformanceWarning =
|
|
|
|
AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
|
2015-11-06 01:38:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
2016-03-04 06:48:50 +00:00
|
|
|
// Async 'transform' animations of aFrames with SVG transforms is not
|
|
|
|
// supported. See bug 779599.
|
2015-11-06 01:38:00 +00:00
|
|
|
if (aFrame->IsSVGTransformed()) {
|
2016-03-04 08:54:25 +00:00
|
|
|
aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
|
2015-11-06 01:38:00 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-12-03 23:32:53 +00:00
|
|
|
bool
|
2016-03-14 00:07:48 +00:00
|
|
|
KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
|
2016-03-04 06:07:04 +00:00
|
|
|
const nsIFrame* aFrame,
|
2016-03-04 08:54:25 +00:00
|
|
|
AnimationPerformanceWarning::Type& aPerformanceWarning) const
|
2015-11-06 01:45:00 +00:00
|
|
|
{
|
2015-12-03 23:32:53 +00:00
|
|
|
// We currently only expect this method to be called when this effect
|
|
|
|
// is attached to a playing Animation. If that ever changes we'll need
|
|
|
|
// to update this to only return true when that is the case since paused,
|
|
|
|
// filling, cancelled Animations etc. shouldn't stop other Animations from
|
|
|
|
// running on the compositor.
|
|
|
|
MOZ_ASSERT(mAnimation && mAnimation->IsPlaying());
|
|
|
|
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
2015-12-24 22:14:02 +00:00
|
|
|
// If a property is overridden in the CSS cascade, it should not block other
|
|
|
|
// animations from running on the compositor.
|
|
|
|
if (!property.mWinsInCascade) {
|
|
|
|
continue;
|
|
|
|
}
|
2015-12-03 23:32:53 +00:00
|
|
|
// Check for geometric properties
|
|
|
|
if (IsGeometricProperty(property.mProperty)) {
|
2016-03-04 08:54:25 +00:00
|
|
|
aPerformanceWarning =
|
2016-03-14 00:07:48 +00:00
|
|
|
AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
|
2015-12-03 23:32:53 +00:00
|
|
|
return true;
|
2015-11-06 01:45:00 +00:00
|
|
|
}
|
2015-12-03 23:32:53 +00:00
|
|
|
|
|
|
|
// Check for unsupported transform animations
|
|
|
|
if (property.mProperty == eCSSProperty_transform) {
|
|
|
|
if (!CanAnimateTransformOnCompositor(aFrame,
|
2016-03-04 06:07:04 +00:00
|
|
|
aPerformanceWarning)) {
|
2015-12-03 23:32:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
2015-11-06 01:45:00 +00:00
|
|
|
}
|
|
|
|
}
|
2015-12-03 23:32:53 +00:00
|
|
|
|
|
|
|
return false;
|
2015-11-06 01:45:00 +00:00
|
|
|
}
|
|
|
|
|
2016-03-03 21:36:36 +00:00
|
|
|
void
|
2016-03-04 08:54:25 +00:00
|
|
|
KeyframeEffectReadOnly::SetPerformanceWarning(
|
|
|
|
nsCSSProperty aProperty,
|
|
|
|
const AnimationPerformanceWarning& aWarning)
|
2016-03-03 21:36:36 +00:00
|
|
|
{
|
|
|
|
for (AnimationProperty& property : mProperties) {
|
2016-03-04 08:54:25 +00:00
|
|
|
if (property.mProperty == aProperty &&
|
|
|
|
(!property.mPerformanceWarning ||
|
|
|
|
*property.mPerformanceWarning != aWarning)) {
|
|
|
|
property.mPerformanceWarning = Some(aWarning);
|
|
|
|
|
|
|
|
nsXPIDLString localizedString;
|
|
|
|
if (nsLayoutUtils::IsAnimationLoggingEnabled() &&
|
|
|
|
property.mPerformanceWarning->ToLocalizedString(localizedString)) {
|
|
|
|
nsAutoCString logMessage = NS_ConvertUTF16toUTF8(localizedString);
|
2016-03-04 06:07:04 +00:00
|
|
|
AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget);
|
|
|
|
}
|
2016-03-03 21:36:36 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-15 00:34:47 +00:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
//
|
|
|
|
// KeyframeEffect
|
|
|
|
//
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
KeyframeEffect::KeyframeEffect(nsIDocument* aDocument,
|
|
|
|
Element* aTarget,
|
2016-02-17 20:37:00 +00:00
|
|
|
CSSPseudoElementType aPseudoType,
|
2016-02-15 00:34:47 +00:00
|
|
|
const TimingParams& aTiming)
|
|
|
|
: KeyframeEffectReadOnly(aDocument, aTarget, aPseudoType,
|
2016-02-26 21:39:30 +00:00
|
|
|
new AnimationEffectTiming(aTiming, this))
|
2016-02-15 00:34:47 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2016-02-15 00:34:47 +00:00
|
|
|
JSObject*
|
|
|
|
KeyframeEffect::WrapObject(JSContext* aCx,
|
|
|
|
JS::Handle<JSObject*> aGivenProto)
|
|
|
|
{
|
|
|
|
return KeyframeEffectBinding::Wrap(aCx, this, aGivenProto);
|
|
|
|
}
|
|
|
|
|
2016-03-11 08:27:16 +00:00
|
|
|
/* static */ already_AddRefed<KeyframeEffect>
|
|
|
|
KeyframeEffect::Constructor(
|
|
|
|
const GlobalObject& aGlobal,
|
|
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
|
|
JS::Handle<JSObject*> aFrames,
|
|
|
|
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
|
|
|
|
ErrorResult& aRv)
|
|
|
|
{
|
|
|
|
return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aFrames,
|
|
|
|
aOptions, aRv);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* static */ already_AddRefed<KeyframeEffect>
|
|
|
|
KeyframeEffect::Constructor(
|
|
|
|
const GlobalObject& aGlobal,
|
|
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
|
|
JS::Handle<JSObject*> aFrames,
|
2016-03-12 13:14:10 +00:00
|
|
|
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
|
2016-03-11 08:27:16 +00:00
|
|
|
ErrorResult& aRv)
|
|
|
|
{
|
|
|
|
return ConstructKeyframeEffect<KeyframeEffect>(aGlobal, aTarget, aFrames,
|
2016-03-12 13:14:10 +00:00
|
|
|
aOptions, aRv);
|
2016-03-11 08:27:16 +00:00
|
|
|
}
|
|
|
|
|
2016-02-26 21:39:49 +00:00
|
|
|
void KeyframeEffect::NotifySpecifiedTimingUpdated()
|
|
|
|
{
|
2016-03-21 08:49:50 +00:00
|
|
|
// Use the same document for a pseudo element and its parent element.
|
|
|
|
nsAutoAnimationMutationBatch mb(mTarget->OwnerDoc());
|
2016-02-26 21:39:49 +00:00
|
|
|
|
|
|
|
if (mAnimation) {
|
|
|
|
mAnimation->NotifyEffectTimingUpdated();
|
|
|
|
|
|
|
|
if (mAnimation->IsRelevant()) {
|
|
|
|
nsNodeUtils::AnimationChanged(mAnimation);
|
|
|
|
}
|
|
|
|
|
|
|
|
nsPresContext* presContext = GetPresContext();
|
|
|
|
if (presContext) {
|
|
|
|
presContext->EffectCompositor()->
|
|
|
|
RequestRestyle(mTarget, mPseudoType,
|
|
|
|
EffectCompositor::RestyleType::Layer,
|
|
|
|
mAnimation->CascadeLevel());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-26 21:39:30 +00:00
|
|
|
KeyframeEffect::~KeyframeEffect()
|
|
|
|
{
|
|
|
|
mTiming->Unlink();
|
|
|
|
}
|
|
|
|
|
2014-08-10 07:06:46 +00:00
|
|
|
} // namespace dom
|
|
|
|
} // namespace mozilla
|